Difference between revisions of "Wml optimisation"
(→Further reading: - added a link to advanced optimalisations and hacks) |
(Update links to macro-reference.html, other links too) |
||
(8 intermediate revisions by 6 users not shown) | |||
Line 1: | Line 1: | ||
− | |||
− | |||
==The basics== | ==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 | + | ===What you should know next=== |
*[[PreprocessorRef#Preprocessor_directives|Macros]] | *[[PreprocessorRef#Preprocessor_directives|Macros]] | ||
*[[EventWML]] | *[[EventWML]] | ||
Line 19: | Line 22: | ||
====Inefficient example==== | ====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 | #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 | ||
Line 177: | Line 179: | ||
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. | 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 '''[[ModificationWML#The_.5Bresource.5D_toplevel_tag|[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== | ==Analogies to "real" programming== | ||
Line 184: | Line 323: | ||
==Further reading== | ==Further reading== | ||
− | [ | + | *[[Advanced_Optimisations_and_Hacks|More complex optimisations]] - read this only if you believe you know everything! |
− | *[ | + | *[https://www.wesnoth.org/macro-reference.html Macro reference] |
*[[LuaWML]] | *[[LuaWML]] | ||
*[[ConventionsWML]] | *[[ConventionsWML]] | ||
+ | |||
+ | [[Category:WML_Tips]] |
Latest revision as of 08:10, 24 August 2021
Contents
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
- More complex optimisations - read this only if you believe you know everything!
- Macro reference
- LuaWML
- ConventionsWML