[System] Unit Recycler & Simple Damage Detection System

Scripts

14 years
edited 8 years
Unit Recycler & Uber simple damage detection
By moyack - 2017


Introduction:

This system is the result of working with a damage detection system that actually can be safe (AKA avoid totally the DestroyTrigger() function), so one solution to this issue is to avoid as much as possible (hopefully completely) the destruction of units and therefore the EVENT_DAMAGED triggers related to them, and the only way to make it possible is by recycling the units. The current result is a system that recycle most type of units (summoned and buildings simply don't work for their harcoded properties).


Why is convenient this system?

because the game won't need to allocate memory creating units, instead, they'll be backed up for a later use, which can improve your map performance. This can be very useful in maps where the unit spawning is a very common task (like AoS, footies, etc).


What's the issue with buildings and summoned units??

Buildings have limitations in moving them (they can be moved but their basement remains in the original place, which won't allow the construction of new units in those places). About summoned units, their dead is totally harcoded by the summoning abilities and can't be stopped by detecting the damage, therefore they can't be recycled.


How it works?

This library takes into account units that are being damaged, and detects if they're going to die (if the damage dealt to a unit is enough to kill them), if that situation happens, then it will be moved to the heaven, a place that you define in your map where the units are restored, cleaned and left ready for later use. Additionally this system offer a set of replacement functions which will help to this system to recycle units more efficiently.


How to install & final comments

  • Create a trigger and call it "UnitRecycler" or whatever you consider it best.
  • Convert it to custom text
  • Paste the content below.
  • In the configuration globals, set the AUTO_LOC = true if you want the system set automatically the Heaven location. Set it to false if you need to set it manually in the HEAVEN_POS variable.
  • That's all :)

That's all??? not actually. By doing only this the library will be able to add damage detection to units and store the fallen units into the heaven. But it's up to you to develop the code to recycle the units. How?? using the functions provided (you can see them in the library comments).

Core Library
Code (jass) Select
1
library UnitRecycler
2
//******************************************************************************
3
//*
4
//* Unit Recycler
5
//* By moyack. 2017
6
//*
7
//* ============================================================================
8
//* Credits to Litany, DioD, Captain Griffen, Troll Brain and other nice guys  *
9
//* for their suggestions, comments and ideas.                                 *
10
//* ============================================================================
11
//*
12
//* This library allows to your map to reuse died units, which saves memory.
13
//* It's very useful in AoS or footies games, where the unit spawning is a
14
//* common task.
15
//*
16
//* How it works?
17
//* =============
18
//*
19
//* The script detects if a unit reach the dying point (defined by the MIN_LIFE
20
//* constant), and if the damage can kill the unit, then it is sent to the dump
21
//* for further recycling. For custom situations like summoning or when a unit
22
//* enters to the map, you just have to use the functions provided by this 
23
//* library.
24
//*
25
//* Functions
26
//* =========
27
//* 
28
//* in order to recycle a unit, you can use these function:
29
//*
30
//*  => RecycleUnit(<unit variable>) returns unit
31
//*     -----------------------------------------
32
//*     This function takes as argument a unit, and returns the unit recycled.
33
//*     if there's a unit of the same typeid in the dump, it will use this unit
34
//*     instead of the one from the function input, removing it immediately, 
35
//*     otherwise this script will return the same unit.
36
//* 
37
//*  => CreateUnitEx(player, unitid, x, y, angle) returns unit
38
//*     ------------------------------------------------------
39
//*     Like the native function but tries to recycle the unit if avaliable in the dump
40
//*
41
//*  => KillUnitEx(<unit variable>) returns nothing
42
//*     -------------------------------------------
43
//*     Like the native function but it recycles the unit before killing it.
44
//*
45
//*  => RemoveUnitEx(<unit variable>) returns nothing
46
//*     ---------------------------------------------
47
//*     Like the native function but it recycles the unit.
48
//*
49
//*  => IsUnitDead(<unit variable>) returns boolean
50
//*     -------------------------------------------
51
//*     This function returns a boolean argument that indicates if the unit is dead or not.
52
//*     Remember that a unit is dead if it's in the Heaven group.
53
//*
54
//*  => ReplaceDummy(<unit variable>) returns unit
55
//*     -------------------------------------------
56
//*     This function returns a replacement unit without killing the output unit.
57
//*
58
//*  => CreateDummy(player, unitid, x, y, angle) returns unit
59
//*     ------------------------------------------------------
60
//*     Like the CreateUnit function but the returned unit won't be affected by the damage
61
//*     detection nor will be recycled automatically
62
//*
63
//*  => TriggerRegisterAnyUnitRecycleEvent(trigger t) returns nothing
64
//*     -------------------------------------------------------------
65
//*     All the trigger registered in this way will activate when a unit is about to be recycled
66
//*     (in other words, it triggers before it dies). You can use this functions to retrieve the
67
//*     the units involved in this event:
68
//*
69
//*      # GetRecycledUnit() returns the units that is going to be recycled
70
//*      # GetRecycleDummyUnit() returns the dummy unit which will die instead of the recycled unit
71
//*      # GetRecycleAttacker() returns the unit that "kills" the recycled unit
72
//*
73
//*  => IsUnitDummy(unit u) returns boolean
74
//*     -----------------------------------
75
//*     Checks if a unit can be recycled or not. Dummy units are not recycled, they're used as 
76
//*     placeholder of the unit about to die...
77
//*
78
//*  => GetUnitsInHeaven() returns group 
79
//*     --------------------------------
80
//*     Returns the group which contains the units in the heaven. Useful to do checks and
81
//*     operations on them.
82
//*
83
//*
84
//* For Damage detection, you just need to use these functions:
85
//*
86
//*  => AddDamageCondition(<Boolexpr variable>) returns nothing
87
//*     -------------------------------------------------------
88
//*     Just add a condition function which manage the damaged unit and the script
89
//*     will use it with all the the units in the DD.D group.
90
//*
91
//*  => DoNonDetectableDamage(unit, widget, damage, boolean_attack, boolean_ranged, attacktype, damagetype, weapontype) returns boolean
92
//*     -------------------------------------------------------------------------------------------------------------------------------
93
//*     Like the UnitDamageTarget function, but it can be used inside condition functions.
94
//*     How to know if you need to use it? if you use UnitDamageTarget() inside a DD function and 
95
//*     it freezes the game until it kills the attacked unit(s), then you have to replace that
96
//*     function by this custom one.
97
//********************************************************************************
98
//* CONFIGURATION PART
99
globals
100
    private constant real     MIN_LIFE = 0.405 // the experimental death value that will be used to activate
101
                                         // the fake death of units. units that reach this value or less
102
                                         // won't die and they will be recycled.
103
    private constant boolean  AUTO_LOC = false// If it's set to true, it will place automatically the heaven in one
104
                                              // non visible corner of the map, else, it will use the HEAVEN_POS
105
                                              // as a heaven location
106
    private constant player   DUMMY_PLAYER = Player(15) // sets the player owner of the unit's heaven
107
    private constant real     MANA_FACTOR = 0.5 // sets the initial mana amount (as percentage of maximum mana)
108
                                                // to recycled units when they are just placed in the game.
109
                                                // This constant is used only by CreateUnitEx function.
110
    
111
    private location HEAVEN_POS = Location(-2500., 2700.) // Indicates the heaven's location.
112
endglobals
113
//* END CONFIGURATION PART
114
115
globals
116
    private group Heaven // where worthy units go when they die...
117
endglobals
118
119
// this is a key functions part just to make homogeneous the unit management, please don't edit this unless
120
// you know what are you doing, ok??
121
private keyword Kill
122
123
private function PrepareUnit takes unit u returns nothing
124
    call SelectUnit(u, false)
125
    call UnitRemoveBuffs(u, true, true)
126
    call UnitResetCooldown(u)
127
    call SetUnitInvulnerable(u, true)
128
    call SetWidgetLife(u, GetUnitState(u, UNIT_STATE_MAX_LIFE))
129
    call PauseUnit(u, true)
130
    call GroupAddUnit(Heaven, u)
131
endfunction
132
133
private function MoveUnit takes unit u returns nothing
134
    call SetUnitOwner(u, DUMMY_PLAYER, true)
135
    call SetUnitX(u, GetLocationX(HEAVEN_POS))
136
    call SetUnitY(u, GetLocationY(HEAVEN_POS))
137
endfunction
138
139
private function PlaceDummy takes unit d, unit u returns nothing
140
    call GroupAddUnit(Kill.corpse, d)
141
    call SetUnitUseFood(d, false)
142
    call SetUnitState(d, UNIT_STATE_MANA, GetUnitState(u, UNIT_STATE_MANA))
143
    call SetUnitFlyHeight(d, GetUnitFlyHeight(u), 0.)
144
    //call SetCSData(d, GetCSData(u)) //used to pass attached data via CSData to the corpse...
145
    call SetUnitPathing(d, false)
146
    call SetUnitX(d, GetUnitX(u))
147
    call SetUnitY(d, GetUnitY(u))
148
endfunction
149
// end key functions...
150
151
private struct Trigger // struct to manage the EventRecycleUnit
152
    private static integer i = 0
153
    static unit R = null // recycled unit
154
    static unit D = null // dummy unit
155
    static unit A = null // attacker unit
156
    trigger t
157
    
158
    static method AddTrigger takes trigger t returns nothing
159
        local thistype T = thistype.allocate()
160
        set T.t = t
161
        set thistype.i = integer(T)
162
    endmethod
163
    
164
    static method evaluate takes unit r, unit d, unit a returns nothing
165
        local integer i = 1
166
        local Trigger T
167
        set thistype.R = r
168
        set thistype.D = d
169
        set thistype.A = a
170
        loop
171
            exitwhen i > Trigger.i
172
            set T =thistype(i)
173
            if T.t != null and TriggerEvaluate(T.t) then
174
                call TriggerExecute(T.t)
175
            endif
176
            set i = i + 1
177
        endloop
178
    endmethod
179
endstruct
180
181
private struct Kill // struct used to manage the killed units...
182
    static group corpse // group of units that won't be touched by the damage detection 
183
    //(corpses and dummy units for instance)
184
    
185
    static method Do takes unit u, unit killer returns nothing
186
        local unit d = CreateUnit(GetOwningPlayer(u), GetUnitTypeId(u), GetUnitX(u), GetUnitY(u), GetUnitFacing(u))
187
        call SetUnitInvulnerable(u, true)
188
        call PlaceDummy(d, u)
189
        call SetWidgetLife(d, 1.)
190
        call Trigger.evaluate(u, d, killer)
191
        call UnitDamageTarget(killer, d, 2., true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, WEAPON_TYPE_WHOKNOWS)
192
        call PrepareUnit(u)
193
        call MoveUnit(u)
194
        set d = null
195
    endmethod
196
    
197
    private static method ManageSummoned takes nothing returns nothing
198
        if IsUnitInGroup(GetSummonedUnit(), Kill.corpse) then
199
            // this part will run when a corpse is resurrected, so this units are suitable to
200
            // be controlled by the damage detection.
201
            call GroupRemoveUnit(Kill.corpse, GetSummonedUnit())
202
            call SetUnitPathing(GetSummonedUnit(), true)
203
        endif
204
    endmethod
205
    
206
    private static method onInit takes nothing returns nothing
207
        local trigger t = CreateTrigger()
208
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SUMMON)
209
        call TriggerAddAction(t, function Kill.ManageSummoned)
210
        set Kill.corpse = CreateGroup()
211
        set t = null
212
    endmethod
213
endstruct
214
215
private struct DD // damage detection struct, because damage detection can be so easy...
216
    static group D // group of unit that will have damage detection
217
    static trigger T
218
    static timer Tm = CreateTimer() // used to control the infinite looping when is used UnitDamageTarget over the same triggering unit 
219
    
220
    // Add the attacked unit to the damage detection if the unit is not in the unit group
221
    private static method AddUnit takes unit d returns nothing
222
        if not IsUnitInGroup(d, thistype.D) and not IsUnitInGroup(d, Kill.corpse) then
223
            debug call DisplayTimedTextToForce(bj_FORCE_ALL_PLAYERS,2,"Added " + GetUnitName(d) + " to the DD")
224
            call TriggerRegisterUnitEvent(thistype.T, d, EVENT_UNIT_DAMAGED)
225
            call GroupAddUnit(thistype.D, d)
226
        endif
227
    endmethod
228
    
229
    private static method Attacked takes nothing returns nothing
230
        call thistype.AddUnit(GetTriggerUnit())
231
    endmethod
232
    
233
    private static method Spelled takes nothing returns nothing
234
        if GetSpellTargetUnit() != null then
235
            call thistype.AddUnit(GetSpellTargetUnit())
236
        endif
237
    endmethod
238
    
239
    static method AntiLoop takes nothing returns nothing
240
        call EnableTrigger(thistype.T)
241
    endmethod
242
    
243
    private static method onInit takes nothing returns nothing
244
        set thistype.T = CreateTrigger()
245
        set thistype.D = CreateGroup()
246
        call TriggerRegisterAnyUnitEventBJ(thistype.T, EVENT_PLAYER_UNIT_ATTACKED)
247
        call TriggerAddAction(thistype.T, function thistype.Attacked)
248
        set thistype.T = CreateTrigger()
249
        call TriggerRegisterAnyUnitEventBJ(thistype.T, EVENT_PLAYER_UNIT_SPELL_EFFECT)
250
        call TriggerAddAction(thistype.T, function thistype.Spelled)
251
        set thistype.T = CreateTrigger()
252
    endmethod
253
254
endstruct // you see, it's not bigger than this :)
255
256
private struct UR // unit recycling struct
257
    static group Temp = CreateGroup()
258
    static unit U = null
259
    
260
    static method FromDump takes nothing returns boolean
261
        if IsUnitInGroup(GetFilterUnit(), Heaven) and IsUnitType(GetFilterUnit(), UNIT_TYPE_DEAD) == false then
262
            set thistype.U = GetFilterUnit()
263
        endif
264
        return false
265
    endmethod
266
    
267
    static method UseRecycled takes unit u returns unit
268
        local real x = GetUnitX(u)
269
        local real y = GetUnitY(u)
270
        local real f = GetUnitFacing(u)
271
        local real m = GetUnitState(u, UNIT_STATE_MANA)
272
        local player p = GetOwningPlayer(u)
273
        set thistype.U = null
274
        call GroupEnumUnitsOfTypeCounted(thistype.Temp, UnitId2String(GetUnitTypeId(u)), Condition(function thistype.FromDump), 1)
275
        if thistype.U != null then
276
            debug call DisplayTimedTextToForce(bj_FORCE_ALL_PLAYERS,2, GetUnitName(thistype.U) + " is being reused...")
277
            call SetUnitInvulnerable(thistype.U, false)
278
            call GroupRemoveUnit(Heaven, thistype.U)
279
            call PauseUnit(thistype.U, true)
280
            call SetUnitPosition(thistype.U, x, y)
281
            call SetUnitFacing(thistype.U, f)
282
            call SetUnitOwner(thistype.U, p, true)
283
            call SetUnitState(thistype.U, UNIT_STATE_MANA, m)
284
            call SetUnitPathing(thistype.U, true)
285
            call PauseUnit(thistype.U, false)
286
            call RemoveUnit(u)
287
            set u = null
288
            return thistype.U
289
        else
290
            return u
291
        endif
292
    endmethod
293
    
294
    private static method onRecycle takes nothing returns boolean
295
        local unit u = GetTriggerUnit()
296
        if IsUnitType(u, UNIT_TYPE_HERO) == false and IsUnitType(u, UNIT_TYPE_STRUCTURE) == false and IsUnitType(u, UNIT_TYPE_SUMMONED) == false and GetWidgetLife(u) - GetEventDamage() <= MIN_LIFE then
297
            call Kill.Do(u, GetEventDamageSource()) // place a dummy to replace the unit
298
                                                       // and returns the corpse
299
            debug call DisplayTimedTextFromPlayer(Player(15),0,0,2, GetUnitName(u) + " has been sent to the dump...")
300
        endif
301
        set u = null
302
        return false
303
    endmethod
304
    
305
    private static method onInit takes nothing returns nothing
306
        local rect r = GetWorldBounds()
307
        set Heaven = CreateGroup()
308
        call TriggerAddCondition(DD.T, Condition(function thistype.onRecycle))
309
        if AUTO_LOC then
310
            call MoveLocation(HEAVEN_POS, GetRectMinX(r), GetRectMinY(r))
311
        endif
312
        call RemoveRect(r)
313
        set r = null
314
    endmethod
315
endstruct
316
317
//*====================
318
//* User functions... *
319
//*====================
320
function RecycleUnit takes unit u returns unit
321
    return UR.UseRecycled(u)
322
endfunction
323
324
function CreateUnitEx takes player p, integer id, real x, real y, real f returns unit
325
    set UR.U = null
326
    call GroupEnumUnitsOfTypeCounted(UR.Temp, UnitId2String(id), Condition(function UR.FromDump), 1)
327
    if UR.U != null then
328
        debug call DisplayTimedTextToForce(bj_FORCE_ALL_PLAYERS,2, GetUnitName(UR.U) + " is being reused...")
329
        call SetUnitInvulnerable(UR.U, false)
330
        call GroupRemoveUnit(Heaven, UR.U)
331
        call PauseUnit(UR.U, true)
332
        call SetUnitPosition(UR.U, x, y)
333
        call SetUnitFacing(UR.U, f)
334
        call SetUnitOwner(UR.U, p, true)
335
        call SetUnitState(UR.U, UNIT_STATE_MANA, GetUnitState(UR.U, UNIT_STATE_MAX_MANA) * MANA_FACTOR)
336
        call SetUnitPathing(UR.U, true)
337
        call PauseUnit(UR.U, false)
338
        return UR.U
339
    endif
340
    return CreateUnit(p, id, x, y, f)
341
endfunction
342
343
function KillUnitEx takes unit u returns nothing
344
    local unit d
345
    if IsUnitType(u, UNIT_TYPE_HERO) == false and IsUnitType(u, UNIT_TYPE_STRUCTURE) == false then
346
        set d = CreateUnit(GetOwningPlayer(u), GetUnitTypeId(u), GetUnitX(u), GetUnitY(u), GetUnitFacing(u))
347
        call Trigger.evaluate(u, d, null)
348
        call PrepareUnit(u)
349
        call PlaceDummy(d, u)
350
        call KillUnit(d)
351
        call MoveUnit(u)
352
	else
353
		call KillUnit(u)
354
    endif
355
    set d = null
356
endfunction
357
358
function RemoveUnitEx takes unit u returns nothing
359
    if IsUnitType(u, UNIT_TYPE_HERO) == false and IsUnitType(u, UNIT_TYPE_STRUCTURE) == false then
360
        call Trigger.evaluate(u, null, null)
361
        call PrepareUnit(u)
362
        call MoveUnit(u)
363
	else
364
		call RemoveUnit(u)
365
    endif
366
endfunction
367
368
function IsUnitDead takes unit u returns boolean
369
    return IsUnitInGroup(u, Heaven)
370
endfunction
371
372
function ReplaceDummy takes unit u returns unit 
373
    local unit d
374
    if IsUnitType(u, UNIT_TYPE_HERO) == false and IsUnitType(u, UNIT_TYPE_STRUCTURE) == false then
375
        set d = CreateUnit(GetOwningPlayer(u), GetUnitTypeId(u), GetUnitX(u), GetUnitY(u), GetUnitFacing(u))
376
        call PlaceDummy(d, u)
377
        call SetWidgetLife(d, GetWidgetLife(u))
378
        call PrepareUnit(u)
379
        call MoveUnit(u)
380
        call SetUnitPathing(d, true)
381
    else
382
        set d = null
383
    endif
384
    return d
385
endfunction
386
387
function CreateDummy takes player p, integer id, real x, real y, real f returns unit
388
    local unit u = CreateUnit(p, id, x, y, f)
389
    call GroupAddUnit(Kill.corpse, u)
390
    return u
391
endfunction
392
393
// TriggerRecyclefunctions. this trigger happens before the triggered unit is going to die...
394
function TriggerRegisterAnyUnitRecycleEvent takes trigger t returns nothing
395
    call Trigger.AddTrigger(t)
396
endfunction
397
398
constant function GetRecycledUnit takes nothing returns unit 
399
    // returns the recycled unit...
400
    return Trigger.R
401
endfunction
402
403
constant function GetRecycleDummyUnit takes nothing returns unit 
404
    // returns the dummy unit...
405
    return Trigger.D
406
endfunction
407
408
constant function GetRecycleAttacker takes nothing returns unit 
409
    // returns the attacker who "kills" the recycled unit...
410
    return Trigger.A
411
endfunction
412
413
constant function IsUnitDummy takes unit u returns boolean
414
    // Checks if a unit can be recycled or not. Dummy units are not recycled, they're used as placeholder...
415
    return IsUnitInGroup(u, Kill.corpse)
416
endfunction
417
418
constant function GetUnitsInHeaven takes nothing returns group
419
    // Checks if a unit can be recycled or not. Dummy units are not recycled, they're used as placeholder...
420
    return Heaven
421
endfunction
422
423
// Damage detection system functions...
424
function AddDamageCondition takes boolexpr b returns nothing
425
    call TriggerAddCondition(DD.T, b)
426
endfunction
427
428
function DoNonDetectableDamage takes unit u, widget t, real damage, boolean attack, boolean ranged, attacktype AT, damagetype DT, weapontype WT returns boolean
429
    call DisableTrigger(DD.T)
430
    call TimerStart(DD.Tm, 0., false, function DD.AntiLoop)
431
    return UnitDamageTarget(u, t, damage, attack, ranged, AT, DT, WT)
432
endfunction
433
434
endlibrary


Changelog
(14/10/2008):
- Small improvements in the core library
- Developed a better example (a tiny AOS, much better than AotZ, DoE and ToB together :P)

(09/09/2008):
- Added a custom trigger event so the system can detect when a unit is about to be recycled
- Added some improvements

(31/12/2008):
- Other minor and final improvements (hopefully)

(06/01/2009):
- Now you can set up the system so it automatically set the heaven in a non playable area of the map (Thanks Troll brain for the idea)
- Added IsUnitDummy function to define if a unit is a placeholder or not.
- Added GetUnitsInHeaven function to retrieve the group that stores the units in Heaven. Useful to do operations with those units.
- Improved the test map with a leaderboard which shows data about units killed per player, units in map, units taht are being recycled, and the percentage of units recycled respect the total of active units.

(29/01/2009):
- Minor improvement in the DD code, now it's more efficient getting units in to the DD group.

(05/04/2009):
- Fixed an unit leak (thanks a lot Litany :D) and improved the CreateUnitEx function.

(18/12/2017):
- Redefined some variables, fixed some bugs with KillUnitEx()
- Added demo map (an endless battle)

Please download the test map for further testing. (last updated: 05/04/2009)
  • UnitRecycler Test.w3x (51.64 KB)
14 years
Cool, a unit recycler :D

I wish the overhead could be smaller :/

... I know! :D
How about only giving the units the locust ability so nothing could happen to them, and you can pause them so that they can't do anything :D
13 years
Doing Unit Recycling and DDS in the same lib is a very bad idea. Have you ever heard of the term modularity?

I'd recommend that you split them up into two different libraries.

edit
Also, your DDS leaks mass events.
13 years
Quote from: nestharus on August 19, 2012, 04:46:55 PM
Doing Unit Recycling and DDS in the same lib is a very bad idea. Have you ever heard of the term modularity?
Yes, I know the term, but one of the reason I could do it in that way was the fact I needed internal and private stuff from the damage detection that should be called into the unit recycling.

QuoteI'd recommend that you split them up into two different libraries.
Hmm, I think this code should be revised, with the new knowledge in vJASS.

Quoteedit
Also, your DDS leaks mass events.
I'll check it out.
13 years
Would you mind in share your scripts here :) So I can link the requirements in this site.

Hugss :D
13 years
Added download link and preview image...