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

[System] MissileRecycler

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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.

JASS:
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:
Going to do a full-revision of this:

JASS:
        //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:

JASS:
            local real x = GetRectMaxX(bj_mapInitialPlayableArea)
            local real y = GetRectMaxY(bj_mapInitialPlayableArea)

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

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

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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.
 
Level 6
Joined
Jun 20, 2011
Messages
249
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)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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.
 
Level 6
Joined
Jun 20, 2011
Messages
249
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
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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.
 
Level 6
Joined
Jun 20, 2011
Messages
249
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.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Ok, I found myself needing a dummy recycler and this is written horribly... so I am writing my own that is actually written well ; |.


#1, you shouldn't be using a queue for this
#2, you should be able to recycle dead units
#3, you should be recycling the units after a certain amount of time to allow death effects to recycle. Think of recycling as corpse decay =)
#4, you should be getting units that have the same facing as the target facing... you aren't doing that right now. This makes the facing stuff pretty pointless.
#5, you shouldn't be resetting the unit. Leave that up to the user. For example, Particle already does this, so it's pointless extra overhead. The only thing you should be changing is the facing and the x,y coords.

So because I'm writing a bunch of stuff and I can't wait for you to update this (which you'll likely never do anyways), I'm writing my own ;p.
 
#3 you should be recycling the units after a certain amount of time to allow death effects to recycle. Think of recycling as corpse decay =)

JASS:
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.

#2 you should be able to recycle dead units

This is a dummy unit recycler, not a unit recycler. Since most dummies have no corpse, you can't revive them so adding support for that would mean creating new units == ruins the point of the system.

But anyway, I think this system works fine as it is. =)
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
I see, interesting...

and killing the unit would not make it produce an effect ;o.


There is still the angle problem tho ;\.

edit
Also you need to use ShowUnit because even if the unit is off the screen with no model, wc3 renders it.

I created 7200 units. With all of them shown with no models at the edge of the map (couldn't see them), wc3 used 700 megs of memory and the game was unplayable. With all of them hidden, wc3 used 163 megs of memory and game ran at 64 fps as if they weren't even there.

Be a man and use ShowUnit, lol.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
The dummies are invisible (dummy.mdx file) so there is no problem there.

ShowUnit is pretty heavyweight function call.

I specifically said in the documentation that you can remove "resets" which you don't need.

Safety is there by default. Enthusiasts can remove the safeties if they are confident they won't break the resource, but I don't recommend it of course.

There is no problem with the angle, you change ANG_N to a higher number to increase the accuracy (8 is a little small). In my map I have ANG_N set to 12 and ANG_STORAGE_MAX set to 8.

A queue is being used so it's FIFO. Then I use timestamps to make sure units are not getting recycled while they are still turning or their effect's death animation is still playing. As purge said, the timestamp can be configured via DEATH_TIME.

Again there is no problem with the angle. I don't know where you are getting your ideas from.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
The dummies are invisible (dummy.mdx file) so there is no problem there.

yes there is.. Don't believe me, do a test for yourself. My comp ran 64 fps with like 40000 units at 200 megs of memory when I hid them. When I didn't hide them (edge of map etc, no models), it was using like 700 megs of memory and wc3 was running at 1 fps. If you aren't going to remove the units, then you better hide them. Projectile creation/destruction isn't the main issue... it's projectile movement and keeping projectiles in the background from hogging resources (which yours currently does) that is ;o. Every unit you have on the map that isn't hidden hogs precious resources. I plan to hide all units that aren't currently auto hidden in my own resources (DummyCaster, etc). Btw, my DummyRecycler loads up 7200 units and uses almost nothing and does not slow wc3 down at all. Loading up all of the units means that no new unit will ever be created : ).


Btw... how do you retrieve the target angle???

If you want an angle of 270, how does your system return a unit that is facing 270 degrees? And I don't mean SetUnitFacing, I mean the unit is already facing 270 degrees. Furthermore, how does it retrieve a unit facing closest to the desired angle? For example, if 30 degrees is passed in, it shouldn't return a unit facing 150 degrees.

I saw nothing in your resource that supported the above.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
But you're going to hide them after they expire right?

edit
had no problems getting death animation to run with a unit that was previously hidden

edit
This just in, pausing a unit also makes wc3 not use resources O-o.

How fast is pause unit?

edit
pause unit is really fast and makes it so that wc3 does not hog resources. Use pause unit instead ;o.

edit
here is memory usage for pause unit and hide unit

306,338 -> hide unit
353,360 -> pause unit

So paused units do consume more resources than hidden units, but PauseUnit is much, much faster. wc3 ran at 64 fps for both.

I'm going to limit them to the same count to see if the memory usage is still different. If it turns out to be the same, then that just means pause unit was able to create many more units than hide unit was able to do ;o.

edit
apparently, they are both truly the same count. Hide unit uses less resources than pause unit does =o. I'm starting to wonder now if you couldn't just make a projectile system with paused projectiles and then unpause them before removing the effect =o.

edit
BRILLIANT, EFFECTS RAN, TRIGGERS RAN, SET UNIT FACING RAN, everything ran!! woo ;D

keep all dummies paused!! O_O
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Nice find, Nestharus. I have added another mention in the credits for you for PauseUnit.

I disagree with caching a lot of units. I only support ANG_N * ANG_STORAGE_MAX dummy units and I recommend to users to keep the multiple of those less than 100. Though I suppose with PauseUnit coming into force I could even recommend up to 200.

Caching larger amounts of dummies is not advisable because not everyone has a lot of RAM. For example, I have 1GB of RAM and Windows 7 already consumes much of that. You have to consider who your target market is: warcraft 3 players. People with modern gaming rigs are less likely to play games that don't push their machines in the graphics range.

Also, I like handle ID offsetting when I make my own maps just because I don't ever hit the handle stack limit. If I pre-cached thousands of units then I'd be wasting my time trying to offset, running a huge risk of overflow.

This is how it works with the queues: I modulate the passed angle value to make sure the angle passed in is between 0 and 360, then I use truncating division by the value of 360 / ANG_N. I made sure each queue head points to the return of that truncating division and so each dummy unit which has that pre-cached facing angle is indexed to that queue and if the queue is empty I simply create a new unit. It works flawlessly. A higher ANG_N increases realism but requires more dummy units to fill each angle.

Update 1.2.1.0 - now has optional support for UnitIndexer, keeps all dummy units paused to consume fewer resources.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Currently I don't use a heap, for simplicity. If it were up to me I would reject your resource as soon as you write it, too.

I am tired of your uncompromising "never going to use someone else's library" nonsense. For what? You didn't even test or understand how my resource worked before "assuming" it doesn't work? Your choices can sometimes suck, man.

There has to be some better investment for you than to try to "show everyone up" all the time. Your resource re-writes are not something I am going to entertain for much longer.

I agree with your previous statement that the approve/reject system should be replaced by something more like Digg.com's approach, where things can get up-voted or buried. That way you can see how many people honestly don't want to see most of the stuff we publish let alone use it.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Ahem, you don't do balancing on the angles at all...


Also, that Projectile system I wrote is getting a rewrite. All projectile systems including it are written wrong. I came up with a good design for projectiles earlier today.

edit
you also don't do custom indexing of dummy units and you don't have a get dummy unit index function


The heap load balancing is very simple and it allows you to always deallocate to an angle with the least amount of units in it. When loading up a new unit, if the angle with the smallest amount of units has much less than the angle with the largest amount of units, some of the units from the large set are moved to the small set, thus keeping balance maintained and allowing you to never have to create units if you preload enough units. Never having to create new units saves on allocation speed. Btw, heaps have little overhead.

edit
figured out a better way to do it than a heap ;D
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
If you know there will be a lot of missiles used throughout most of the game, you can increase ANG_STORAGE_MAX to an overstocked value. This would accomplish the same thing.

I can implement a heap if it keeps you from trying to re-write this resource, and I have been meaning to do something like that for a while but I have not had time to think it through.

The dummy indexing could be useful. But I think this + particle would do that decently, don't you think?

Anitarf also wrote an xedummy implementation after he saw MissileRecycler which was better for him and does balance the dummy units across all facing angles, however it is o(n*2) or something as he uses recursive function calls.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Bribe, I actually came up with a great way to do the recycling super fast because I realized a behavior.

When I'm done with the lib, you can take what you want or w/e. Because you're always adding or removing one, you are only moving the thing 1 position. This means that you can use a linked list (as it's forwards or backwards).

For load balancing, you're moving many positions, but that's np as it's rare =).


I've been working on this lib for like 4 hours now =o.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Technically, it's still a "flavor" to balance the dummy units. If you don't balance, the system still works fine.

Here's what it does: when you recycle a dummy, the system first finds what angle the dummy is already facing. If that queue has room for the dummy unit, it puts it there. If not, then it searches to find the first available slot. If there are no available slots, the "recycle" variable will point to 0 and the dummy unit is removed, skipping the need to iterate over each queue head only to find out there is no hospitable queue.

Balancing the angles, if it can be done with o(1) or o(2) complexity, will only save you a handful of easy peasy loops anyway. This is the loop:

JASS:
                set i = ANG_N
                loop
                    set i = i - 1
                    exitwhen stackN[i] < ANG_STORAGE_MAX
                endloop

Since ANG_N should never be higher than 16, you get an average of 8 iterations within the loop (if the dummy's default queue is full or if all the queues are not full). It's not the best but it's a planetary trajectory from the worst.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Bribe, take a look at this =)

JASS:
    private struct List extends array
        integer count
        private thistype next
        private thistype prev
        
        static method operator first takes nothing returns thistype
            return thistype(0).next
        endmethod
        static method operator last takes nothing returns thistype
            return thistype(0).prev
        endmethod
        method forward takes nothing returns nothing
            local thistype node = next
            if (count > node.count) then
                set next = node.next
                set next.prev = this
                set node.next = this
                set prev = node
                set thistype(0).next = node
                set node.prev = 0
            endif
        endmethod
        method backward takes nothing returns nothing
            local thistype node = prev
            if (count < node.count) then
                set prev = node.prev
                set prev.next = this
                set node.prev = this
                set next = node
                set thistype(0).prev = node
                set node.next = 0
            endif
        endmethod
        method backwardMany takes nothing returns nothing
            local thistype node = prev
            loop
                exitwhen count_p < node.count
                set node = node.prev
            endloop
            set prev.next = 0
            set thistype(0).prev = prev
            set next = node
            set prev = node.prev
            set node.prev = this
            set prev.next = this
        endmethod
        static method add takes thistype this returns nothing
            set prev = thistype(0).prev
            set thistype(0).prev.next = this
            set thistype(0).prev = this
        endmethod
    endstruct

That is the list of list angles. The add just adds a new angle to the list. It is not the dummy units ;o.

Because count pretty much always changes by 1, the node will either go forward or backward. The sort is O(1) then. The only time that the count changes by more than one is when you are moving units from one node to another node because the difference between the two nodes is too great ^)^. This makes sure that all lists have around the same number of units in them ;p.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Honestly I was thinking of a different approach that would only need 1 or 2 methods. Basically a stack that stores the precise order of vacant dummy slots, so if I take two from 45 and one from 90, the stack would be: [0]=45, [1]=45, [2]=90. That way there is no need to find vacant slots but it is not exactly "balanced" though balancing is not always useful (for example if you are taking more dummies from one angle than another angle). This way you NEVER need a loop :D
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Here is my work in progress (untested, but looking good):

JASS:
library MissileRecycler initializer Init requires optional UnitIndexer /*

    MissileRecycler v 1.2.1.0
    =========================================================================
    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.

    =========================================================================
    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 = 16

    -   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 changing this be-
    cause at 16 resolution you will not be able to see the dummy unit turn
    (a higher ANG_N would be redundant, for example).

    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 200. 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 = 16             //# 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 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 group protect   = CreateGroup() //Used to prevent double frees.
        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 GroupRemoveUnit(protect, u)
            debug call Print("Recycling")
        else
            debug call Print("Creating new")
            static if LIBRARY_UnitIndexer then
                set UnitIndexer.enabled = false
            endif
            set u = CreateUnit(OWNER, DUMMY_ID, x, y, facing)
            static if LIBRARY_UnitIndexer then
                set UnitIndexer.enabled = true
            endif
            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
        if GetUnitTypeId(u) == DUMMY_ID and UnitAlive(u) and not IsUnitInGroup(u, protect) then
            if queueStackN == 0 then
                debug call Print("Stack is full - removing surplus unit")
                call RemoveUnit(u)
                return
            endif
            //Get recycled dummy node instance.
            set this = recycle
            set recycle = queueLast[this]
            //Get the last vacant queue 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
            //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)
        static if LIBRARY_UnitIndexer then
            set UnitIndexer.enabled = false
        endif
        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 stack[n] = CreateUnit(OWNER, DUMMY_ID, x, y, angle)
                call PauseUnit(stack[n], true)
                set n = n + 1
                exitwhen n == end
            endloop
            set queueNext[n - 1] = 0
            exitwhen i == 0
        endloop
        static if LIBRARY_UnitIndexer then
            set UnitIndexer.enabled = true
        endif
        call TimerStart(gameTime, 1000000., false, null)
    endfunction

endlibrary
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Does your structure use a FIFO-esqueue approach? I used to use arrays for it but I changed it to FIFO queues because of the timestamps, so there is some time for the dummy unit(s) to turn and to play their effect's death animation. It takes about 3/4 of a second for the unit to turn 180 degrees, for example, but it takes even longer to finish the death animation so that's why I use a 2 second recycling delay (configurable of course). The FIFO structure just makes it so that the timestamps are not necessary, but they are very good for a failsafe in more extreme circumstances.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Here it is...

It stays sorted and everything =o.


Decide what you want to do.

JASS:
library Dummy /* v1.0.0.0
*************************************************************************************
*
*   Allows one to create dummy units that are either at or are close
*   to the angle specified.
*
*   Dummy recycling minimizes the number of dummy units on the map while supporting near
*   instant SetUnitFacing.
*
*       Errors
*       ----------------------------
*
*           Any error will result in the system disabling itself and an error message
*
*           ->  May not kill dummies
*           ->  May not remove dummies
*           ->  May not attempt to recycle non dummies
*
*************************************************************************************
*
*   Credits
*
*       Thanks to Vexorian for dummy.mdx
*
*************************************************************************************
*
*   */uses/*
*
*       */ optional UnitIndexer     /*  hiveworkshop.com/forums/jass-functions-413/unit-indexer-172090/
*
************************************************************************************
*
*   SETTINGS
*
*/
globals
    /*
    *   The unit id of dummy.mdx
    */
    private constant integer DUMMY_ID = 'h000'
    
    /*
    *   The space between angles for the recycler
    *
    *   Angles used are angles from 0 to 359 in intervals of ANGLE_SPACING
    *
    *   Higher spacing means less units but lower accuracy when creating the facing
    *
    */
    private constant integer ANGLE_SPACING = 15
    
    /*
    *   Max projectiles per angle
    *
    *   Max projectiles is 360/ANGLE_SPACING*MAX_PROJECTILES
    */
    private constant integer MAX_PROJECTILES = 16//125
    
    /*
    *   Maximum difference between queue sizes
    */
    private constant integer LOAD_BALANCE = 16
    
    /*
    *   How much to delay before recycling dummy
    */
    private constant real RECYCLE_DELAY = 2
endglobals
/*
************************************************************************************
*
*   struct Dummy extends array
*
*       Creators/Destructors
*       ----------------------------
*
*           static method create takes real x, real y, real facingDeg returns Dummy
*           method destroy takes nothing returns nothing
*
*       Fields
*       ----------------------------
*
*           readonly unit unit
*
*       Operators
*       ----------------------------
*
*           static method operator [] takes unit dummyUnit returns thistype
*
************************************************************************************/
    private keyword Queue
    
    globals
        private unit array dummies
        private Queue dummyCount = 0
        private constant player DUMMY_OWNER = Player(15)
    endglobals
    
    function IsUnitDummy takes unit whichUnit returns boolean
        return dummies[GetUnitUserData(whichUnit)] == whichUnit
    endfunction
    
    private struct List extends array
        integer count
        thistype next
        thistype prev
        
        static method operator first takes nothing returns thistype
            return thistype(0).next
        endmethod
        static method operator last takes nothing returns thistype
            return thistype(0).prev
        endmethod
        method forward takes nothing returns boolean
            local thistype next = this.next
            local thistype prev = this.prev
            if (0 == next) then
                set next = thistype(0).next
                if (count - LOAD_BALANCE > next.count) then
                    set count = count - 1
                    set next.count = next.count + 1
                    
                    set this = next
                    set next = this.next
                    
                    if (count > next.count) then
                        set this.next = next.next
                        set this.next.prev = this
                        
                        set next.next = this
                        set this.prev = next
                        
                        set next.prev = prev
                        set prev.next = next
                    endif
                    
                    return true
                endif
            elseif (count > next.count) then
                set this.next = next.next
                set this.next.prev = this
                
                set next.next = this
                set this.prev = next
                
                set next.prev = prev
                set prev.next = next
            endif
            return false
        endmethod
        method backward takes nothing returns boolean
            local thistype next = this.next
            local thistype prev = this.prev
            if (0 == prev) then
                set prev = thistype(0).prev
                if (count + LOAD_BALANCE < prev.count) then
                    set count = count + 1
                    set prev.count = prev.count - 1
                    
                    set this = prev
                    set prev = this.prev
                    if (count < prev.count) then
                        set this.prev = prev.prev
                        set this.prev.next = this
                        
                        set prev.prev = this
                        set this.next = prev
                        
                        set next.prev = prev
                        set prev.next = next
                    endif
                    
                    return true
                endif
            elseif (count < prev.count) then
                set this.prev = prev.prev
                set this.prev.next = this
                
                set prev.prev = this
                set this.next = prev
                
                set next.prev = prev
                set prev.next = next
            endif
            return false
        endmethod
    endstruct
    
    private struct Queue extends array
        static timer time
        
        private real stamp
        thistype next
        thistype last
        
        private method backward takes nothing returns boolean
            return List(this).backward()
        endmethod
        private method forward takes nothing returns boolean
            return List(this).forward()
        endmethod
        private static method operator max takes nothing returns thistype
            return List.last
        endmethod
        private static method operator min takes nothing returns thistype
            return List.first
        endmethod
        private method operator count takes nothing returns integer
            return List(this).count
        endmethod
        private method operator count= takes integer count returns nothing
            set List(this).count = count
        endmethod
        static method add takes thistype dummy returns nothing
            local thistype this = thistype.min
            local thistype min
            
            set last.next = dummy
            set last = dummy
            
            call SetUnitFacing(dummies[dummy], (this - 1)*ANGLE_SPACING)
            set dummy.stamp = TimerGetElapsed(time) + RECYCLE_DELAY
            
            set count = count + 1
            if (forward()) then
                set min = thistype.min
                set dummy = this.next
                
                set min.last.next = dummy
                set min.last = dummy
                set next = dummy.next
                set dummy.next = 0
                
                call SetUnitFacing(dummies[dummy], (min - 1)*ANGLE_SPACING)
            endif
        endmethod
        static method pop takes thistype angle returns integer
            local thistype this = angle/ANGLE_SPACING + 1
            local thistype dummy = next
            local thistype max
            
            if (0 == dummy or dummy.stamp < TimerGetElapsed(time)) then
                return 0
            endif
            
            set next = dummy.next
            
            set count = count - 1
            if (backward()) then
                set max = thistype.max
                set angle = max.next
                
                set last.next = angle
                set last = angle
                set max.next = angle.next
                set angle.next = 0
                
                call SetUnitFacing(dummies[angle], (max - 1)*ANGLE_SPACING)
            endif
            
            return dummy
        endmethod
    endstruct
    
    struct Dummy extends array
        debug private static boolean enabled = true
        debug private boolean allocated
        
        static method operator [] takes unit dummyUnit returns thistype
            debug if (not enabled) then
                debug return 1/0
            debug endif
            return GetUnitUserData(dummyUnit)
        endmethod
        method operator unit takes nothing returns unit
            debug if (not enabled) then
                debug set this = 1/0
            debug endif
            return dummies[this]
        endmethod
        private static method getClosestAngle takes integer angle returns integer
            set angle = angle - angle/360*360
            if (0 > angle) then
                set angle = angle + 360
            endif
            return angle - (angle - angle/ANGLE_SPACING*ANGLE_SPACING)
        endmethod
        static method create takes real x, real y, real facingDeg returns Dummy
            local integer this
            
            debug if (not enabled) then
                debug set this = 1/0
            debug endif
            
            set this = Queue.pop(getClosestAngle(R2I(facingDeg)))
            if (0 == this) then
                set this = dummyCount + 1
                set dummyCount = this
                set dummies[this] = CreateUnit(DUMMY_OWNER, DUMMY_ID, x, y, facingDeg)
                call SetUnitUserData(dummies[this], dummyCount)
                call PauseUnit(dummies[this], true)
            else
                call SetUnitX(dummies[this], x)
                call SetUnitY(dummies[this], y)
                call SetUnitFacing(dummies[this], facingDeg)
            endif
            
            debug set allocated = true
            
            return this
        endmethod
        method destroy takes nothing returns nothing
            debug if (not enabled) then
                debug set this = 1/0
            debug endif
            
            debug if (0 == GetUnitTypeId(unit) or 0 == GetWidgetLife(unit) or not allocated) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, "DUMMY RECYCLER FATAL ERROR: ATTEMPTED TO RECYCLE INVALID DUMMY")
                debug set enabled = false
                debug set this = 1/0
            debug endif
            
            debug set allocated = false
            
            call Queue.add(this)
        endmethod
    endstruct
    
    /*
    *   Initialization
    */
    private module Init
        private static method onInit takes nothing returns nothing
            local unit dummy
            local integer blockAngle = 360/ANGLE_SPACING
            local integer angle = blockAngle*ANGLE_SPACING
            local List mz = 1
            local integer count = 0
            
            static if LIBRARY_UnitIndexer then
                set UnitIndexer.enabled = false
            endif
            
            set Queue.time = CreateTimer()
            call TimerStart(Queue.time, 604800, false, null)
            
            set List(1).prev = 0
            set List(0).next = 1
            loop
                set mz.next = mz + MAX_PROJECTILES
                set count = MAX_PROJECTILES
                
                set dummyCount = dummyCount + 1
                if (dummyCount == blockAngle) then
                    set dummyCount = dummyCount + 1
                    set blockAngle = blockAngle + ANGLE_SPACING
                endif
                set Queue(mz).next = dummyCount
                loop
                    exitwhen 0 == count
                    set count = count - 1
                    
                    set dummyCount.next = dummyCount + 1
                    set dummy = CreateUnit(DUMMY_OWNER, DUMMY_ID, 0, 0, angle)
                    set dummies[dummyCount] = dummy
                    call SetUnitUserData(dummy, dummyCount)
                    call PauseUnit(dummy, true)
                    
                    set dummyCount = dummyCount + 1
                    if (dummyCount == blockAngle) then
                        set dummyCount = dummyCount + 1
                        set blockAngle = blockAngle + ANGLE_SPACING
                    endif
                endloop
                set dummyCount.next = 0
                set Queue(mz).last = dummyCount
                
                set angle = angle - 1
                exitwhen 0 == angle
                set mz = mz + MAX_PROJECTILES
                set mz.prev = mz - MAX_PROJECTILES
            endloop
            set List(0).prev = mz
            set mz.next = 0
            
            static if LIBRARY_UnitIndexer then
                set UnitIndexer.enabled = true
            endif
            
            set dummy = null
        endmethod
    endmodule
    private struct Inits extends array
        implement Init
    endstruct
endlibrary

edit
ok... this'll take some pondering to see which implementation gives better results... hm...


Btw, I spent like 9.5 hours at this point on the above script ;o.

edit
still have logical errors, bleh
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
I have a few comments/questions about your approach:

- What is the purpose of having an owning player? I am interested if you have thought of something I haven't.

- I like the idea of using SetUnitUserData as a reverse-lookup sort of deal. But at this point shouldn't UnitIndexer be the one generating the indices?

- "R2I(angleR - angleR/360*360)" will be the same as R2I(angleR) because angleR is a real.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
What is the purpose of having an owning player? I am interested if you have thought of something I haven't.

If you shoot something like peasants as a wc3 missile, they will be colored using the owning player color. For example, peasants shot by a unit owned by player red will be red. This is just following wc3 standard behavior.

I like the idea of using SetUnitUserData as a reverse-lookup sort of deal. But at this point shouldn't UnitIndexer be the one generating the indices?

No. The indexes used on dummies are typically, if not always, different from the indexes used by other units. Furthermore, generating using UnitIndexer = fired triggers. Also, many dummies will suck up the indexes that would otherwise be used by other units. By making dummies use their own indexes, you can have many dummies w/o causing UnitIndexer to lose its max index count ; ). The primary thing a dummy unit can be used for is retrieving a particle given a unit.

"R2I(angleR - angleR/360*360)" will be the same as R2I(angleR) because angleR is a real.

That is.. correct ;p, my bad ;D.

edit
Also, my approach has slower creation/destruction times, but it will never end up creating a new unit unless you have run into the max projectiles. This means that overall, it is likeliest to use less units ^)^. However, your approach should work as it keeps the final counts of all of the queues equal. Then again, if one queue becomes empty, you end up creating/destroying units, which isn't good ;o. You have to remove the units because your queues don't remain balanced. This approach can keep any units that it makes. Because its queues are balanced, the necessity to create new units means that the map is using more projectiles than the limits allowed in the settings ;p.

See, there are pros and cons to both approaches, which is why this makes it difficult to decide ; |.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Ok, so for the rare circumstance you actually need to change the player color, I recommend the user to change the owner manually. No reason to have it on for all missiles.

You also need to use ModuloInteger, not the one line x - x / y * y, because Atan2 returns -PI to PI and ModuloInteger has negative-value protection which yours is lacking. So for half the time you will get accurate results, and the other half the time you will get errors.

The way MissileRecycler works with dummies is that it creates a new unit when the queue responding to the desired facing angle is empty. You can't simply draw from an irrelevant queue.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Yes, I know ; ). However, because your queues always keep w/e balances they have, you can't keep any unit you create or it'll result in unbalanced queues. That's the point I was making : ).

Also, the modulo integer thing works just fine. -360--360*50/50 == -(360-360*50/50), which is modulo. You just get the negative answer, and I handle the negatives.

I'll get rid of the set player color stuff ;p.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
-180 degrees is the same as 180, so no, the BJ does not break it. In fact the modulo operator works exactly the same as the BJ in most languages I know of save for Python. I have tested it pretty extensively by spamming right-clicks from the unit's position to the position he was ordered to. New units are eventually created if I am clicking in the same spot all the time, and the debug messages print exactly what you'd expect.

But I see what you're suggesting by balancing the trees now. For example if one queue is nearing depletion and the other queues are full, maybe it's time to shift units from a nearby angle and add a bit to its timestamp just in case.

I like your thinking on it. A balanced stack of queues would be ideal and not just a flavor.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
JASS:
   function ModuloInteger takes integer dividend, integer divisor returns integer
       local integer modulus = dividend - (dividend / divisor) * divisor
       // If the dividend was negative, the above modulus calculation will
       // be negative, but within (-divisor..0).  We can add (divisor) to
       // shift this result into the desired range of (0..divisor).
       @if (modulus < 0) then
           set modulus = modulus + divisor
       endif@
       return modulus
   endfunction

It doesn't do -angle, it does angle + 360. Like I said, the BJ doesn't break it. ModuloInteger is the perfect function for this. Look at what Anitarf did, for example, when ModuloInteger would have simplified it infinitely:

JASS:
            loop
                exitwhen face>0.0
                set face=face+360.0
            endloop
            loop
                exitwhen face<360.0
                set face=face-360.0
            endloop
 
Top