• 🏆 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!

[System] Missile

Level 6
Joined
Jun 20, 2011
Messages
249
JASS:
library Missile /* v2.0.1

*/uses/* 
*/  CTL                         /*  hiveworkshop.com/forums/jass-resources-412/snippet-constant-timer-loop-32-a-201381/
*/  AdvLoc                      /*  hiveworkshop.com/forums/submissions-414/snippet-lacking-loc-209322/
*/  optional WorldBounds        /*  hiveworkshop.com/forums/jass-functions-413/snippet-worldbounds-180494/
*/  optional MissileRecycler    /*  hiveworkshop.com/forums/jass-resources-412/system-missilerecycler-206086/

Credits:
    -   emjlr3 for the deflect angle equation
    -   AceHart for the parabola equation
    
**********************************************************************/
globals
    //***********************************************************************
    //  This dummy must use Vexorian's dummy model and it's movement type
    //  should be "Hover" if you want to correctly move over water, otherwise
    //  use "None". The dummy must also have Crow Form. If you don't have a
    //  dummy unit in your map then use the one MissileRecycler provides.
    private constant integer DUMMY_RAW_CODE = 'e000'
endglobals
/**********************************************************************
*
*   struct Missile
*
*       static method create takes real originX, real originY, real originZ...
*                               ...real angle, real distance, real targetZ returns Missile
*           -   The angle is in radians.
*           -   Missiles have the following values to be set by you.
*               -   real speed
*               -   real turn
*               -   real open / curve
*               -   real height / arc
*               -   real acceleration
*               -   real collision
*               -   unit target
*               -   unit source
*               -   string model
*               -   integer data
*
*       real x
*       real y
*       real z
*           -   The missile's position
*
*       real slide
*           -   The amount of distance it has covered.
*       AdvLoc origin
*           -   The missile's origin Loc
*       AdvLoc impact
*           -   Where the missile will aim to.
*
*       method deflect takes real x, real y returns nothing
*           -   Deflects the missile from the target point, changing
*           -   it's course.
*
*       method bounce takes nothing returns nothing
*           -   Bounces the missile from it's current Z position.
*           -   Useful when assigning targets to missiles that were
*           -   already active, it fixes the rough Z position change.
*
***********************************************************************
*
*   (optional)
*   static method onCollide takes Missile this, unit justHit returns boolean
*       -   Runs every time the missile collides with something.
*       -   If returns true the missile is destroyed.
*   
*   (optional)
*   static method onPeriod takes Missile this returns boolean
*       -   Runs every period.
*       -   If returns true the missile is destroyed.
*
*   (optional)
*   static method onFinish takes Missile this returns boolean
*       -   Runs whenever the missile finishes it's course.
*       -   If returns true the missile is destroyed.
*
*   (optional)
*   static method onMissile takes Missile this, Missile hit, real range returns boolean
*       -   Runs whenever the missile encounters another missile.
*       -   If returns true the missile is destroyed.
*
*   (optional)
*   static method onRemove takes Missile this returns nothing
*       -   Runs whenever the missile is deallocated.
*
*   module MissileStruct
*
*       static method launch takes Missile toLaunch returns nothing
*
***********************************************************************
*
*   DISCLAIMER:
*
*       -   Missile's default owner is Player(15) and this is meant to
*       -   not be modifiable.
*
*       -   For curved missiles the "x" and "y" coordinates are only
*       -   correct in the moment of launch or impact, but during it's
*       -   air time they ignore the curve it has.
*
*       -   This system can support up to 600 proyectiles with complex
*       -   mathematics (curve, arc, collision) before it lags.
*
**********************************************************************/
    
    struct MissileList extends array
        implement LinkedList
    endstruct
    
    struct Missile extends array
    
        //***********************************************************************
        //  Why LinkedList? In order to reach a high level of dynamism i'm basing
        //  this system on iterating through multiple linked lists, each struct
        //  has one and each struct iterates through it.
        implement LinkedList
        
        AdvLoc impact
        AdvLoc origin
        
        real slide
        
        readonly real x
        readonly real y
        readonly real z
        
        private real    cA  //current angle
        
        private effect  fx  //effect
        private string  fP  //model
        
        private real    dS
        
        readonly unit   dummy
        readonly group  unitsHit
        
        boolean recycle
        boolean wantDestroy
        
        unit    target
        unit    source
        real    collision
        real    height
        real    turn
        real    open
        real    damage
        real    speed
        real    acceleration
        integer data
        
        method operator model= takes string path returns nothing
            call DestroyEffect(fx)
            set fP=path
            set fx=AddSpecialEffectTarget(path,dummy,"origin")
        endmethod
        
        method operator model takes nothing returns string
            return fP
        endmethod
        
        method operator curve= takes real value returns nothing
            set open=Tan(value)*origin.distance
        endmethod
        
        method operator curve takes nothing returns real
            return Atan(open/origin.distance)
        endmethod
        
        method operator arc= takes real value returns nothing
            set height=Tan(value)*origin.distance/4
        endmethod
        
        method operator arc takes nothing returns real
            return Atan(4*height/origin.distance)
        endmethod
        
        method operator scale= takes real v returns nothing
            call SetUnitScale(dummy,v,v,v)
            set dS=v
        endmethod
        
        method operator scale takes nothing returns real
            return dS
        endmethod
        
        static method createEx takes unit whichUnit, AdvLoc o, AdvLoc i returns thistype
            local thistype this=allocate()
            
            set source = null
            set target = null
            set acceleration = 0
            set height = 0
            set turn = 0
            set open = 0
            set unitsHit = CreateGroup()
            set collision = 0
            set recycle = false
            set wantDestroy = false
            set fP = ""
            
            set x = o.x
            set y = o.y
            set z = o.z
            set origin = o
            set impact = i
            
            set cA = origin.angle
            set slide=0
            
            set dummy=whichUnit
            call MoveLocation(Loc.global,o.x,o.y)
            call SetUnitFlyHeight(dummy,o.z-GetLocationZ(Loc.global),0)
            
            call MissileList.base.insertNode(this)
            
            return this
        endmethod
        
        static method createLoc takes AdvLoc o, AdvLoc i returns thistype
            call AdvLoc.link(o,i)
            static if LIBRARY_MissileRecycler then
                return createEx(GetRecycledMissile(o.x,o.y,o.z,o.angle*bj_RADTODEG),o,i)
            else
                return createEx(CreateUnit(Player(15),DUMMY_RAW_CODE,o.x,o.y,o.angle*bj_RADTODEG),o,i)
            endif
        endmethod
        
        static method create takes real ox, real oy, real oz, real a, real d, real iz returns thistype
            local AdvLoc o = AdvLoc.create(ox,oy,oz)
            local AdvLoc i = AdvLoc.create(ox+d*Cos(a),oy+d*Sin(a),iz)
            call AdvLoc.link(o,i)
            static if LIBRARY_MissileRecycler then
                return createEx(GetRecycledMissile(o.x,o.y,o.z,o.angle*bj_RADTODEG),o,i)
            else
                return createEx(CreateUnit(Player(15),DUMMY_RAW_CODE,o.x,o.y,o.angle*bj_RADTODEG),o,i)
            endif
        endmethod
        
        method bounce takes nothing returns nothing
            call origin.move(x,y,z)
            set slide=0
        endmethod
        
        method deflect takes real tx, real ty returns nothing
            local real a = 2*Atan2(ty-y,tx-x)+bj_PI-cA
            call impact.move(x+(origin.distance-slide)*Cos(a),y+(origin.distance-slide)*Sin(a),impact.z)
            call this.bounce()
        endmethod
        
        method destroy takes nothing returns nothing
            set wantDestroy=true
        endmethod
        
        method terminate takes nothing returns nothing
            call DestroyEffect(fx)
            call DestroyGroup(unitsHit)
            set recycle=false
            set fx=null
            
            static if LIBRARY_MissileRecycler then
                call RecycleMissile(dummy)
            else
                call RemoveUnit(dummy)
            endif
            
            call impact.unlock()
            call origin.unlock()
            
            call MissileList(this).removeNode()
            
            call this.removeNode()
            call this.deallocate()
        endmethod
        
        method move takes nothing returns nothing
        
            local real a
            local real d
            local real s
            local real h
            local real tx
            local real ty
            local real ox
            local real oy
            local AdvLoc o
            
            loop
                exitwhen head
                set o = origin
                set ox = o.x
                set oy = o.y
                set h = height
                
                if target!=null and GetUnitTypeId(target)!=0 then
                    call impact.move(GetUnitX(target),GetUnitY(target),GetUnitFlyHeight(target))
                    set a = Atan2(impact.y-y,impact.x-x)
                    set slide = origin.distance-SquareRoot((impact.x-x)*(impact.x-x)+(impact.y-y)*(impact.y-y))
                else
                    set a = o.angle
                    set target = null
                endif
                
                if turn!=0 and not(Cos(cA-a)>=Cos(turn)) then
                    if Sin(a-cA)>=0 then
                        set cA = cA+turn
                    else
                        set cA = cA-turn
                    endif
                else
                    set cA = a
                endif
                
                set d = o.distance
                set s = slide+speed
                set slide = s
                call SetUnitFacing(dummy,cA*bj_RADTODEG)
                
                set tx = x+speed*Cos(cA)
                set ty = y+speed*Sin(cA)
                set speed = speed + acceleration
                set x = tx
                set y = ty

                if h!=0 or o.slope!=0 then
                    call MoveLocation(Loc.global,tx,ty)
                    set z = 4*h*s*(d-s)/(d*d)+o.slope*s+o.z
                    call SetUnitFlyHeight(dummy,z-GetLocationZ(Loc.global),0)
                    call SetUnitAnimationByIndex(dummy,R2I(Atan(origin.slope)-Atan((8*h*s-4*d*h)/(d*d))*bj_RADTODEG)+90)
                endif
                
                if open!=0 then
                    set a = 4*open*s*(d-s)/(d*d)
                    set tx = tx+a*Cos(cA+1.57)
                    set ty = ty+a*Sin(cA+1.57)
                    call SetUnitFacing(dummy,(cA+Atan(-(8*open*s-4*d*open)/(d*d)))*bj_RADTODEG)
                endif
                
                static if LIBRARY_WorldBounds then
                    if tx>WorldBounds.maxX or tx<WorldBounds.minX or ty>WorldBounds.maxY or ty<WorldBounds.minY then
                        call destroy()
                    else
                        call SetUnitX(dummy,tx)
                        call SetUnitY(dummy,ty)
                    endif
                else
                    call SetUnitX(dummy,tx)
                    call SetUnitY(dummy,ty)
                endif
                
                
                if s>=d then
                    set recycle = true
                endif
                
                set this = next
                
            endloop
        endmethod
    
    endstruct
    
    globals
        private integer                     ACTIVE  =   0
        private integer                     SIZE    =   0
        private trigger                     FIRE    =   CreateTrigger()
        private integer             array   STACK
        private integer             array   INSTANCES
        private TimerGroup32        array   TIMER
        private Missile             array   NODE
    endglobals
    
    //***********************************************************************
    //  This function runs periodically. Can you see the trigger evaluation
    //  at the end? If you've read T32 then you know exactly what it does.
    //  The loop above is for cleaning up, the SIZE variable keeps track of
    //  how many instances have been deallocated by the user, if higher than
    //  0 then some of them need to be removed. STACK[SIZE] stores the value
    //  of the deallocated instances.
    private function Execute takes nothing returns nothing
        loop
            exitwhen SIZE==0
            set ACTIVE=ACTIVE-1
            set SIZE=SIZE-1
            set INSTANCES[STACK[SIZE]]=INSTANCES[STACK[SIZE]]-1
            if INSTANCES[STACK[SIZE]]==0 then
                call TIMER[STACK[SIZE]].stop()
                if ACTIVE==0 then
                    return
                endif
            endif
        endloop
        call TriggerEvaluate(FIRE)
    endfunction
    
    //***********************************************************************
    //  Adds a new instance to the given struct index (This system attaches
    //  indexes to every struct you implement MissileStruct to) If the amount
    //  of INSTANCES[index] was 0 then it adds the struct's iterate method to
    //  the FIRE trigger for it's evaluation. ACTIVE keeps track of all
    //  allocated instances, if it was 0 that means the timer isn't even
    //  running yet, it needs to be started.
    private function StartPeriodic takes integer index returns nothing
        if INSTANCES[index]==0 then
            call TIMER[index].start()
        endif
        set ACTIVE=ACTIVE+1
        set INSTANCES[index]=INSTANCES[index]+1
    endfunction
    
    //***********************************************************************
    //  Adds the struct's index to the stack to clear it in the Execute
    //  function above.
    private function StopPeriodic takes integer index returns nothing
        set STACK[SIZE]=index
        set SIZE=SIZE+1
    endfunction
    
    module MissileStruct
        
        private static method missileTerminate takes Missile this returns nothing
            static if thistype.onRemove.exists then
                call onRemove(this)
            endif
            call this.terminate()
            call StopPeriodic(thistype.typeid)
        endmethod
        
        static method unpin takes Missile this returns nothing
            call this.removeNode()
            call StopPeriodic(thistype.typeid)
        endmethod

        static if thistype.onMissile.exists then
            private static method onMissileLoop takes Missile this returns nothing
                local Missile node = MissileList.base
                local real ox = this.x
                local real oy = this.y
                loop
                    set node = node.next
                    exitwhen MissileList(node).head
                    loop
                        exitwhen node.head
                        if node!=this and onMissile(this,node,SquareRoot((ox-node.x)*(ox-node.x)+(oy-node.y)*(oy-node.y))) then
                            call missileTerminate(this)
                        endif
                        set node = MissileList(node).next
                    endloop
                endloop
            endmethod
        endif

        //***********************************************************************
        //  First it takes the struct's first instance, which is the next to the
        //  head (NODE[thistype.typeid].next) and starts iterating through it until it hits
        //  the head again. Then checks if the missile is marked for recycling,
        //  if so it runs the methods and destroys the missile.
        private static method missileIterate takes nothing returns nothing
            local unit u
            local Missile this = NODE[thistype.typeid].next
            call this.move()
            loop
                exitwhen this.head
                
                if this.wantDestroy then
                    static if thistype.onFinish.exists then
                        call thistype.onFinish(this)
                    endif
                    call missileTerminate(this)
                else
                    if this.recycle then
                        static if thistype.onCollide.exists then
                            static if thistype.onFinish.exists then
                                if this.target==null then
                                    if thistype.onFinish(this) then
                                        call missileTerminate(this)
                                    else
                                        set this.recycle = false
                                    endif
                                elseif thistype.onCollide(this,this.target) then
                                    call missileTerminate(this)
                                else
                                    set this.recycle = false
                                endif
                            else 
                                if this.target==null then
                                    call missileTerminate(this)
                                elseif thistype.onCollide(this,this.target) then
                                    call missileTerminate(this)
                                else
                                    set this.recycle = false
                                endif
                            endif
                        else
                            static if thistype.onFinish.exists then
                                if thistype.onFinish(this) then
                                    call missileTerminate(this)
                                else
                                    set this.recycle = false
                                endif
                            else
                                call missileTerminate(this)
                            endif
                        endif
                    else
                        static if thistype.onCollide.exists then
                            if this.collision!=0 then
                                call GroupEnumUnitsInRange(bj_lastCreatedGroup,this.x,this.y,this.collision,null)
                                loop
                                    set u = FirstOfGroup(bj_lastCreatedGroup)
                                    exitwhen u==null
                                    if not(IsUnitInGroup(u,this.unitsHit)) and u!=this.target and thistype.onCollide(this,u) then
                                        call missileTerminate(this)
                                    endif
                                    call GroupAddUnit(this.unitsHit,u)
                                    call GroupRemoveUnit(bj_lastCreatedGroup,u)
                                endloop
                            endif
                        endif
                    endif
                endif
                
                static if thistype.onMissile.exists then
                    call onMissileLoop(this)
                endif
                
                static if thistype.onPeriod.exists then
                    if thistype.onPeriod(this) then
                        call missileTerminate(this)
                    endif
                endif
                
                set this = this.next
            endloop
            
            set u=null
        endmethod
        
        static method launch takes Missile this returns nothing
            call StartPeriodic(thistype.typeid)
            call NODE[thistype.typeid].insertNode(this)
        endmethod
        
        private static method onInit takes nothing returns nothing
            set NODE[thistype.typeid] = Missile.createNode()
            set TIMER[thistype.typeid] = TimerGroup32.create(function thistype.missileIterate)
        endmethod
        
    endmodule
    
endlibrary
JASS:
struct TESTER extends array

    static integer COUNT = 0
    static unit FOOTMAN = null
    static timer t = CreateTimer()
    static boolean b = false
        
    implement MissileStruct
    
    static method onExpire takes nothing returns nothing
        local real x = GetUnitX(FOOTMAN)
        local real y = GetUnitY(FOOTMAN)
        local real a = GetRandomReal(-bj_PI,bj_PI)
        local Missile new = Missile.create(x,y,65,x+1000*Cos(a),y+1000*Sin(a),0)
        set new.speed=10
        set new.model="abilities\\weapons\\WyvernSpear\\WyvernSpearMissile.mdl"
        set new.height=100
        set new.source=FOOTMAN
        set new.collision=80
        call launch(new)
    endmethod
    
    static method onEsc takes nothing returns nothing
        if b then
            call PauseTimer(t)
        else
            call TimerStart(t,0.005,true,function thistype.onExpire)
        endif
        set b = not b
    endmethod
    
    static method onInit takes nothing returns nothing
        local trigger trig=CreateTrigger()
        call TriggerRegisterPlayerEvent(trig,Player(0),EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddAction(trig,function thistype.onEsc)
        set FOOTMAN=CreateUnit(Player(0),'hfoo',0,0,270)
    endmethod
    
endstruct
Upcoming features:
-onDestructable method
-Improve onMissile's efficiency
-Math improvements
 
Last edited by a moderator:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
A lot of variables should be readonly instead of public. The source unit can remain public, for example, but most should be readonly, especially the dummy unit.

GetWidgetLife(this.target)>=0.405 should have the ">=" changed to ">" but even then it needs to be replaced entirely with IsUnitType, UNIT_TYPE_DEAD. The GetUnitTypeId check should remain of course.

bj_PI/2 should be stored into a constant to avoid computing it dynamically.

While I like that you made it a module much of it could be centralized (as much of it is uniform and isn't module-specific). This would keep the code way way shorter.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
The unit should be readonly, for example if you wanted to add an additional special effect to the missile. You could also centralize the OnInit method and make it take "code" as an argument, meaning you could also just return nothing from that module and make it slightly shorter than it already is.

I really like this resource and I'm planning to use it for my TimeWarp spell when I update it soon.
 
Level 6
Joined
Jun 20, 2011
Messages
249
Updated to v1.0.5.
-Some minor changes that increase the FPS by 4
-Reduced the amount of code in the module
-Fixed an issue in which missiles that lost their targets because it died cause the missile to never end
 
Level 7
Joined
Oct 11, 2008
Messages
304
-Fixed an issue in which missiles that lost their targets because it died cause the missile to never end

Thank you, serious :D
I show'd you that thing in your demo and you fix.

I'm planning use this, actually is better then other projectiles system (IMO).
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Dirac, if LinkedListModule recycled using the "prev" variable, you could iterate forwards without causing a disruption when nodes get removed (eliminating need for the "safe" variable). I did that method in that one demo I showed you. This would improve performance by a tiny bit but it makes LinkedListModule a lot easier to work with at the same time.

For other improvements I would look for ways to save repeat-referencing an array variable. For example, this.distance should be stored into a local. A local declaration is about the same speed as an array lookup. So even if you save just 1 array lookup you are saving a lot of performance. Which really matters with projectile systems.
 
Level 6
Joined
Jun 20, 2011
Messages
249
You're right in both suggestions.
For the first time i get what you meant with the "prev" recycling.
Updated to 1.0.6 to apply both of your suggestions, LinkedListModule will be updated too
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
The .move method in the Missile struct does affect performance because the locals need to be created each time. What I advise is to turn that into a static method, iterate all instance through it (for example make it a simple function call by the "Execute" private function) and let each struct iterate themselves as well. This way the locals only get created once per 0.03125 instead of once per instance per 0.03125.

I still see some areas that you could chomp out repeat-array references using locals, for example this.target and this.x/y are used a lot.

Also adding some double-free protection could be useful to the destroy method (and don't make it debug-only, performance doesn't matter so much here).

"model" should just be a readonly variable instead of using both the operator and "fP".

The following variables you should just set from the "destroy" method, instead of the "create" method, because arrays are already initialized to those values by default. Nulling handles at the end of the destroy method will also keep the handle ID stack as low as possible so they aren't consumed by recycled instances.

JASS:
            set source=null
            set target=null
            set acceleration=0
            set height=0
            set turn=0
            set open=0
            set collides=false
            set recycle=false
            set fP=""
            
            set sM=0
            set sX=0
            set sY=0
            set slide=0
 
Last edited:
Level 6
Joined
Jun 20, 2011
Messages
249
Ok there are two options now:
-Revert the module to the way it was before (more text, all the "move" method inlined in the module) to gain that bonus speed with local variables. There is no way to call an "onImpact" or "onPeriod" method out of an stack that isn't inside the module.
-Leave it like it is now
Also in the next version i'll fix an issue that caused missiles that chase targets with flying height to have wrong Z values.

EDIT: You're not going to believe this, but moving the whole "nulling" process to the destroy method causes the fps to drop BY A HUGE AMOUNT
Nulling at allocate = 17 fps with 700 projs
Nulling at deallocate = crashes with 700 projs

EDIT2: Just tested by adding the move method to the module and it gained 7 fps (24)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Dirac you missed what I said. I think you understand programming better than you understand my explainations. Which is fine because my mind works quite similar.

Instead of this:

JASS:
                    if this.move() and thistype.onImpact(this,this.target) then
                        call this.destroy()
                    endif

Do this:

JASS:
                    if thistype.onImpact(this,this.target) then
                        call this.destroy()
                    endif

Then, change your .move method to this:

JASS:
        private static method move takes nothing returns nothing
            local real a
            local real tx
            local real ty
            local real d
            local real ox
            local real oy
            local unit u
            local thistype this=NODE[INDEX].next
            loop
                exitwhen this.head
                set ox = this.oX
                set oy = this.oY
                set u = this.target
                if u!=null and not(IsUnitType(u,UNIT_TYPE_DEAD)) and GetUnitTypeId(u)!=0 then
                    set tx=GetUnitX(u)
                    set ty=GetUnitY(u)
                    set a=Atan2(ty-this.y,tx-this.x)
                    call MoveLocation(GetZ,tx,ty)
                    if this.turn!=0 then
                        if Sin(a-this.cA)>=0 then
                            set this.angle=this.cA+this.turn
                        else
                            set this.angle=this.cA-this.turn
                        endif
                    else
                        set this.angle=a
                    endif
                    set d=SquareRoot((tx-ox)*(tx-ox)+(ty-oy)*(ty-oy))
                    set this.distance=d
                    set this.slide=SquareRoot((this.x-ox)*(this.x-ox)+(this.y-oy)*(this.y-oy))
                    set this.fZ=(GetLocationZ(GetZ)+this.oZ-GetUnitFlyHeight(u))/d
                    call SetUnitFacing(this.dummy,this.cA*bj_RADTODEG)
                else
                    set d=this.distance
                    set this.target=null
                    set this.slide=this.slide+this.sM
                endif
                set this.x=this.x+this.sX
                set this.y=this.y+this.sY
                
                if this.open!=0 then
                    set a=4*this.open*this.slide*(d-this.slide)/(d*d)
                    set tx=this.x+a*Cos(this.cA+HALF_PI)
                    set ty=this.y+a*Sin(this.cA+HALF_PI)
                    call SetUnitFacing(this.dummy,(this.cA+Atan(-(8*this.open*this.slide-4*d*this.open)/(d*d)))*bj_RADTODEG)
                else
                    set tx=this.x
                    set ty=this.y
                endif
            
                if this.height!=0 or this.fZ!=0 then
                    set this.z=4*this.height*this.slide*(d-this.slide)/(d*d)+this.fZ*(d-this.slide)+this.tZ
                    call MoveLocation(GetZ,tx,ty)
                    call SetUnitFlyHeight(this.dummy,this.z-GetLocationZ(GetZ),0)
                    call SetUnitAnimationByIndex(this.dummy,R2I((Atan(-(8*this.height*this.slide-4*d*this.height)/(d*d))-Atan(this.fZ))*bj_RADTODEG)+90)
                endif
                
                set this.sX=this.sX+this.aX
                set this.sY=this.sY+this.aY
                
                call SetUnitX(this.dummy,tx)
                call SetUnitY(this.dummy,ty)
                
                if this.slide>=d then
                    call this.destroy()
                endif
                set this=this.next
            endloop
            set u = null
        endmethod

And simply call .move from the "Expire" function.

I can hardly believe that nulling the variables from the destroy method is causing that. I would have to see some evidence (like your demo code you're using to test).

Also, the dummy unit should just have Amrf added to it from object editor. No reason to require the JASS script for it. It doesn't need to be removed from the dummy either. I have an object merger line in MissileRecycler which you can use to create the perfect projectile for this system as well.
 
Level 6
Joined
Jun 20, 2011
Messages
249
How are you supposed to call onImpact? remember that it's called for every instance and only when the missile either hits a target or ends it's course. If i call the move method on the Expire method how do i know which missiles hit a target? It really makes no sense to me.
Also to benchmark i'm using the demo code on the main post but instead of 0.05 timeout i'm using 0.005 (which generates almost 700 active projectiles)
Also i couldn't believe it either but it does, go ahead and test it yourself
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
You could set a boolean "this.toDestroy = true" and the module will check if it's supposed to be destroyed or not. So you'd change the comparison to "if this.toDestroy and this.onImpact() then"

Remember also that modules can access private members of both the library the module is declared in as well as the library it's implemented in, so this "toDestroy" thing could be a private keyword.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
One array lookup is about the same-speed as a local declaration, and the "move" method would end up having around 10 local declarations (once you optimize it). Doing it my way would add maybe 2 or 3 array lookups and saves on the function call as well. There is nothing wrong with iterating twice, because iterating is not where the bottleneck is.
 
Level 6
Joined
Jun 20, 2011
Messages
249
Finished applying your suggestions (v1.0.7), it seems that it's working fine after all, it's slightly faster and saves up module code size.
I would make MissileRecycler non-optional if it wasn't because recycled missiles display the previous effect's destroy animation.
Regarding the nulling of the array variables, i have no idea what on earth is causing this issue, it seems that array values are barely initialized, if the user doesn't assign a proper value before using them reding them is slower (and according to my results a LOT slower)

v1.0.7 changelog
-Increased efficiency and reduced module code size
-Fixed an issue homing missiles had when chasing units with flying height.
-Removed Autofly as a requirement, the dummy is supposed to have crow form set with the object editor
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
You can optimize your "move" method even further to this:

JASS:
        static method move takes nothing returns nothing
            local real a
            local real d
            local real s
            local real x
            local real y
            local real tx
            local real ty
            local real ox
            local real oy
            local unit u
            local unit m
            loop
                exitwhen this.head
                set x = this.x
                set y = this.y
                set ox=this.oX
                set oy=this.oY
                set m = this.dummy
                set u = this.target
                if u!=null and not(IsUnitType(u,UNIT_TYPE_DEAD)) and GetUnitTypeId(u)!=0 then
                    set tx=GetUnitX(u)
                    set ty=GetUnitY(u)
                    set a=Atan2(ty-y,tx-x)
                    call MoveLocation(GetZ,tx,ty)
                    if this.turn!=0 and not(Cos(this.cA-a)>=Cos(this.turn)) then
                        if Sin(a-this.cA)>=0 then
                            set this.angle=this.cA+this.turn
                        else
                            set this.angle=this.cA-this.turn
                        endif
                    else
                        set this.angle=a
                    endif
                    set d=SquareRoot((tx-ox)*(tx-ox)+(ty-oy)*(ty-oy))
                    set s=SquareRoot((x-ox)*(x-ox)+(y-oy)*(y-oy))
                    set this.distance=d
                    set this.slide=s
                    set this.fZ=(GetLocationZ(GetZ)+GetUnitFlyHeight(u))/d
                    call SetUnitFacing(m,this.cA*bj_RADTODEG)
                else
                    set this.target=null
                    set this.slide=this.slide+this.sM
                    set d=this.distance
                    set s=this.slide
                endif
                
                set this.x=x+this.sX
                set this.y=y+this.sY
                
                if this.open!=0 then
                    set a=4*this.open*s*(d-s)/(d*d)
                    set x=x+a*Cos(this.cA+HALF_PI)
                    set y=y+a*Sin(this.cA+HALF_PI)
                    call SetUnitFacing(m,(this.cA+Atan(-(8*this.open*s-4*d*this.open)/(d*d)))*bj_RADTODEG)
                endif
                
                if this.height!=0 or this.fZ!=0 then
                    set this.z=4*this.height*s*(d-s)/(d*d)+this.oZ+this.fZ*s
                    call MoveLocation(GetZ,x,y)
                    call SetUnitFlyHeight(m,this.z-GetLocationZ(GetZ),0)
                    call SetUnitAnimationByIndex(m,R2I((Atan(-(8*this.height*s-4*d*this.height)/(d*d))-Atan(-this.fZ))*bj_RADTODEG + 90.5))
                endif
                
                set this.sX=this.sX+this.aX
                set this.sY=this.sY+this.aY
                
                call SetUnitX(m,x)
                call SetUnitY(m,y)
                
                if s>=d then
                    set this.recycle=true
                endif
                
                set this=this.next
            endloop
            set u = null
            set m = null
        endmethod

I have cached a lot of your repeat-references into locals so it should give a good speed boost. Also don't forget you need to round up as well, so notice how I changed the +90 outside of the R2I to a +90.5 inside the R2I.

You can also do most of the SetUnitAnimationByIndex math from the create method or from one of the arc/height operators. Look at how Berb does it in his Custom Projectiles library, it's very simple math for him but way more complicated for you.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
I recommend looking at XE Missile by Anitarf (it is found in the XE library now). The math it uses is almost as simple as it gets, and I built my Missile library off of an even further optimized version using some of his tricks and some of Berb's tricks. Tonight I will link you to that system (I have to upload it first) and then the combination of yours + mine will be the best ever.
 
This looks very decent :)
I like it. This is the first projectile system I stumble upon that's the kind I'd use ;)
Kenny's has way too much overhead, Berb's is slightly better, but it has some overhead too, Bribe's old projectile system was bad, etc...

One problem: Why aren't you caching the Cos(value)/Sin(value) reals in those operators into locals? Sin and Cos are pretty slow functions :/ (They should be faster than SquareRoot and Pow though)

And over here:

return createEx(CreateUnit(Player(15),DUMMY_ID,ox,oy,a*bj_RADTODEG),ox,oy,oz,a,d,z)

The player you're creating it for should be configurable.

I'm going to do some benchmarks for this.
 
Level 6
Joined
Oct 23, 2011
Messages
182
this should have onFinish method.. onImpact is only called when collision is true right?

I don't want my missile to use collision but still have some method called when missile finishes its course
 
Level 14
Joined
Sep 28, 2011
Messages
968
* - This system can support up to 600 proyectiles with complex
* - mathematics (curve, arc, collision) before it lags.
With what computer?A nasa computer or an P90 or an Athlon 64 but this number seems to be unrealistic it lags already with 600 units on an Athlon 64 and WC3 hardly turns on an P90 (Same if WC3 editor turn without laging exessively with Windows 98. I used a computer of this type one day for editing a small RPG I made).
 
Level 6
Joined
Jun 20, 2011
Messages
249
@kSityl
The onImpact method is called when the missiles finishes it course as well, but the unit it takes it equal to "null", just make sure that the target hit is null
@noob
On my computer.
Also without the complex mathematics it runs at 50 fps.
@Mag
Nestharus said that Cos and Sin were pretty fast. Idk what to do about that, and yes i'm trying to implement Berb's way of moving missiles, but the code is almost unreadable due the overhead it has
 
Level 14
Joined
Sep 28, 2011
Messages
968
On my computer
It does not indicate the power.
If you were not serious some people could imagine it is a benchmark with the powerfullest compter of the world just for making think your missile system is very fast but it is clear it is not slow so why indicate a missile number before lag?
 
Level 14
Joined
Sep 28, 2011
Messages
968
I will try.
But if someone use 400 missiles in same time in its map it is that there is a bug or that your map is strange so what is more interesting would be the number of usefull things users will use and the normal speed in an ordinary map not the speed in a map where heroes with 99999 poligons launch 835 missiles per seconds in an arena with way too many doodadds for beeing typed because no one make maps like that.
 
Level 16
Joined
Aug 7, 2009
Messages
1,403
I agree on the fact that it has some unnecessary functions, but I found most of them useful (unless you're trying to make spells that only fire one linear missile; 4 from these spells per hero)

I also agree on the fact that this needs some extra functions too.
I didn't want to go off-topic, but now that I'm posting here, I should mention it: I found it kinda ironic when you called Berb's code unreadable.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
I haven't approved this yet because I don't like having it when approved resources break backwards compatibility, but effectively what I meant in the last VM would be to change your existing in-module collision block to this:

JASS:
            static if thistype.onCollide.exists then
                if this.collides then
                    call GroupEnumUnitsInRange(bj_lastCreatedGroup,this.x,this.y,MISSILE_COLLISION_SIZE,null)
                    loop
                        set u=FirstOfGroup(bj_lastCreatedGroup)
                        exitwhen u==null
                        if thistype.onCollide(this,u) then
                            call missileTerminate(this)
                        endif
                        call GroupRemoveUnit(bj_lastCreatedGroup,u)
                    endloop
                endif
            endif

And instead of having a constant collision the collision size should be specific to the missile. Instead of a boolean "collides" you could just check "collision != 0".
 
Level 6
Joined
Jun 20, 2011
Messages
249
Simplifying the math this system uses would outcome in malfunction and loss of features, so i decided to keep using the parabola function and it's derivative.
Update v1.1.0
-this.collides was removed and changed to this.collision which is now a real variable and uses the library's constant global as default value (controls the radio of missile collision for units it encounters on it's course)
-the onImpact method will now only be called once the missile reaches the end of it's course, that means that if it didn't have a target the unit it takes is equal to null
-added the new onCollision method that is only called when the missile encounters a unit on it's course, if your struct doesn't have this method the collision feature wont work.
 
Level 6
Joined
Jun 20, 2011
Messages
249
What should i do with the 'distance' when i set a target on my missile?
when i put 0, it doesnt seem to work
is it supposed to be max range it can travel? do i need to put 99999 or something?
After some tests i can't really tell why setting distance to 0 causes the system to malfunction (other than dividing by 0, but i already fixed that and still caused problems)
JASS:
                        call BJDebugMsg("prev")
                        set this.angle=a
                        call BJDebugMsg("next")
Apparently causes the thread to crash since "next" isn't being displayed, and "a" has been initialized already with a valid value.
Conclusion, just set the distance to the distance between the start point and the target, or to any other value that isn't 0 (setting it to 1 works)
 
Level 7
Joined
Dec 3, 2006
Messages
339
JASS:
                    if this.turn!=0 and not(Cos(this.cA-a)>=Cos(this.turn)) then
                        if Sin(a-this.cA)>=0 then
                            set this.angle=this.cA+this.turn
                        else
                            set this.angle=this.cA-this.turn
                        endif
                    else
                        set this.angle=a
                    endif

Maybe it's to do with the if then else? try setting this.angle=a before this check:

if this.turn!=0 and not(Cos(this.cA-a)>=Cos(this.turn)) then
 
Level 6
Joined
Jun 20, 2011
Messages
249
There's no a single Tan function in the code.
Also i already fixed the code (and will update soon) i forgot that "angle" was an operator and it involved dividing x by distance (x/0)
I came up with this new issue: It seems that the creation of onCollide creates new issues such as, the following: if the missile has a target but it hits targets on it's way then it would make the user to code a method for when missiles hits a target by both of the ways: collision and aimed. Also if a collision missile suddenly gains a new target on it's course it would cause the same issue.
I do know that the current way of iterating through missiles is a bit rough, but because of the way the system works is very hard to find an alternative
 
Level 6
Joined
Jun 20, 2011
Messages
249
Hypothetical situation:
I have a missile that's supposed to collide with the first unit it encounters and deals damage to it, it will travel 1000 units before it dies.
JASS:
static method onCollide takes Missile this, unit hit returns boolean
    if hit!=this.source then
        // deal damage to unit
        return true //ends missile
    endif
    return false //keep missile going
endmethod

static method onImpact takes Missile this, unit hit returns boolean
    return true //this method is only called when the missile traveled 1000 units already
endmethod
Pretty simple, it should work.
Now, i have another ability in my map that gives homing capacities to missiles that enter a given AoE, it also sets their collision range to 0 to prevent it from crashing with any other unit until it reaches it target.
Whats the problem?
This missile i created does NOTHING to it's target because when it reaches it the onImpact method is called, and it would only destroy the missile.
Also, i was thinking of creating an unit group per missile to prevent the same missile from colliding with the same unit twice (would cause issues if you want the missile to bounce multiple times from units, but this scenario is rather unusual and i would make sure to make the group readonly so you can remove units from it)
 
Also, i was thinking of creating an unit group per missile to prevent the same missile from colliding with the same unit twice (would cause issues if you want the missile to bounce multiple times from units, but this scenario is rather unusual and i would make sure to make the group readonly so you can remove units from it)

Or, you could use a Table for each missile and save booleans using the handle ids of the units as indices.
 
Level 6
Joined
Jun 20, 2011
Messages
249
Ok let me explain again.
Currently the onImpact method is called when the missile reaches its objective, it may be a target or a point. The onCollide method is called when the missile hits a unit in it's path.
I have 2 abilities: one is a simple missile that i fire towards a given location and it stuns the first unit it collides with and the other is an AoE that redirects missiles that go through it to targets (if there's none) and removes the collision capacities of the missile.
The code for the first ability looks like this
JASS:
static method onCollide takes Missile this, unit hit returns boolean
    if hit!=this.source then
        call StunUnit(hit,2.0) //stuns the unit for 2 seconds
        return true
    endif
    return false
endmethod

static method onImpact takes Missile this, unit hit returns boolean
    return true
endmethod
Notice that when the missile ends it's course and hits no units on it's path it only get's destroyed. Alone the missile works perfectly, the issue comes when you try to handle missiles with alien factors (such as the second ability)
If because the second ability the missile gains a target and it's collision is set to 0 then it wont call the onCollide method anymore and instead call the onImpact method when it hits it's target, but this wont receive the stun.
My solution to this issue: instead of the current interface use the following
static method onImpact takes Missile this returns nothing this method is called when the missile is destroyed (for aesthetics purposes)
static method onCollide takes Missile this, unit hit returns boolean this method is called when the missile hits ANY unit, if returns true the missile is destroyed.
This is how the code looks after the modification
JASS:
static method onCollide takes Missile this, unit hit returns boolean
    if hit!=this.source then
        call StunUnit(hit,2.0) //stuns the unit for 2 seconds
        return true
    endif
    return false
endmethod

static method onImpact takes Missile this returns nothing
    call DestroyEffect(AddSpecialEffect(this.model,this.x,this.y))
endmethod
 
Level 6
Joined
Jun 20, 2011
Messages
249
Bribe you may have rushed into approving this system since it clearly needed some changes. (IMO)
But I really like this new version now and I hope that from now on changes are minimal. (Unless ofc you want me to use Nest's Position for this, but i'm not so sure about that. If you want to pick destructibles, items or widgets you can do it in the onPeriod method)
v1.2.0
-Removed the onImpact method, now the onCollide is called whenever it collides with any unit,
-Added the new onFinish method which is called when the missile ends it's course (read the documentation for more info)
-Manually destroying the missle (call missile.destroy) causes the onFinish method to trigger.
-Setting distance to 0 no longers causes the system to malfunction.
-Fixed some issues with missiles that were supposed to be destroyed through returning true in any of the methods but weren't
-Added a unit group handle per missile, this is to prevent missiles from colliding with the same unit twice, the group is accessible and it's name is "unitsHit". (DISCLAIMER: the missile's target isn't inside this group)
-Added WorldBounds as an optional requirement to check if the missile left the map (to prevent crashes)
-All methods are optional now, you can fire missiles with nothing more than visual purposes.
-The default value to "collision" is now 0
-Fixed an issue where the target of the missile could be picked through collision if the missile was slow enough.
 
Top