• 🏆 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!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[GUI] Efficient Arrays - Multiinstanceabilty for Timers, Custom Values, and more

Level 40
Joined
Dec 14, 2005
Messages
10,532

Efficient allocation and deallocation of array indexes – Multiinstanceabilty for Timers, Custom Values, and more


Difficulty (Relative to GUI)
7/10 (Moderate)

Within [trigger] tags, <> surrounding text denotes that that text is a field which you should fill yourself. The text enclosed is a description of what you should put there.

Demo maps for all content explained in this tutorial are available at the bottom of the post.



Often, when you are trying to achieve one of many things such as a multiinstanceable spell which uses a timer, or an extended custom value system, you run into problems with how to approach it. How would you achieve such a feat? The logical approach would be to use a series of arrays.

However, there are two significant problems posed by this; how do you make sure a slot is not permanently occupied, and how do you make sure you don't have a massive performance hit from attempting to fix this taking hostage of slots?

The answer is surprisingly simple. Let's follow along the idea of an extended custom value.

Variables


To begin, we'll need some variables.

The first step is to decide what our custom value will need to be able to store. In this case, let's say we want to be able to store a Real and a Unit.

Our variables thus are:

System variables
NameTypeArray?Value
CurrentIndexIntegerNo0 (Default)
CVMaxIndexIntegerNo0 (Default)
CVFreeIndexIntegerNo0 (Default)
CVAvailableIndexesIntegerYesN/A

Custom value arrays
NameTypeArray?Value
CVUnitDataArrayUnitYesN/A
CVRealDataArrayRealYesN/A

Note: No variables are reusable between systems, except CurrentIndex.

Code


Allocating an array slot


This should simply give the unit the last available freed custom value, or if none exists, extend the list.

Note: YourUnit is the unit to get/lose a Custom Value extension.

  • Allocate
    • Actions
      • Set CurrentIndex = CVFreeIndex
      • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
        • If - Conditions
          • CurrentIndex Equal to 0
        • Then - Actions
          • Set CVMaxIndex = (CVMaxIndex + 1)
          • Set CurrentIndex = CVMaxIndex
        • Else - Index
          • Set CVFreeIndex = CVAvailableIndexes[CurrentIndex]
      • Set CVAvailableIndexes[CurrentIndex] = -1
      • Unit - Set Custom Value of YourUnit to CurrentIndex
      • Set CVUnitDataArray[CurrentIndex] = <Initial value of the unit's Unit custom value>
      • Set CVRealDataArray[CurrentIndex] = <Initial value of the unit's Real custom value>
This may look a little weird at first, but it actually works quite well.

Basically, the system gets the current available index. If that index is 0 (nonexistant), then it increases the indexes available by 1, and gives it that index.

Otherwise, it moves the next current available index to the one freed before the last (now occupied) index is freed, stored via an array.

Freeing an array slot


Freeing an index is much simpler; the last available freed index is stored in an array at the position of the newly freed index, and the newly freed index is set to be the currently available index.

  • Deallocate
    • Actions
      • Set CurrentIndex = (Custom Value of YourUnit)
      • Set CVAvailableIndexes[CurrentIndex] = CVFreeIndex
      • Set CVFreeIndex = CurrentIndex
      • -------- The next bit is optional, if you want to be paranoid about cleaning up for yourself, which is generally a good idea --------
      • Unit - Set Custom Value of YourUnit to 0
And that's it! The "custom values," in this case, are simply accessed by the array slots at the custom value of the subject unit.

Allocate...

CurrentIndex = 0
CVMaxIndex = 1
CurrentIndex = 1
CVAvailableIndexes[CurrentIndex] = -1

Allocate...

CurrentIndex = 0
CVMaxIndex = 2
CurrentIndex = 2
CVAvailableIndexes[CurrentIndex] = -1

Allocate...

CurrentIndex = 0
CVMaxIndex = 3
CurrentIndex = 3
CVAvailableIndexes[CurrentIndex] = -1

Deallocate 3...

(CurrentIndex = 3)
CVAvailableIndexes[CurrentIndex (3)] = CVFreeIndex (0)
CVFreeIndex = 3

Allocate...

CurrentIndex = 3
CVFreeIndex = 0
CVAvailableIndexes[CurrentIndex] = -1

Deallocate 3...

(CurrentIndex = 3)
CVAvailableIndexes[CurrentIndex (3)] = CVFreeIndex (0)
CVFreeIndex = 3

Deallocate 1...

(CurrentIndex = 1)
CVAvailableIndexes[CurrentIndex (1)] = CVFreeIndex (3)
CVFreeIndex = 1

Allocate...

(CurrentIndex = 1)
CVFreeIndex = CVAvailableIndexes[CurrentIndex (1)] (3)
CVAvailableIndexes[CurrentIndex (1)] = -1

Allocate...

(CurrentIndex = 3)
CVFreeIndex = CVAvailableIndexes[CurrentIndex (3)] (0)
CVAvailableIndexes[CurrentIndex (3)] = -1

And so on...


Further Application – An MUI Spell on a Timer


One of the largest multiinstanceability problems GUI tends to run into is timers, since you need globals or Jass systems to pass information through them. However, they are also fixed by simply applying this method to the spell.

Variables


Combined Systems Variables
NameTypeArray?Value
CurrentIndexIntegerNo0 (Default)
SPMaxIndexIntegerNo0 (Default)
SPFreeIndexIntegerNo0 (Default)
SPAvailableIndexesIntegerYesN/A
SPInstancesIntegerYesN/A

We're going to have to add a few more variables to have this work right; otherwise, you lose a good deal of efficiency, among other things.

Spell-Related Variables
NameTypeArray?Value
SPTimerTimerNoNew Timer (Default)
SPMaxInstanceIntegerNo0 (Default)

And then our custom spell data...

Custom Spell Data Variables
NameTypeArray?Value
SPExampleArrayN/AYesN/A

Note: No variables are reusable between spells, except CurrentIndex. Theoretically the indexing variables (under Combined Systems Variables) could be reused, but there is a limit of 8190 instances per set of variables, so don't overuse them!

Code


Now, the spell will allocate the index with the first method, and then store it to its stack of instances with the second.

Note that you can remove the first method entirely, but this means you have to individually move every variable back upon freeing the instance (as seen in the deallocation section of the loop). This will cause deallocation to be a bit slower.

Only the Combined Systems example uses the Old System Variables variables, but is faster at deallocating.

The Lightweight System example is faster at allocating and a little less variable-heavy, but slower at deallocating. This is probably the ideal choice for your trigger, but it depends on personal preference (as spells with lots of fields can take ages to write the deallocators for this way).

How does the spell work? The spell focuses itself around one timer, rather than one per instance of the spell. Every time this timer expires, every instance of the spell will be executed (via looping through the array).

The deallocation is a bit odder. You take all of the values stored on the instance to deallocate, and move the values stored on the last instance to said instance. This way, the array shrinks in size without needing to move every value after the deallocated instance to the previous slot.

  • SpellCast
    • Events
      • Unit - A Unit Starts the Effect of an Ability
    • Conditions
      • (Ability Being Cast) Equal to <Some Spell>
    • Actions
      • -------- Do other spell setup here --------
      • Set CurrentIndex = SPFreeIndex
      • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
        • If - Conditions
          • CurrentIndex Equal to 0
        • Then - Actions
          • Set SPMaxIndex = (SPMaxIndex + 1)
          • Set CurrentIndex = SPMaxIndex
        • Else - Index
          • Set SPFreeIndex = SPAvailableIndexes[CurrentIndex]
      • Set SPAvailableIndexes[CurrentIndex] = -1
      • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
        • If - Conditions
          • (SPMaxInstance Equal to 0)
        • Then - Actions
          • Countdown Timer - Start SPTimer as a Repeating timer with timeout <spell refresh rate>
        • Else - Actions
      • Set SPInstances[SPMaxInstance] = CurrentIndex
      • -------- Allocate all spell arrays here using CurrentIndex, as done in the custom value examples, such as the following --------
      • Set SPExampleArray[CurrentIndex] = <some value>
      • Set SPMaxInstance = (SPMaxInstance + 1)
Now, since we don't have anything to directly attach the array values to, the periodic section of the spell can simply loop through them, as such:

  • SpellPeriodic
    • Events
      • Time - SPTimer Expires
    • Conditions
    • Actions
      • For Each (Integer A) from 1 to SPMaxInstance - 1, do Actions
        • Loop - Actions
          • Set CurrentIndex = SPInstances[(Integer A)]
          • -------- Do spell stuff here, with CurrentIndex being your index --------
          • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
            • If - Conditions
              • -------- Ready to deallocate index --------
            • Then - Actions
              • Set SPMaxInstance = SPMaxInstance – 1
              • Set SPInstances[(Integer A)] = SPInstances[SPMaxInstance]
              • Set SPAvailableIndexes[CurrentIndex] = SPFreeIndex
              • Set SPFreeIndex = CurrentIndex
              • -------- Make sure the newly placed index still gets hit this iteration --------
              • Custom script: set bj_forLoopAIndex = bj_forLoopAIndex - 1
              • Custom script: set bj_forLoopAIndexEnd = bj_forLoopAIndexEnd - 1
              • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
                • If - Conditions
                  • (SPMaxInstance Equal to 0)
                • Then - Actions
                  • Countdown Timer - Pause SPTimer
                  • Custom script: exitwhen true
                • Else - Actions
            • Else - Actions
              • -------- You can put more spell stuff in the Else if you want it to only occur when the spell does not end --------
          • -------- You should NOT do any more spell stuff here --------
  • SpellCast
    • Events
      • Unit - A Unit Starts the Effect of an Ability
    • Conditions
      • (Ability Being Cast) Equal to <Some Spell>
    • Actions
      • -------- Do other spell setup here --------
      • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
        • If - Conditions
          • (SPMaxInstance Equal to 0)
        • Then - Actions
          • Countdown Timer - Start SPTimer as a Repeating timer with timeout <spell refresh rate>
        • Else - Actions
      • -------- Allocate all spell arrays here using SPMaxInstance, as done in the custom value examples, such as the following --------
      • Set SPExampleArray[SPMaxInstance] = <some value>
      • Set SPMaxInstance = (SPMaxInstance + 1)
Now, since we don't have anything to directly attach the array values to, the periodic section of the spell can simply loop through them, as such:

  • SpellPeriodic
    • Events
      • Time - SPTimer Expires
    • Conditions
    • Actions
      • For Each (Integer A) from 1 to SPMaxInstance - 1, do Actions
        • Loop - Actions
          • -------- Do spell stuff here, with (Integer A) being your index --------
          • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
            • If - Conditions
              • -------- Ready to deallocate index --------
            • Then - Actions
              • Set SPMaxInstance = SPMaxInstance – 1
              • -------- Move the array indexes to (Integer A) from SPMaxInstance, as done with the Custom Value examples, such as the following --------
              • Set SPExampleArray[(Integer A)] = SPExampleArray[SPMaxInstance]
              • -------- Make sure the newly placed index still gets hit this iteration --------
              • Custom script: set bj_forLoopAIndex = bj_forLoopAIndex - 1
              • Custom script: set bj_forLoopAIndexEnd = bj_forLoopAIndexEnd - 1
              • If (All Conditions are True) Then do (Then actions) Else do (Else actions)
                • If - Conditions
                  • (SPMaxInstance Equal to 0)
                • Then - Actions
                  • Countdown Timer - Pause SPTimer
                  • Custom script: exitwhen true
                • Else - Actions
            • Else - Actions
              • -------- You can put more spell stuff in the Else if you want it to only occur when the spell does not end --------
          • -------- You should NOT do any more spell stuff here --------


Vexorian: Allocation/Deallocation algorithm.
DiscipleOfLife: For telling me to use it.
Alakon/Squiggy: For inspiring me to write this.
Several poor souls: For looking over it at my request.
 

Attachments

  • ExampleLightweightSystemSpell.w3x
    24.5 KB · Views: 470
  • ExampleCombinedSystemsSpell.w3x
    25.1 KB · Views: 449
  • ExampleCustomValueSystem.w3x
    19.1 KB · Views: 437
Last edited:
Magnificent tutorial. It contains all the needed information, such as:

  • A brief overview of what the tutorial is about.
  • An explanation of the code.
  • The code it self.
  • A step by step guide on how to create this effect.
  • Very clear and easy to understand by any Triggerer, as long as that person has a minimal understanding of GUI, Variables, and the World Editor.
 
Level 9
Joined
Mar 25, 2005
Messages
252
Why don't you use the allocation algorithm jasshelper uses for structs for your unit indexes? It is faster than what you have here and you wouldn't have to transfer all the data you have stored to CVMaxIndex around in deallocate and also then one could use variables to hold units' indexes without having to worry about the units' indexes changing due to some other index being deallocated.
 
Level 40
Joined
Dec 14, 2005
Messages
10,532
It is faster than what you have here
I believe it should be faster for deallocation but slower for allocation, for the reason below.

and you wouldn't have to transfer all the data you have stored to CVMaxIndex around in deallocate
I guess the main reason that I didn't use that algorithm is that this point didn't come to mind.

and also then one could use variables to hold units' indexes without having to worry about the units' indexes changing due to some other index being deallocated.
This seems unlikely to me (if worst comes to worst, just call GetUnitUserData again), but given the above advantage, I'll most likely switch the algorithm anyways.

EDIT: Whee, updated to vJass allocators.

EDIT2: Fixed an annoying bug caused by how badly For loops are implemented in GUI.

EDIT3: Demo map for the Lightweight System for spells is now available.

EDIT4: Demo map for the Combined Systems for spells is now available.
 
Last edited:
Top