Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
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.
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
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
I know it's bad though I like it more than when I had no balance.
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.
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.
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 ; ).
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.
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.
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.
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.
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.
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.
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.
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
This is specifically for facing angle preservation for arrow like missiles. I too agree that the unit indexer part should be changed but I'd like to test the new unit Dex that you made first
I've never seen this before, but it looks like one of the best resources I've seen by Bribe. How could this possibly have been graveyarded because of nesthaus code? I guess it must have had a big update
The only thing it would need from what I can tell is to be initialized from a module. But even then I don't think it's that important - who will be creating missiles during init?
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?
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.
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.
I have added a function called "RecycleMissileDelayed" which uses a TriggerSleepAction and the user can specify how long to wait for the animation to complete. I recommend 2 seconds in case the TSA is inaccurate.
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:
Updated - implemented changes proposed by BPower so the need to delay missile restocking is gone. I also changed the RemoveUnit call to a UnitApplyTimedLife.
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
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.
1) just use any dummy recycling for that
2) just set the constant DEATH_TIME if you think you need more than 2 seconds. edit: if you're talking about for a projectile- which this is for- you recycle the missile at the end of the journey. a timed recycler is unneeded for that.
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.
Nes - thank you for reminding me that I hadn't updated the original post with the O(1) RecycleMissile method and getting rid of the unit group. I have now updated it.
I'll take a look into what you said about balancing the queues.
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.
Ok then. So, I didn't need the sight ability afterall if the 'dumi' has a sight initially. And I will set the owner to the player directly instead to an ally. Then before recycling, I will give it back to Player(15). Thanks again Bribe
I see, I see, so high and low are not a sorted list keeping track of everything in between, they are just 2 scalar pointers to the relevant angles. Pretty freaking awesome! Thanks for illustrating it, this makes it so much less imposing than it was before
This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
By continuing to use this site, you are consenting to our use of cookies.