-
Notifications
You must be signed in to change notification settings - Fork 16
Lua Optimisation Notes
To start out, i’ll say that this is generally not necessary to concern yourself with when it comes to small scripts. It can, however, make a massive difference on large ones! Even for small scripts though, some of the low-hanging fruit isn’t even harder to do than scripting normally, it’s just something that makes a shockingly big difference when all logic says it really shouldn’t. Luabind, i guess. :V
As CC’s Lua is currently structured, accessing an object-bound property like self.Pos
has a lot of overhead, making it a lot slower than it seemingly should be. This also applies to pointers to any other object, such as what you might get from MovableMan:GetMOsInRadius()
, which means that most tricks here will also translate readily to that, but in most cases this is harder to avoid and less frequent than self
. As such, self
will be the main focus of this page.
Luckily for us, locals are very fast. Rule of thumb, if you have a self
-bound property that you need to read more than once per run of a particular code block, localising it first will make it faster. Further luckily, CC's Lua supports references/pointers to objects and vectors, which means that you can set a local variable to point to self.Pos
, and it’ll return the same value as reading the vector directly.
function Update(self)
local selfVel = self.Vel;
local selfMass = self.Mass;
local selfPos = self.Pos;
-- do your code here
end
And it’s a WHOLE LOT faster! The above example is with 2000 iterations in a for-loop, so if you just access self.Pos
a few times per frame, that’ll barely be one µs, which is… nothing. Then again, doing the same with a local variable will barely be one ns, which is even less nothing, and it’s not harder to use. You can also make changes to the parent vector by manually changing X
and Y
of the variable, but if you’re doing this to both coordinates, it’s slower than just shoving a new vector directly into self.Pos
.
Note that while this applies for other vector values like self.Vel
, numerical values like self.Mass
are passed into the variable by value, and as such don’t subsequently reflect changes to the parent value. It’s still faster to access though, so if you need to read any property from any pointer (e.g. self.Mass
, MO.Age
, etc) more than once in a code block, it’ll be faster if you put it into a local variable as the first thing done.
Also VERY important to note, this applies to script-exclusive variables defined by you as well!
Not quite as extreme a difference, but it can definitely bog your script down if you access self-bound variables a lot. Keep in mind that this also applies to setting and changing variables, so as much as possible, math is to be done on locals, and then change the pointer property at the very end.
Secondary note, this does not apply for the pointer itself; accessing self
on its own is just as fast as accessing a variable containing it. Thankfully.
If you’re frequently using external functions, e.g. math.random()
to get random numbers, you can make slight gains by defining them locally:
It’s not much, but it’s easy to do and demonstrably runs a bit faster. As a tangential note to random numbers, if you find yourself in a situation where truly prodigious amounts of random calls are needed, you can benefit from making a Doom-style table of pre-generated random values and simply iterating through that.
local randomTable = {};
local randomTableIterator = 0;
local randomTableSize = 500;
for i = 1, randomTableSize do
table.insert(randomTable, Random());
end
local function GetRandom()
randomTableIterator = randomTableIterator + 1;
if randomTableIterator > randomTableSize then
randomTableIterator = 1;
end
return randomTable[randomTableIterator];
end
The obvious downside to this is that iterating through a pre-generated table will see random values eventually recurring, but in most cases, people won't notice this. It's mainly obvious when random values are used to generate persistent and static visuals over a larger area, which will result in repeating patterns.
Additionally, it is possible to localize functions from entities like SceneMan
and MovableMan
, and this is also faster than calling them directly:
Do note, the syntax changes slightly, as you have to pass a pointer to the entity in as the first argument (thanks Trez :v). The format for defining them locally is as follows:
local Random = math.random;
local ShortestDistance = SceneMan.ShortestDistance;
local randomVec = Vector(Random(-5, 5), Random(-5, 5));
local A = ShortestDistance(SceneMan, selfPos, randomVec, true); -- Note SceneMan as the first parameter!
You may have noticed one significant problem with locals: Since they're scoped, you can't read them across functions or updates. The immediate temptation is to define it as a global outside of the functions, but this can cause problems! Scripts in CC are cached on a per-script basis, not per object, so one object changing globals can have unintended effects on other objects with the same script. It's perfectly fine for constants, though!
Currently, the only reliable way I know of to have persistent per-object variables is to shove them into self
, which is what we normally want to avoid. So, what do we do? Let's say we have these four self
-bound variables that need to persist across frames.
function Create(self)
self.A = 1;
self.B = 2;
self.C = 3;
self.D = 4;
end
This looks innocent enough, and in most cases, it is. However, remember that the cost of accessing self
-properties goes for both getting and setting, and we can't just make a local variable to these for setting, though if they were tables or vectors, we could. Therein lies the solution: Tables. A Lua table can have properties just like any other object in CC, and it is MUCH faster to access.
function Create(self)
local var = {};
var.Pos = self.Pos;
var.Vel = self.Vel;
var.A = 1;
var.B = 2;
var.C = 3;
var.D = 4;
self.var = var;
end
function Update(self)
local var = self.Var;
-- do code
end
Doing it this way, we only have to access self
once to localise all of our variables, and since it's a table bound to self
, changes to them are persistent. As an added bonus, vectors like self.Pos
and self.Vel
can also be added this way, in a manner similar to what's described earlier and with the same limitations (e.g. if you set var.Pos
to a new vector, it no longer refers to self.Pos
). If you need to access the variables within custom functions, simply pass var
in as a parameter.
Unfortunately, vector math is expensive in CC.
VERY expensive. Even with the overhead of accessing self.Pos
eliminated, it takes over a millisecond to add those two vectors 2000 times per frame! There’s unfortunately not much to do about that exact operation; it is possible to write custom vector arithmetic functions that are a bit faster, but i happened to run across a very particular brand of bullshit that’s a whole lot faster than vectors.
TABLES, AGAIN. They bypass having to go through luabind, and thus avoid a whole lot of overhead. Unfortunately, tables don’t come with vector functions. That’s where this comes in:
https://github.com/comradeshook/CCCPLuaTomfoolery/blob/main/TableVectorLibrary.lua
I’ve written a library of all CC’s vector functions to be used with Lua tables instead, plus a few extra for good measure. The main gist of it is, make a new table vector (hereafter, TVector) with VecNew(x, y)
, and treat it like a regular vector with the functions provided; TVectors have X
and Y
components just like normal.
Thanks to metatable fuckery, you can also do regular arithmetic operations on them like you would normally (e.g. local A = TVector1 + TVector2
), but more advanced vector functions are now defined externally instead of on the TVectors directly. For example, to get the magnitude of a TVector, you'd use VecGetMagnitude(TVector)
instead of Vector:GetMagnitude()
. All vector functions available in the source code have been ported to this library and should work as expected (if not, tell ComradeShook!), they're just all prefixed with "Vec" now and take the target TVectors as parameters.
After you’ve done all the math and processing, you can either manually add the table components into the vector ala Vector.X = Vector.X + TVector.X
, or you can use VecTableToVector()
to convert it into a table for easier addition, like so: Vector = Vector + VecTableToVector(TVector)
. Remember though, vector math, creation and functions are slow!
In order to use it in your script, simply put the library wherever you want to keep it, and put the following line into your mod, ideally on one of the first lines:
dofile("yourmodname.rte/pathto/TableVectorLibrary.lua");
This will define all the functions necessary for use in your script. Do note that using this library for small scripts is completely overkill, as the added complexity isn't worth the small performance gains you'll see. While it's percentage-wise very fast, shaving a few microseconds off is rarely going to matter. On large scripts, though?
It matters a whole lot!