Playing with Starcraft 2 Editor to understand how a good RTS is made

When working on Iron Marines engine at work we did some research on other RTS games in order to have more knowledge on how they did some stuff and why. In this post, in particular, I want to share a bit of my research on the SC2 Editor which helped a lot when making our own editor.

The objective was to see what a Game Designer could do or not with the SC2 Editor in order to understand some decisions about the editor and the engine itself.

Obviously, by taking a look at the game mods/maps available it is clear that you could build entire games over the SC2 engine, but I wanted to see the basics, how to define and control the game logic.

As a side note, I love RTS games since I was a child, I played a lot of Dune 2 and Warcraft 1. I remember playing with the editors of Command & Conquer and Warcraft 2 also, it was really cool, so much power 😉 and fun. With one of my brothers, each one had to make a map and the other had to play and beat it (we did the same with Doom and Duke Nukem 3d editors).

SC2 Editor

SC2 maps are built with Triggers which are composed by Events, Conditions and Actions to define parts of the game logic. There are a lot of other elements as well that I will talk a bit after explaining the basics.

Here is an image of the SC2 Editor with an advanced map:

Trigger logic

The Triggers are where the general map logic is defined. They are triggered by Events and say which Actions should be performed if given Conditions are met. Even though behind the scenes the logic is C/C++ code and it is calling functions with similar names, the Editor shows it in a natural language like “Is Any Unit of Player1 Alive?” which helps for quick reading and understanding.

This is an example of a Trigger logic of a SC2 campaign map:

Events

Events are a way to Trigger the Trigger logic, in other words, when an event happens the logic is executed. Here is an example of an event triggered when the unit "SpecialMarine" enters the region "Region 001":

Conditions

Conditions are evaluated in order to execute the actions or not. Here is an example of a condition checking if unit "BadGuy" is alive or not:

Actions

Actions are executed when the event happened and the conditions are met. They could be anything supported by the editor, from ordering a structure to build a unit to showing a mission objective update on screen, among other things.

This example shows an action that enqueues to unit "BadGuy" an attack order with unit "SpecialMarine" as target, replacing existing enqueued orders in that unit. There is another action after that which turns off the Trigger in order to avoid processing its logic again.

The idea with this approach is to build the logic in a descriptive way, the Game Designer has tools to fulfill what he needs in terms of game experience. For example, he needs to make it hard to save a special unit when you reach its location, then he sends a wave of enemies to that point.

I said before that the editor generates C/C++ code behind the scenes, so, for my example:

The code generated behind the scenes is this one:

Here is a screenshot of the example I did, the red guy is the SpecialMarine (controlled by the player) and the blue one is the BadGuy (controlled by the map logic), if you move your unit inside the blue region, BadGuy comes in and attack SpecialMarine:

Even though it is really basic, download my example if you want to test it 😛 .

Parameters

In order to make the Triggers work, they need some values to check against, for example, Region1, is a region previously defined, or “Any Unit of Player1”. Most of the functions for Events, Conditions and Actions have parameters of a given Type, and the Editor allow the user to pick an object of that Type from different sources: a function, a preset, a variable, a value or even custom code:

It shows picking a Unit from units in the map (created instances).

It shows Unit picking from different functions that return a Unit.

This allows the Game Designer to adapt, in part, the logic to what is happening in the game while keeping the main structure of the logic. For example, I need to make the structures of Player2 explode when any Unit of Player1 is in Region1, I don’t care which unit I only care it is from Player1.

Game design helper elements

There are different elements that help the Game Designer when creating the map: Regions, Points, Paths and Unit Groups, among others. These elements are normally not visible by the Player but are really useful to the Game Designer to have more control over the logic.

As said before, the SC2 Editor is pretty complete, it allows you to do a lot of stuff, from creating custom cutscenes to override game data to create new units, abilities, and more but that's food for another post.

Our Editor v0.1

The first try of creating some kind of editor for our game wasn't so successful. Without the core of the game clearly defined we tried to create an editor with a lot of the SC2 Editor features. We spent some days defining a lot of stuff in abstract but in the end we aimed too far for a first iteration.

So, after that, we decided to start small. We starting by making a way to detect events over the "being defined core" at that point. An event could be for example: "when units enter an area" or "when a resource spot was captured by a player".

Here are some of the events of one of our maps:

Note: Even though they are Events we named them Triggers (dunno why), so AreaTrigger is an empty Trigger in terms of SC2 Editor with just an Event.

Events were the only thing in the editor, all the corresponding logic was done in code in one class, corresponding to that map, which captures all events and checks conditions for taking some actions, normally, sending enemies to attack some area.

Here is an example code for some of the previous defined events:

It wasn't a bad solution but had some problems:

  • The actions were separated from the level design which played against the iteration cycle (at some point our project needed between 10 and 15 seconds to compile in the Unity Editor).
  • Since it needs code to work, it requires programming knowledge and our team Game Designers aren't so good with code.

Our Editor v0.2

The second (and current) version is more Game Designer friendly, and tends to be more similar to SC2 Editor. Most of the logic is defined in the editor within multiple triggers. Each Trigger is defined as a hierarchy of GameObjects with specific components to define the Events, Conditions and Actions.

Here is an example of a map using the new system:

This declares for example a trigger logic that is activated by time, it has no conditions (so it executes always given the event) and it sends some enemies in sequence and deactivates itself at the end.

We also created a custom Editor window in order to help creating the trigger hierarchy and to simplify looking for the engine Events, Conditions and Actions. Here is part of the editor showing some of the elements we have:

All those buttons automatically create the corresponding GameObject hierarchy with the proper Components in order to make everything work according to plan. Since most of them need parameters, we are using the Unity built-in feature of linking elements given a type (a Component), so for example, for the action of forcing capture a Capturable element by team Soldiers, we have:

Unity allow us to pick a Capturable element (CapturableScript in this case) from the scene. This simplifies a lot the job of configuring the map logic.

Some common conditions could be to check if a resource spot is controlled by a given player or if a structure is alive. Common actions could be, send a wave of enemy units to a given area or deactivate a trigger.

The base code is pretty simple, it mainly defines the API while the real value of this solution is in the custom Events, Conditions and Actions.

Pros

  • Visual, and more Game Designer friendly (it is easier for Programmers too).
  • Faster iteration speed, now we can change things in Editor directly, even in runtime!
  • Easily extensible by adding more Events, Conditions and Actions, and transparent to the Game Designers since they are automatically shown in our Custom Editor.
  • Take advantage of Unity Editor for configuring stuff.
  • Easy to disable/enable some logic by turning on/off the corresponding GameObject, which is good for testing something or disable one logic for a while (for example, during ingame cinematics).
  • More control to the Game Designer, they can test and prototype stuff without asking programming team.
  • Simplified workflow for our ingame cinematics.
  • Compatible with our first version, both can run at the same time.

Cons

  • Merge the stage is harder now that it is serialized with the Unity scene, with code we didn’t have merge problems or at least it was easier to fix. One of the ideas to simplify this is to break the logic in parts and use prefabs for those parts, but it breaks when having links with scene instances (which is a common case).
  • A lot of programming responsibility is transferred to the scripting team which in this case is the Game Design team, that means possibly bad code (for example, duplicated logic), bugs (forget to turn off a trigger after processing the actions) and even performance.

Conclusion

When designing (and coding) a game, it is really important to have a good iteration cycle in each aspect of the game. Having switched to a more visual solution with all the elements at hand and avoiding code as much as we could, helped a lot with that goal.

Since what we end up doing looks similar to a scripting engine, why didn't we go with a solution like uScript or similar in the first place? the real answer is I didn't try in depth other Unity scripting solutions out there (not so happy with that), each time I tried them a bit they gave me the feeling it was too much for what we needed and I was unsure how they perform on mobile devices (never tested that). Also, I wasn't aware we would end up need a scripting layer, so I prefered to evolve over our needs, from small to big.

Taking some time to research other games and play with the SC2 Editor helped me a lot when defining how our engine should work and/or why we should go in some direction. There are more aspects of our game that were influenced in some way by how other RTS games do it, which I may share or not in the future, who knows.

I love RTS games, did I mention that before?

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)

Using Unity Text to show numbers without garbage generation

The idea of this post is to show different ideas and analysis on how to use Unity UI Text to show numbers without garbage generation. I need this for a framerate counter and other debug values.

Test case

Shows a fixed digit length number in screen, regenerated each frame with a new random value.

Shows how the test scene used for all test cases work.

Using Strings

Since strings are immutable in c#, common operations on strings generates new strings and hence allocates new heap memory. If you are using strings as temporary values like showing a changing number in a UI text then that memory becomes garbage. In PC that garbage could go unnoticed but not in mobile devices since that could derive in a hiccup when the garbage collector decides to collect it.

The idea with these tests is to try to use make the label work with strings without garbage generation. To detect generated garbage I am using the Unity profiler and avoiding ToString() of int, float, etc, to just calculate the cost of the string manipulation for now.

String concatenation

String concatenation generates 30 Bytes per frame since internally String.Concat() calls String.InternallyAllocateStr().

It is not as bad as expected, it is just creating a new string with the length of the first string plus the second and then it copies their values. Obviously it becomes worse when multiple concatenations are done in secuence.

Test code:

Text text;
 
static readonly string[] numbers = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
 
void Start () {
    text = GetComponent<Text> ();
}
 
void Update () {
     
    string a = numbers[UnityEngine.Random.Range(0, numbers.Length)];
    string b = numbers[UnityEngine.Random.Range(0, numbers.Length)];
 
    text.text = a + b;
}

String format

Using string.Format() generates 176 Bytes per frame, internally is using String.FormatHelper + StringBuilder.ToString().  The first one creates a new StringBuilder and the second is the transform from StringBuilder to string.

Test code:

 Text text;
 
 static readonly string[] numbers = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
 
 void Start () {
     text = GetComponent<Text> ();
 }
 
 void Update () {
     string a = numbers[UnityEngine.Random.Range(0, numbers.Length)];
     string b = numbers[UnityEngine.Random.Range(0, numbers.Length)];
 
     text.text = string.Format ("{0}{1}", a, b);        
     
 }

String Builder Format

Using cached StringBuilder improves the previous one a bit, it generates 86 Bytes per frame, the AppendFormat is generating garbage and then the set_Length() (used to clear the StringBuilder).

Test code:

 Text text;
 StringBuilder stringBuilder = new StringBuilder(20, 20);
 
 static readonly string[] numbers = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
 
 void Start () {
     text = GetComponent<Text> ();
     stringBuilder.Length = 3;
 }
 -j
 void Update () {
     string a = numbers[UnityEngine.Random.Range(0, numbers.Length)];
     string b = numbers[UnityEngine.Random.Range(0, numbers.Length)];
 
     stringBuilder.Length = 0;
     stringBuilder.AppendFormat ("{0}{1}", a, b);
 
     text.text = stringBuilder.ToString();
 }

Note: If I change the StringBuilder starting capacity and max capacity, the cost is the same but goes to ToString() method instead, but internally to the same method String.InternallyAllocateStr().

String Builder only Append

Instead of using StringBuilder.AppendFormat, change to use only String.Append. This reduces the cost to only 30 Bytes per frame (the same of the first one), the only cost here is the set_Length() which internally calls String.InternallyAllocateStr().

Test code:

 Text text;
 StringBuilder stringBuilder = new StringBuilder(20, 20);
 
 static readonly string[] numbers = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
 
 void Start () {
     text = GetComponent<Text> ();
     stringBuilder.Length = 3;
 }
 
 void Update () {
     string a = numbers[UnityEngine.Random.Range(0, numbers.Length)];
     string b = numbers[UnityEngine.Random.Range(0, numbers.Length)];
 
     stringBuilder.Length = 0;
     stringBuilder.Append (a);
     stringBuilder.Append (b);
 
     text.text = stringBuilder.ToString();
 }

Note: Does the same behaviour if I change starting and max capacity, the cost is the same but is on ToString() instead of set_Length().

String Builder by replacing chars

If instead of Append I replace chars directly by using [] and avoid the set_Length(), the cost is the same, 30 Bytes per frame, since the String.InternallyAllocateStr() goes to set_Chars().

Test code:

 Text text;
 StringBuilder stringBuilder = new StringBuilder(20, 20);
 
 static readonly char[] numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
 
 void Start () {
     text = GetComponent<Text> ();
     stringBuilder.Length = 3;
 }

 void Update () {
     char a = numbers[UnityEngine.Random.Range(0, numbers.Length)];
     char b = numbers[UnityEngine.Random.Range(0, numbers.Length)];
 
     stringBuilder [0] = a;
     stringBuilder [1] = b;
 
     text.text = stringBuilder.ToString();
 }

Note: Again, does the same behaviour if I change starting and max capacity, instead of set_Chars(), the cost is in ToString() method.

String Builder, access internal string by reflection

There is a suggestion at in this post to access by refleciton to _str field from StringBuilder class to avoid the cost of ToString() method.

Test code:

 Text text;
 StringBuilder stringBuilder = new StringBuilder(20, 20);

 static System.Reflection.FieldInfo _sb_str_info = 
        typeof(StringBuilder).GetField("_str", 
        System.Reflection.BindingFlags.NonPublic | 
        System.Reflection.BindingFlags.Instance);
 
 static readonly char[] numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
 
 void Start () {
     stringBuilder.Length = 3;
 
     text = GetComponent<Text> ();
 }
 
 void Update () {
     stringBuilder[0] = numbers[UnityEngine.Random.Range(0, numbers.Length)];
     stringBuilder[1] = numbers[UnityEngine.Random.Range(0, numbers.Length)];
     stringBuilder[2] = (char) 0;
 
     var internalValue = _sb_str_info.GetValue (stringBuilder) as string;
     text.text = internalValue;
 }

In this case, there is no garbage at all. However, I see no change in the UI Text even though the editor shows the text field value is changing, like it is not being redrawn in screen. I suppose that could be because the string pointer is not changing and by taking a look at the Text code from the Unity UI it is comparing with != instead of Equals... not sure here.

 public virtual string text
 {
     get
     {
         return m_Text;
     }
     set
     {
         if (String.IsNullOrEmpty(value))
         {
             if (String.IsNullOrEmpty(m_Text))
                 return;
             m_Text = "";
             SetVerticesDirty();
         }
         else if (m_Text != value)
         {
             m_Text = value;
             SetVerticesDirty();
             SetLayoutDirty();
         }
     }
 }

I tried by forcing layout and vertices dirty after updating internal string, just in case, but had no luck (sad face).

Caching strings

Another option suggested in this blog post is to precache strings for different numbers but that is only reasonable for a small amount of digits. I like it because it is simple and could be generated at runtime, and works well for debug numbers like FPS where the number is normally between 0 and 60.

I tried it and it works really well and generates 0 Bytes per frame.

Test code:

 Text text;
 
 string[] generated;
 
 // Use this for initialization
 void Start () {
     text = GetComponent<Text> ();
 
     generated = new string[100];
 
     // should go from 0 to 99.
     for (int i = 0; i < 100; i++) {
         generated [i] = string.Format ("{0:00}", i);
     }
 }
 
 // Update is called once per frame
 void Update () {
     int random = UnityEngine.Random.Range (0, generated.Length);
     text.text = generated [random];
 }
 

Rendering numbers directly

One possible way to avoid all this garbage (I mean both the code and the unused memory) is to not use strings at all but to just render to the screen images for each number digit, where each digit is a different sprite.

When making TinyWarriors prototype I did a basic number rendering where I could specify the number of digits and it just created multiple Unity UI Images inside a horizontal layout.

Shows a test using images for each digit instead of a text.

Test code:

 public Image[] numbers;
 
 // in order, like 0, 1, 2, ..., 9
 public Sprite[] numberSprites;
 
 public bool fillZero = true;
 
 void Start()
 {
     SetNumber (0);
 }
 
 public void SetNumber(int number)
 {
     int tens = (number % 100) / 10;
     int ones = (number % 10);
 
     var tensActive = fillZero || tens != 0;
     var onesActive = fillZero || number > 0;
 
     numbers [0].gameObject.SetActive (tensActive);
     numbers [1].gameObject.SetActive (onesActive);
 
     if (tensActive)
         numbers [0].sprite = numberSprites [tens];
 
     if (onesActive)
         numbers [1].sprite = numberSprites [ones];
 }
 
 public void Update()
 {
     int random = UnityEngine.Random.Range (0, 100);
     SetNumber (random);
 }

The code could be adapted to support more digits. When profiling it in editor there is a lot of garbage generation, around 1KB per frame, in Canvas.SendWillRendereCanvases() because it is forcing a material rebuild each time a sprite is changed. However, I tested it on devices and it doesn’t so it must be something related with the Unity editor.

Other strategies

Other strategies include minimizing the garbage generation by reducing the text update frequency, for example, by avoiding updating the text if the number didn't change and/or updating the text from time to time and not every frame.

Conclusion

Since I just wanted a solution for a framerate counter (and other debug numbers) the last solutions are perfect and I believe those could even be extrapolated for other game needs, like showing the player points in an arcade game, with a bit of extra thinking.

References

Here is a list of some articles, forum and blog posts I took a look during the tests and the post writing.

Unity memory optimizations article - https://unity3d.com/es/learn/tutorials/temas/performance-optimization/optimizing-garbage-collection-unity-games

Memory management reference - http://www.memorymanagement.org/

FPS implementation caching strings - http://catlikecoding.com/unity/tutorials/frames-per-second/

Using reflection to set StringBuilder string to avoid garbage - http://www.defectivestudios.com/devblog/garbage-free-string-unity/

FPS Asset - http://blog.codestage.ru/unity-plugins/fps/

Another FPS Asset - https://www.assetstore.unity3d.com/en/#!/content/6513

StringBuilder API - https://msdn.microsoft.com/en-us/library/system.text.stringbuilder(v=vs.110).aspx

Performance tips for Unity for mobile - https://divillysausages.com/2016/01/21/performance-tips-for-unity-2d-mobile/

Unity UI Source code - https://bitbucket.org/Unity-Technologies/ui

Untiy Community Library - https://github.com/UnityCommunity/UnityLibrary

 Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

Guardar

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)