-
Notifications
You must be signed in to change notification settings - Fork 21
Lua Optimization and Organization Tips and Tricks
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.
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 beif (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
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
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:
- 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);
-
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 messagecannot 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. - 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. thecouroutine.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