System MissileRecycler

Discussion in 'Tutorials and Resources' started by Bribe, Oct 27, 2011.

  1. Bribe

    Bribe vJass errors are legion

    Ratings:
    +67 / 0 / -0
    Recycles missile dummy units while considering their facing angle.

    Warning: this uses an ObjectMerger statement to create a dummy unit with the rawcode 'dumi'. I haven't written a Nestharus-style safe Lua script nor prepared a test map at this stage. That is on my to-do list still.

    JASS:
    library MissileRecycler initializer Init /*
    
        MissileRecycler v 1.2.0.2
        =========================================================================
        Credits:
        -------------------------------------------------------------------------
        Written by Bribe
        Vexorian, Anitarf and iNfraNe for the dummy.mdx model file
        Nestharus for the Queue data structure.
    
        =========================================================================
        Requirements:
        -------------------------------------------------------------------------
        -   Updated JassHelper by Cohadar
    
        =========================================================================
        Introduction:
        -------------------------------------------------------------------------
        Recycling dummy units is important because the CreateUnit call is one of,
        if not the, most processor-intensive native in the entire game. Creating
        just a couple dozen dummy units in a single thread causes a visible frame
        glitch for that instant. The overhead is even higher if you are using a
        Unit Indexing library in the map which causes some extra evaluations per
        new unit.
    
        There are also reports of removed units leaving a little trail of RAM
        surplus in their wake. I have not been able to reproduce this so I don't
        know if this is still a factor in 1.26.
    
        I was motivated to create this system because removed units might be un-
        safe in very large numbers and CreateUnit is a very heavy process.
    
        The thing that makes this system different than others is the fact that
        it considers the facing angle of the dummies being recycled, which I have
        never seen another system even attempt. Considering the facing angle is
        important because it takes almost 1 second for the unit to turn around,
        which looks especially weird if you are creating a unit-trail or if you
        are shooting arrow-shaped objects as projectiles. For fireball effects or
        effects that generally don't depend on facing angle, this system would be
        a bit wasteful.
    
        Worst case scenario, it will take 0.09 seconds for the projectile to turn
        to the angle you need. This is 1/8 of the normal worst case scenario if
        you weren't recycling dummies considering facing. On average, it takes
        roughly 0.045 seconds to turn to the angle you need (which is not even
        noticable). However, I have made this completely configurable and you are
        able to change the values to whatever needs you have.
    
        =========================================================================
        Calibration Guide:
        -------------------------------------------------------------------------
        The thing that surprised me the most about this system was, no matter how
        complex it turned out, it became very configurable. So I should let you
        know what the constants do so you know if/how much you want to modify.
    
        constant integer ANG_N = 8
    
        -   How many different angles are recognized by the system. You can't do
        360 different angles because then you're going to have thousands of dummy
        units stored and that's ridiculous, the game lags enough at 1000 units.
        Increasing ANG_N increases realism but decreases the chance that a dummy
        unit will be available to be recycled. I don't recommend making this any
        lower, though the max I'd recommend would be 16.
    
        constant integer ANG_STORAGE_MAX = 12
    
        -   How many dummy units are stored per angle. This limit is important
        because you might have a spike at one point in the game where many units
        are created, which could lead to many of those dummy units never being
        used again.
            In general, I advise that the factor of ANG_N x ANG_STORAGE_MAX does
        not exceed 100. More than that is too much in my opinion.
            Preloads ANG_N x ANG_STORAGE_MAX dummy units. Preloading dummies is
        useful as it dumps a lot of CreateUnit calls in initialization where you
        won't see a frame glitch.
    
        =========================================================================
        API Guide:
        -------------------------------------------------------------------------
        You obviously need some functions so you can get a recycled dummy unit or
        recycle it. Therefore I provide these:
    
        function GetRecycledMissile
            takes real x, real y, real z, real facing
                returns unit
    
            Returns a new dummy unit that acts as a projectile missile. The args
            are simply the last three arguments you'd use for a CreateUnit call,
            with the addition of a z parameter to represent the flying height -
            it isn't the absolute z but relative to the ground because it uses
            SetUnitFlyHeight on that value directly.
    
        function RecycleMissile
            takes unit u
                returns nothing
    
            When you are done with that dummy unit, recycle it via this function.
            This function is pretty intelligent and resets that unit's animation
            and its facing angle so you don't have to.
    */
        //=======================================================================
        // Save the map, then delete the exclaimation mark in the following line.
        // Make sure that you don't have an object in your map with the rawcode
        // 'dumi' and also configure the model path (war3mapImported\dummy.mdl)
        // to the dummy.mdx model created by Vexorian.
        //! external ObjectMerger w3u ewsp dumi unam "Missile Dummy" ubui "" uhom 1 ucol 0.01 umvt "None" umvr 1.00 utar "" uspa "" umdl "war3mapImported\dummy.mdl" umxr 0.00 umxp 0.00 ushr 0 uerd 0.00 udtm 0.00 ucbs 0.00 uble 0.00 uabi "Aloc,Amrf"
    
        //Thanks to Vexorian that Optimizer 5.0 no longer kills natives
        native UnitAlive takes unit id returns boolean
    
        globals
            //-------------------------------------------------------------------
            // You must configure the dummy unit with the one created from the
            // ObjectMerger statement above.
            //
            private constant integer DUMMY_ID = 'dumi'      //The rawcode of the dummy unit.
            private          player  OWNER    = Player(15)  //The owner of the dummy unit.
    
            private constant integer ANG_N = 8              //# of indexed angles. Higher value increases realism but decreases recycle frequency.
            private constant integer ANG_STORAGE_MAX = 12   //Max dummies per indexed angle. I recommend lowering this if you increase ANG_N.
    
            private constant real DEATH_TIME = 2. //Allow the special effect on
            //the unit to complete its "death" animation in this timeframe. Must
            //be higher than 0.74 seconds to allow the unit time to turn. This
            //number should not be lower than the maximum death-animation time of
            //your missile-units' effect attachments, just to be safe.
        endglobals
    
        globals
            private constant integer ANG_VAL = 360 / ANG_N //Generate angle value from ANG_N.
            private constant integer ANG_MID = ANG_VAL / 2 //The middle value of angle value.
    
            //Misc vars
            private unit array stack       //Recycled dummy units.
            private integer array stackN   //How many units are in the stack?
            private real array timeStamp   //Prevents early recycling of units.
            private integer array queueNext
            private integer array queueLast
            private integer array recycle
            private timer gameTime = CreateTimer() //Used for visual continuity.
            private group protect  = CreateGroup() //Used to prevent double frees.
        endglobals
    
        static if DEBUG_MODE then
            private function Print takes string s returns nothing
                //Un-comment this next line if you want to know how the system works:
                //call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 999, "[MissileRecycler] " + s)
            endfunction
        endif
    
        //=======================================================================
        // Get a recycled dummy missile unit. If there are no recycled dummies
        // that are already facing the angle you need, it creates a new dummy for
        // you.
        //
        function GetRecycledMissile takes real x, real y, real z, real facing returns unit
            local integer i = ModuloInteger(R2I(facing), 360) / ANG_VAL
            local integer this = queueNext[i]
            local unit u
            if this != 0 and TimerGetElapsed(gameTime) >= timeStamp[this] then
                //Dequeue this
                set queueNext[i] = queueNext[this]
                if queueNext[i] == 0 then
                    set queueLast[i] = i
                endif
                set stackN[i] = stackN[i] - 1
                //Recycle this index
                set recycle[this] = recycle[0]
                set recycle[0] = this
                //Old unit will return as new
                set u = stack[this]
                call SetUnitFacing(u, facing)
                call GroupRemoveUnit(protect, u)
                debug call Print("Recycling")
            else
                debug call Print("Creating new")
                set u = CreateUnit(OWNER, DUMMY_ID, x, y, facing)
            endif
            call SetUnitX(u, x)
            call SetUnitY(u, y)
            call SetUnitFlyHeight(u, z, 0)
            set bj_lastCreatedUnit = u
            set u = null
            return bj_lastCreatedUnit
        endfunction
    
        //=======================================================================
        // You should recycle the dummy missile unit when its job is done.
        //
        function RecycleMissile takes unit u returns nothing
            local integer i
            local integer this = recycle[0]
            if GetUnitTypeId(u) == DUMMY_ID and UnitAlive(u) and not IsUnitInGroup(u, protect) then
                if this == 0 then
                    debug call Print("Stack is full - removing surplus unit")
                    call RemoveUnit(u)
                    return
                endif
                //Recycle this
                set recycle[0] = recycle[this]
                //Index the dummy unit to an available facing angle.
                set i = R2I(GetUnitFacing(u)) / ANG_VAL
                if stackN[i] < ANG_STORAGE_MAX then
                    set i = ANG_N
                    loop
                        set i = i - 1
                        exitwhen stackN[i] < ANG_STORAGE_MAX
                    endloop
                endif
                //Enqueue this
                set queueNext[queueLast[i]] = this
                set queueLast[i] = this
                set queueNext[this] = 0
                set stackN[i] = stackN[i] + 1
                //Allow a time barrier for the effect to destroy/turn to complete.
                set timeStamp[this] = TimerGetElapsed(gameTime) + DEATH_TIME
                set stack[this] = u
                //Prevent double-free of this unit.
                call GroupAddUnit(protect, u)
                //Reset the dummy's properties.
                call SetUnitFacing(u, i * ANG_VAL + ANG_MID)
                call SetUnitVertexColor(u, 255, 255, 255, 255)
                call SetUnitAnimationByIndex(u, 90)
                call SetUnitScale(u, 1, 0, 0)
                //call PauseUnit(u, false) -- you can disable "resets" that you don't need to worry about.
            debug else
                debug call BJDebugMsg("[MissileRecycler] Error: Attempt to recycle invalid unit.")
            endif
        endfunction
    
        //=======================================================================
        // Map the dummy units to their facing angles (map below is if ANG_N is
        // 4 and ANG_STORAGE_MAX is 3).
        //
        // angle[0] (0)   -  [4] [5] [6]
        // angle[1] (90)  -  [7] [8] [9]
        // angle[2] (180) - [10][11][12]
        // angle[3] (270) - [13][14][15]
        //
        private function Init takes nothing returns nothing
            local integer end
            local integer i = ANG_N
            local integer n = i
            local integer angle
            local real x = GetRectMaxX(bj_mapInitialPlayableArea)
            local real y = GetRectMaxY(bj_mapInitialPlayableArea)
            loop
                set i = i - 1
                set stackN[i] = ANG_STORAGE_MAX
                set queueNext[i] = n
                set angle = i * ANG_VAL + ANG_MID
                set end = n + ANG_STORAGE_MAX
                set queueLast[i] = end - 1
                loop
                    set queueNext[n] = n + 1
                    set stack[n] = CreateUnit(OWNER, DUMMY_ID, x, y, angle)
                    set n = n + 1
                    exitwhen n == end
                endloop
                set queueNext[n - 1] = 0
                exitwhen i == 0
            endloop
            call TimerStart(gameTime, 1000000., false, null)
        endfunction
    
    endlibrary
     
    • Like Like x 2
  2. Tom_Kazansky

    Tom_Kazansky --- wraith it ! ---

    Ratings:
    +157 / 0 / -0
    hmm... gonna try this, creating/removing projectiles is something that's avoidable (and should be done), too bad that I haven't seen a missile recycling system, oh well, it's here now :thup:

    EDIT: erm... can I use TimerUtils instead of TimerUtilsEx?
     
  3. Bribe

    Bribe vJass errors are legion

    Ratings:
    +67 / 0 / -0
    Just delete TimerUtils and paste TimerUtilsEx in its place (make sure you also copy the lines that say "library TimerUtils requires TimerUtilsEx" & "endlibrary").

    It is 100% backwards compatible, Magtheridon96 and I made sure of that.
     
  4. Sgqvur

    Sgqvur FullOfUltimateTruthsAndEt ernalPrinciples, i.e shi

    Ratings:
    +62 / 0 / -0
    Bribe:
    1.
    private code expireCode = null //Prevents trigger evaluations or cloned functions.
    ...
    set expireCode = function Expire
    ...
    call TimerStart(NewTimerEx(this), RAbsBJ(0.235 * r), false, expireCode)

    1. xD ... What "trigger evaluations or cloned functions" are you writing about in a timer callback?!?!
     
  5. Bribe

    Bribe vJass errors are legion

    Ratings:
    +67 / 0 / -0
    Notice that TimerStart is above the Expire function.
     
  6. Sgqvur

    Sgqvur FullOfUltimateTruthsAndEt ernalPrinciples, i.e shi

    Ratings:
    +62 / 0 / -0
    Bribe:
    1. Notice that TimerStart is above the Expire function.

    1.
    JASS:
    function Recycle takes nothing returns nothing
        call TimerStart(CreateTimer(), 0, false, function Expire)
    endfunction
    
    function Expire takes nothing returns nothing
    endfunction


    No "trigger evaluations or cloned functions" generated by jasshelper just pjass error Undefined function Expire.
     
  7. Bribe

    Bribe vJass errors are legion

    Ratings:
    +67 / 0 / -0
    Because to make it work otherwise would require me to define "private keyword Expire" which would allow the expire function to be called from above it, although that generates trigger evaluations and cloned functions.

    Originally I had those two methods in a struct so it would have been more obvious then.
     
  8. Bribe

    Bribe vJass errors are legion

    Ratings:
    +67 / 0 / -0
    Updated to use normal TimerUtils.
     
  9. Bribe

    Bribe vJass errors are legion

    Ratings:
    +67 / 0 / -0
    Updated to 1.2.

    - Fixed a bug with recycling of dead units which allowed their effect's death animation to still be displayed.

    - Should be faster now that it uses a FIFO queue data structure and timestamps instead of a linear stack and a timer per instance.

    - Added double-free and dead-unit safety thanks to the idea of Anitarf.

    Overall, this update is recommended for all users of the previous versions of MissileRecycler.
     
  10. BhoszKen13

    BhoszKen13 New Member

    Ratings:
    +0 / 0 / -0
    been wanting to find one of this. :))) helps me a lot with increasing pc perfomance with map. :DDD *like!*
     

Share This Page