• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Projectiles Library (What can I do to optimize?)

Status
Not open for further replies.
Level 18
Joined
Jan 21, 2006
Messages
2,552
This is the code for a projectiles library that I've been working on. If anybody could go through the code and tell me if I'm doing anything seriously wrong, or where I could improve things, I would appreciate the feedback.

JASS:
library Projectiles requires Vectors optional TimerUtils
globals
//**********************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//* Many default constants.
//*
//*
    private constant integer        FLY_ABIL_ID                 = 'Amrf'    //* This is medihv's crow-form ability.
//*
    public constant real            COLLISION_DEFAULT           = 50        //* This is the range at which units/projectiles will "collide".
    public constant real            COLLISION_MAX               = 180       //* This is the maximum value for projectile collision. If 'COLLISION_DEFAULT'
                                                                            //* is higher then this it will not over-ride it.
//*
    public constant boolean         EXPIRATION_DEFAULT          = true      //* The projectile's tendency to die once it has "finished". Setting this to false
                                                                            //* should only be done if you are managing the projectiles yourself.
//*
    private constant boolean        hitDead                     = false     //* Projectiles will "strike" dead units.
    private constant boolean        hitEnemy                    = true      //* Projectiles will "strike" enemy units.
    private constant boolean        hitAllied                   = false     //* Projectiles will "strike" allied units.
    private constant boolean        hitStructure                = false     //* Projectiles will "strike" structures.
//*
//*
//*****************************************
endglobals

interface projectileinterface
//*************
//* Projectile Interface
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* The following functions can be re-declared in child-structs and gain their logical functionality. For example, declaring
//* 'onUnitCollision' will allow you to customize the actions executed when the projectile comes into contact with a unit.
//* 
//*
    method onLoop takes nothing returns nothing defaults nothing                        //* Each iteration that updates the projectile.
    method onFinish takes nothing returns nothing defaults nothing                      //* Once the projectile reaches the end of its path (unless otherwise specified).
    method onBirth takes nothing returns nothing defaults nothing                       //* When the projectile starts its projection path.
    method onUnitCollision takes unit u returns nothing defaults nothing                //* When the projectile collides with a unit (u = collide with -unit-).
    method onCollision takes projectileinterface p returns nothing defaults nothing     //* When the projectile collides with another projectile (p = other projectile).
    method onGround takes nothing returns nothing defaults nothing                      //* Projectile has struck the ground.
//*
//*
endinterface



struct projectile extends projectileinterface
//***************************
//* Projectiles 
//* ¯¯¯¯¯¯¯¯¯¯¯
//* Data-structure that supports the creation/launch (as well as other functions) of in-game projectiles. The projectiles
//* are meant to mimic the basic ones as close as possible.
//*
//*

    // Primary handle-base for the projectile.
    readonly unit           toUnit          = null              //* This unit is "projected" as if it were a projectile.
    
    readonly real           speed          
    readonly real           arc 
    real                    collision       = COLLISION_DEFAULT 
    real                    timescale       = 1.00              //* Time-scale is at 100% by default.
    
    // Active feature data
    boolean                 activeUnitCollision     = false     //* The projectile can collide with units.
    boolean                 activeCollision         = false     //* The projectile can collide with other projectiles.
    
    private static location p_sampleLoc     = Location(0, 0)    //* Location used often for 'MoveLocatin' and 'GetLocationZ'
    private static group    p_sampleGroup   = CreateGroup()     //* Group used to enumerate nearby units.
    private static thistype p_sampleProj                        //* Used to store 'this' when doing a group enum.
    
    
    // These vectors store information including projectile motion.
    private s_vector3d  p_currentPos
    private s_vector3d  p_currentVelocity
    private s_vector3d  p_currentAcceleration
    private s_vector3d  p_currentTargetPos                      //* When the target-position vector is changed during flight, it will activate the projectile's 
                                                                //* "homing" capabilities, which will eliminate 'arc' and move directly towards the target.
    private real        p_timeRemaining
    
    // --------
    // The following method operators allow the user more access to the projectile's vectors, without being able to
    // do anything "illegal".
    //
    // Returns a COPY of the position vector.
    method operator position takes nothing returns s_vector3d
        return .p_currentPos.copy()
    endmethod
    // Returns a COPY of the velocity vector.
    method operator velocity takes nothing returns s_vector3d
        return .p_currentVelocity.copy()
    endmethod
    // Returns a COPY of the acceleration vector.
    method operator acceleration takes nothing returns s_vector3d
        return .p_currentAcceleration.copy()
    endmethod
    // Returns a COPY of the target-position vector.
    method operator targetposition takes nothing returns s_vector3d
        return .p_currentTargetPos.copy()
    endmethod
    //
    
    
    // Destroy removes projectile from the stack, but it does not kill the unit.
    method onDestroy takes nothing returns nothing
        call RemoveSavedInteger(thistype.p_table, GetHandleId(.p_loop), 1)
        call PauseTimer(.p_loop)
        call DestroyTimer(.p_loop)
        // Don't forget to destroy the vectors!
        call .p_currentPos.destroy()
        call .p_currentTargetPos.destroy()
        call .p_currentVelocity.destroy()
        call .p_currentAcceleration.destroy()
    endmethod
    // We don't want the projectile struct to leak so we need to remove it once its trajectory has completed.
    method onFinish takes nothing returns nothing
        call this.destroy()
    endmethod
    

    private static hashtable            p_table         = InitHashtable()
    
    // In order to update the stack, we must use a timer. If the 'TimerUtils' library is available then it will use 'NewTimer' instead of 'CreateTimer'.
    private static constant real        p_loopPeriod    = 0.03  //* Rate that the timer will update the stack. Increasing it will decrease the performance.
    static if (LIBRARY_TimerUtils) then
        private timer p_loop = NewTimer()                //* Uses TimerUtils. The timer is never destroyed so its not that important, I just wanted to
    else                                                        //* use the 'optional' keyword and try out static if statements.
        private timer p_loop = CreateTimer()             //* This does not.
    endif
    
        
    boolean         toDestroy           = EXPIRATION_DEFAULT    //* If this is 'false' then the projectile will not run .onFinish() and its remaining time
                                                                //* will not decrease.
    boolean         hitDead             = hitDead               //* Dead units are valid targets.
    boolean         hitEnemy            = hitEnemy              //* Enemy units are valid targets.
    boolean         hitAllied           = hitAllied
    boolean         hitStructure        = hitStructure

                                                                
    static method onLoopUnitEnum takes nothing returns boolean
        local thistype this = .p_sampleProj
        local unit filt = GetFilterUnit()
        local integer c = 0
        local real x
        local real y
        local real z
        
        set x = GetUnitX(filt)-this.p_currentPos.x
        set y = GetUnitY(filt)-this.p_currentPos.y
        call MoveLocation(.p_sampleLoc, x, y)
        set z = GetLocationZ(.p_sampleLoc)+GetUnitFlyHeight(filt) - this.p_currentPos.z
        
        if not (IsUnitInRange(this.toUnit, filt, this.collision) and (SquareRoot(x*x + y*y + z*z) <= this.collision))  then
            return false
        endif
        // .onUnitCollision is only called if a valid target is struck.
        if (hitDead == (IsUnitType(filt, UNIT_TYPE_DEAD) and GetUnitTypeId(filt)==0)) then
            set c=c+1
        endif
        if (hitEnemy == IsUnitEnemy(filt, GetOwningPlayer(this.toUnit))) then
            set c=c+1
        endif
        if (hitAllied == IsUnitAlly(filt, GetOwningPlayer(this.toUnit))) then
            set c=c+1
        endif
        if (hitStructure == IsUnitType(filt, UNIT_TYPE_STRUCTURE)) then
            set c=c+1
        endif
        if (c==4) then
            call this.onUnitCollision(filt)
        endif
        return false
    endmethod
    // Updates stack .p_stack on a periodic timer.
    static method onLoopStatic takes nothing returns nothing
        local integer j
        local thistype this
        local s_vector3d vec
    
        set this = LoadInteger(.p_table, GetHandleId(GetExpiredTimer()), 1)
    
        // Scale the added acceleration based on the time-scale of the projectile.
        set this.p_currentVelocity.x = this.p_currentVelocity.x+(this.p_currentAcceleration.x*this.timescale*this.timescale)
        set this.p_currentVelocity.y = this.p_currentVelocity.y+(this.p_currentAcceleration.y*this.timescale*this.timescale)
        set this.p_currentVelocity.z = this.p_currentVelocity.z+(this.p_currentAcceleration.z*this.timescale*this.timescale)
        
        // Scale the added velocity (to position) based on time-scale.
        set this.p_currentPos.x = this.p_currentPos.x+(this.p_currentVelocity.x*this.timescale)
        set this.p_currentPos.y = this.p_currentPos.y+(this.p_currentVelocity.y*this.timescale)
        set this.p_currentPos.z = this.p_currentPos.z+(this.p_currentVelocity.z*this.timescale)

        // Update the projectile.
        call SetUnitX(this.toUnit, this.p_currentPos.x)
        call SetUnitY(this.toUnit, this.p_currentPos.y)
        call MoveLocation(.p_sampleLoc, this.p_currentPos.x, this.p_currentPos.y)
        call SetUnitFlyHeight(this.toUnit, this.p_currentPos.z-GetLocationZ(.p_sampleLoc), 0)
        
        // .onLoop should be executed after the projectile has been updated but before any of the other user methods.
        call this.onLoop()        
        
        // Since 'doSync' was just called, .p_sampeLoc will still have the correct coordinates.
        if (this.p_currentPos.z <= GetLocationZ(.p_sampleLoc)) then
            // Indicates that the projectile has struck the ground.
            call this.onGround()                       
        endif
        // Projectiles will only collide with projectiles if .activeCollision is true (active).
        if (this.activeCollision) then
            //set j = 0
            //loop
            //    exitwhen (j == .p_stackTrack)
            //    if not (.p_stack[j] == this) then
            //        set vec = .p_stack[j].p_currentPos.copy()
            //        call vec.sub(this.p_currentPos)
            //        if (vec.magnitude() < this.collision) then
            //            // Indicates that a projectile has come within collision-range of another projectile.
            //            call this.onCollision(.p_stack[j])
            //        endif
            //        call vec.destroy()
            //    endif
            //    set j = j + 1
            //endloop
        endif
        if (this.toDestroy) then
            set this.p_timeRemaining = this.p_timeRemaining - .p_loopPeriod
            if (this.p_timeRemaining <= 0.00) then
                // Indicates the projectile's trajectory is complete.
                call this.onFinish()             
            endif
        endif
        // Projectiles will only collide with units if .activeUnitCollision is true.
        if (this.activeUnitCollision) then
            // Enumerate nearby units so that they can be "detected". Store 'this' in global variable for referencing in enum response.
            set .p_sampleProj = this
            call GroupEnumUnitsInRange(.p_sampleGroup, this.p_currentPos.x, this.p_currentPos.y, COLLISION_MAX, Filter(function thistype.onLoopUnitEnum))
        endif
    endmethod
    
    
    // Launches the projectile with provided input. If the projectile cannot be created with specified input then this
    // method will return false.
    method doLaunch takes s_vector3d start, s_vector3d target, real speed, real pitchArc returns boolean
        local real d
        local real a
        if (start==0) or (target==0) then
            return false
        endif
        // Initialize vectors and specific info.
        set .p_currentPos = start.copy()
        set .p_currentTargetPos = target.copy()
        set .speed = speed                                  //* Speed/arc are never used internally, but they are there for user reference in any
        set .arc = pitchArc                                 //* case.

        call SaveInteger(thistype.p_table, GetHandleId(.p_loop), 1, this)
        call TimerStart(.p_loop, thistype.p_loopPeriod, true, function thistype.onLoopStatic)
        
        set d = SquareRoot((start.x-target.x)*(start.x-target.x) + (start.y-target.y)*(start.y-target.y))       //* This is a 2D distance.
        set a = Atan2(target.y-start.y, target.x-start.x)
        
        if (d==0.0) or (speed==0) then
            set .p_currentPos.x = .p_currentTargetPos.x
            set .p_currentPos.y = .p_currentTargetPos.y
            set .p_currentPos.z = .p_currentTargetPos.z
            set .p_timeRemaining = 0.0
            // This indicates the projectile will be instant.
            set .p_currentAcceleration = s_vector3d.createEx(0, 0, 0)
            set .p_currentVelocity = s_vector3d.createEx(0, 0, 0)
            // The projectile should be destroyed immediately (ignoring the inaccuracy of the timer).
        else
            set .p_timeRemaining = d/speed
            set .p_currentAcceleration = s_vector3d.createEx(0, 0, -8*pitchArc*speed*speed/d)
            set .p_currentVelocity = s_vector3d.createEx(speed*Cos(a), speed*Sin(a), -.p_currentAcceleration.z * (d/speed)/2 + (target.z-start.z)/(d/speed))
        endif

        // Scale projectile vectors
        set .p_currentAcceleration.x=.p_currentAcceleration.x*thistype.p_loopPeriod*thistype.p_loopPeriod
        set .p_currentAcceleration.y=.p_currentAcceleration.y*thistype.p_loopPeriod*thistype.p_loopPeriod
        set .p_currentAcceleration.z=.p_currentAcceleration.z*thistype.p_loopPeriod*thistype.p_loopPeriod
        set .p_currentVelocity.x=.p_currentVelocity.x*thistype.p_loopPeriod
        set .p_currentVelocity.y=.p_currentVelocity.y*thistype.p_loopPeriod
        set .p_currentVelocity.z=.p_currentVelocity.z*thistype.p_loopPeriod
     
        // Sync projectile and call .onBirth user-method. If a projectile is "instant" it will not be destroyed until the first
        // timer-loop. This is a maximum inaccuracy of the timer's loop timeout.
        call SetUnitX(.toUnit, .p_currentPos.x)
        call SetUnitY(.toUnit, .p_currentPos.y)
        call MoveLocation(thistype.p_sampleLoc, .p_currentPos.x, .p_currentPos.y)
        call SetUnitFlyHeight(.toUnit, .p_currentPos.z-GetLocationZ(thistype.p_sampleLoc), 0)
        
        call .onBirth()
        return true
    endmethod
    
    
    // Creates a projectile based on a unit.
    static method create takes unit u returns thistype
        local thistype proj = thistype.allocate()
        set proj.toUnit = u
        call UnitAddAbility(u, FLY_ABIL_ID)
        call UnitRemoveAbility(u, FLY_ABIL_ID)
        return proj
    endmethod
    
endstruct
endlibrary

Here's the code to the ranged-projectiles structure that extends more to the user-end.

JASS:
library Ranged requires Projectiles
globals
//*****************
//* Configuration
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯
//*
//*
//* The unit used to create ranged projectiles. The dummy model I am using is Vexorian's pitch animation model, so I can 
//* simulate projectile "tilt".
    public constant integer         DUMMY_ID            = 'dumy'
//*
//*
//****************************
endglobals

struct rangedprojectile extends projectile
//********************
//* Ranged Projectile
//* ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
//* A structure with methods that allows the user to easily create missiles that look/act exactly like WarCraft III missiles.
//*
//*
    readonly unit       source          = null                          //* Source from which the projectile came.
    readonly effect     model           = null                          //* The effect that represents the projectile's "model".
    readonly string     modelpath
    
    private static location     p_sampleLoc         = Location(0, 0)    //* Used to optimize GetLocationZ
    
    method onDestroy takes nothing returns nothing
        call KillUnit(.toUnit)
        if (.model!=null) then
            call DestroyEffect(.model)
        endif
    endmethod
    
    // Each time the projectile is updated on the timer-loop.
    method onLoop takes nothing returns nothing
        local s_vector3d vec = .velocity
        call SetUnitFacingTimed(.toUnit, Atan2(vec.y, vec.x)*bj_RADTODEG, 0)
        call SetUnitAnimationByIndex(.toUnit, R2I(bj_RADTODEG*Atan2(vec.z, SquareRoot(vec.x*vec.x + vec.y*vec.y)+0.5)+90))
        // Since we are only given a copy of the velocity, we must destroy it.
        call vec.destroy()
    endmethod
    
    // Easy-to-use constructor method.
    static method create takes unit source, real heightoff, real targx, real targy, real speed, real pitchArc, string modelpath returns thistype
        local thistype r
        local unit u
        local real x
        local real y
        local real theta
        local s_vector3d vec
        local s_vector3d vecb
        
        set x = GetUnitX(source)
        set y = GetUnitY(source)
        set theta = Atan2(targy-y, targx-x)
        set u = CreateUnit(GetOwningPlayer(source), DUMMY_ID, x, y, theta*bj_RADTODEG)
        set r = thistype.allocate(u)
        set r.source = source
        set r.modelpath = modelpath
        set r.model = AddSpecialEffectTarget(modelpath, u, "origin")
        
        call MoveLocation(thistype.p_sampleLoc, x, y)
        set vec = s_vector3d.createEx(x, y, GetLocationZ(thistype.p_sampleLoc)+GetUnitFlyHeight(source)+heightoff)
        call MoveLocation(thistype.p_sampleLoc, targx, targy)
        set vecb = s_vector3d.createEx(targx, targy, GetLocationZ(thistype.p_sampleLoc))
        call r.doLaunch(vec, vecb, speed, pitchArc)
        call vec.destroy()
        call vecb.destroy()
        
        return r
    endmethod
endstruct

endlibrary

Now, here is the problem. When I have "projectile collision" and "unit collision" factored out, this system can handle quite a few projectiles at once. Once I put those two components into the equation, it will only support a handful. When I spammed the creation of these it actually ended up lagging ridiculously, and then telling me that it was unable to allocate an ID for my vectors (indicating that I've either leaked vectors, which I don't think I have, or something else).
 
Last edited:
Level 18
Joined
Jan 21, 2006
Messages
2,552
Bribe said:
and ".allocate()"?

What do you mean? This method is always there.

JASS:
s_vector3d extends vectorinterface
s_vector2d extends vectorinterface

These are included in the Vectors library.

This is still unfinished though, since distance and speed should both be able to have an input of 0 (making them "instant" projectiles). Also, .p_currentTargetPos will be able to be updated (activating "homing").

I updated the code so it now handles "instant" projectiles. A projectile is "instant" if its travel-distance is 0, or its speed is set to 0.

I updated the code again with my Ranged library and an updated version of projectiles.

I updated it again with a small amount of optimization. I just removed a few function calls that were not necessary.

I copied the library and re-wrote a few components so that it uses a hashtable instead of a projectile stack. It seems that this improved the performance a little, but I have not bench marked anything. I should really find out how to bench mark stuff.

Okay I've updated the code once again. I have two versions, one that uses hashtables (I was thinking about perhaps making Tables an optional library) and another that uses a stack. I've also added some configuration boolean variables to the struct so that the user can choose what features he/she wants for each projectile.

The values controlling this are:
JASS:
boolean activeUnitCollision
boolean activeCollision

This makes it easier for the user to be diverse, and allows optimal performance on projectiles that don't need the extra garbage. If there is just a single unit creating these projectiles on right-click ("smart" order), then the frame-rate does not drop at all.

In order to really test the system, I had a few extra units tossing projectiles across the map. At about 100 projectiles, the frame-rate is still completely intact, but around 130 I would say it starts getting noticeable (could be the amount of visible units, too).

Both the hashtables and the stack seem to perform at relatively similar levels, I'm thinking that the hashtables may be a little bit quicker though.

I have not yet implemented a good method of unit/projectile collision or projectile/projectile collision, but currently I am using GroupEnumUnitsInRange for units. My section that includes the available targets is also very much in progress.

JASS:
boolean hitDead
boolean hitAllied
boolean hitEnemy
boolean hitStructure

I would really appreciate it if someone could help me out in improving this thing.
 
Last edited:
This is outstanding.

I just have one quick question -

How does the game engine know to recognize things like "s_vector3d" and ".allocate()"?

.allocate() is included with vJASS and requires jass helper in order to be compiled...

cannot give suggestions coz right now I'm still not familiar with using extends...

PS: maybe you should merge the two posts above before anyone bugs you about double posting... ^^...
 
Level 12
Joined
May 21, 2009
Messages
994
Ehm don't hang me upon this, but shouldn't those methods in the interface be "stub" methods? Well I don't know if they need to, but I just heard something about it. By the way this looks very similiar to Anachron's and I's Missile System. But our system is coded in a different way.

Good job & luck.
 
Level 18
Joined
Jan 21, 2006
Messages
2,552
You can do it either way. Stub methods and interfaces have similar functionality, kind of like how delegate and extends have "similar" functionality.

I could remove the interface, and use stub methods, but I want the interface to be visible to the reader at the top, so he knows what he can/cannot do.
 
Level 12
Joined
May 21, 2009
Messages
994
You can do it either way. Stub methods and interfaces have similar functionality, kind of like how delegate and extends have "similar" functionality.

I could remove the interface, and use stub methods, but I want the interface to be visible to the reader at the top, so he knows what he can/cannot do.

Thats a pretty good decision as well I believe. I don't know that much about speed so I don't know if the one is faster than the other.
 
Level 14
Joined
Jun 3, 2005
Messages
209
I am going to bet that commenting out the lines:
JASS:
call SetUnitFacingTimed(.toUnit, Atan2(vec.y, vec.x)*bj_RADTODEG, 0)
call SetUnitAnimationByIndex(.toUnit, R2I(bj_RADTODEG*Atan2(vec.z, SquareRoot(vec.x*vec.x + vec.y*vec.y)+0.5)+90))
will improve speed a lot. Squareroots are one of the worst functions in terms of performance, and this line is using it probably over 30 times per second per projectile just for an animation.

And look, there it is again!
JASS:
SquareRoot(x*x + y*y + z*z) <= this.collision
We can get rid of easily like this:
JASS:
x*x + y*y + z*z <= this.collision*this.collision
...which is exactly the same check but without the square root.

Why are we checking both
JASS:
not (IsUnitInRange(this.toUnit, filt, this.collision)
...and...
JASS:
(SquareRoot(x*x + y*y + z*z) <= this.collision)
...? The first one is just a 2D version of the second, and it's not so quick as to be worth trying.

Now this next bit is very inefficent; it should only call onCollision if all four conditions are true:
JASS:
        // .onUnitCollision is only called if a valid target is struck.
        if (hitDead == (IsUnitType(filt, UNIT_TYPE_DEAD) and GetUnitTypeId(filt)==0)) then
            set c=c+1
        endif
        if (hitEnemy == IsUnitEnemy(filt, GetOwningPlayer(this.toUnit))) then
            set c=c+1
        endif
        if (hitAllied == IsUnitAlly(filt, GetOwningPlayer(this.toUnit))) then
            set c=c+1
        endif
        if (hitStructure == IsUnitType(filt, UNIT_TYPE_STRUCTURE)) then
            set c=c+1
        endif
        if (c==4) then
            call this.onUnitCollision(filt)
        endif
        return false
...so just use:
JASS:
if (a) and (b) and (c) and (d) then
    this.onUnitCollision(filt)
endif
The advantage of this is that if a fails, the game won't bother checking b, c, or d; saving us time. For this reason, put the most likely ones to fail first (the order you have there looks fine).

GroupEnumUnitsInRange should be okay for the collision checks, I don't know any better method of doing it if you have a lot of units. Try making these changes and let me know how it goes.
 
Status
Not open for further replies.
Top