Skip to content
This repository has been archived by the owner on Jan 5, 2024. It is now read-only.

Lua Optimization and Organization Tips and Tricks

Gareth YR edited this page Jan 2, 2020 · 1 revision

This guide is designed to help people make their Lua scripts faster and more efficient. Some of it is general programing knowledge, some applies to Lua in general and some just to Cortex Command. The tips are organized in terms of difficulty, so the first ones should be very easy to use while the later ones require more knowledge and confidence.

Basic Concepts

1. Use native code
This one is pretty obvious, but wherever you can you should always use CC's built-in Lua functions or native Lua functions over things you write from scratch. Even simple operations are best done with CC's C++ code because it is inherently far faster than Lua.

--TLDR, don't reinvent the wheel

2. Operator short circuiting
This is a feature in most modern programming languages, including Lua. While the name may sound complicated, the meaning and use of it is simple - in any statement with an and or an or, the game will only run the code after an and if the first condition evaluates to true, and will only run the code after an or if the first condition evaluates to false. This works because boolean logic dictates that the rest of the code is irrelevant in these cases, since the condition will already evaluate to false.

This is extremely helpful for a variety of reasons, chief of which is code efficiency. For example, the code

if (SceneMan:MovePointToGround(someVector, 1) and someValue == true) then ...

is significantly less efficient than its reverse, because checking the value of a variable is cheap while MovePointToGround is an expensive function. In this case it would be better to write

if (someValue == true and SceneMan:MovePointToGround(someVector, 1)) then ...

This, of course, applies for chains of operators, just be sure to check that everything is bracketed properly if you're combining or and and operators. For example

if (cheapCondition and (moderateCondition or expensiveCondition) and veryExpensiveCondition) then ...

In addition to efficiency, this allows you to save lines and reduce code complexity when doing safety checks. For example, instead of writing

if (someActor ~= nil) then
    if (someActor.Team == 1) then
        ...

You can just do

if (someActor ~= nil and someActor.Team == 1) then ...

because the condition after the and will not be run unless the first condition evaluates to true.

Note: You can simplify this even more since Lua will evaluate any non-nil, non-false variable as true in condition checks, so the ~= nil can be removed and the code can be

if (someActor and someActor.Team == 1) then ...

--TLDR, reorder any and and or checks so the least expensive code runs first

3. Timers
Using Timers can be a great way to make expensive code not slow down the game by running it less frequently. It's a pretty simple concept - if some piece of code takes 0.1 seconds to run and you run it every frame the game will be constantly slowed down by that 0.1 second. However, if you run it every 5 seconds or even every half second, it becomes less of a problem.

Using this is pretty straightforward since CC's Lua provides you with built-in Timers. Simply set one up (usually in your Create function) with

self.someTimer = Timer();

and check it (usually in your Update function) with

if (self.someTimer:IsPastSimMS(someNumberOfMilliseconds)) then
    ... --run expensive code here
    self.someTimer:Reset();
end

Note: If, for some reason, you don't want your expensive code to run at a predictable interval, you can use math.random to make run within some reasonable but random range of time.

--TLDR, use Timers to make expensive code only run some of the time so it doesn't slow down the game

Intermediate Concepts

4. Tables and value caching

I want to preface this by mentioning that Lua as a language is built around use of tables. They're an incredibly useful, flexible tool and I recommend anyone who wants to seriously use Lua (for CC modding or otherwise) take the time to really learn how they work. My table tutorial has decent information and a number of useful links so I'd recommend looking there if you're unclear on how to use tables. This section will assume you have at least some knowledge of how to use tables.

Ultimately this section is actually quite simple to do and, depending on how you write code, can make things a lot faster and easier to read. The basic idea of it is, as much as possible, put any values that you'll need later and that are expensive to obtain into a table, a self variable (which is actually secretly a table anyway) or a local variable.
A simple example might be when you need to calculate the distance between two vectors and use it several times, you would store it in a variable instead of discarding it

local dist = SceneMan:ShortestDistance(v1, v2, true);
if (dist < 10) then
    ...
elseif (dist > 25) then
    ...
else
    ...
end

Compared to running the calculation multiple times, this code saves a bit of computation. Most people probably do this, though I've seen cases where people don't think to do it, or forget to, so it's important to remember that it can be surprisingly helpful.

Where this gets far more useful though, is when you combine it with tables. For example, if you wanted to make all the actors on the scene start floating if they're below 75 health, you might do

for actor in MovableMan.Actors do
    if (actor.Health < 75) then
        actor.Vel.Y = -2;
    end
end

However, this is very expensive to run every frame and ultimately wasteful. Instead, by caching values in tables and using timers to offset the expense of iterating through MovableMan.Actors, we can write a far cheaper and almost equally effective version

if (someTimer:IsPastSimMS(someInterval)) then
    for actor in MovableMan.Actors do
        if (actor.Health < 75) then
            self.affectedActors[actor.UniqueID] = actor;
        end
    end
end
for _, actor in pairs(self.affectedActors) do
    if (actor and actor.Health < 75 and MovableMan:IsActor(actor)) then
        actor.Vel.Y = -2;
    else
        self.affectedActors[actor.UniqueID] = nil; --We need to remember to remove the actor from the cache if it doesn't exist or shouldn't be affected
    end
end

While this code is longer, you can be sure its impact on the game will be a lot lower, even more so if your condition is rare (e.g. actors with a specific PresetName).


Another useful trick is using tables instead of long chains of if-elses. This is mostly for readability, but I think it's still worth mentioning. For example, instead of

if (valueToCheck == 1) then
    valueToSet = 5;
elseif (valueToCheck == 7) then
    valueToSet = 3;
elseif (valueToCheck == 8) then
    valueToSet = 3.5;
elseif (valueToCheck == 11) then
    valueToSet = "eleven";
elseif (valueToCheck == 33) then
    valueToSet = true;
elseif (valueToCheck == "start") then
    valueToSet = "start";
elseif (valueToCheck == 42) then
    valueToSet = 24;
else
    valueToSet = 123;
end

You can write

local valueTable = {[1] = 5, [7] = 3, [8] = 3.5, [11] = "eleven", [33] = true, "start" = "start", [42] = 24};
valueToSet = valueTable[valueToCheck];
if (valueToSet == nil) then
    valueToSet = 123;
end

This may seem like a contrived example, but it actually happens surprisingly often, and you can save yourself a lot of lines of code and a little computation.

Note: This can be made even smaller with the use of a trick mentioned in section 5, by writing it as follows

local valueTable = {[1] = 5, [7] = 3, [8] = 3.5, [11] = "eleven", [33] = true, "start" = "start", [42] = 24};
valueToSet = valueTable[valueToCheck] or 123;

--TLDR, use infrequent checks and tables to store values that are expensive to obtain, then use those values in frequent checks.

5. Ternary operators
Ternary operators are a very useful tool to condense if-else checks with variable assignment into one line. Lua doesn't have them directly, but it has a very close equivalent using boolean logic. For example, rather than writing

local valueToSet;
if (valueToCheck == "someString") then
    valueToSet = value1;
else
    valueToSet = value2;
end

You can write

local valueToSet = valueToCheck == "someString" and value1 or value2;

This works by using boolean logic to set the value of valueToSet to 1 if valueToCheck evaluates to true, and to 2 if valueToCheck evaluates to false. The one limitation to this method, vs a regular ternary operator, is that if value1 evaluates to false (i.e. is either false or nil), valueToSet will end up set to value2 instead of value1. In these cases, it is suggested to reorder your statement so value1 and value2 swap places. For example, this will incorrectly set valueToSet to true when valueToCheck is "someString"

local valueToSet = valueToCheck == "someString" and false or true;

But this will properly set it to false

local valueToSet = valueToCheck ~= "someString" and true or false;

Note that this logic can be chained, so you could handle a whole bunch of condition checks in one line (though I personally find that too many gets confusing to read). For example, instead of

local valueToSet;
if (valueToCheck == "someString") then
    if (valueToCheck2 == "someOtherString") then
        valueToSet = value1a;
    else
        valueToSet = value1b;
    end
else
    if (valueToCheck2 == "yetAnotherString") then
        valueToSet = value2a;
    else
        valueToSet = value2b;
    end
end

You could write

local valueToSet = valueToCheck == "someString" and (valueToCheck2 == "someOtherString" and value1a or value1b) or (valueToCheck2 == "yetAnotherString" and value2a or value2b);

For more information on the ternary operator, see the and/or section of http://lua-users.org/wiki/TernaryOperator

Note: You can use similar boolean logic to set default values for function parameters (or any variable). If you're using a function with a parameter that you want to make optional, you can do something like the following

function(optionalParameter)
    optionalParameter = optionalParameter or 5; --5 is the default value here, you can of course set it to whatever you need it to be
end

--TLDR, you can use boolean logic to mimic ternary operators and save space, or to set default values for variables

Advanced Concepts

6. Coroutines
Coroutines are the fear of many a Lua scripter, but are actually very simple and can be very useful to improve performance. Cortex Command is not multi-threaded, so every single Lua script has to run every single frame, one after the other, and finish running before the game can continue. This is why scripting lag happens - complex code takes time to run and slows down the game.
Coroutines are a good way to lessen this lag because they allow you to run your complex code over multiple frames. The downside is that things don't happen instantly, so it's not helpful for code that has to run and return a result right away.

With this limitation in mind, coroutines are very simple to set up, simply put the code you want to run as a coroutine in a separate function and initialize it with

local myCoroutine = coroutine.create(functionToRun);

This should be done exactly once, so it's probably good to put it in your Create function, but that's up to you.
Once it's initialized, your coroutine will be in a suspended state. You can always check the state of it with

coroutine.status(myCoroutine);

In order to actually run the code in it, you'll need to use

coroutine.resume(myCoroutine);

and it'll run until you tell it to take a break.

There are a few important things to know about the coroutine.resume function:

  1. You can pass arguments into it in order to pass them into the function being run as a coroutine, i.e. coroutine.resume(someVar1, someVar2);
  2. coroutine.resume will always return true or false depending on whether or not the coroutine was able to run successfully. If you try to resume a couroutine that has already finished, it'll return false as the first return value, and the message cannot resume dead coroutine as the second return value. If it's not finished, it'll return true as well as potentially returning other values that will be talked about below.
  3. When you use coroutine.resume, your coroutine will run in protected mode. This means that any errors will occur silently instead of printing to the console. Instead, they'll behave similarly to when the coroutine finishes, i.e. the couroutine.resume call will return false and the error message.
    This can be a source of frustration as your coroutine can appear to have finished when it hasn't actually, so make sure to be careful that it's actually finished when it returns false.

Now, in order to interrupt the coroutine and put this feature to proper use, you'll need to call the coroutine.yield function from inside the coroutine function. This essentially pauses the code inside the coroutine at the line where coroutine.yield is called, and allows it to be resumed from the next coroutine.resume call.
One additional benefit of coroutine.yield is that you can pass arguments into it, and they'll be returned as the 2nd, 3rd, etc. return values of the coroutine.resume. For example

coroutine.yield(v1, v2); --Here v1 and v2 will be returned by your coroutine.resume call

If you want your coroutine to output information when it's completed its work, you can simply use return to return the values you want to the coroutine.resume call, basically like you would with any other function. When you do, they will be outputted with the last successful return (i.e. where the first return value is true, so before the coroutine is dead).
As mentioned above, once a coroutine has completed its work its status will show as dead and attempting to resume it will return false and a specific error message (remember that it'll also return false and a different error message if there's an error in your code). At this point you don't need it anyore and can nullify your coroutine variable or simply leave it to the garbage collector.

Here is a very simple example to show how coroutines can be used

function MathThing(argument1, argument2)
    for i = 1, 1000 do
        argument1 = argument1 + argument2;
        if (i%10 == 0) then --Take a break every 10 iterations
            coroutine.yield(argument1);
        end
    end

    return argument1, argument2;
end

function Create(self)
    self.myCoroutine = coroutine.create(MathThing);
    self.num1 = 10;
    self.num2 = 7;
end

function Update(self)
    local coroutineIsAlive, result1, result2 = coroutine.resume(self.myCoroutine, self.num1, self.num2);
    print("Coroutine is alive: "..tostring(coroutineIsAlive)..", result is "..tostring(result1));
    if (coroutineIsAlive) then
        self.num1 = result1; --Set self.num1 to result1 while the coroutine is alive, so you can see the results of its progress as it goes
    end

    if (not coroutineIsAlive) then
        print("Coroutine is finished, final result is "..tostring(self.num1)); --Print out our victory!
    end
end

Look here for more information on coroutines.

--TLDR, use coroutines to run expensive code that doesn't have to do something right away over multiple frames.

7. Namespaces
Namespaces don't help with efficiency directly, but are great for organization. So great, in fact, that Lua itself uses them and you probably didn't even know it; the math library is a namespace, as is the table library, the coroutine library, and any other library you might use.

Essentially, namespaces let you set up your own internal set of global variables and functions without polluting the overall set of globals (better known as the global namespace). Say you have some set of utility functions and a few constants to go with them; instead of having a bunch of global functions and constants cluttering things up, you can put it all in its own namespace. You can then use things from this namespace in other scripts, providing access to all sorts of useful things.

So now that you know what namespaces are for, how do you use them? It turns out that they're incredibly simple in Lua - just make a global table for your namespace (like I said in section 4, tables are an incredibly useful, flexible tool).

Below is example code for setting up and using a namespace, then accessing it from another script. Let's say your utility stuff is in a file called MyActorUtilities.lua and your code that wants to use them is in SomeActorScript.lua MyActorUtilities.lua is as follows

MyActorUtilities = {};

function MyActorUtilities.LaunchUpwards(actor, launchRate)
    actor:AddForce(Vector(0, -1 * actor.Mass * launchRate or 1), Vector())
end

SomeActorScript.lua is as follows

-- Note the use of require here, this means that your Utils will be loaded and stored, so it can be used efficiently on as many scripts as you want.
-- However, it also means your script has to be in the package path, which is done with the line above.
-- The ? mark there tells it to put any lua files in myMod.rte onto the package path, though you can also put in specific file names.
package.path = package.path..";myMod.rte/?.lua";
require(MyActorUtilities);

function Create(self)
    ...
end

function Update(self)
    ...
    if (self:GetController():IsState(Controller.BODY_JUMPSTART)) then
        MyActorUtilities.LaunchUpwards(self);
    end
end

There are more complicated ways of expanding this namespace handling, but they're out of the scope of this tutorial. Look here for more information and details.

--TLDR, use namespaces to organize your code and make it externally accessible without cluttering up globals and risking naming collisions