Lucas
Klassmann | Blog

Game Development with Lua

REAL-WORLD EXAMPLES OF HOW TO EMBED LUA PART 2

Updated in 2023-10-20 · First published in 2023-10-20 · By Lucas Klassmann

Introduction

Scripting and Modding games are part of the daily game development routine and also where Lua can be really handy. It is easy to embed and allows you to expose many core functionalities to parts of the game where speed and performance are not exactly a priority, but on the other hand, the flexibility of extending your game becomes crucial. To demonstrate more Lua features, let’s use a small demo game I developed as part of this article.

The small game, Wars, was made in C with SDL 2 and Lua 5.3 to explore more advanced uses that can be needed during the development of applications and games.

Wars Game

There is no intention to teach you about game development in this article, only how to use some useful features from Lua in your game. You will find the game example in the repository. Use it as a reference to your game development project.

Inside the source code, there are many examples using Lua API and most of them are not covered by this article.

The feature I think is important and we are covering is how to expose a Vector structure as user data and the use of metatables to allow arithmetic operations, access to members, type validation on function calls, and also giving it a string representation.

Before jumping to it, let’s revisit some basic uses of Lua and how it is applied in the game.

Revisiting Common Lua Uses

Calling global functions

I designed the little game engine with an event system as it is similar to Love2D, Godot, and other engines. The events in SDL 2 are filtered. Those I find useful like key buttons pressed, mouse clicks, mouse movements, and the game load. They call simple global functions in the main game script: game.lua.

When we want to call a global function that is used by an event we just use lua_getglobal to load the function chunk to the stack and push the arguments that will be used by the function.

The stack is going to look like this:

Keydown Stack

After preparing the stack with the function chunk and the arguments needed to call it, we just call lua_pcall, with 1 for the number of arguments we are passing to the function, and the other arguments are 0(zero) because we do not expect returned values. Here is the complete implementation:

1
2
3
4
5
6
void level_keydown(Level *level, const char *key) {
   lua_State *L = level->script->L;
   lua_getglobal(L, "_keydown");
   lua_pushstring(level->script->L, key);
   lua_pcall(L, 1, 0, 0);
}

The function _keydown will be called whenever we hit a key in the keyboard. I also do not verify any errors in this case.

Optional arguments in a function call

A simple function to draw text on the screen was implemented with some optional arguments. The minimum of arguments are needed to draw a text with a transparent background, but you can give other arguments to let the engine know that the background will be also drawn in a color.

Note: There is no type of validation, but it can be easily implemented if you wish.

First, I get the size of the stack, using lua_gettop, when the C function is called, this information tells me how many arguments are passed to the function. Knowing that I am able to compare the position of the argument I expect with the size of the stack. If the argument is not present, I let a default value.

This simple comparison makes the function flexible. Let's check the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int api_draw_text(lua_State *L) {
   Font *font = NULL;
   int num_args = lua_gettop(L);
   Vector *position;
   SDL_Color fg;
   SDL_Color bg;
   bool shaded = false;
   const char *text = NULL;

   if (num_args > 0)
       font = lua_touserdata(L, 1);

   if (num_args > 1)
       text = luaL_checkstring(L, 2);

   if (num_args > 2)
       position = luaL_checkudata(L, 3, "Vector");

   if (num_args > 3)
       fg = lua_read_color(L, 4);

   if (num_args > 4)
       shaded = lua_toboolean(L, 5);

   if (num_args > 5)
       bg = lua_read_color(L, 6);

   graphics_draw_text(font, text, *position, fg, shaded, bg);
   return 0;
}

Another way to give the function yet more power is to check the type of the arguments using lua_type:

1
2
3
4
5
6
...
if (lua_type(L, 1) == LUA_TNUMBER && lua_type(L, 2) == LUA_TUSERDATA) {
   lua_Number scalar = luaL_checknumber(L, 1);
   Vector *a = luaL_checkudata(L, 2, "Vector");
   v = vector_add_scalar(*a, scalar);
...

In the example, we use lua_type to check if the argument is a Number or user data.

The code above is a partial example because it is part of another function explained below, but as you can see, it is easy to check the type and increase the level of the function polymorphism.

Table values in C

I use colors in many parts of the game: when drawing rectangles or loading the color that the Renderer uses for clearing the screen. Whether receiving the color via arguments or loading a variable from a script, I decided to use a table to define a color in Lua. The structure is a table with the {r=0, g=0, b=0} fields. I highlight here the way I used to transform it into an SDL_Color structure.

First, I check if the value on the top of the stack is really a table with lua_istable. Then I read the three fields and load their values onto the stack with lua_getfield, for later convert them to integers with lua_tointeger. I finish cleaning the stack and pop the 3 values with lua_pop, and return the color structure SDL_Color:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static SDL_Color lua_read_color(lua_State *L, int idx) {
   SDL_Color c;
   if (lua_istable(L, idx)) {
       lua_getfield(L, idx, "r");
       lua_getfield(L, idx, "g");
       lua_getfield(L, idx, "b");
       c.r = lua_tointeger(L, idx + 1);
       c.g = lua_tointeger(L, idx + 2);
       c.b = lua_tointeger(L, idx + 3);
       lua_pop(L, 3);
   }
   return c;
}

Note: this is a helper function that I use in other parts of the code.

Exposing complex value types

The idea

A basic vector math was implemented to give me access to interpolation with Lerp between two vectors. The math allows me to have nice and smooth movements for the player aircraft when moving the mouse. The player aircraft is not just set in the mouse position, but it is given some time to fly towards the mouse target position.

In this case, especially, I did not want to only expose common user data, like a simple C structure, but it was important to allow Lua to perform some arithmetic operations like adding two vectors. The operations actually are executed in C but are triggered by Lua metamethods. This is what I want to allow Lua to do:

1
local vector_c = vector_a + vector_b

Another useful feature is to give access to structure members x and y:

1
2
local mouse_x = mouse_vector:x()
local mouse_y = mouse_vector:y()

And have a string representation when used with print. Which can be helpful while debugging.

1
2
print(vector_a)
-- Vector<10, 40>

All those ideas are possible to achieve through metatables.

What is a metatable

Metatables, as the Lua documentation tells us, are just like ordinary tables in implementation, but used for another purpose: Let Lua know how to treat values due to certain events.

Every value in Lua can have a metatable associated with it. But this is especially useful with user data or other tables when their operations go far beyond the common operations expected from those values.

In the documentation, there is a list of events like __add, __sub, __mul, __index and so on that can be used to declare methods that will operate when such events are triggered and then augment those values with special behaviors.

There are other events that can be defined by the standard or third-party libraries, so you need to find them to understand what each one is about.

To achieve this, you just need to fill a metatable with the methods and which functions will be called when the method is used by Lua and set the metatable to a value. It is important to create the metatable and define its methods before using it in a value.

In C, you must first create a new metatable with a name, set its methods from an array of lua_Reg, and then every time you expose a value to Lua you retrieve the metatable by name and associate it to the value in question.

Here is an example of what a Lua metatable looks like, and how the methods are used in Lua: Metamethods

Note: I defined my own x() and y(). we can add arbitrary methods to bind to the value.

Let’s see the actual code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static const struct luaL_Reg vector_methods[] = {
       {"__add",      api_vector_add},
       {"__sub",      api_vector_sub},
       {"__mul",      api_vector_mul},
       {"__div",      api_vector_div},
       {"lerp",       api_vector_lerp},
       {"x",          api_vector_access_x},
       {"y",          api_vector_access_y},
       {"__tostring", api_vector_tostring},
       {NULL, NULL}
};

Some of the operations allowed with the definition above:

1
2
3
4
5
local vector_c = vector_a + vector_b        -- Add two vectors
local vector_c = vector_a * 10              -- Multiply by a scalar
vector_a = vector_a:lerp(vector_target)     -- Apply lerp to vector_a
local height = vector_a:y()                 -- Accessing y from vector_a
print(vector_a)                             -- Vector<100, 145>

If you have seen lua_setfuncs already, it is the same array type and function used with a metatable.

Before delving into the metatable creation let’s take a look at some behaviors we are attaching to the Vectors values.

Defining arithmetic operations for a value

Arithmetic operations are defined with events like __add, __sub, __mul, __div, and so on. Those are treated like a binary operation, which receives two arguments and you return usually one result value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static const struct luaL_Reg vector_methods[] = {
       {"__add",      api_vector_add},
       {"__sub",      api_vector_sub},
       {"__mul",      api_vector_mul},
       {"__div",      api_vector_div},
       {"lerp",       api_vector_lerp},
       {"x",          api_vector_access_x},
       {"y",          api_vector_access_y},
       {"__tostring", api_vector_tostring},
       {NULL, NULL}
};

The Vector addition implementation follows as an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int api_vector_add(lua_State *L) {
   Vector v;

   if (lua_type(L, 1) == LUA_TNUMBER && lua_type(L, 2) == LUA_TUSERDATA) {
       lua_Number scalar = luaL_checknumber(L, 1);
       Vector *a = luaL_checkudata(L, 2, "Vector");
       v = vector_add_scalar(*a, scalar);
   } else if (lua_type(L, 1) == LUA_TUSERDATA && lua_type(L, 2) == LUA_TNUMBER) {
       Vector *a = luaL_checkudata(L, 1, "Vector");
       lua_Number scalar = luaL_checknumber(L, 2);
       v = vector_add_scalar(*a, scalar);
   } else if (lua_type(L, 1) == LUA_TUSERDATA && lua_type(L, 2) == LUA_TUSERDATA) {
       Vector *v1 = luaL_checkudata(L, 1, "Vector");
       Vector *v2 = luaL_checkudata(L, 2, "Vector");
       v = vector_add(*v1, *v2);
   } else {
       v = vector_new(0.0, 0.0);
   }
   Vector *ptr = lua_newuserdata(L, sizeof(Vector));
   *ptr = v;
   luaL_getmetatable(L, "Vector");
   lua_setmetatable(L, -2);
   return 1;
}

Some important points here:

  • I check the arguments types, lua_type and luaL_checkudata, to decide what to do with them. I will explain more below.
    • It is possible to add two vectors or add a vector and a scalar value(Number)
    • After checking the type I call the proper internal function.
  • I always return a new value even if the operation is invalid, in this case I return a Vector with zeroes (0.0, 0.0)
  • Note that the value in the arguments is a pointer when it is user data and you need to dereference it.
  • In the end, before finishing the operation I associate the new vector with the Vector metatable.
Defining access methods to structure members

Another possibility is to add in a metatable a custom method. It allows us to call those methods associated with the value. I used it to declare access to internal Vector members.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static const struct luaL_Reg vector_methods[] = {
       {"__add",      api_vector_add},
       {"__sub",      api_vector_sub},
       {"__mul",      api_vector_mul},
       {"__div",      api_vector_div},
       {"lerp",       api_vector_lerp},
       {"x",          api_vector_access_x},
       {"y",          api_vector_access_y},
       {"__tostring", api_vector_tostring},
       {NULL, NULL}
};

The implementation of the method is really straightforward. We retrieve the Vector and push its x value into the stack and return:

1
2
3
4
5
int api_vector_access_x(lua_State *L) {
   Vector *v = luaL_checkudata(L, 1, "Vector");
   lua_pushnumber(L, v->x);
   return 1;
}

In Lua, I call the method like this using a colon:

1
local pos_x = vector_a:x()

The access method will be called with the self(the Vector value itself) and we will have access to the user data to access its members. Note the use of luaL_checkudata to retrieve value as a Vector.

Note: It is possible to use the __index to give access to the members because it is a way to customize the field lookup, but I found it more complex for my case.

Metatable definition and module creation

After presenting how metatables are used, let's do two things here: define our Vector metatable with the methods listed above and create another table that will be used as a module and will be loaded with the require() function. We have two functions that will be responsible for each part.

We have many things named Vector and also many steps to make the metatable work properly. I have some definitions to help you understand what we are talking about:

Vectors

Going back to the implementation, first, let’s create our metatable and the table that will be used in the module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int module_vector(lua_State *L) {
   int pos = lua_gettop(L);

   // VECTOR METATABLE
   luaL_newmetatable(L, "Vector");
   luaL_setfuncs(L, vector_methods, 0);

   lua_pushvalue(L, -1);
   lua_setfield(L, -2, "__index");


   // VECTOR MODULE TABLE
   lua_newtable(L);
   pos = lua_gettop(L);
   lua_pushcfunction(L, api_vector_new);
   lua_setfield(L, pos, "new");
   lua_pushcfunction(L, api_vector_lerp);
   lua_setfield(L, pos, "lerp");
   lua_pushcfunction(L, api_vector_magnitude);
   lua_setfield(L, pos, "magnitude");
   lua_pushcfunction(L, api_vector_distance);
   lua_setfield(L, pos, "distance");
   return 1;
}

Let’s break down the code into parts. First, we create a new metatable called Vector and set all the methods associated with it from vector_methods.

We also duplicate the metatable reference on the stack and set the field __index to itself. This will make the value associated with this metatable to look for fields in the metatable.

1
2
3
4
5
6
...
luaL_newmetatable(L, "Vector");
luaL_setfuncs(L, vector_methods, 0);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
...

We also create a table, it is common to set this table as a global variable, but we want to set it as a module to allow be imported by other scripts instead, so we let it be on top of the stack. This table has common function definitions, but the highlight here is the function new which will be used as the Vector value constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
...
lua_newtable(L);
pos = lua_gettop(L);

lua_pushcfunction(L, api_vector_new);
lua_setfield(L, pos, "new");
lua_pushcfunction(L, api_vector_lerp);
lua_setfield(L, pos, "lerp");
lua_pushcfunction(L, api_vector_magnitude);
lua_setfield(L, pos, "magnitude");
lua_pushcfunction(L, api_vector_distance);
lua_setfield(L, pos, "distance");
...

In Lua we are going to use it like this:

1
2
local Vector = require("core.vector")
local player_position = Vector.new(100, 100)

The Vector returned by the require will be our table with the constructor new. Its main use is just to be a bridge to our Vector type. Note that it is not the actual Vector structure and neither the metatable as described in the table above.

The last part is to create the module associated with the function above. We actually did not create anything yet. The things we mentioned above are inside a function that will be called when the function require() loads the module called “core.vector”. When this happens, the code above will be executed and the definitions will be set:

1
2
3
4
5
6
7
void api_math_open(lua_State *L) {
   luaL_requiref(L, "core.vector", module_vector, 0);
   lua_pop(L, 1);

   luaL_requiref(L, "core.rect", module_rect, 0);
   lua_pop(L, 1);
}

Note: we also have another type, Rect, which works similarly to Vector.

Implementing the constructor

The function new will be responsible for creating new Vector values and setting the proper metatable to them. Other functions will also create Vector values. Every time a function creates a new Vector value, it will need to associate the value with the metatable.

Here is the implementation in C of the constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int api_vector_new(lua_State *L) {
   double a = luaL_checknumber(L, 1);
   double b = luaL_checknumber(L, 2);
   Vector v = vector_new(a, b);

   Vector *ptr = lua_newuserdata(L, sizeof(Vector));
   *ptr = v;
   luaL_getmetatable(L, "Vector");
   lua_setmetatable(L, -2);
   return 1;
}

Some important things to describe:

  • lua_newuserdata allocates for you a block of memory of the type chosen.
    • It will be on top of the stack and you can use it freely.
  • luaL_checknumber allows us to check if the arguments are actually numbers
  • luaL_getmetatable retrieves from the Registry Table and puts it onto the stack the previously created metatable and then lua_setmetatable associates it with the new Vector.
Conclusion about metatables

This last section finishes the setup of a metatable for a type. As seen, there are some steps to be done but it will make your code implementation with Lua more robust.

Extra: Type validation with metatables

After defining the metatable, as mentioned before, you have to associate the value with a metatable to continue having the benefits, but it will be possible to check the user data type.

It is possible to validate if there is a metatable associated with a value when a C function is called as shown in the Vector arithmetic addition example.

Checking the metatable is used to make sure that you receive the correct value or even to allow different value types to the same arguments.

As a reminder, you can also check the value type for primitive types with lua_type.

Use luaL_checkudata to check if the value on the stack is user data associated with the metatable of the name, the name is the same used when we create with lua_newmetatable, otherwise, it will return NULL.

Here is the same example but highlighting what I just explained:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int api_vector_add(lua_State *L) {
   Vector v;
   if (lua_type(L, 1) == LUA_TNUMBER && lua_type(L, 2) == LUA_TUSERDATA) {
       lua_Number scalar = luaL_checknumber(L, 1);
       Vector *a = luaL_checkudata(L, 2, "Vector");
       v = vector_add_scalar(*a, scalar);
   } else if (lua_type(L, 1) == LUA_TUSERDATA && lua_type(L, 2) == LUA_TNUMBER) {
       Vector *a = luaL_checkudata(L, 1, "Vector");
       lua_Number scalar = luaL_checknumber(L, 2);
       v = vector_add_scalar(*a, scalar);
   } else if (lua_type(L, 1) == LUA_TUSERDATA && lua_type(L, 2) == LUA_TUSERDATA) {
       Vector *v1 = luaL_checkudata(L, 1, "Vector");
       Vector *v2 = luaL_checkudata(L, 2, "Vector");
       v = vector_add(*v1, *v2);
   } else {
       v = vector_new(0.0, 0.0);
   }
   Vector *ptr = lua_newuserdata(L, sizeof(Vector));
   *ptr = v;
   luaL_getmetatable(L, "Vector");
   lua_setmetatable(L, -2);
   return 1;
}

In the example above, I allow different behaviors, depending on the values used. It is possible to add a scalar value to a Vector or add two vectors. I have to check the order and the type of each argument.

The End

As described in the article, Lua can work with complex values and objects giving them specific behavior as desired. All those capabilities bring a powerful tool when developing applications and games that require a lot of customization and extensibility. I hope I was clear enough and those examples can be helpful for you.

Thank you!

Extra Resources