Welcome to the probably last entry in this tutorial series. Today, we are going to have a look at the last remaining undocumented RealtimeAPI functions, which allow you to load arbitrary external Lua code, control the garbage collector, and let functions run seemingly in parallel.
Code injection
With Lua being an interpreted rather than a compiled language, it allows you to introduce code into your application at runtime. From one liners to entire file libraries, it’s all possible thanks to loadstring(), loadfile() and dofile().
loadstring()
loadstring() is a function that takes a string as an argument and executes it as if it were Lua code as soon as you add a pair of (). This allows you to construct new Lua code on the fly.
local f = loadstring("local a = 10; return a + 20")
-- execute code through () syntax
log.message( f() )
-- returns 30
loadstring() is always executed in a global context. You will not be able to access local variables and functions. If you wish to interact with data in your ShiVa function, you need to make all relevant data global by omitting the “local” keyword:
-- global variable a
a = 5
-- loadstring will use the global a for its calculation
local f = loadstring("a = a + 5")
-- execute f
f()
log.message ( "a is now " ..a )
-- returns 10
An easy way to evaluate a loadstring() expression is through the “return” prefix. Imagine the formula “a = a + 5” as a user input in a math test game; the example block from above could also be written like this:
-- still global variable a
a = 5
-- loadstring will use the global a for its calculation
local f = loadstring("return " .."a = a + 5")
log.message ( "a is now " ..f() )
-- returns 10
The loadstring() function is powerful, but comes with a major drawback: it is quite an expensive function when compared to its alternatives and may result in incomprehensible code through the use of global variables. Before you use it, make sure that there is no simpler way. Most of the time, loadstring() is used to construct new variable names or introduce specialized version of functions. If you use ShiVa tables and function parameters instead, you can most likely completely avoid loadstring().
dofile()
Instead of loading long strings, it is often easier to load an entire file with all your external Lua code. This can be done through dofile(). In addition to sending data to and from ShiVa using global variables, you can also define functions inside external Lua files, which makes writing libraries and including them with dofile() a viable, if not that elegant, option. let’s consider this example file:
-- file: executeMe.lua
log.message("file received:" ..num)
num = num + 6
function fileSquare(nNum)
return nNum*nNum
end
The corresponding ShiVa script could look something like this:
-- define global variable num
num = 12
-- execute an external file, and load its functions
local l = dofile("Z:\\executeMe.lua")
-- this will immediately log "file received: 12" as num was defined globally
-- then num = num + 6 will be executed and its result 18 will be available to ShiVa
log.message ( num )
-- finally, we call a function that was defined in the external Lua file
log.message ( "square in file: " ..fileSquare(num) )
Note that all functions loaded with dofile() are in the global context, which means they can be called from any script in your game.
Garbage collector
Lua is a scripting language that ships with an automatic memory manager known as garbage collector (GC). That means, unlike other languages like C and C++, you do not have to concern yourself with purging memory of objects, pointers, variables etc. that are no longer in use, so memory leaks can be prevented. This has both advantages and disadvantages. Fortunately, there are ways you can take manual control of the GC, in case the automatic settings make your code slower than it should be at the expense of increased memory usage.
Runtime GC
Lua 5.0.x, which is used in the ShiVa engine runtimes, has only very limited options for the garbage collector. The function gcinfo() returns the current memory usage, as well as the threshold for the garbage collector. When the number of bytes used crosses the threshold, Lua runs the garbage collector, which reclaims the memory of all dead objects. The byte counter is adjusted, and then the threshold is reset to twice the new value of the byte counter.
-- only runtime engine
local memUsage, threshold = gcinfo()
log.message ( "Lua Memory usage in kB: " ..math.trunc ( memUsage, 1 ) )
You can force a GC cycle through the function collectgarbage([limit]). If the optional [limit] parameter is smaller than the byte counter, then Lua immediately runs the garbage collector.
Editor GC
Luckily, the ShiVa 2.0 Editor uses Lua 5.2.3, which gives much more control over the garbage collector. Everything is now controlled by the function collectgarbage([opt]) and its new list of parameters:
"collect": performs a full GC cycle (default) "stop": stops GC "restart": restarts GC "count": returns Lua memory usage in kb
There are even more options, but those are the most important ones. If you want to know more, please refer to the official Lua 5.2 docs. This is how checking for the current Lua memory usage looks like, not how the memory usage changes when you put a variable on the stack:
-- more modern version
local memUsageModern = collectgarbage ("count")
log.message ( "Before: " ..math.floor ( memUsageModern ) )
-- now load something into memory
local a=12
memUsageModern = collectgarbage ("count")
log.message ( "After: " ..math.floor ( memUsageModern ) )
If you have very expensive loops with large objects, it can be beneficial for performance to temporarily disable the GC. don’t forget to turn it on afterwards though!
-- our "expensive" function
function f(y)
local x = (y*y + y ) / 2
return function() return f(x) end
end
-- disable GC
collectgarbage('stop')
local g = f(0)
-- long loop
for i=1,10000 do
g = g()
end
-- do some checks
log.message("garbage after FOR call:" ..collectgarbage("count"))
-- force full GC cycle
collectgarbage()
-- check if memory is clear again
log.message ( "After GC: " ..collectgarbage ("count") )
-- continue regular GC operation
collectgarbage('restart')
Example courtesy of luatut.com.
Coroutines
Lua does not have true multithreading (yet). Instead, if you want to run functions seemingly
in parallel in Lua, you have to use coroutines. A program with threads runs several threads
concurrently. Coroutines on the other hand are collaborative: A program with coroutines is,
at any given time, running only one of its coroutines and this running coroutine only
suspends its execution when it explicitly requests to be suspended.
Why then would you want to use coroutines in your game if it does not use more cores, if it
does not really speed up your game? Because you can distribute your workloads better,
producing a smoother experience, making your game feel faster. For certain workloads
like loading in a large table of objects, textures or other gamefiles, coroutines are the
tool of choice.
Concept
In order to adapt coroutines for ShiVa, we will have to understand the basic concept first. Consider two functions:
function f1()
local i = 0
while i
If we just call them one after the other, we get a sequential output of co1_0..199 and then
co2_0..199. Futhermore, the entire engine stalls until all 2x200 iterations are computed.
For a small example like this, you will see a bit of stutter, but imagine doing this with a
large array of game models - the game will grind to a halt for presumably several
seconds.
The coroutine.*() API has 3 main functions:
"create": transform a function into a coroutine "yield": pause a coroutine, keep the current state "resume": starts or resumes a coroutine until a yield() is encountered
Let's transform both example functions into (anonymous) coroutines and define a yield point after every iteration of the expensive while-loop. Note once again the global context:
co1 = coroutine.create(function ()
local i = 0
while i
To execute these coroutines, we need to use resume(), which will run the while loop exactly once until the yield() is encountered. Running resume() again will compute the second interation of the while loop, and so forth.
coroutine.resume(co1)
-- logs "co1_0"
coroutine.resume(co1)
-- logs "co1_1"
-- how about the other one?
coroutine.resume(co2)
-- logs "co2_0"
-- back to the first one
coroutine.resume(co1)
-- logs "co1_2"
Of course, executing long loops manually one by one is not the answer. But neither are for-loop, mind you:
for i=1,300 do
coroutine.resume(co1)
coroutine.resume(co2)
end
A loop like this will simply execute the coroutines sequentially again in one frame until all 2x200 iterations have finished. To make coroutines work with ShiVa, we will have to adapt our code slightly.
Coroutines in ShiVa
First, we are moving our anonymous functions into true ShiVa functions. You can also optionally define parameters, like so for the second coroutine:
--------------------------------------------------------------------------------
function loader.co2 ( nIn )
--------------------------------------------------------------------------------
local i = 0
while i
It contains mostly the same code as before, but the function now uses a standard ShiVa layout
and will be listed in an AIModel. Shortly before the coroutine finishes, it switches the
this.done_co2() flag to TRUE, so our main thread can test against this control flag in order
to know when the function has finished.
The coroutines themselves will be run in a ShiVa AIModel state, to keep everything compact
and simple. in onEnter, we will reset the notification flags and define our functions as
coroutines in a global context. Remember to keep the global names unique in order to avoid
conflicts.
--------------------------------------------------------------------------------
function loader.coheavy_onEnter ( )
--------------------------------------------------------------------------------
-- reset coroutine control variables
this.done_co1 ( false )
this.done_co2 ( false )
-- define functions as coroutines
t_co1 = coroutine.create( this.co1 )
t_co2 = coroutine.create( this.co2 )
--------------------------------------------------------------------------------
end
--------------------------------------------------------------------------------
The magic happens in onEnterFrame(). As this part of the state is run once per frame, our coroutines will yield after every single iteration and give the ShiVa engine time to do other things, like updating the picture, input, physics, and so forth. In other words, we are spreading out a loop of 200 iterations onto 200 frames, instead of dumping it all into a single one. Note the absence of a for-loop:
--------------------------------------------------------------------------------
function loader.coheavy_onLoop ( )
--------------------------------------------------------------------------------
coroutine.resume(t_co1)
coroutine.resume(t_co2)
-- return to idle if coroutines are finished
if ( this.done_co1 ( ) and this.done_co2 ( ) ) then
this.idle ( )
end
--------------------------------------------------------------------------------
end
--------------------------------------------------------------------------------
As a means to return to an idle() state, we test against the two this.done*() flags and exit
to another state when both are true.
The function loader.co2 has an additional function parameter, which we can address through
coroutine.resume():
This makes it possible to transmit data to the very next iteration of t_co2, possibly
sending data that was just created in t_co1, creating an interlocking chain or threads
without the locking problems commonly found in true multithreading languages.
The possible applications of coroutines include:
- loading large arrays of objects in the background while a scene in the foreground behaves
normally / can be animated
- animated loading screens / loading mini games
- pseudo "streaming" content from a distant server one object at a time, instead of pausing
until everything is loaded
- cleanly coding interdependent functions that rely on each others' data
- taking functions with long wait cycles off the CPU, avoiding writing busy loops
- and many more
Coroutines are an interesting and rather complex topic, which is impossible to cover in a
blog tutorial like this. We highly recommend looking at online resources like
- Lua
Doc: coroutine basics,
- Lua
Doc: coroutine iterators,
- Lua Doc: coroutine API, and
-
StackOverflow in order to get a better
handle on them.