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

Level 31
Joined
Jul 10, 2007
Messages
6,306
oh doh, lolz, w/e ; P


it's 6 am atm, I've been up all night (and the night before, and the night before, and the night before), so my brain is slightly fried atm ;D

4th all nighter ftw, or is it #5? >.<

edit
Ok, I came up with an implementation idea that is even faster ^^ (my current implementation idea failed).


#1: array of lists of nodes represented by value angle
#2: array of lists of lists represented indexed by count list sizes

count will always be in a range defined by how much smaller/bigger your counts can be. From here, you can update min/max pointers (pretty easy). Now you'll be able to retrieve any list while keeping everything sorted ; P. Yes, I'm still awake as I've been trying to figure out a good way to do this all night =O. Now that I've figured out how to do it, I'm going to bed ;D. I'll implement it when I get up ^_^.

I really don't care who submits it, I just want a good dummy recycler.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
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.
    Anitarf and Nestharus for coming up with the idea of having a balanced
        queue of units instead of just putting the dummies in any available
        queue. This was not my idea but it's a really good strategy.

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

    =========================================================================
    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 qlFill
        private integer array qlNext
        private integer array qlPrev //Used to keep the queues sorted.
    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
    
    private function TrySwap takes integer a, integer b, boolean forward returns nothing
        local integer prev
        local integer next
        if qlFill[a] > qlFill[b] and (not forward and a != qlPrev[ANG_N]) or (forward and b != qlNext[ANG_N]) then
            //Swap next/prev pointers for both nodes.
            set next = qlNext[b]
            set prev = qlPrev[a]
            set qlPrev[next] = a
            set qlNext[prev] = b
            set qlNext[a] = next
            set qlPrev[b] = prev
            set qlPrev[a] = b
            set qlNext[b] = a
            //Recursively swap until the list is balanced.
            if forward then
                call TrySwap(a, next, true)
            else
                call TrySwap(b, prev, false)
            endif
        endif
    endfunction
    
    //=======================================================================
    // 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
            //Lower the queue's position in the list if it's not already the
            //lowest.
            set qlFill[i] = qlFill[i] - 1
            call TrySwap(qlPrev[i], i, false)
            //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 = recycle
        if GetUnitTypeId(u) == DUMMY_ID and UnitAlive(u) and not IsUnitInGroup(u, protect) then
            if this == 0 then
                debug call Print("Stack is full - removing surplus unit")
                call RemoveUnit(u)
                return
            endif
            //Put the dummy in the least-filled queue.
            set i = qlNext[ANG_N]
            //Raise the queue's position in the list if it's not already the
            //highest.
            set qlFill[i] = qlFill[i] + 1
            call TrySwap(i, qlNext[i], true)
            //Get recycled dummy node instance.
            set recycle = queueLast[this]
            //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 qlNext[i] = n
            set qlPrev[n] = i
            set qlFill[i] = ANG_STORAGE_MAX
            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
        set qlPrev[0] = ANG_N
        static if LIBRARY_UnitIndexer then
            set UnitIndexer.enabled = true
        endif
        call TimerStart(gameTime, 1000000., false, null)
    endfunction

endlibrary
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
nop, not good. I came up with a better algorithm earlier, which is the array of stacks idea.

array[total count of list] = first

add the lists to the dif counts. From here, because the lists will always be close to each other w/ 0 holes, u can have min/max vars ; ).


The algorithm you have up is like a bubble sort, lol... see, I realized that you had to do iterations earlier for most cases, hence why I changed it =o.

If you need to do more than O(1) for the sorting, then the algorithm is no good : ).


I'm still working on the new one, but it's still buggy
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
edit
I added stuff to this post, so be sure to take a look at the new stuff ;p
end edit

Too bad it's still bugging out for you. Your explanation of it makes sense though - but the array will often have holes I think. But it's smart to index by count and angle. Very good thinking.

If the allowed variance in the queues is >1, then it will have holes, you are correct. If it's 1, then it will never have holes (which is why I talked about benching SetUnitFacing).

There is one other interesting thing... you can put the active counts into a list. If you have a variance of 16, then your list size will be a maze of 16. This will allow you to get the next active count = O.

For this list, every value will be unique, meaning that the list will only ever at most swap 2 nodes. You won't have one node magically go up. The reason that the swapping doesn't work that way for my old implementation was because the values weren't unique, so you could have 125 125 125, then get 126 125 125 and have to move the 126 to the end ;o.

So anyways, the list of counts will allow you to have holes, the array of lists referenced by count will allow you to retrieve the min/max stuff for recycling, and the array of queues referenced by the angle will allow you to retrieve a dummy given an angle.

That is the final design >: O. Not bad if I do say so myself, pure O(1) operations ; ).


If you want to code it all and save me the trouble of doing it, then let me know. I'm personally just not in the mood to work with dummy recyclers anymore after working on it so long yesterday and through half of today : |. Just make sure that it's good, and I will pound it into perfection as I will be needing it for the projectile system I'll be doing.

Also, please use this for comments
JASS:
/*
*  comment
*/

Rather than this
JASS:
//comment

The first one is easier to read. Also, your docs are very hard on my eyes. tbh, I started working on the dummy recycler w/o even reading through a lot of your stuff primarily because I didn't like your docs. You have a huge useless preface that I and other people using your resource don't care about.


how does this help me use dummy recycling in any way???
JASS:
    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.

It doesn't, it's just useless information. Put that into the post so that people who want to just use the resource can have easy and direct access to the API. Keep the docs in the code uncluttered please ; ). Uncluttered docs are just so much easier to get through.

This, as I'm sure you know, is unsafe and isn't really acceptable in public resources anymore
//! 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

The user will have to import the model from the map anyways, so might as well just keep it all in a map. Can also do a small Lua generation snippet if you want ;o.

And really, all of your protection stuff in there... what I might do is write my own version with debug only protection that supports your API so that people can pick between whatever they want. You can also support my version's API ; |. I think that's the best scenario. Really, we're doing it two very dif ways and both have dif things we want our resources to do ;p. I like mine to be as optimal as possible and disable it and crash it if any errors occur. You want to just protect against all errors regardless w/o crashing it and so on so that the map can just limp its way through =p. They are just two different styles = ).


So anyways, I think that that is the best approach at this point. Also, what you could do is write the resource and then I could modify it up a bit and we could have two versions on this thread, one with my style stuff and one with yours. They would both share the same API, that way a user could pick whichever one they wanted. They could also have dif style docs if you want to keep with your current doc style (I seriously can't handle that style).

In fact, this might be a good solution for UnitIndexer as well. You want hook RemoveUnit right (even though it'll cut the speed down by quite a lot as it'll run through 3x rather than 2x). Yea, this might be a good approach to take for resources from now on. As long as they share the same APIs, no worries. If people prefer different style APIs, then as long as they all support all of the styles, it's all good ^_^.


edit
As for the projectile system debate, all current projectile systems, including the one I have up, are written incorrectly and should be terminated once I get a correctly designed projectile system up (hence the reason I started working on a DummyRecycler ^^). They all have very serious design flaws. The reason I rewrote all of my damage stuff was because my damage stuff, and all other damage stuff, had serious design flaws as well =O. Now look at damage, it's designed beautifully. It uses the same design (or possibly a better design) than top commercial games ;D. And yes, Purge's stuff was also written incorrectly.

And btw, I know you may not like all of the rewrites, but very few people are willing to work with me on rewriting their code. Most people say good enough. This is the reason I do the rewrites, because good enough is not good enough for me ; ). For example, Dirac's damage stuff was good enough for most people, but to me it looked like absolute garbage. Purge's stuff seemed good enough for most people, but I didn't like his lib either. Dirac's projectile system seems good enough for most people, but to me it just looks like a heavyweight monster that's going for the world championship.. you obviously couldn't run 2D projectiles effectively on it... furthermore, it isn't very flexible at all >.<. You get what you get with it. This means that if you wanted 2D projectiles, u'd need a 2D projectile resource. If you wanted 3D, then you'd need a whole separate resource... and it just goes on and on... this is why I try to make it so that my own resources are highly flexible. It means that #1, you can easily extend off of them to add your own functionality to them. #2, you don't have to add band-aids to get your own functionality in (resulting in a jumble of unmanageable spaghetti code). I do realize that most of my older resources, like Unit Indexer and Unit Event, are like this, but I have been getting away from the business of spaghetti code ever since vex fixed his optimizer ; ). Also, if you notice, I continue to change my doc styles and comment styles with the goal of making them as easy to read as possible ^_^.

So anyways, back on topic... getting a good dummy recycler with O(1). Two flavors, one version with safety always on (your style) and one version that disables itself and crashes thread with debug only safety (my style) ^_^. We are obviously never going to agree on the safety styles, which is why two versions are necessary. You'll likely never use my style and I'll never use your style, so just have two versions to support both styles ;o. Also, a function API + struct API. I personally like the API I did better, and I'm sure that you like your API more, so support both. Furthermore, you are still missing some functions in your API, like IsUnitDummy and what not ; ). Also, I personally like to pass in the unit index of the unit whereas you like to pass in the unit. In my code, I'm always working with indexes, not units, so it's just easier for me to pass in an index. In the resource itself, I'm always working with indexes, so it's easier to take an index. Passing in the unit handle just results in more GetUnitUserData calls and more array reads in order to convert the index back into a unit to be passed into the function. To support both styles, you'd have wrapper functions that take units but call the other functions that take indexes ; ). This makes everyone happy.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Just don't forget the two styles ; ). I know most users won't care about them, but the more advanced users will care ;o. For example, some users prefer safety on at all times and others prefer debug only with thread crashing. The thread crashing helps users figure out where the error occurred (so that the map doesn't continue spamming error messages).

What I'm expecting to happen is that we'll keep passing the resource back and forth between us until we get something that we both like ^)^. I know for one that I don't think you should have any groups in this ; ).
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
I went ahead and submitted my resource since I think that it's unfair for you to take all of my work. The only thing I took from you in that resource was the idea to return a dummy based on the inputted angle. I took 0 of your code and 0 of any of your other ideas. For you, you'd be taking my designs, my ideas, my research, etc, etc ; |. It just seems unfair considering I put like 22 hours of work into all of that. The main work was not the coding but coming up with the design... Notice that I went through several different designs before coming to the one that is being used in the resource I submitted.

Designing a good way to implement an idea typically takes much more work than coming up with that idea.


Because you are personally involved with these resources, another moderator is needed to decide which resource to keep and which to not keep. Obviously, I believe that yours should be graveyarded and mine should be kept, and you can just go cry in a corner if you are against that. Taking someone else's work is just not cool.
 
Last edited:
Level 1
Joined
Aug 24, 2010
Messages
1
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.


I am sort of at ends on how to post on something as horrendous as this post. It has been quite some time since I have done WC3 modding but I still avidly discuss coding with Nestharus.

His resolve to never use another person's library is not something that should remotely be looked down upon. In coding it is always a competition to see who can create a better version of a resource. This is applicable to any real world scenario as well.

None of the resources Nestharus has written was code written by someone else. In the case of this resource he saw it as an idea that he could use to create his OWN version. As it turns out almost this entire thread is him explaining his methods to you with you trying to take them and implement them as your own ideas.

Nestharus has no reason to credit you for the same reason that when I write my own Age Verification scripts I do not credit the 900 others that have been made. It was my code not theirs. It is his own unique code not yours.

From reading this thread it is clear he has a right to submit his own resource since it is all his own code and it is actually you who is trying to use his resource inside of your own flawed one.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Ahem, I did use one aspect of his code, and that was the idea of using timestamps for recycling rather than timers ^)^. That was a good idea, so I took it. I just added credit to my submission for that idea as the implementation was the same.

I did not add credit for the idea of recycling dummies with unit facing as the implementations for that were drastically different. I only added credit for implementations that I took from him, and the only one I took was the timestamp one. He may say that I'm using queues as well, but the idea to use queues did not come from his resource or from his suggestions, which is why he isn't getting credited for their use.


And no, I am not attempting to one up his resource because of some ego. I need a Dummy Recycler and I refuse to use his because his implementation is bad. Him changing his implementation to my implementation is ofc messed up to say the least.

edit
btw, thanks Phtes for talking some sense into me about him stealing my work.


edit
If you support Dummy's API and fix the angle stuff and then give credit for all of the implementations of mine, then we could have both resources up with my 2 styles idea (your safety style and my safety style). Just please be sure to give credit where credit is due as I spent a long time on that design. Also, you could keep it as the round robin design, add support for Dummy's API, and then keep it as your own stuff as the round robin thing was yours =). From here, we'd have two different dummy recyclers with two different behaviors and pros/cons to each one =). Ofc, we'd really need to weigh the pros/cons of the round robin style vs the other style as I don't know if the extra unit creation/destruction is worth it.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Nestharus I'm going to have to assume you are ip spoofing and trolling me. You can implement a better model all you want but the key elements that provide UTILITY here were designed by myself and the queue idea was anitarfs. I spent a lot of effort coining up with the original implementation and making sure it didn't bug, I don't care how much time you spent on your rewrite of my resource you are notorious for not giving credit and this is just another example of it. I will ask ralle for a different approval system like you asked for, because you're using a lot of really dirty tactics.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Phtes is one of my friends that used to mod. I asked him for his advice on the matter on msn.

And btw, you can look in the docs. I've already given you credit for everything that I used from your implementation.

edit
And btw, I think that you are being slightly crazy right now. We're both in chatroom waiting for you, but I don't know how much longer he'll stay on =o, so hurry up >.<

edit
Also, I know that you put a lot of effort into this, which is why I was being nice and so on, I just found it unfair for you to take my implementations, which is again why I asked Phtes for advice on msn.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
It's easy to log in twice on the chat room as well, I really just can't take you seriously. Especially not with this guy having only 1 post. How long did it take for you to add TheDamien to the credits of ASCII? But with Cohadar's JassHelper we can graveyard the newer implementations anyway. But that won't happen because I would want to graveyard a lot of your rewrites if I were thinking about it objectively and not siding with you so much of the time.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
The new version has new functions that Cohador's version does not ;\, which is why I didn't call for it to be gy'd.


And seriously Bribe, you are being crazy. Everyone in chatroom is waiting for u.

edit
btw, using 2 accounts is an offense that you can be banned for (it's very serious). IF you seriously think I am using two accounts, then contact Ralle about it.
 
2 accounts?

Laughing.png


Lembidi has at least 4 excluding his account (Lembidi) that I know of, and possibly more.
 
Maybe this could utilize DummyUnitStack? Unless there's something I'm missing.

Also, where's there ever a discussion about libraries requiring off-site resources (Nestharus -.-). I suppose we link wc3c scripts, so it shouldn't matter too much I guess.

EDIT: Apparently the only reason it's required is because ofset UnitIndexer.enabled = false

If you want to include UnitDex the syntax isUnitDex.Enabled=false
 
Last edited:
I personally would remove the print function and just inline the debugging messages. It's only one line. I understand it was easier for you to code though.

JASS:
set bj_lastCreatedUnit = u
set u = null
return bj_lastCreatedUnit

I forgot. What was the consensus on using globals like bj_lastCreatedUnit?

Are you trying to prevent a leak or something?

Regardless this can be approved.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
The bj_lastCreatedUnit is just a free global that I used to prevent the un-nulled handle leak.

As far as the 'print' function I find it a good programming practice. Let's say you want to change the color of that particular debug message or change a prefix. Rather than searching through all of your debug messages and making that change you can make it from just one function. Since debugging is 95% of the work with code, it works well.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
You should move the following from RecylceMissle to GetRecycledMissile.
The reason is, that if you scale a dummy unit and attach a special effect to it
the effect will also scale to let's say 0.5 of it's original size.
If you Recycle the dummy unit you re-scale unit and effect during the effect death time.

JASS:
            call SetUnitVertexColor(u, 255, 255, 255, 255)
            call SetUnitScale(u, 1, 0, 0)

I mention this, because I really like MissileRecycler, since you avoid fireing onIndex/onDeindex events unlike Nestharus Dummy does.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
If you like having duplicated code, go for it ;)

I am well past the point where that degree of efficiency matters. I really like ExecuteFunc as it saves from having to create a trigger, actions, etc. and ExecuteFunc also doesn't require code to be above the calling function. It's probably my favorite native in JASS because of it.

Many of my GUIJASS resources would be impossible were it not for ExecuteFunc, as I'd have to resort to converting the entire trigger to JASS otherwise:

http://www.hiveworkshop.com/forums/...dexer-1-2-0-2-a-197329/?prev=status=a&u=Bribe
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,454
Ok, current WIP. Highlights:
- Uses UnitUserData instead of a unitgroup for double-free protection.
- Now easily works with any Unit Indexer, including my GUI system thanks to the new UnitIndexerGUI library
- No longer needs a loop to find which angle to index a dummy to.
- Still doesn't balance the angle queues but I really don't want this resource to be 3x as long just for that one minor, situational benefit. Allocation and deallocation are extremely fast this way, and it hardly needs any variables.


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

    MissileRecycler v 1.4.0.1
    =========================================================================
    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 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 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" ubui "" uhom 1 ucol 0.01 umvt "None" umvr 1.00 utar "" uspa "" umdl "war3mapImported\dummy.mdl" umxr 0.00 umxp 0.00 ushr 0 uerd 0.00 udtm 0.00 ucbs 0.00 uble 0.00 uabi "Aloc,Amrf"

    //Thanks to Vexorian that Optimizer 5.0 no longer kills natives
    native UnitAlive takes unit id returns boolean

    globals
        //-------------------------------------------------------------------
        // You must configure the dummy unit with the one created from the
        // ObjectMerger statement above.
        //
        private constant integer DUMMY_ID = 'dumi'      //The rawcode of the dummy unit.
        private          player  OWNER    = Player(15)  //The owner of the dummy unit.

        private constant integer ANG_N = 8              //# of indexed angles. Higher value increases realism but decreases recycle frequency.
        private constant integer ANG_STORAGE_MAX = 12   //Max dummies per indexed angle. I recommend lowering this if you increase ANG_N.

        private constant real DEATH_TIME = 2. //Allow the special effect on
        //the unit to complete its "death" animation in this timeframe. Must
        //be higher than 0.74 seconds to allow the unit time to turn. This
        //number should not be lower than the maximum death-animation time of
        //your missile-units' effect attachments, just to be safe.

        //If you are mixing vJass with GUI (I'll be the last to judge)
    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)
            //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:
Level 22
Joined
Feb 6, 2014
Messages
2,466
Just some suggestions:
- Allow GetRecycleUnit (can't think of a function name) with no angle parameter (for non-moving/facing dummy units). I think it is best if it would return the unit facing with greatest number in the stack. For example, most dummies with 0 facing angle were unused, then return a unit from that group.
- Add an AddRecycleTimer(unit, time) which it will recycle the dummy after a certain time.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Mine does this in O(1) : p, but as Bribe pointed out, it's like 3x longer, lol...

Still doesn't balance the angle queues but I really don't want this resource to be 3x as long just for that one minor, situational benefit. Allocation and deallocation are extremely fast this way, and it hardly needs any variables.



See, there's actually a trick to keeping everything balanced. At the start, you have an equal number of dummies for each angle. When you get one, you then get something like this

-1, 0

where -1 means that there are some angles that have 1 less dummy in them than other angles. If you take another one out, then you get this

-2, 0

At this point, the thing is not balanced, so you take a dummy from one of the things in 0 to get this again

-1, 0

Suppose you put a dummy back. It would always end up going into a negative queue. What happens if you have this then?

0, 1

This would be after all dummies are back, plus a few extra. Well, that is nothing more than this

-1, 0

Where the 0 just has fewer angles than the -1 now.

The point I'm making is that you will only have at most two different angle counts if you do it right.

Suppose that every collection of units for an angle is a doubly linked list. You can organize these lists in two ways. The first way is by angle. This will allow you to retrieve a unit closest to a desired facing. The second way is by count. Every count can be a doubly linked lists of those doubly linked lists that store units.

array[count] -> list of angles -> list of units

You take a unit out from a linked list. The count goes down by 1, moving it down to a lower rank. If the lower rank is 2 away from the highest rank, or vice versa, then take one out of the lowest/highest thing, whichever is appropriate.


The algorithm is pretty simple and just makes sense to use. I don't mind if anyone uses the above algorithm for their things. It's a good algorithm : ). Just be sure to credit me for the algorithm and we're cool ^_-.

Currently, only my dummy recycling makes use of that algorithm, but I'd like to see more libraries use it. Maybe someone will even find a way to improve it, like someone found a way to expand on the idea I had for allocation to reduce code generation further =).


Also, hint hint on organizing by count. I don't remember how I originally implemented this thing, but you could do a low and high list and sometimes swap them. There are plenty of other ways you can implement it too. This is where you can get creative and really show off your skills. I don't think I implemented this part well either when I did it all those years ago. I'd do it well now though >: (, but I'd have to think about how I'd want to implement this.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
There's one other cool thing. Uhm, I figured out my own way to remove need of timers. I don't know if this is your way or not : o.

Do priority queues for the units, where priority is based on difference between unit facing and target facing. If first unit in queue isn't target facing, or queue is empty, create a new unit : ).

Priority queue could be implemented with a linked heap, a linked list, or something.

Don't know if you're already doing this? : o

You could also just assume the oldest unit, that being the first, will always have the smallest delta. This isn't always the case, so you may be allocating extra units, but it'll be fast : ). I'd prob go with this approach.
 
Top