Wml optimisation

From The Battle for Wesnoth Wiki

The basics

The first thing you need to know: If you have no problems with performance, then don't bother. Any optimisation is supposed to help you in following situations:

  • The game freezes for several seconds after each user action
  • Your scenario loading time is longer than several minutes
  • Your add-on codebase is larger than 5 megabytes

What you should know next

How do I optimise my code?

So, you've written your campaign, scenario or era and it works. Congratulations, but your work is far from over! If your campaign or scenario is big enough, you'll often notice that it runs very slowly, and you need to do something about it. Or maybe you have a powerful computer and you'd like to make sure that other, weaker machines won't have troubles with running your content. Or perhaps, you just want to learn how to write code that not only works, but is also efficient.

The first thing to know, since apparently it's not stressed enough, macros do not improve performance! Macros only make your code more readable and easier to maintain. Try to refrain from using big macros, and more importantly, avoid using macros inside macros (inside macros, inside macros... yes, I've seen it happen, and yes I've tried doing it myself when I was new to wml). Under the covers, WML macros are replaced by the code they represent, and if there are macros inside macros (especially big ones), lines of code multiply horribly fast.

What's a better way to write code that appears often? Custom events' and while loops! By defining a custom event, and firing it when needed, you'll save a lot of code lines, make your code nicer to read and much more efficient. Same goes for the while loop: instead of copy/pasting your code, try to use a loop, whenever you can. Let's take a look at this example problem: whenever side 1 unit enters a village, you want to place 5 walking corpses around this village. Let's say there are 6 villages in question with coordinates (1,1), (2,2), (3,3), (4,4), (5,5), (6,6).

Inefficient example

   #NOTE: remember, this should work alright, but it is not efficient! Imagine if you have much more villages in mind, or you want to spawn more zombies
   
   #define SPAWN_ZOMBIES X Y SIDE
   [unit]
   side={SIDE}
   type=Walking Corpse
   x,y={X},{Y}
   placement=map_passable
   [unit]
   [unit]
   side={SIDE}
   type=Walking Corpse
   x,y={X},{Y}
   placement=map_passable
   [unit]
   [unit]
   side={SIDE}
   type=Walking Corpse
   x,y={X},{Y}
   placement=map_passable
   [unit]
   [unit]
   side={SIDE}
   type=Walking Corpse
   x,y={X},{Y}
   placement=map_passable
   [unit]
   [unit]
   side={SIDE}
   type=Walking Corpse
   x,y={X},{Y}
   placement=map_passable
   [unit]
   #enddef
   
   [event]
   name=moveto
       [filter]
       side=1
       x,y=1,1
       [or]
       x,y=2,2
       [/or]
       [or]
       x,y=3,3
       [/or]
       [or]
       x,y=4,4
       [/or]
       [or]
       x,y=5,5
       [/or]
       [or]
       x,y=6,6
       [/or]
       [/filter]
   
       {SPAWN_ZOMBIES $unit.x $unit.y 2}
   [/event]

Looks pretty innocent, right? But imagine that there are more villages. Or better yet, imagine you want to increase the amount of zombies, you'll have to copy/paste even more, and it's easy to make a mistake while doing it.

More efficient example

   #first, let's define our custom event:
   [event]
   name=spawn_zombies
   first_time_only=no
   #it's easy to forget, but you have to set first_time_only in custom events too
   {VARIABLE i 0}
   [while]
       [variable]
       name=i
       less_than_equal_to=5
   #repeating the loop 5 times for 5 zombies, note how easy it is now to change the amount of zombies you want to spawn!
       [/variable]
       [do]
           [unit]
           side=2
           type=Walking Corpse
           x,y=$unit.x,$unit.y
   #unit variable is passed to this event from the parent event. 
           placement=map_passable
           [unit]
           {VARIABLE_OP i add 1}
       [/do]
   [/while]
   {CLEAR_VARIABLE i}
   [/event]
   
   [event]
   name=moveto
       [filter]
       side=1
       x,y=1,1
       [or]
       x,y=2,2
       [/or]
       [or]
       x,y=3,3
       [/or]
       [or]
       x,y=4,4
       [/or]
       [or]
       x,y=5,5
       [/or]
       [or]
       x,y=6,6
       [/or]
       [/filter]
   
       [fire_event]
       name=spawn_zombies
       [/fire_event]
   [/event]

Fired events vs macros

But if fire events are so cool and efficient, what is the advantage of using macros? Good question. As you can see, custom events have one issue: it's harder to set dynamic variables in fired events. When defining macros, you specify arguments and those arguments are declared whenever we're using that macro and can be changed in any way you, and more importantly, someone else wants. Remember, that you might want to start a bigger project and invite someone to help you with the code. Macros are obvious and transparent in usage - when reading a macro definition you always know what is the changing variable and what is a constant. In custom events, it is not so obvious.

Let's take a look at one of the default macros included with wesnoth:

   #define ABILITY_REGENERATES
   # Canned definition of the Regenerate ability to be included in an [abilities]
   # clause.
   [regenerate]
       value=8
       id=regenerates
       name= _ "regenerates"
       female_name= _ "female^regenerates"
       description= _ "Regenerates:
   The unit will heal itself 8 HP per turn. If it is poisoned, it will remove the poison instead of healing."
       affect_self=yes
       poison=cured
   [/regenerate]
   #enddef

This is a good macro, because replacing it by a custom event would be complicated and messy and it wouldn't save a lot of code, anyway. Another example is

   #define GENERIC_UNIT SIDE TYPE X Y
   # Creates a generic unit of TYPE belonging to SIDE at X,Y, which has a
   # random name, gender and traits (just like a recruited unit).
   [unit]
       side={SIDE}
       type={TYPE}
       x={X}
       y={Y}
       generate_name=yes
       random_traits=yes
       random_gender=yes
       upkeep=full
   [/unit]
   #enddef

This time, making a custom event would be fairly easy, but it would be much less transparent in usage. The code {GENERIC_UNIT 1 Spearman 12 3} is self-explanatory, simple and versatile.

Macros that are included in multiple scenarios

If you have a macro that's included in every scenario of a campaign, in every map of a map-pack, or in multiple units of an era, then the macro will be expanded multiple times. If macro is an [event] and the expanded version is huge (for example, if it uses nested macros), then using the [resource] tag can reduce it to a single copy.

This can be an easy and quick optimisation, as the old macro can be replaced with one that just contains the [load_resource] tag.

Do not treat macroses as functions

Main advantage of macroses is that they allow you to reuse your code with less effort. But you need to take into account how exactly they are used by Wesnoth. Look at the example

   #define CREATE_REINFORCMENT SIDE 
   {VARIABLE temp_side {SIDE}}
   [variable]
       name = temp_side
       value = 1
   [/variable]
   [if]
       {VARIABLE_CONDITIONAL temp_side equals 1}
       [then]
           {GENERIC_UNIT 1 "Walking Corpse" 12 3}
       [/then]
   [/if]
   [if]
       {VARIABLE_CONDITIONAL temp_side equals 2}
       [then]
           {GENERIC_UNIT 2 "Spearman" 3 16}
       [/then]
   [/if]
   {CLEAR_VARIABLE temp_side}
   #enddef
   [event]
   name=turn 10
       {CREATE_REINFORCMENT 1}
       {CREATE_REINFORCMENT 2}
   [/event]

In this example I wanted to create additional units for each side at turn 10. Side 1 is undeads and side 2 is loyalists. Everything looks good, but there is a mistake. When Wesnoth loads scenario, all macroses are substituted with their meaning. This example becomes:

   [event]
       name="turn 10"
       [set_variable]
           name="temp_side"
           value=1
       [/set_variable]
       [if]
           [variable]
               equals=1
               name="temp_side"
           [/variable]
           [then]
               [unit]
                   generate_name=yes
                   random_gender=yes
                   random_traits=yes
                   side=1
                   type="Walking Corpse"
                   upkeep="full"
                   x=12
                   y=3
               [/unit]
           [/then]
       [/if]
       [if]
           [variable]
               equals=2
               name="temp_side"
           [/variable]
           [then]
       # PART 1 BEGINS
               [unit]
                   generate_name=yes
                   random_gender=yes
                   random_traits=yes
                   side=2
                   type="Spearman"
                   upkeep="full"
                   x=3
                   y=16
               [/unit]
       # PART 1 ENDS
           [/then]
       [/if]
       [clear_variable]
           name="temp_side"
       [/clear_variable]
       [set_variable]
           name="temp_side"
           value=2
       [/set_variable]
       [if]
           [variable]
               equals=1
               name="temp_side"
           [/variable]
           [then]
       # PART 2 BEGINS
               [unit]
                   generate_name=yes
                   random_gender=yes
                   random_traits=yes
                   side=1
                   type="Walking Corpse"
                   upkeep="full"
                   x=12
                   y=3
               [/unit]
       # PART 2 ENDS
           [/then]
       [/if]
       [if]
           [variable]
               equals=2
               name="temp_side"
           [/variable]
           [then]
               [unit]
                   generate_name=yes
                   random_gender=yes
                   random_traits=yes
                   side=2
                   type="Spearman"
                   upkeep="full"
                   x=3
                   y=16
               [/unit]
           [/then]
       [/if]
       [clear_variable]
           name="temp_side"
       [/clear_variable]
   [/event]

Obviously part 1 and part 2 will never happen. It makes conditional check useless while you code becomes more complex. This is very common mistake even in advanced scenarios and add-ons. To avoid this problem, you need to follow the rule: Do not make conditional actions based on values of arguments. If you need to do this, then you need to think over situation again. In our example it might be:

   [event]
   name=turn 10
       {GENERIC_UNIT 1 "Walking Corpse" 12 3}
       {GENERIC_UNIT 2 "Spearman" 3 16}
   [/event]

Analogies to "real" programming

Now that you know how to use macros and custom events to optimise your code, let's take a moment to think how you could use your knowledge outside Wesnoth. You've probably heard about c++, python, lua or any other programming language. Just like them, WML has some common features but under slightly different names. For example, instead of making custom events, you're making custom functions and procedures that can be used further in your code in a very similar way. That's why understanding optimisation is so important - it'll help you understand real programming problems, and develop good habits!

Further reading

This page was last edited on 24 August 2021, at 08:10.