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

Triggers and Scripting Mechanics in Starcraft II

Level 11
Joined
Jul 20, 2004
Messages
2,760
1. Introduction
Blizzard’s game mechanics are extremely powerful, and it is commonly known that they improve their environment with every additional game they develop. From Warcraft II to Starcraft II, the featured map editors have greatly improved, increased in flexibility and adapted to fit to modders’ various needs. Experienced programmers might find the environment obnoxious and crude, but be it as it is, many people use the provided engine to create extraordinary maps and mods.

While most of the concepts explained here might be familiar to the old Warcraft III modders, I hope I might bring something new to the veterans, while sheading some light into a newcomer’s mind about how the scripting language (supported by the Graphical User Interface) can be put to good work into giving life to great ideas and projects. This is a beginner tutorial, so it’s fine if you don’t know much about Blizzard modding.

Note
This tutorial is relatively theoretical, so at the end of it you should end up understanding generic, fundamental concepts, but crucially important for developing your modding projects. The regular programmer might find some of my explanations quite silly, maybe even ridiculous, but keep in mind that it’s the essence of the concepts we are interested in, rather than the academic shell.


2. Triggers – what’s up with that?
Starcraft II is a multi-threaded environment. In simple words, actors (be them units, buildings, players themselves etc.) perform actions at the same time and relatively independent of one another.

Example
Consider two marines fighting each other on the battle-field, while SCVs collect minerals at the base, the barracks are busy building more units and the players independently scan the map and issue various orders. This is all very complex and it is clear that all these actors act independent of one another – the SCVs collecting the minerals have nothing to do with the marines shooting bullets at one another.
Again, we are talking about the basic actions of the actors rather than complex scenarios, such as an opponent marine attacking the SCVs, causing the latter to run away. Even in this case the SCVs run independently of the marine’s actions (though the player’s decision to set the SCVs on the run or the marine to attack them has reason behind it of course, but this is a different type of ‘action’ and is not the direct subject of interest for our study).

Nevertheless, the question remains how to tame such a wild environment? What can the modder do to ensure that everyone plays by the rules of your games, while constructing everything in a structured and clean manner? The answer is very simple: using triggers.

Triggers are fundamental objects in the programming environment with a dual purpose. It enables:
  • The programmer to structure his/her code – container. While it is true that Starcraft II Editor enables programmers to write custom actions outside a trigger’s ‘box’, any piece code you write and want to be executed by the system must be ultimately placed or referenced into a trigger.
  • The system to run programmable actions – actuator.

3. The Structure of a Trigger
Warcraft III JASS programmers might remember that all triggers had to follow the Event – Conditions – Actions schema, the trigger object actually being implemented like this in the background. This does not remain true in Starcraft II, and we will see that Blizzard actually differentiates between the GUI (conceptual) and Scripting (effective) structure of a trigger.

Keep in mind, though, that this difference is purely conceptual. The two perspectives are merely point-of-view related and are in the end analogous (though these abstract concepts will certainly detail some aspects regarding triggers and their functionality).

attachment.php

a) Conceptual Structure (GUI)
Conceptually speaking, the structure of a trigger is rather basic: Events – Conditions – Actions (with Local Variables to be discussed a bit later). The scenario is as follows:
(1) An event of interest for the Trigger occurs. By event of interest we understand the event registered for the specified Trigger.
(2) The Trigger checks whether the conditions are fulfilled. While the event is something intrinsically generic, conditions are circumstantial.
(3) The actions are executed in order, one by one! This is the actual code the environment is expected to execute!

attachment.php

b) Effective Structure (Script)
While the conceptual structure is really simple to understand, the underlying structure of a trigger is even simpler. Essentially, two steps are taken into consideration: creation & reaction of the thread.

  • At creation, the body of the trigger is specified (by referencing a user-defined function). Events are also attached to the trigger via specific functions.
  • Whenever the trigger reacts to the registered events, the system
    • creates a new thread of execution (threads are special structures that allow the environment to run code in parallel – though we do not directly work with them, it’s important to understand what they are and where they are used)
    • places the thread into a context (very important step to be discussed a bit later)
    • executes the body function “inside the thread”
    • destroys the thread, cleaning up resources

    The trigger responds and generates a thread for every registered event, whenever it occurs. Let’s assume for instance that our trigger is set to respond to Unit - Any Unit dies. Every time a unit on the map dies, the system will generate a thread and execute the code for each of the units. If a nuke kills 100 units at the same time, 100 threads will be generated by the trigger, each executing the same target function. If any other event takes place (such as units taking damage, buildings producing units etc.), the trigger will simply ignore them.

    In a second (more generic) example, Trigger T1 is sensitive to events 2, 5 and 6 (ignoring the other 3). It will generate Threads 1, 2 and 3 as a response to these events, independent one to another. The threads run the same code (depending on the moment in which the events occurred, in parallel or during separate periods of time).

    attachment.php

    So where are the conditions then, you might ask? Since the list of conditions is simply a check that determines whether the actions are executed or not, it is simple enough to integrate them into the action function in the form of a composed condition. If at least one of the conditions is evaluated as false, then it is enough to halt the execution of the ‘actions’.

    Here is how the map editor translates an execution function from GUI to Script:
    JASS:
    bool gt_Trigger1_Func (bool testConds, bool runActions) {
        // Place Variable Declarations & initializations
       
        // Conditions
        if (testConds && any_condition_false)
            return false;
    
        if (!runActions)
            return true;
    
        // Place actions here
        return true;
    }

    Note
    Even though at this point the functionality of the two parameters may not seem highly relevant, it is mandatory that the function takes them. Trigger functions must return false only if and only if any conditions fail.


    4. Contexts
    If you remember, I earlier mentioned the idea of context and how it is very important from the point of view of a trigger. Well, the context is very important when we talk about events actually, because they help us determine the actors involved in an event.

    Let’s assume we make a trigger respond to an event of the form “Any Unit takes Damage”.
    What information does this event give us standalone? A unit somewhere on the map has been inflicted damage. Very nice, but does that insignificant information help us? Not really, that doesn’t do much, especially since we’d like to operate with the actors or maybe some parameters of the action itself.

    It would be nice for instance to be able to tell which specific unit got damaged, or what caused the damage. In many situations one would also like to know the amount of damage inflicted. This is where the context kicks in. Generically speaking, the context particularizes a generic event by taking into consideration all the actors and parameters relevant to the event.
    Context functions help reference the context data. The system allows the programmer to choose from a high range of native functions as to ensure that for all contexts all relevant parameters and actors can be determined. The programmer has access to all functions in all contexts, but most of these functions are particular to certain contexts.

    For example, assume a trigger responds to the generic event “Any unit dies”. While context functions such as (Triggering Unit) and (Killing Unit) make sense, a context function such as (Created Unit) is highly irrelevant (and in this context will probably return a null reference). You can learn which context function is appropriate for which event with experience and practice.

    Note
    The context is determined by the wording of the event. If you are not sure which context function to use, read the event and try to decide whether the context function makes sense. In the case of generic functions such as (Triggering Unit), decide to which unit is the event directly referring to. “A unit dies” for instance clearly refers to the (Dying Unit), not the Killer. To reference the dying unit, therefore, you will always use (Triggering Unit).


    5. Resources
    We begin now to understand what triggers are, how they work and in what context. But even though triggers are easy to control in an isolated box, the resources (items/actors/objects/etc.) they operate on are global (also referred to as shared resources), making the results of the actions of multiple triggers more difficult to control.

    Let’s have another example in which we consider the event “Any unit dies”. We hypothetically assume that a Protoss Colossus kills four Zerglings with a single energy beam. Our trigger will generate four threads and run four separate instances of the action function. Due to the way contexts work, each thread refers to its own (Triggering Unit) and (Killing Unit). But because the singular Colossus killed all four Zerglings, the (Killing Unit) of all threads refers to the same unique unit.

    attachment.php

    We say that the Colossus is a resource shared by the generated threads. But wait! What if there was another trigger that reacted to the same event (Any unit dies)? In that case, even though the pair (Killing Unit, Triggering Unit) is unique for the threads generated by the first triggers, between the two triggers, the pairs in fact match one by one (the exact same number of threads being generated for the second trigger too).

    So now this might actually sound really confusing to some people. The resources are global, yet the references are local?! Yep, that’s right! Think of it this way: from the trigger’s point of view, whenever an event occurs, only the elements directly related to the event are generally of interest. It is indeed possible to connect additional data for inter-thread sharing (via Banks), but this is beyond the purpose of today’s tutorial. Just keep in mind that threads may operate on common resources, and it becomes problematic when these threads attempt to manipulate properties of the objects (such as a unit’s health, energy, armor, etc.) when they affect the correct functionality of one-another.


    6. Local and global variables
    Variables are an inevitable concept any programmer must deal with (whether we talk about direct scripts or GUI-assisted code). Variables can be seen as labeled containers that can store information for later usage. You can either use them to:
    • Store programmer-specific values into them: numeric values, strings, Booleans (logical true/false values)
    • Reference objects (such as units, structures, timers, triggers, players etc.)

    The mechanics of local and global variables are identical. They work the same and can be used identically in the code (the system makes absolutely no difference when it comes to usage). The difference between them is quite sensible and there is a reason why local variables are incorporated in the body of a trigger.

    a) Global variables are visible by any thread, anywhere. They are created at map initialization and specified globally in the GUI (just like triggers and composite actions), independent of thread instances. Can be seen as resources.
    b) Local variables belong to threads. They are visible and can only be manipulated by their owner thread. Every time a trigger generates the context, it also creates and places the specified set of local variables into that context. Therefore, local variables are not shared and context-dependent.

    Note
    From the Scripting perspective, local variables are declared and initialized at function call.

    Let’s now take an example to further understand the power of local variables and see why they are indispensable for what people formerly referred to as “Multi-Instance” abilities. The ability we are going to develop is going to enhance the standard Blink’s functionality. We are not interested now in how balanced or complex the ability is, but in the coding perspective.

    Holy Blink – Teleports the caster to a nearby location. Upon arrival, heal all friendly units in a nearby radius by 5% of the units’ cumulated life amount (if for instance there are 5 zealots in the area with 24 life each, the spell heals each zealot by 5x24x5% = 6 life points).

    Our trigger clearly reacts to the usage of the Blink ability, more specifically at the end of a successful cast. The generated thread will detect the units in the vicinity of the caster, compute the healing amount out of their cumulative life points, and then heal the units back. The resulting GUI code is presented below.

    attachment.php

    Because stalkers generally blink in groups, it is very likely that the trigger may generate more than a thread at a time. Therefore, the usage of global variables for performing intermediate computations is not viable – keep in mind that global variables are shared by multiple threads, but the operation of the spell is purely context-dependent.

    We will therefore define three local variables.
    • uGroup – stores the units in the vicinity of the caster (the group needs to be iterated twice, once for computing the cumulative life amount and the second time for the healing itself).
    • u – current unit while iterating through the Unit Group. All iterators (For structures) need an iterating variable – which is (most probably in all situations) context-dependent.
    • TLife – accumulator into which life of units in group is added.

    The functionality is simple: for each stalker blinking in the group, the trigger generates a separate thread, each with its own group of units, iterator, and accumulator. This does not exclude the possibility of a unit belonging to two or more such groups simultaneously (especially if the Stalkers blink very close to one another). That unit will, however, be treated by each of the threads and healed by the appropriate amount. Remember, even though the reference (variable) is local, the resource (unit) is still global!


    7. Parallelism and scheduling
    Alright, we now know that triggers may generate threads in parallel and independent to one-another. We also know that these threads operate on common resources such as units, structures, players, global variables etc. The question however remains how the system handles the simultaneous threads.

    Experiments show that the environment can always actively run only one (trigger) thread at a time, independent of the hardware of capabilities of the host system (such as the processor’s number of virtual cores). This simplifies certain complex scenarios - such as those further explained in chapter 8. It should be noticed that even though only one thread runs at a time, multiple threads can coexist.

    So what does the system do when threads are created by triggers? It adds the threads to a special queue (yep, you can think of threads as if they were waiting for their turn to run in a long line). When the system is thread-idle (executes no thread at the moment), it extracts the first element (head) from the queue and begins executing its code (Body).

    How long does the thread execute, and when are the other threads given a chance to run too? There are three situations in which a thread will stop executing and making room for another thread.

    (1) The executing thread reaches a Wait instruction. It relinquishes access to the processing unit and ‘falls asleep’ (goes into an idle state). Once the time period is was programmed to wait ends (whether it is 0.4, 5, 60 or whatever amount may come in mind), it is reawakened and placed back at the end of the queue.

    attachment.php

    (2) The executing thread has run uninterrupted for more than t0 seconds (t0 is a fixed period of time imposed by the system). The system interprets this as a timeout and kills the running thread. If it still had actions to perform, those actions will never take place. This is an abnormal situation and should be avoided!
    (3) The thread finishes execution in time. It relinquishes access to the processing unit and ends its execution successfully. It is obviously not added back to the queue but instead removed by the system as it has done its work.

    Understanding how the scheduler works is important when we will discuss next about the problems of code parallelism.


    8. Races & Locks
    We already talked about resources and how they are shared between threads. At most times it is possible to avoid the usage of such resources simply by smartly using the context set up by the trigger – though remember that even a local context operates with global objects. There are, however, situations in which the usage of global resources simply cannot be avoided!

    Consider a group of triggers that operate on a common global unit group. The triggers can work in parallel on the unique group by performing all combinations of operations on it (adding and removing units to/from the group, iterating through the group, comparing it with other groups etc.).
    Assume now that T1 performs operation O1 on the group while in parallel T2 performs operation O2 (we consider the two operations independent of one-another). If the order of the operations influences the final state of the unit group, it is said that we have a race. Think of the final state of the group as a destination, and the orders of the operations as alternative paths. The chosen path should not influence the destination.

    attachment.php

    To solve races, Starcraft II GUI environment features a special mechanism known as Critical Section. With the aid of a special lock variable, the system allows only one thread to execute a code section at a time. That way, global resources with potential race condition can be locked by an ‘owning’ thread, all the other threads attempting to access the same resource needing to wait for access on the resource.

    Critical sections are blocked by using an additional Boolean global variable (referred to by Blizzard as lock). Whenever the variable’s value is false, a thread is currently accessing the critical section. When no threads are accessing the resource, the variable is left on true.

    Note
    It is up to the programmer to set up the critical sections. Threads that do not place resource access in a critical section will ignore the restriction imposed by the owning thread and freely access the resource. Use critical sections with care though – if used unnecessarily, they will generate useless code processing traffic (and too high processing traffic can generate lag) and can even lead to unpleasant situations (such as deadlocks).

    Remember
    Races may occur only if threads operate on the same data simultaneously. Due to the functionality of the scheduler (responsible with thread maintenance), only threads that contain Wait actions can induce race conditions –such threads having interrupted execution. To avoid race conditions, try to perform all the critical operations uninterrupted by Wait instructions. That way no Critical Section is needed to guard the resource as the thread waits.

    Terminology
    We say that blocks of Actions (instructions) that run uninterrupted are Atomic. The atomicity of a block is guaranteed because the scheduler never (normally) forces threads to halt their execution unwillingly. Instead, threads themselves may choose to willingly pause their execution via the Wait function call (generally while waiting after another thread).


    9. Conclusions
    I am sure many of you probably found this tutorial a bit too abstract for your taste. Don’t be disappointed if you haven’t grasped every single concept developed here though, or if you don’t understand everything in depth. Some of the problems will reveal themselves with practice, others you might never have to deal with.

    Experiment with the concepts, learn to harness them, and if you have any questions do not hesitate to contact me and I will do my best to provide you a proper answer.


    Frequently Asked Questions
    1. From the effective structure of a trigger, am I to assume that the trigger is simply a thread generator?
    Yep, in simple words, that’s pretty much it. We can also say that the trigger filters the various events and selectively generates the response in the form of a series of actions.

    2. Can I register more than one event/trigger? When does the trigger react?
    Yes you can. The trigger will react whenever any of the specified events occur. This makes very little sense if the events are very different, as the set of actions may not be universally suitable. Remember! The same piece code is executed for ALL events. Take extra care when registering multiple events on the same trigger.

    3. Can I make a trigger execute only one thread at a time?
    You can try to disable the thread every time it fires and then re-enable it at the end, but if the event occurs for multiple actors at the same time (like in the example of dying units and the nuke), you will have more than one thread generated by the trigger and placed into the scheduler. Remember races and guard the entire actions block by placing it into a Critical Section!

    4. There are situations in which the same effect might be achieved either by turning highly generic event in a more particular one, or by using a condition instead to narrow down potential triggering actors and values. What’s the difference between the two?
    This is a very interesting question. Triggers will have to create the thread instance and set up the context before it can check the conditions. If the event however fails, the thread is not created at all. Therefore, particularized events are more efficient than supplementary conditions. This is however very circumstantial, and in most situations it is impossible to set up an event that can incorporate in it all the necessary conditions.

    5. Can I use local triggers?
    Yes you can – we usually refer to the creating trigger as the parent, and the created trigger as the child. It is even possible to make the parent wait after its child before continuing its execution. It is up to you how you use this feature, though.

    6. So do threads truly run in parallel?
    No. As explained in chapter 7, only one thread will actually run at a time.

    7. What’s a deadlock and how can I avoid it?
    Deadlock situations occur when multiple threads wait one after another, without any chance of exiting the wait state. This usually occurs due to the insertion of critical sections – so as mentioned before, great care when you use them!

    A simple example of a deadlock is when threads T1 (holding resource R1) and T2 (holding resource R2) attempt to get the other thread’s resource at the same time. T1 halts its execution while waiting for R2, but T2 is also frozen due to it waiting after R1. Because neither of the threads can relinquish its resource before entering the critical section, the two threads are indefinitely locked.

    attachment.php

    8. Is there a limit of the number of threads that may be scheduled at a time?
    Yes. Experiments suggest that the current version imposes a limit of 1024 active threads at a time.
 

Attachments

  • Holy Blink.jpg
    Holy Blink.jpg
    115.2 KB · Views: 3,043
  • TriggerEffective.jpg
    TriggerEffective.jpg
    43.6 KB · Views: 2,659
  • TriggerStructure.jpg
    TriggerStructure.jpg
    26.3 KB · Views: 2,716
  • TriggerContext.jpg
    TriggerContext.jpg
    79.6 KB · Views: 3,023
  • TriggerConceptual.jpg
    TriggerConceptual.jpg
    33.7 KB · Views: 2,803
  • Race.jpg
    Race.jpg
    36.3 KB · Views: 2,769
  • Deadlock.jpg
    Deadlock.jpg
    28.7 KB · Views: 2,668
  • Scheduler.jpg
    Scheduler.jpg
    15.7 KB · Views: 2,982
Last edited:

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,188
As far as I am aware, SC2's virtual machine is single threaded. Although many scripting threads can be generated to run at the same time, the virtual machine will execute them 1 at a time from start to finish until all schedualed threads complete within a frame. Use of thread suspension functions (wait) will reschedual the thread to continue at a later time and the machine will then proceede to another thread.

As such, every block of code can be considered atomic and so multi-threading functions such are semiphores, mutexes and critical sections are not needed. This also means that race conditions and deadlocks are not possible.

Ofcourse, I have no evidence to support this next to word of moth from other people who appeared to have experience using the language.

I guess the easiest test for parallelisim is to get 2 threads tun print different messages out in a big slow loop. If the messages start to interleave at any time (switch) then it employs multi-tasking. If messages appear sequentially (all first sort followed by all second sort) then it does not employ multi-tasking and so all blocks of code can be considered atomic.
 
Level 11
Joined
Jul 20, 2004
Messages
2,760
This was indeed a very nice experiment. I should have conducted it earlier myself, though for some odd reason I considered it unnecessary. I will need to update certain parts of the tutorial that rather clash with my initial belief regarding the system.

Let's start with the conclusions I've drawn after running a few experiments. It seems the VM implements a modified Round-Robin scheduler. It mainly consists of a queue in which threads are placed upon trigger's response to events (creation mechanism detailed in chapter 3). The time quanta allocated to a thread is a fixed value t0, but it is rather large (I think of the order of seconds?).
Thing is, the system does not preempt the thread after it has consumed its entire time quanta (like the classical RR scheduler does), but instead kills it. The only way threads can relinquish access to the "processor" is (just like you mentioned) via a Wait call. Only this way are threads popped from the head of the queue and pushed at its tail. Of course if the thread finishes executing all the instructions it is removed from the scheduler too.

And now to answer your comment (my sincerest apologizes if the responses sound a bit harsh, it's 3 AM here).

As far as I am aware, SC2's virtual machine is single threaded.
Threads exist! The lack of active preemption does not remove the thread concept. If threads did not exist it would be impossible to schedule the execution of actions as a response to events. It's the implemented scheduler that acts a bit different from what one might expect; it rather resembles a "First Come First Served" policy, which is highly impractical in nowadays' Operating Systems. In FCFS schedulers, the time quanta is set to infinity - threads cannot be preempted, but instead they need to relinquish processor control themselves.

As such, every block of code can be considered atomic and so multi-threading functions such are semiphores, mutexes and critical sections are not needed. This also means that race conditions and deadlocks are not possible.
Because of the scheduler's non-preemptive behavior, you are right indeed that synchronization mechanisms no longer require native support (I understand now why the Blizzard locks - or Critical Sections as they name them - can be implemented via busy-waiting). Uninterrupted blocks of code (wait-free) can be seen as atomic (there is no way to forcefully interrupt a thread without completely killing it). If even the tiniest Waits, however, are placed anywhere in the actions, they generate - roughly speaking - a "context switch".

And as a very simple example we might consider the first question in the FAQ. When a trigger generates multiple threads (as the event occurs multiple times), one can ensure the execution of one thread at a time only by placing it into a critical section (disabling the trigger has no effect once the thread has already been generated, and I will have to correct this too in the tutorial). Of course one might argue why would someone need such a special trigger that also contains waits inside it. While I admit this might be a bit of an "extreme" case, we all know that with a bit of ingenuity and lots of experimenting, modders can put all these mechanisms to good use. So yes, I do believe the Critical Sections can be put to good use, though they are not as important as specified in the tutorial.

I will update the specific sections accordingly and let you know when I have performed the proper modifications.
 
Level 5
Joined
Apr 14, 2006
Messages
140
Very good tutorial!
I thought I'd never find what I was looking for =).

Alas, after reading all the posts, I'm a bit confused. For example, if someone was to do a StarCraft 2 trigger like system, what would he need to implement? And is crritical section really useful?
 
Level 1
Joined
Dec 1, 2013
Messages
1
Multiple Triggers w/Same Event

My initialization trigger was getting very crowded, and like any good programmer I want to organize my code more logically. I moved my leaderboard, player, timer, and UI initialization into separate triggers all based on the Map Initialization event. They seem to work fine - and I make sure there are no cross dependencies (like if there is a shared variable I make sure anything during init that needs it is in the same trigger). As long as I don't assume initialization in the Map Initialization event is it safe to break them out like that? I'm treating it much like a constructor (in OO programming) so I know not to assume anything inside that trigger.
 
Top