Optimizing Script Tips
It's no secret scripts are an incredible tool. The ability to easily insert your own code right into modules and have it work along side native engine functions is one of the main reasons OpenBOR can replicate any game play feature imaginable. But like Uncle Ben said,
"with great power comes great responsibility."
Done right, scripts can help set your module apart not only from the endless parade of tired BOR clones, but also from each other. Believe it or not, a well conceived script can also optimize your module to consume less resources. This same power can also wreak havoc - transforming your would be work of art into a clunky resource hog.
If you are asking why you should care, that's up to you. Do you want portability? It's important to be aware there is a vast gulf between consoles, mobile devices, and PCs. Your aging laptop can easily brute force through resource intensive modules, but even the latest Androids might hack up a lung. Beyond that, optimized scripts don't just help the platform, they will help YOU in the module building process.
This guide is intended to provide an overview of optimizing techniques. It assumes you already have some proficiency with scripting, or at the very least have experimented a bit. Note that script is an art form, and as such there can be exceptions to every rule. These are general tips that apply to most situations. Several of them may overlap. I'll try to provide an explanation as to why you should follow each tip. That's often where the real knowledge lies, so if you pull a TLDR, it's your own fault.
Avoid Inline Scripts
Inline scripts are code that is written right into the animations of a model's text file using the
@script and
@end_script tags. This is one of the most commonly seen, and unfortunately, worst ways to add animation script. Instead, you should write functions and call them with a
@cmd tag. Inline code does have its uses, but it should be saved for very special circumstances.
Why?
Inline code runs on every single frame of a given animation. This means you must either add code to avoid taking action except on specific frames, or just put up with code executing when it doesn't need to. Additionally, by its nature inline code is impossible to reuse (more on that later), and any local variables you need must be reacquired each time the code runs.
Inline code is instead best used as a means to execute predefined functions in the rare case you want to call the function on every frame.
Whenever Possible, Break Tasks Down to Reusable Functions
Just as you should avoid inline code, you should be breaking tasks down into functions. Remember that functions can call other functions. Therefore, try to break big tasks down into smaller bits of reusable code. Then you can piece them back together as needed.
There is a tiny bit of overhead for a function call, but this is more than made up for by the memory and time you can save by building portions of code that can be reused over and over again.
Why?
Script requires memory to compile and prepare for use. The more you can reuse code, generally the more you can save memory (assuming proper use of
#import and
#include... more on that later). Additionally, human time is also very expensive. By defining and executing simple functions, then executing those functions with other functions to assemble more complicated tasks like a set of Lego blocks, you centralize your maintenance points. This also makes life easier when something goes wrong.
Avoid Magic Numbers
Magic numbers are any static/constant values (typically numbers, hence the name) inserted directly into code. For instance, you might have a function that takes a value of 0 to 2, then performs an action accordingly. In your function, the code looks something like this:
C:
switch(value)
{
case 0:
// do something.
break;
case 1:
// do something else.
break;
case 2:
// do something different than before.
break;
}
Then you call the function like this:
Code:
@cmd my_doing_something_function 1
That doesn't look so terrible at first glance, but it is a very poor practice that will eventually cause you headaches. Instead, use the
#define directive to create named constants, like this:
C:
#define DO_SOMETHING 0
#define DO_SOMETHING_ELSE 1
#define DO_SOMETHING_DIFFERENT 2
switch(value)
{
case DO_SOMETHING:
// do something.
break;
case DO_SOMETHING_ELSE:
// do something else.
break;
case DO_SOMETHING_DIFFERENT:
// do something different than before.
break;
}
You can (and should) use the #defined constant in your function call too, so now it looks like this:
Code:
@cmd my_doing_something_function DO_SOMETHING_ELSE
See how the function call is a lot more readable? You know you're telling it to do "something else" without needing to go back and review the function itself.
Why?
This has no effect on CPU or memory use, but any experienced coder will tell you the single most important thing you can do is make the code readable. Magic values are almost impossible to understand and maintain without wasting valuable time analyzing. If something goes wrong you have to fix it all by hand. That's a job that could take days. I've written an entire article about magic numbers
you can check out here for further details.
Turn On NOCMDCOMPATIBLE
The
nocmdcompatible command is a binary option in script.txt that when turned on (1) disables the compatibility mode for processing
@cmd tags. By default this value is 0, meaning OpenBOR processes tags in a less optimal way to accommodate older modules.
Why?
The
@cmd tag executes native or user defined functions on the frame they are placed. In compatibility mode, when there are multiple
@cmd tags on a frame, the frame number is evaluated for every tag. Here's an example:
Code:
@cmd f1
@cmd f2
@cmd f3
frame data/chars/ffff/1.png
Compatibility mode enabled (nocmdcompatible 0).
Code:
if(frame==3)
{
f1();
}
if(frame==3)
{
f2();
}
if(frame==3)
{
f3();
}
With compatibility mode disabled (
nocmdcompatible 1), the engine compiles with a more optimal algorithm where the frame number is evaluated only once:
Code:
if(frame==3)
{
f1();
f2();
f3();
return;
}
Avoid Global Vars
Global variables are great tools, but it is important to note they are intentionally designed to remain in memory until you clear them or shut down the module. Certain settings allow them to remain in save files even when the engine is turned off. This means two things:
- You have to make sure these variables are deleted. Depending on your function design, that might mean adding and executing execute code in other unrelated events. It can add a lot of unwanted complexity that is error prone and difficult to keep up with. And of course extra code can mean extra work for the engine.
- If you forget or make just one tiny mistake in step 1, the variable stays around forever. It might cause bugs by retaining a value you thought was blank, and will likely give your module a memory leak (gradually using more and more memory over time).
What to do then? Easy, use
local vars. Most of the time I see global vars in use, it is to track values across several iterations of an animation script or monitor some entity specific value. There's no need for global variables to do this. Local vars will work just fine.
Why?
Unlike global variables, the engine cleans up local vars when the script they are defined in is unloaded. Note, the
script, not the
function. So while you can't use a local var to do something like pass information from an animation script to an update script, you can pass information around within either one of those all day. And when the script is done, the variable dies with it. It's more convenient for you, and more optimal for your module.
In other words, most of the time you are using global variables, you could probably use local variables instead, in which case you should. Save the global variables for the jobs that only they can do.