System Missile

Dirac

22710180
Reaction score
147
JASS:
library Missile /* v1.2.0

*/uses/* 
    */  LinkedListModule            /*  thehelper.net/forums/showthread.php/168775-LinkedListModule
    */  optional WorldBounds        /*  hiveworkshop.com/forums/jass-functions-413/snippet-worldbounds-180494/
    */  optional MissileRecycler    /*  hiveworkshop.com/forums/jass-resources-412/system-missilerecycler-206086/
    
**********************************************************************/
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_ID                = 'e000'
endglobals
/**********************************************************************
*
*   struct Missile
*
*       static method create takes real originX, real originY, real originZ...
*       ...real angle, real distance, real targetZ returns Missile
*           -   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 distance
*           -   The amount of distance the Missile will travel.
*       real slide
*           -   The amount of distance is left to travel.
*       real angle
*           -   Facing angle of the missile. (Radians)
*
*       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 rought 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.
*
*   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.
*
*       -   If you wish to enumerate across all proyecitles you must
*       -   learn how to use the LinkedListModule. This system provides
*       -   a struct that keeps track of all active proyectiles called
*       -   "MissileList", start looping from "MissileList.base.next".
*
**********************************************************************/

    globals
        private constant real   HALF_PI = bj_PI/2
        private location        GetZ    = Location(0,0)
    endglobals
    
    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
        
        real x
        real y
        real z
        
        private real oX     //origin x
        private real oY     //origin y
        private real oZ     //origin z
        
        real slide
        real distance
        
        private real    cA  //current angle
        private real    fZ  //z slope
        private real    tZ  //target z
        
        private real    sX  //speed x vector
        private real    sY  //speed y vector
        private real    sM  //speed module
        
        private real    aX  //acceleration x vector
        private real    aY  //acceleration y vector
        private real    aM  //acceleration module
        
        private effect  fx  //effect
        private string  fP  //model
        
        readonly unit   dummy
        readonly group  unitsHit
        
        boolean recycle
        boolean wantDestroy
        
        unit    target
        unit    source
        real    collision
        real    heightv
        real    turn
        real    open
        integer data
        
        method operator speed= takes real value returns nothing
            set sX=value*Cos(cA)
            set sY=value*Sin(cA)
            set sM=value
        endmethod
        
        method operator speed takes nothing returns real
            return sM
        endmethod
        
        method operator acceleration= takes real value returns nothing
            set aX=value*Cos(cA)
            set aY=value*Sin(cA)
            set aM=value
        endmethod
        
        method operator acceleration takes nothing returns real
            return aM
        endmethod
        
        method operator angle= takes real value returns nothing
            set cA=value
            set sX=sM*Cos(value)
            set sY=sM*Sin(value)
            set aX=aM*Cos(value)
            set aY=aM*Sin(value)
            if target!=null and distance!=0 then
                call MoveLocation(GetZ,x+(distance-slide)*Cos(value),y+(distance-slide)*Sin(value))
                set fZ=(tZ+GetLocationZ(GetZ)-z)/distance
            endif
        endmethod
        
        method operator angle takes nothing returns real
            return cA
        endmethod
        
        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)*distance
        endmethod
        
        method operator curve takes nothing returns real
            return Atan(open/distance)
        endmethod
        
        method operator arc= takes real value returns nothing
            set height=Tan(value)*distance
        endmethod
        
        method operator arc takes nothing returns real
            return Atan(height/distance)
        endmethod
        
        static method createEx takes unit whichUnit, real ox, real oy, real oz, real a, real d, real tz 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 sM=0
            set sX=0
            set sY=0
            
            call MoveLocation(GetZ,ox,oy)
            set oX=ox
            set oY=oy
            set oZ=oz+GetLocationZ(GetZ)
            set distance=d
            
            set angle=a
            set slide=0
            
            set x=oX
            set y=oY
            set z=oZ
            
            call MoveLocation(GetZ,ox+d*Cos(a),oy+d*Sin(a))
            set tZ=tz
            if d!=0 then
                set fZ=(tz+GetLocationZ(GetZ)-oZ)/d
            endif
            set dummy=whichUnit
            call SetUnitFlyHeight(dummy,oz,0)
            
            call MissileList.base.insertNode(this)
            
            return this
        endmethod
        
        static method create takes real ox, real oy, real oz, real a, real d, real z returns thistype
            static if LIBRARY_MissileRecycler then
                return createEx(GetRecycledMissile(ox,oy,oz,a*bj_RADTODEG),ox,oy,oz,a,d,z)
            else
                return createEx(CreateUnit(Player(15),DUMMY_ID,ox,oy,a*bj_RADTODEG),ox,oy,oz,a,d,z)
            endif
        endmethod
        
        
        method bounce takes nothing returns nothing
            set oX=GetUnitX(dummy)
            set oY=GetUnitY(dummy)
            call MoveLocation(GetZ,oX+distance*Cos(angle),oY+distance*Sin(angle))
            set fZ=(tZ+GetLocationZ(GetZ)-z)/distance
            set oZ=z
            set x=oX
            set y=oY
            set slide=0
        endmethod
        
        method deflect takes real tx, real ty returns nothing
            set distance=distance-slide
            set angle=2*Atan2(ty-y,tx-x)+bj_PI-cA
            call this.bounce()
            call SetUnitFacing(dummy,cA*bj_RADTODEG)
        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 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 tx
            local real ty
            local real ox
            local real oy
            loop
                exitwhen this.head
                set ox=this.oX
                set oy=this.oY
                if this.target!=null and not(IsUnitType(this.target,UNIT_TYPE_DEAD)) and GetUnitTypeId(this.target)!=0 then
                    set tx=GetUnitX(this.target)
                    set ty=GetUnitY(this.target)
                    set a=Atan2(ty-this.y,tx-this.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((this.x-ox)*(this.x-ox)+(this.y-oy)*(this.y-oy))
                    set this.distance=d
                    set this.slide=s
                    set this.fZ=(GetLocationZ(GetZ)+GetUnitFlyHeight(this.target))/d
                    call SetUnitFacing(this.dummy,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 tx=this.x
                set ty=this.y
                set this.x=tx+this.sX
                set this.y=ty+this.sY
                
                if this.open!=0 then
                    set a=4*this.open*s*(d-s)/(d*d)
                    set tx=tx+a*Cos(this.cA+HALF_PI)
                    set ty=ty+a*Sin(this.cA+HALF_PI)
                    call SetUnitFacing(this.dummy,(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,tx,ty)
                    call SetUnitFlyHeight(this.dummy,this.z-GetLocationZ(GetZ),0)
                    call SetUnitAnimationByIndex(this.dummy,R2I((Atan(-(8*this.height*s-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
                
                static if LIBRARY_WorldBounds then
                    if IsPointInRegion(WorldBounds.worldRegion,tx,ty) then
                        call SetUnitX(this.dummy,tx)
                        call SetUnitY(this.dummy,ty)
                    else
                        call this.destroy()
                    endif
                else
                    call SetUnitX(this.dummy,tx)
                    call SetUnitY(this.dummy,ty)
                endif
                
                
                if s>=d then
                    set this.recycle=true
                endif
                
                set this=this.next
            endloop
        endmethod
    
    endstruct
    
    //***********************************************************************
    //  This part of the code might be confusing to some. It's very similar
    //  to how CTL works. Never the less it deserves an explanation.
    globals
        private integer                     COUNT   =   0
        private integer                     ACTIVE  =   0
        private integer                     SIZE    =   0
        private timer                       TIMER   =   CreateTimer()
        private trigger                     FIRE    =   CreateTrigger()
        private boolexpr            array   METHOD
        private integer             array   STACK
        private integer             array   INSTANCES
        private triggercondition    array   COND
        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 TriggerRemoveCondition(FIRE,COND[STACK[SIZE]])
            endif
            if ACTIVE==0 then
                call PauseTimer(TIMER)
                return
            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
            set COND[index]=TriggerAddCondition(FIRE,METHOD[index])
        endif
        if ACTIVE==0 then
            call TimerStart(TIMER,0.03125,true,function Execute)
        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
    
    //***********************************************************************
    //  Gives the struct an index. COUNT keeps track of the amount of structs
    //  with the module implementation. NODE[] is the head of the struct's
    //  linked list of missiles.
    private function IndexStruct takes code func returns integer
        set COUNT=COUNT+1
        set NODE[COUNT]=Missile.createNode()
        set METHOD[COUNT]=Filter(func)
        return COUNT
    endfunction
    
    module MissileStruct
            
        private static integer INDEX = 0
        
        private static method missileTerminate takes Missile this returns nothing
            call this.terminate()
            call StopPeriodic(INDEX)
        endmethod
        
        //***********************************************************************
        //  First it takes the struct's first instance, which is the next to the
        //  head (NODE[INDEX].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[INDEX].next
            call this.move()
            loop
                exitwhen this.head
                
                static if thistype.onPeriod.exists then
                    if thistype.onPeriod(this) then
                        call missileTerminate(this)
                    endif
                endif
                
                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
                        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
                
                set this=this.next
            endloop
            
            set u=null
        endmethod
        
        static method launch takes Missile this returns nothing
            call StartPeriodic(INDEX)
            call NODE[INDEX].insertNode(this)
        endmethod
        
        private static method onInit takes nothing returns nothing
            set INDEX=IndexStruct(function thistype.missileIterate)
        endmethod
        
    endmodule
    
endlibrary
Demo
JASS:
struct TESTER extends array

    static integer COUNT = 0
    static unit FOOTMAN = null
    static timer t = CreateTimer()
    static boolean b = false
    
    static method onCollide takes Missile this, unit hit returns boolean
        if hit!=this.source then
            call this.deflect(GetUnitX(hit),GetUnitY(hit))
        endif
        return false
    endmethod
    
    static method onFinish takes Missile this returns boolean
        call DestroyEffect(AddSpecialEffect(this.model,this.x,this.y))
        return true
    endmethod
        
    implement MissileStruct
    
    static method onExpire takes nothing returns nothing
        local Missile new = Missile.create(GetUnitX(FOOTMAN),GetUnitY(FOOTMAN),65,GetRandomReal(0,bj_PI*2),1000,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)
            set b=false
        else
            call TimerStart(t,0.005,true,function thistype.onExpire)
            set b=true
        endif
    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
 

luorax

Invasion in Duskwood
Reaction score
67
ProjectileStruct, what? It sounds horrible. Bullet would be better IMO (and original too), because we've got so many "Projectiles" and ProjectileStruct sounds horrible, yet again.

You should also create a test map with some demo abilities demonstrating the capabilities of the system.
 

Dirac

22710180
Reaction score
147
Bullet isn't an accurate name, after all this handles any type of proyectile and it requires the user to use a struct, it's uber fast (can handle more than 900 proyectiles with curves and arcs before crashing) and API wise.
Also it handles the dummy's pitch angle if the user is using vexorian's dummy model.
Consider my soon-to-be-uploaded submission to the hero contest at the hive the demo map, i'll post it in here too.
 

Laiev

Hey Listen!!
Reaction score
188
:eek: You did a system just for your hero? ^_^ sweet.

I agreed with Luorax, ProjectileStruct is just horrible...

Bullet is strange.

Maybe 'Missile'.
 

Dirac

22710180
Reaction score
147
Missile sounds good, and yes i created a proyectile system just for my hero because i needed a way to iterate through missiles
Is there anything else other than the name that needs to be changed?
 

Tom_Kazansky

--- wraith it ! ---
Reaction score
157
Bullet sounds like this only has missile that fly straight (or smth alike)

This dummy must use Vexorian's dummy model and it's movement type should be "Hover" if you want to correctly move over water

I didn't know this, thanks :p

sorry that I can't comment on your code :eek:
 

Dirac

22710180
Reaction score
147
Changed the name of the resource to Missile and fixed every documentation need.
Also added the acceleration variable

EDIT: Had to update again to v1.0.1 because it had a bug, a big one, on missile instance deallocation, it's now fixed and tested
EDIT: New issues popped out so i had to change the way it iterates through instances each period, version v1.0.2 is stable but doesn't allow the user to fully iterate through all missiles, so i added the "MissileList" struct that will be keeping track of all active missiles, to search do the following
JASS:
local Missile node = MissileList.base.next
loop
    exitwhen node.list.head

    //...
    
    set node = node.list.next
endloop

EDIT: Updated to v1.0.4 thanks to many suggestions by Bribe: reduced the amount of code in the module significantly
 

kingkingyyk3

Visitor (Welcome to the Jungle, Baby!)
Reaction score
216
Use vector will make the system even faster, as vector only involves simple maths operation. :)
 

Bribe

vJass errors are legion
Reaction score
67
Vector sux for projectile systems because of all the extra array lookups required.
 

Dirac

22710180
Reaction score
147
I know the math, vector is an ugly wrapper with no deallocation method. If anything it would be slower.

EDIT: 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
 

Dirac

22710180
Reaction score
147
Updated to v1.0.7
-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
 

Cohadar

master of fugue
Reaction score
209
* - Missile's default owner is Player(15) and this is meant to
* - not be modifiable.
Bad, put a constant for it and say do not modify unless you know what you are doing.
Some people might want to change it, for example I reserve Player(15) for hero selection system.

And using constant collision size kind of makes this whole lot less useful.
 

Dirac

22710180
Reaction score
147
Some people might want to change it, for example I reserve Player(15) for hero selection system.
And how is that going to cause any kind of issue anyways? Missiles are supposed to not give vision to any player that fires it, and even if you want vision, the dummy has none.
Anyways the dummy is accesible and you can change the owner [ljass]SetUnitOwner(m.dummy,Player(0),true)[/ljass] after the missile is created.

And using constant collision size kind of makes this whole lot less useful.
True

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.
 

Dirac

22710180
Reaction score
147
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.
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top