LuaEngine: Using Lua With Objective-C

10
Aug/09
0

In this series of posts, I want to explore the, already well covered, topic of using Lua for game scripting. My direction will include Objective-C as the host language, though the beginning code will stick to C. The end goal of these posts, is to implement a bridge between Lua and Objective-C that will allow us to expose obj-c objects to Lua script, without a large amount of “setup” code.

I’m going to make the assumption that anyone reading this has already gotten Lua compiled for their target platform, and can jump right into embedding Lua into a simple application.

The Basics

The first step to creating a generic Lua engine in Objective-C is introducing yourself to how Lua’s C API works and how we can use it to share data between script and native code. So, here is the bare minimum we need to bring Lua into our app:

int main(int argc, char **argv) {
   if (argc != 2) {
      printf("Usage: %s \n", argv[0]);
      return 1;
   }
   lua_State *lua = lua_open();
   luaL_openlib(lua); // Open standard libs (math, io, os, etc)
   luaL_dofile(lua, argv[1]); // Run the script supplied by the user
   lua_close(lua); // Shut down our lua vm.
}

The above code will:

  • Start a Lua VM (lua_open)
  • Initialize standard libs (luaL_openlib)
  • Run the Lua script supplied by the user. (luaL_dofile)
  • And finally, terminate the Lua VM. (lua_close)

This can be tested by providing a Lua script on the command line, like:

./luatest test.lua

So, lets pretend that we wanted to expose functionality to Lua that would allow us to write Lua script that would manage the location of a game object. For simplicity, lets call that object a Box. The Box is a simple quad that we’ll render in our game. The goal, will be to have the game code remain static, while we implement various movement algorithms for our Box.

Sharing Data

Our first phase will be to expose global values that represent the X & Y coordinates of the Box. Through those global variables we can just set the coordinates that we want, and the render loop of our game can read them in for translation. So, building onto the above code we could add:

static float box_x = 0.0f;
static float box_y = 0.0f;
int initializeGlobals(lua_State *l) {
   lua_pushnumber(l, box_x);
   lua_setglobal(l, "BoxX");
   lua_pushnumber(l, box_y);
   lua_setglobal(l, "BoxY");
}

I’ve added two global variables (box_x & box_y). Please note, I am in no way saying this is the best way to implement this, but it certainly is the easiest for this small example.

With those two global variables defined, the function initializeGlobals, when called, will take the supplied Lua VM and push the values of both global variables into the Lua environment. It does this by first pushing box_x’s value onto the Lua stack. Then, it calls lua_setglobal and supplies the name “BoxX”. What this does is, pops the value, of box_x that we pushed onto the stack, off and adds it to the global registry under the name “BoxX”. This will allow any Lua script which runs from now on, to access a global variable named, BoxX. Initially, the value will be our initial value of box_x, but the Lua script will be able to change that as it wishes. We’ll also have to implement code later on to read back the current value of that global variable if we ever want to do anything else with it.

Now, all we have to do is make a call to initializeGlobals inside our main function from above.

luaL_openlib(lua);
// Begin new code
initializeGlobals(lua);
// End new code
luaL_dofile(lua, argv[1]);

To test it out we can write a quick Lua script like the following:

-- Testing global variables
print("X: ", BoxX, " Y: ", BoxY)

Which should give an output like:

X:      0        Y:     0

Ok, so now we can initialize our BoxX and BoxY variables so our code can use them. But, like I said before, we now need to write a little more code so we can read back their values. The code uses a very similar pattern to how we set the values, but as expected, a bit in reverse:

void readGlobals(lua_State *l) {
   lua_getglobal(l, "BoxX");
   box_x = lua_tonumber(l, 1);
   lua_getglobal(l, "BoxY");
   box_y = lua_tonumber(l, 2);
   lua_pop(l, 2);
   printf("x = %.3f  y = %.3f\n", box_x, box_y);
}

So, instead of pushing our values on, and telling Lua that we want to set new globals, we now tell Lua what globals we want to get and Lua pushes their values onto the stack. So, we start off by getting the value of BoxX by calling lua_getglobal. Once that’s done, the value of BoxX will be on the top of the stack for us. Once the value is on the stack, we can actually use it to do anything we want, like use it for a Lua function’s parameter, or any number of things. But instead, we’re simply going to copy the value from the stack, using lua_tonumber, and specifying stack index 1. Something to take note of, is the fact that lua_tonumber does not pop the value off the stack. So we’re left to clean up after we’re done.

After doing the same for BoxY a call to lua_pop telling it to remove the top 2 elements from the stack cleans up after everything we’ve done, and we can rest assured that we’re being good stack using citizens.

Just like before, we need to add a call to readGlobals to our main function. Since we want to get the values of our globals after the script has had a chance to modify them, we need to place the call after our call to luaL_dofile. Like so:

luaL_dofile(lua, argv[1]);
// Begin new code
readGlobals(lua);
// End new code
lua_close(lua);

Accompanying Lua script to test our changes out:

print("X: ", BoxX, " Y: ", BoxY)
BoxX = 1.5
BoxY = 1.0

The output should look something like:

X:      0        Y:     0
x = 1.500  y = 1.000

Excellent. Now we can share data with our Lua scripts. But that still keeps us rather limited in what we can do in Lua. So, our next step will be to expose native code, so we can make use of highly optimized routines, or just already implemented functionality of the game engine.

Sharing Functionality

Now we have the ability to update our Box’s position. Since we want to have the logic for moving our box each frame implemented in Lua, we might want to start working on implementing a movement algorithm. So lets implement a very basic movement scheme which will bounce the box around the screen. Very simple. But first, we need a random velocity for our box. We can do this very simply in Lua, but for lack of a better example, I’m going to use this as a reason for exposing native code to Lua.

First, we’re going to want a function that can provide us with the x and y components to a randomly generated velocity. I’d also like to allow the Lua script to specify what the magnitude of that velocity should be, that way changing the Box’s speed doesn’t require a recompile.

In order to do this, we first need to define a native function that Lua can call. Since Lua is going to be calling this function directly, it has to match Lua’s C function prototype. This means the function has to return an int, and must take a single parameter of type lua_State*. The lua_State pointer is a pointer to the Lua VM that the function is being called from, and the return value of the function is the number of values the function returned on the stack.

Here’s a function that would generate a random 2D velocity (x & y components) that can be called directly from Lua:

int genRandomVelocity(lua_State *l) {
   float speed = luaL_checknumber(l, 1);
   srand(time(NULL));
   float x = (float)rand()/(float)RAND_MAX;
   float y = (float)rand()/(float)RAND_MAX;
   float len = sqrt(x*x + y*y);
   x = (x/len) * speed;
   y = (y/len) * speed;
   lua_pushnumber(l, x);
   lua_pushnumber(l, y);
   return 2;
}

genRandomVelocity starts out by checking to make sure the first argument (stack item #1) is a number. If it is, the number is copied to our local variable speed. Once it has the value for speed, it generates a random 2D vector, normalizes it, and then scales it to the Lua supplied magnitude (speed).

Once the vector is calculated it pushes the component values onto the stack using lua_pushnumber. The values should be pushed in the order that you want the user to retrieve them. Finally, the function returns 2, telling Lua that it has 2 elements on the stack that it is returning.

So now that we have a function to generate our velocity we can expose it to Lua from inside our initializeGlobals function with a quick call to lua_register like so:

void initializeGlobals(lua_State *l) {
   lua_pushnumber(l, box_x);
   lua_setglobal(l, "BoxX");
   lua_pushnumber(l, box_y);
   lua_setglobal(l, "BoxY");
   // Begin New Code
   lua_register(l, "randvelocity", &genRandomVelocity); // Tell Lua about our function
   // End New Code
}

Which allow us to use the Lua function randvelocity inside our script:

-- Generate a new random velocity
x, y = randvelocity(2.0)
print("vx: ", x, " vy: ", y)

Which should generate an output like this:

vx:     0.00092774751828983      vy:    0.012465523555875

Which will naturally give us different values each time we run it.

Ok, so now our Lua script can utilize data, and functionality, implemented in our native code. But, unless we plan on storing every piece of Lua functionality in its own file, we need a way to call Lua functions from native code.

As we’ve seen, by using luaL_dofile our script will be read, and evaluated, by the Lua VM. So, we’ve been relying on the evaluation in order to execute our previous test code. What we can do now, is modify our script by adding a function called ‘update’, and moving our Box position code into it. By leaving our velocity generation code outside of the function we can still take advantage of the evaluation to initialize our Box’s velocity.

-- Generate a new random velocity
x, y = randvelocity(0.0125)
print("vx: ", x, " vy: ", y)
 
function update ()
   print("X: ", BoxX, " Y: ", BoxY)
   BoxX = 1.5
   BoxY = 1.0
end

Now, if we run the same version of our native code that we had prior to making this change, all the output we should see is something like:

vx: 	0.011539350263774	 vy: 	0.0048055569641292
x = 1.000  y = 0.000

Which is great, because that means it is still running the code that we’re using for our velocity when it loads the Lua file, and not executing our update function at all. So, whenever we want to actually call that update function, we need to add some native code which, for now, can be added in right before our call to readGlobals:

luaL_dofile(lua, argc[1]);
// New code...
lua_getglobal(lua, "update"); // Push the 'update' function onto the stack
if (lua_pcall(lua, 0, 0, 0) != 0) // Call the function that's on the stack
   printf("An error has occured running 'update': %s\n", lua_tostring(lua, -1));
// End new code
readGlobals(lua);

Calling a Lua function from C follows a very similar pattern to getting values from our globals. First we need to tell Lua that we want the global “update” to be pushed on the stack. It doesn’t matter that ‘update’ is a function, Lua will treat it just like it treats any other global value. So our call to lua_getglobal will push update onto the stack for us, so we can use it however we want. In order to execute it though, we need to call lua_pcall (or one of the other lua_call-like functions) which will execute the function currently on the stack, and return 0 if it executed successfully. lua_pcall will also pop the function off of the stack, so we don’t have to worry about cleaning it up.

If the call to lua_pcall fails, Lua will push the error string onto stack index -1. Which is why I’ve printed out that stack element, as a string, if an error occurs. The strings generated will be what you would expect a Lua interpreter to tell you if you have syntax errors, etc.

Now that we can call our Lua function from native code, we can move towards simulating an actual game loop. By looping over lua_getglobal, lua_pcall, and readGlobals we can use the update function like it would be used on a per-frame position update.

function update ()
   BoxX = BoxX + x
   BoxY = BoxY + y
end
   for (int frame=0; frame<10; ++frame) {
      lua_getglobal(lua, "update");
      if (lua_pcall(lua, 0, 0, 0) != 0)
         printf("An error has occured running 'update': %s\n", lua_tostring(lua, -1));
      readGlobals(lua);
   }

Giving us an output like:

vx: 	0.010487142950296	 vy: 	0.0068021933548152
x = 1.000  y = 0.000
x = 1.010  y = 0.007
x = 1.021  y = 0.014
x = 1.031  y = 0.020
x = 1.042  y = 0.027
x = 1.052  y = 0.034
x = 1.063  y = 0.041
x = 1.073  y = 0.048
x = 1.084  y = 0.054
x = 1.094  y = 0.061
x = 1.105  y = 0.068

If we actually had a render loop, we’d see our box move across the screen in a random direction. But it would still leave the view, and we’d never see it again. So if we go back to our initializeGlobals we can put a couple more globals in to expose our viewport width and height. Lets assume we’re using an orthogonal projection with a view of x=[-1.0, 1.0], and y=[-1.5, 1.5].

lua_setglobal(l, "BoxY");
// Begin New Code
lua_pushnumber(l, -1.5f);
lua_setglobal(l, "MIN_HEIGHT");
lua_pushnumber(l, 1.5f);
lua_setglobal(l, "MAX_HEIGHT");
lua_pushnumber(l, -1.0f);
lua_setglobal(l, "MIN_WIDTH");
lua_pushnumber(l, 1.0f);
lua_setglobal(l, "MAX_WIDTH");
// End New Code
lua_register(l, "randvelocity", &genRandomVelocity);

This probably isn’t the most elegant solution, but it will work for this example, and we can now implement our update function like so:

-- x and y are defined outside the function as the x and y components of
-- our velocity vector.
function update ()
   nx = BoxX + x
   ny = BoxY + y
   if nx < MIN_WIDTH or nx > MAX_WIDTH then
      nx = nx - x * 2.0
      x = x * -1.0
   end
   if ny < MIN_HEIGHT or ny > MAX_HEIGHT then
      ny = ny - y * 2.0
      y = y * -1.0
   end
   BoxX = nx
   BoxY = ny
end

Running our code now, and modifying our velocity magnitude to 0.5, should give us an output like this:

vx:     0.47514313459396         vy:    0.15568877756596
x = 1.000  y = 0.000
x = 0.525  y = 0.156
x = 0.050  y = 0.311
x = -0.425  y = 0.467
x = -0.901  y = 0.623
x = -0.425  y = 0.778
x = 0.050  y = 0.934
x = 0.525  y = 1.090
x = 1.000  y = 1.246
x = 0.525  y = 1.401
x = 0.050  y = 1.246

A nice, and bouncy, moving quad. Updated entirely from our Lua script.

The next step..

Now we have some basic Lua support capabilities that can help us to move some of our game logic into script. This is great, if all we wanted to do was expose some globals, and share some functions. So, our next challenge, is to generalize our code so we don’t have to keep using the boilerplate Lua C API to add globals, and expose functions.

Code for this post.