• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Projectiles Library

Status
Not open for further replies.
Level 18
Joined
Jan 21, 2006
Messages
2,552
Custom Projectiles

I've re-written this a few times, optimized it, and added enough functionality to make it actually useful and very easy to create projectiles that mimic the WarCraft III projectiles.

When a projectile is created, the user must designate a unit as the "projectile" object. This unit will constantly have its position refreshed so it will not be able to move as a projectile (though orders can still be given). Typically a unit is designated as a projectile and then immediately launched --in which case it is flagged as "active".

Once a projectile is active it will soar through the air with the given input on doLaunch() which takes a two vectors (start/finish 3D points), a speed and an arc. The projectile will then soar through the air with the given input.

When a projectile is created, there are a few available methods that allow you to control the way the projectile will act, such as setProjectileTarget[() which will cause the projectile's target to be updated in sync with a unit. If projectile homing for that projectile is enabled, the projectile will follow the target properly (like War3 projectiles).

The dynamic aspect of the system comes from the ability to extend the projectile, and define methods for special projectile events such as onUnitCollision (takes unit) or onFinish. If a projectile is assigned a target, then the .target member will be available to the user (if damage were to be dealt, or other effects).

I was thinking of uploading this as a submission but I hate making descriptions : (

Projectiles
JASS:
library Projectiles requires Vectors

globals
//************************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Includes default definitions of projectile settings in addition to other values that are used
//* within the system but rely on a value in order to function.
//*
//* >> Medihv's crow-form ability which allows non-flying units to have their fly-height modified;
    public      constant integer            FLY_ABIL_ID                     = 'Amrf'          
//*
//* >> Default constants;
    public      constant real               PROJ_COLLISION_DEFAULT          = 70.00
    public      constant real               PROJ_COLLISION_MAX              = 200.00
//*
    public      constant real               PROJ_MOTION_REF                 = 0.03              //* This identifies the amount of time (in seconds) that lapses
                                                                                                //* between each iteration that the projectiles are "updated";
//* >> Active feature defaults;
    private     constant boolean            PROJ_TARG_FOLLOW_ON             = true
    private     constant boolean            PROJ_UNIT_COLLIDE_ON            = true              //* The unit-collision feature of projectiles is on by default.
    private     constant boolean            PROJ_FACING_ROTATION_ON         = true              //* Whether or not the unit's facing will be updated to match the
                                                                                                //* direction of the velocity vector.
    private     constant boolean            PROJ_PITCH_ROTATION_ON          = true              //* Whether or not a unit will have its pitch-rotation adjusted
                                                                                                //* when the projectile is updated. Only guaranteed to work with
                                                                                                //* the dummy.mdx included in the imports (by Vexorian).
//*
    private     constant boolean            PROJ_EXPIRE_ON                  = true              //* By default, projectiles are flagged to be destroyed after their
                                                                                                //* determined path unless otherwise specified. If it is necessary
                                                                                                //* to manipulate projectiles after their determined lifespan, then
                                                                                                //* this should be set to false.
    private     constant boolean            PROJ_TO_KILL_ON                 = true              //* Once the projectile expires, the unit associated with the 
                                                                                                //* projectile will in turn be killed. 
//*
//*
//*********************************************************************************************************
endglobals

interface projectileinterface
//***************************
//* Projectile Interface
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Includes the names for methods (and their parameters) that users can declare in child-structures to
//* acquire reference to a projectile on different occurances that prompt a response.
//*
//* >> Responses associated with time-line "events";
    method  onStart             takes nothing returns nothing           defaults nothing
    method  onFinish            takes nothing returns nothing           defaults nothing
//*
//* >> Responses associated with physical surrounding occurances;
    method  onGround            takes nothing returns nothing           defaults nothing
//*
//* >> Responses associated with widget collision;
    method  onUnitCollision     takes unit u returns nothing            defaults nothing
  //method  onDestCollision     takes destructable d returns nothing    defaults nothing
//*
//*
//*********************************************************************************************************
endinterface

globals
//********************
//* Dynamic Storage
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* In order to optimize the Projectiles efficiency, certain global variables are required to make special
//* calculations and operations; such as GetLocationZ().
//*
    private     projectile          projRef
    private     location            loc                     = Location(0, 0)
    private     group               grp                     = CreateGroup()
//*
//*
//*********************************************************************************************************
endglobals

struct projectile extends projectileinterface
//*******************************************
//* Projectile
//* ¯¯¯¯¯¯¯¯¯¯
//* Primary data-structure that encapsullates the projectile functionality. Contains methods for creating,
//* destroying, and manipulating certain aspects of the projectile motion.
//*
//* #######################################################################################################
//*
    readonly    unit            toUnit              = null
    readonly    unit            target              = null
    readonly    unit            source              = null              //* In many situations it is necessary for projectiles to have a "source" unit
                                                                        //* to gather information about where the projectile came from.
//*
    readonly    real            speed                                   //* In order to launch a projectile, a "speed" and "arc" input is necessary. These
    readonly    real            arc                                     //* values are stored in readonly-scope variables so that they can be referenced 
                                                                        //* externally.
//*
    public      real            timescale           = 1.00
    private     real            priv_collision      = PROJ_COLLISION_DEFAULT
//*
//* These are used to maintain necessary projectile checks. They are also used to determine the features
//* that should be included in each specific projectile, such as "activeUnitCollision".
//*
    private     boolean         active              = false                         //* This is automatically updated depending on whether the projectile
                                                                                    //* has been launched. 
    public      boolean         activeTargetFollow  = PROJ_TARG_FOLLOW_ON       
    public      boolean         activePitch         = PROJ_PITCH_ROTATION_ON   
    public      boolean         activeRotation      = PROJ_FACING_ROTATION_ON       //* Whether the unit's facing will be updated to match the 2D direction
                                                                                    //* of the projectile's velocity vector.
    public      boolean         activeUnitCollision = PROJ_UNIT_COLLIDE_ON
//*
//* In order to prevent multiple responses of certain projectile events, the "state" of the projectile is 
//* stored in a private-scope variable. The events that are included in this are:
//*     • method onGround
//*     • method onFinish
//*
    private     boolean         priv_ground         = false
    private     boolean         priv_finish         = false
//*
//* Other private status checks:
    private     boolean         priv_follow         = false
    private     boolean         priv_unitfollow     = false
//*
//* Projectile motion is dictated by 4 vectors: position, velocity, acceleration, and target-position. If
//* these values are manipulated they can be used to achieve certain effects.
//*
    private     s_vector3d      posVec
    private     s_vector3d      velVec
    private     s_vector3d      accVec
    private     s_vector3d      tarVec
//*
//* Projectiles are updated in a stack; and require certain data on-hand in order to properly update and
//* manage the stack.
//*
    private         integer         priv_index             
    private static  integer         priv_stackDex       = 0                 //* The amount of projectiles currently in the stack.
//*
    private static  thistype array  priv_stack
    private static  timer           priv_stackLoop      = CreateTimer()
    private static  constant real   priv_stackLoopRef   = PROJ_MOTION_REF
//*
    private         real            priv_timeleft     
    public          boolean         toDestroy           = PROJ_EXPIRE_ON
    public          boolean         toKill              = PROJ_TO_KILL_ON
//*
//* #######################################################################################################
//*
    method operator collision takes nothing returns real
        return .priv_collision
    endmethod
    method operator collision= takes real r returns nothing
        if (r>PROJ_COLLISION_MAX) then
            set r=PROJ_COLLISION_MAX
        endif
        set .priv_collision=r
    endmethod
//*
//*
    method setProjectileTargetPos takes real x, real y, real z returns boolean      //* The projectile-target-pos can be manipulated many times.
        local real xA  = .posVec.x
        local real yA  = .posVec.y
        if (.tarVec.x==x and .tarVec.y==y and .tarVec.z==z) then               
            return false
        endif
        set .tarVec.x       = x
        set .tarVec.y       = y
        set .tarVec.z       = z       
        set xA              = .posVec.x
        set yA              = .posVec.y
        set xA              = SquareRoot((x-xA)*(x-xA) + (y-yA)*(y-yA))
        set yA              = (SquareRoot(.velVec.x*.velVec.x + .velVec.y*.velVec.y)/thistype.priv_stackLoopRef)
        set .priv_timeleft  = xA/yA
        set .priv_follow    = true      // This must be flagged to notify the "projectile"
        return true                     // that the target has been changed.
    endmethod
    method setProjectileTarget takes unit who returns boolean                       //* The projectile-target can only be set once.
        if (.target == null) then
            set .target = who
            set .priv_unitfollow = true
            return true
        endif
        return false
    endmethod
    method setProjectileSource takes unit who returns boolean                       //* The projectile-source can only be set once.
        if (.source == null) then
            set .source = who
            return true
        endif
        return false
    endmethod
//*
//*
    method onDestroy takes nothing returns nothing
        set thistype.priv_stackDex = thistype.priv_stackDex - 1
        set thistype.priv_stack[.priv_index] = thistype.priv_stack[thistype.priv_stackDex]
        set thistype.priv_stack[.priv_index].priv_index = .priv_index
        call .posVec.destroy()
        call .velVec.destroy()
        call .accVec.destroy()
        call .tarVec.destroy()
        if (.toKill) then
            call KillUnit(.toUnit)
        endif
        if (thistype.priv_stackDex == 0) then
            call PauseTimer(thistype.priv_stackLoop)
        endif
    endmethod
//*
//*
    method doLaunch takes s_vector3d start, s_vector3d finish, real speed, real arc returns boolean
        local real d
        local real a
        local real r
        local unit u
        if (start == 0) or (finish == 0) or (.active) then
            // The launch method will return false under several circumstances. If start/finish vectors are
            // null structs or if the projectile has already been launched.

            return false
        endif
        // ----
        // Setup the projectile vectors and static information;
        set .posVec.x   = start.x
        set .posVec.y   = start.y
        set .posVec.z   = start.z
        set .tarVec.x   = finish.x
        set .tarVec.y   = finish.y
        set .tarVec.z   = finish.z
        set .speed      = speed
        set .arc        = arc
        // ----
        // 
        set d = SquareRoot((finish.x-start.x)*(finish.x-start.x) + (finish.y-start.y)*(finish.y-start.y))
        set a = Atan2(finish.y-start.y, finish.x-start.x)
        // In the situation where the distance between start/finish is 0.00 or the speed of the projectile is
        // 0, then it will result in an "instant" projectile.
        if (d == 0.00) or (speed == 0.00) then
            set .posVec.x       = .tarVec.x
            set .posVec.y       = .tarVec.y
            set .posVec.z       = .tarVec.z
            set .priv_timeleft  = 0.00
        else
            set .priv_timeleft  = d/speed
            set r               = thistype.priv_stackLoopRef
            set .accVec.z       = (-8*arc*speed*speed/d)
            set .velVec.x       = speed*Cos(a)
            set .velVec.y       = speed*Sin(a)
            set .velVec.z       = (-.accVec.z * (d/speed)/2 + (finish.z-start.z)/(d/speed))
            set .accVec.z       = .accVec.z *r*r
            set .velVec.x       = .velVec.x *r
            set .velVec.y       = .velVec.y *r
            set .velVec.z       = .velVec.z *r
        endif
        // ----
        // The projectile's position should be updated immediately to match the new one.
        set u = .toUnit
        call SetUnitX(u, .posVec.x)
        call SetUnitY(u, .posVec.y)
        call MoveLocation(loc, .posVec.x, .posVec.y)
        call SetUnitFlyHeight(u, .posVec.z-GetLocationZ(loc), 0)
        set u = null
        // ----
        // Run the .onStart interface method and set .active to true so that it cannot be launched again.
        call .onStart()
        set .active = true
        return true
    endmethod
//*
//*
    private static method doLoopEnum takes nothing returns boolean
        local unit filt = GetFilterUnit()
        local real xA   = GetUnitX(filt)
        local real yA   = GetUnitY(filt)
        local real zA
        local real xB   = projRef.posVec.x
        local real yB   = projRef.posVec.y
        local real zB   = projRef.posVec.z
        call MoveLocation(loc, xA, yA)
        set zA = GetUnitFlyHeight(filt)+GetLocationZ(loc)
        if (((xA-xB)*(xA-xB) + (yA-yB)*(yA-yB) + (zA-zB)*(zA-zB)) <= projRef.collision*projRef.collision) then
            call projRef.onUnitCollision(filt)
        endif
        set filt = null
        return false
    endmethod
    private static method doLoop takes nothing returns nothing
        local integer i = .priv_stackDex - 1
        local thistype dat
        local s_vector3d vA
        local s_vector3d vB
        local s_vector3d vC
        local real x
        local real y
        local real z
        local real x2
        local real y2
        local real z2
        local real e
        local unit u
        loop
            exitwhen (i < 0)
            set dat = .priv_stack[i]
            if (dat != 0) then
                // ----
                // If the projectile is active, the acceleration vector must be added to the velocity
                // and the velocity added to the position. If the projectile is not active these vectors
                // will have no roll in its motion.
                //
                if (dat.active) then
                    set e       = dat.timescale
                    set vA      = dat.velVec
                    set vB      = dat.accVec
                    set vA.x    = vA.x + vB.x *e*e
                    set vA.y    = vA.y + vB.y *e*e
                    set vA.z    = vA.z + vB.z *e*e
                    set vB      = dat.posVec
                    set vB.x    = vB.x + vA.x *e
                    set vB.y    = vB.y + vA.y *e
                    set vB.z    = vB.z + vA.z *e
                else
                    set vA      = dat.velVec
                    set vB      = dat.posVec
                endif
                // ----
                // In any situation, update the projectile's position with its position vector.
                //
                set u = dat.toUnit
                set x = vB.x
                set y = vB.y
                set z = vB.z
                call SetUnitX(u, x)
                call SetUnitY(u, y)
                call MoveLocation(loc, x, y)
                call SetUnitFlyHeight(u, z-GetLocationZ(loc), 0)
                // ----
                // In addition to the position (which is automatically updated) a projectile can also have its
                // facing rotation and pitch rotation updated so that they match the direction of the velocity.
                // Each rotation has its own setting variable (see above declarations).
                //
                set x2 = vA.x
                set y2 = vA.y
                set z2 = vA.z
                if (dat.active) and (dat.activePitch) and (vA.magnitude() > 0.00) then
                    // It is assumed in this feature that the model includes the same pitch animations as the one
                    // by Vexorian and iNfRaNe; it has been tweaked to be specific to -that- model.
                    call SetUnitAnimationByIndex(u, R2I(bj_RADTODEG*Atan2(z2, SquareRoot(x2*x2 + y2*y2)+0.5)+90))
                endif
                if (dat.active) and (dat.activeRotation) and (vA.magnitude() > 0.00) then
                    // Unlike pitch, this can be changed for any projectile.
                    call SetUnitFacingTimed(u, Atan2(y2, x2)*bj_RADTODEG, 0)
                endif
                // ----
                // If the projectile's height is below the terrain-height, then run the .onGround method. This
                // method should only run once until the projectile has surfaced above the terrain (won't recur).
                //
                if (z <= GetLocationZ(loc)) then
                    if not (dat.priv_ground) then
                        // Once we execute .onGround, it will flag .priv_ground which will disallow it from firing
                        // again until the projectile has surfaced above the terrain.
                        call dat.onGround()
                        set dat.priv_ground = true
                    endif
                else
                    if (dat.priv_ground) then
                        set dat.priv_ground = false
                    endif
                endif
                // ----
                // Projectiles will collide with units enumerated within a spherical area. Unit-collision-size is not
                // factored into the enumeration. 
                //
                if (dat.activeUnitCollision) then
                    set projRef = dat
                    call GroupEnumUnitsInRange(grp, x, y, dat.collision, Filter(function thistype.doLoopEnum))
                    // The group will not need to be cleared, since .doLoopEnum will always return false.
                endif
                // ----
                // Target tracking (specifically, units) requires an "active" boolean config value in order for each
                // specific projectile to allow tracking, that is, following a moving target.
                //
                // There are two variations to this, typically when you want to track a unit (which is common) and when
                // you simply want to track the vector target (which could possibly be moved using setTargetPos().
                //
                if (dat.active) and (dat.activeTargetFollow) then
                    // If the projectile is active and has active-target-tracking, it will narrow down its filter to 
                    // see whether or not the projectile is being tracked to a unit, and whether or not its target
                    // has been changed from the original position.
                    if (dat.priv_unitfollow) then
                        // Unit-follow means the target-vector will be updated to match the 3D position of the given
                        // unit-target.
                        set x = GetUnitX(dat.target)
                        set y = GetUnitY(dat.target)
                        call MoveLocation(loc, x, y)
                        set z = GetLocationZ(loc)+GetUnitFlyHeight(dat.target)
                        call dat.setProjectileTargetPos(x, y, z)
                    endif
                    if (dat.priv_follow) then
                        // If .priv_follow is active this simply means that the target has been moved. Since the
                        // active-target-tracking is enabled in this scope, we can allow the projectile to be adjusted
                        // so that it "homes" on the target.
                        set vC      = dat.tarVec
                        set x       = Atan2(vC.y-vB.y, vC.x-vB.x)
                        set y       = SquareRoot((vC.x-vB.x)*(vC.x-vB.x) + (vC.y-vB.y)*(vC.y-vB.y))
                        set z       = SquareRoot((vA.x*vA.x + vA.y*vA.y))
                        set z2      = vC.z-vB.z
                        
                        set vA.x         = z * Cos(x)
                        set vA.y         = z * Sin(x)
                        set vA.z         = z * Tan(Atan2(z2, y))
                        set dat.accVec.z = 0
                    endif
                endif
                // ----
                // If the projectile's life-span expires then the system will automatically destroy the projectile based
                // on whether or not the expire constant is on/off (can be changed per projectile).
                //
                if (dat.active) then    
                    set dat.priv_timeleft = dat.priv_timeleft - .priv_stackLoopRef
                    if (dat.priv_timeleft <= 0.00) and not (dat.priv_finish) then
                        call dat.onFinish()
                        set dat.priv_finish = false
                        if (dat.toDestroy) then
                            call dat.destroy()
                        endif
                    endif
                endif
                
            endif
            set i = i - 1
        endloop
        set u = null
    endmethod
//*
//*
    static method create takes unit u returns thistype
        local thistype dat  = thistype.allocate()
        local real x        = GetUnitX(u)
        local real y        = GetUnitY(u)
        
        call UnitAddAbility(u, FLY_ABIL_ID)
        call UnitRemoveAbility(u, FLY_ABIL_ID)
        if (.priv_stackDex == 0) then
            call TimerStart(.priv_stackLoop, .priv_stackLoopRef, true, function thistype.doLoop)
        endif
        call MoveLocation(loc, x, y)
        
        set dat.toUnit                  = u
        set dat.priv_index              = .priv_stackDex
        set dat.posVec                  = s_vector3d.createEx(x, y, GetLocationZ(loc)+GetUnitFlyHeight(u))
        set dat.tarVec                  = s_vector3d.createEx(0, 0, 0)
        set dat.velVec                  = s_vector3d.createEx(0, 0, 0)
        set dat.accVec                  = s_vector3d.createEx(0, 0, 0)
        set .priv_stack[.priv_stackDex] = dat
        set .priv_stackDex              = .priv_stackDex + 1
        return dat
    endmethod
//*
//*
//*********************************************************************************************************
endstruct

endlibrary

I'll include a test-map too where you can play around with it. It includes the vectors library that is required as well as an example of a struct that extends the projectile.
 

Attachments

  • Projectiles.w3x
    50 KB · Views: 55
Last edited:
Nice job, I'll view the test map later. I like the code. I'll read it more in-depth later too. =P

All I have to mention at the moment is that you use SquareRoot() a lot. It is a slow math function, so try to replace it a bit if you can, ex:
JASS:
        if (SquareRoot((xA-xB)*(xA-xB) + (yA-yB)*(yA-yB) + (zA-zB)*(zA-zB)) <= projRef.collision) then
            call projRef.onUnitCollision(filt)
        endif

Can be:
JASS:
        if (xA-xB)*(xA-xB)+(yA-yB)*(yA-yB)+(zA-zB)*(zA-zB) <= projRef.collision*projRef.collision then
            call projRef.onUnitCollision(filt)
        endif

However, I know sometimes it might be necessary. Those times you can just use it normally.

Otherwise, nice job. =D Perhaps you could post the vectors library as well.
 
Level 18
Joined
Jan 21, 2006
Messages
2,552
I've updated it with your suggestion, though looking through all of the SquareRoot() calls found that this instance is the only occurring instance where SquareRoot() is not needed. In most situations the actual distance is necessary for trig functions.

** I just realized that I accidentally put this in the wrong section.
 
Last edited:
Level 18
Joined
Jan 21, 2006
Messages
2,552
The unit-collision is kind of a double-edged sword, because while its really nice --enumerating over dead bodies is a real frame-rate dropper it seems. It causes more lag than lots and lots of projectiles without the unit collision.

I was going to include projectile collision in this, but I don't have the time to learn the theory behind it well enough to use it in code.
 
Status
Not open for further replies.
Top