1. Join Texturing Contest #30 now in a legendary battle of mythological creatures!
    Dismiss Notice
  2. The Aftermath has been revealed for the 19th Terraining Contest! Be sure to check out the Results and see what came out of it.
    Dismiss Notice
  3. Melee Mapping Contest #3 - Results are out! Congratulate the winners and check plenty of new 4v4 melee maps designed for this competition!
    Dismiss Notice
  4. The winners of our cinematic soundtrack competition have been decided! Step by the Music Contest #11 - Results to check the entries and congratulate the winners!
    Dismiss Notice
  5. Check out the Staff job openings thread.
    Dismiss Notice

[System] MissileRecycler

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

  1. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    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 have saved over my original test map data so I will have to rebuild the original tests I was using with this resource one day. Until then, I can provide a simple map for you, upon request, if you want to C&P the object editor data and/or model from that.

    Code (vJASS):

    library MissileRecycler initializer PreInit requires optional UnitIndexer, optional UnitDex, optional UnitIndexerGUI /*

        MissileRecycler v 1.4.5.2
        =========================================================================
        Credits:
        -------------------------------------------------------------------------
        Written by Bribe
        Vexorian, Anitarf and iNfraNe for the dummy.mdx model file
        Nestharus for the Queue data structure and for finding that paused units
            consume very few CPU resources.

    =========================================================================
        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 how serious it is.

        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 before this. Since then,
        MissileRecycler has inspired Anitarf to update XE with the same angle-retaining
        capability and Nestharus has created Dummy. Considering the facing angle is
        important because it takes 0.73 seconds for the unit to turn around,
        which - when overlooked - 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.

        With default settings and the 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 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 real DEATH_TIME = 2.0 //seconds

        - Should not be less than the maximum time a death animation needs to play.
        Should not be lower than .73 to ensure enough time to turn.
        Should not be too high otherwise the dummies will take too long to recycle.

        constant integer ANG_N = 8

        -   How many different angles are recognized by the system. Don'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, and 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 result in too high of a dummy population.
            In general, I advise that the product of ANG_N x ANG_STORAGE_MAX does
        not exceed 100 or 200. More than that is excessive, but you can
        hypothetically have it up to 8190 if Warcraft 3's memory management
        were better.
     
            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. In the 1.4 update, preloading is done 0 seconds
        into the game to ensure any Indexers have already initialized.

        private function ToggleIndexer takes boolean flag returns nothing
        -   Put what you need in here to disable/enable any indexer in your
        map. if flag is true, enable indexer. If false, disable.

        =========================================================================
        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" ufoo 0 utyp "" 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(bj_PLAYER_NEUTRAL_EXTRA)  //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

        private function ToggleIndexer takes boolean flag returns nothing
            static if LIBRARY_UnitIndexer then
                set UnitIndexer.enabled = flag
            elseif LIBRARY_UnitIndexerGUI then
                set udg_UnitIndexerEnabled = flag
            elseif LIBRARY_UnitDex then
                set UnitDex.Enabled = flag
            endif
        endfunction

        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 real array timeStamp   //Prevents early recycling of units.
            private integer array queueNext
            private integer array queueLast
            private integer recycle = 0
            private timer gameTime  = CreateTimer() //Used for visual continuity.
            private integer array queueStack
            private integer queueStackN = 0 //Used to avoid searching the queues.
        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
                //Recycle this index
                set queueLast[this] = recycle
                set recycle = this
                //Add queue index to available stack
                set queueStack[queueStackN] = i
                set queueStackN = queueStackN + 1
                //Old unit will return as new
                set u = stack[this]
                call SetUnitFacing(u, facing)
                call SetUnitUserData(u, 0)
                //Reset the dummy's properties.
                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 call Print("Recycling")
            else
                debug call Print("Creating new")
                call ToggleIndexer(false)
                set u = CreateUnit(OWNER, DUMMY_ID, x, y, facing)
                call ToggleIndexer(true)
                call PauseUnit(u, true)
            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
            if GetUnitTypeId(u) == DUMMY_ID and UnitAlive(u) and GetUnitUserData(u) != -1 then
                if queueStackN == 0 then
                    debug call Print("Stack is full - removing surplus unit")
                    call UnitApplyTimedLife(u, 'BTLF', DEATH_TIME)
                    return
                endif
                //Recycle this
                set recycle = queueLast[this]
                //Index the dummy unit to an available facing angle.
                //Get the last vacant angle index.
                set queueStackN = queueStackN - 1
                set i = queueStack[queueStackN]
                //Enqueue this
                set queueNext[queueLast[i]] = this
                set queueLast[i] = this
                set queueNext[this] = 0
                //Allow a time barrier for the effect to destroy/turn to complete.
                set timeStamp[this] = TimerGetElapsed(gameTime) + DEATH_TIME
                set stack[this] = u
                call SetUnitFacing(u, i * ANG_VAL + ANG_MID)
                call SetUnitOwner(u, OWNER, false)
                //Prevent double-free of this unit.
                call SetUnitUserData(u, -1)
            debug else
                debug call BJDebugMsg("[MissileRecycler] Error: Attempt to recycle invalid unit.")
            endif
        endfunction

        //=======================================================================
        // I didn't need this function after all
        //
        function RecycleMissileDelayed takes unit u, real r returns nothing
            call RecycleMissile(u)
        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)
            local unit u
            call ToggleIndexer(false)
            loop
                set i = i - 1
                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 u = CreateUnit(OWNER, DUMMY_ID, x, y, angle)
                    set stack[n] = u
                    call PauseUnit(u, true)
                    call SetUnitUserData(u, -1)
                    set n = n + 1
                    exitwhen n == end
                endloop
                set queueNext[n - 1] = 0
                exitwhen i == 0
            endloop
            call ToggleIndexer(true)
            call TimerStart(gameTime, 1000000., false, null)
            set u = null
        endfunction

        private function PreInit takes nothing returns nothing
            static if LIBRARY_UnitIndexerGUI then
                call OnUnitIndexerInitialized(function Init)
            else
                call Init()
            endif
        endfunction

    endlibrary
     
     
    Last edited: Apr 17, 2018
  2. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    Made an update to address unit-type safety and removed the useless Show/Hide unit features.
     
  3. Magtheridon96

    Magtheridon96

    Joined:
    Dec 12, 2008
    Messages:
    6,007
    Resources:
    26
    Maps:
    1
    Spells:
    8
    Tutorials:
    7
    JASS:
    10
    Resources:
    26
    Going to do a full-revision of this:

    Code (vJASS):
            //Misc vars
            private unit newUnit = null    //Returning a local unit leaks the handle index.
            private unit array stack       //Used as a 2-D array of dummy units.
            private integer array stackN   //Used to track multiple indices for the 2-D array.
            private code expireCode = null //Prevents trigger evaluations or cloned functions.
        endglobals


    If you'd remove the
     = null
    at the end, you'd be increasing efficiency and have no effect on the code.
    newUnit isn't used anywhere (except in one function, but you're setting it before that, so it shouldn't cause any thread crashes)

    call SetUnitFlyHeight(newUnit, z, 0)

    I'm not really sure about this. Maybe you should mention that the given Z should be the distance from the ground and not from the 0-height base of the map.

    Other than that, GetRecycledMissile is an excellent function.

    IAbsBJ

    D:
    I know inlining it won't make much of a difference, but the red color is evil :3

    local integer this = recycle[0]


    You just gave me an idea. ;D

    BJDebugMsg

    More evil red color D:

    Code (vJASS):
                local real x = GetRectMaxX(bj_mapInitialPlayableArea)
                local real y = GetRectMaxY(bj_mapInitialPlayableArea)


    You can add static ifs and make WorldBounds an optional requirement.

    Code (vJASS):
    if GetUnitTypeId(u) == DUMMY_ID then
                //Reset the dummy's properties
                call SetUnitVertexColor(u, 255, 255, 255, 255)
                call SetUnitAnimationByIndex(u, 90)
                call SetUnitScale(u, 1, 0, 0)
                call Recycle(u, GetAngleIndex(GetUnitFacing(u)))
            debug else
                debug call BJDebugMsg("[MissileRecycler] Error: Attempt to recycle invalid unit-type.")
            endif


    You can make that comparison debug-only to ameliorate that function's speed :p (I got a 100/100 on my vocabulary test today ;D)

    One final comment:
    You should ask the user to modify the object merger line so that the model would have a correct path.

    edit
    Other than that, this is a pretty good library.
    I'm looking forward to implement this into a map and use it for a projectile
    system and claim that I'm the sole writer of this system and whatever Bribe says is a lie. xD
     
  4. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    The code is more readable keeping the BJ's. Inlining IAbsBJ is a waste of map size and if you care about efficiency in DEBUG_MODE you're doing something wrong.

    Debug mode should also have the same code results, its only addition should be displaying error messages. Safety is important and most people don't even know what debug mode is.

    If you want the speed you have to manually edit the resource to remove the safety, but because it's a public resource the safety should be "on" by default.

    However, I have removed the = null parts because they were in fact very useless.

    I have updated the documentation that says the flying height is not absolute but relative to the ground.

    Also, world bounds is completely unnecessary. locust units can't be enumerated and I could even create the units at 0,0 but I felt it in better interest to create them off at a corner.
     
  5. Magtheridon96

    Magtheridon96

    Joined:
    Dec 12, 2008
    Messages:
    6,007
    Resources:
    26
    Maps:
    1
    Spells:
    8
    Tutorials:
    7
    JASS:
    10
    Resources:
    26
    You're right ;P
    I always edit people's systems and optimize them anyways xD
     
  6. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    I have updated this to fix a couple issues with the angle the dummies were supposed to be already facing. When I was testing I found that the dummies were definitely taking longer than 0.09 seconds to turn and I knew something was amiss, so I have fixed it now.
     
  7. Axarion

    Axarion

    Joined:
    Sep 30, 2009
    Messages:
    675
    Resources:
    1
    Spells:
    1
    Resources:
    1
    Thanks.

    Well this looks really nice and was really needed to be done. Keep the good work up!
     
  8. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    Updated to use normal TimerUtils interface while Vexorian is updating.
     
  9. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    Approved.

    If you disagree, please reply with your argument.
     
  10. Dirac

    Dirac

    Joined:
    Jun 20, 2011
    Messages:
    249
    Resources:
    3
    JASS:
    3
    Resources:
    3
    The facing real should take radians instead of degrees
    The speed gain is barely noticeable.
    Recycling missiles that had special effects attached to them cause this to malfunction (the destruction of the effect is visible)
     
  11. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    Have you tested this with the proper object merger dummy? It works 100% for me.
     
  12. Dirac

    Dirac

    Joined:
    Jun 20, 2011
    Messages:
    249
    Resources:
    3
    JASS:
    3
    Resources:
    3
    I edited the post, i messed up something
     
  13. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    Typically I've wanted to show the exploding effect of the missile anyway. A projectile system should wait for the effect to complete before recycling the unit as it is, or if he wanted to use both he could hide the unit during that time and then recycle it (so the next unit won't show that effect).

    ShowUnit is apparently a very heavy operation as well, which is why I omitted it from the system.

    The speed difference is pretty strong. Check Anitarf's benchmarks on wc3c (he benchmarked this very resource against createunit/removeunit). IIRC it is at least 100% faster (most overhead due to the timer).

    Taking degrees is faster for performance because I index by degrees. I could make a wrapper function that takes radians though.
     
  14. Dirac

    Dirac

    Joined:
    Jun 20, 2011
    Messages:
    249
    Resources:
    3
    JASS:
    3
    Resources:
    3
    I benchmarked this with my Missile system creating a new missile every 0.005 while another missile dies as well, there wasn't a speed difference but it did created conflicts with the effect attached to the missile (new missiles displayed the previous effect's destroy animation)
    And since JASS users always work with radians this should take them and convert them on it's own, it has nothing to do with speed performance, after all, the user has to convert it to degrees before giving it to this function
     
  15. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    Unless the user was already working with degrees, for one reason or another. It'd be a wrapper that converts it for people who are working with degrees. I seriously will not make it radians-only.

    You would need to provide benchmarks if you want to show anything. The benchmarks which have been posted onto wc3c.net already prove what yours doesn't.
     
  16. Dirac

    Dirac

    Joined:
    Jun 20, 2011
    Messages:
    249
    Resources:
    3
    JASS:
    3
    Resources:
    3
    After some tests this resource seems to add 3 or 4 fps to my Missile library, however it really creates issues with destroyed effects, you should increase the time a dummy has to wait before it can be recycled, or at least make it configurable.
    Most missiles will display the previous missile's destroy effect if they're created close to it's destruction.
     
  17. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    I could add a "RecycleMissileEx" which takes a duration to wait before recycling.
     
  18. Dirac

    Dirac

    Joined:
    Jun 20, 2011
    Messages:
    249
    Resources:
    3
    JASS:
    3
    Resources:
    3
    Please do.
     
  19. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    I'm planning to update this so it no longer requires dynamic timers. The recycling structure would be somewhat similar to what Anitarf did with xeDummy but will be able to protect a missile from being put back in before finishing its facing angle or before finishing its attachment's death animation (I think 2 seconds should be enough to protect against showing the death animation).
     
    Last edited: Mar 15, 2012
  20. Bribe

    Bribe

    Joined:
    Sep 26, 2009
    Messages:
    7,757
    Resources:
    25
    Maps:
    3
    Spells:
    10
    Tutorials:
    3
    JASS:
    9
    Resources:
    25
    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.