Lucas
Klassmann | Blog

Lua for Dynamic AI in Games

Implementing AI scripting for your game characters

Updated in 2025-01-31 · First published in 2025-01-31 · By Lucas Klassmann

Introduction

Imagine this: you're designing a game. You want a simple enemy that adapts to the player's movements, but every tweak requires you to rebuild the game—again and again. Frustrating, right? What if there were a way to dynamically update your enemy's behavior without recompiling your code? Enter Lua: a lightweight scripting language that integrates seamlessly with C, giving developers flexibility and power.

Once again, we’ll dive into an example of what Lua is capable of. Previously, we explored more complex use cases, such as building a small HTTP server and using Lua API calls from C. This time, however, we’ll focus on a simpler but fundamental task in game development: scripting objects and giving them some level of intelligence.

In this project, a green rectangle (the player) is detected by a red rectangle (the enemy). After a cooldown period, the enemy starts chasing the player. If the player moves far enough away, the enemy returns to its initial position. The enemy's behavior is dynamically controlled by Lua. Along the way, you’ll see how Lua empowers developers to implement dynamic features like AI state machines, script reloading, and more.

The Setup: A Game Built with C and Lua

Enemy AI

The game is straightforward:

  • The Player: A green rectangle controlled with the WASD keys. The player moves faster than the enemy, creating a natural challenge.

  • The Enemy: A red rectangle controlled by Lua script, which determines its behavior using a state machine. The enemy can idle, chase the player, or return to its starting position based on proximity.

  • Dynamic AI: Lua script define the enemy’s AI, allowing real-time adjustments. Pressing the R key reloads the Lua script, enabling changes to AI logic without restarting the game.

This example not only demonstrates how Lua integrates with C but also highlights practical benefits for game development, including modularity, flexibility, and rapid iteration.

How C and Lua Work Together

At the heart of this project lies the integration of C and Lua, you can learn more in other articles from the blog. The game uses SDL2 for rendering and input handling, while Lua provides the logic for the enemy AI. Let’s break down the highlights of the implementation:

AI Behavior with Lua

The enemy’s AI is implemented as a state machine in Lua with three states:

  • IDLE: The enemy remains stationary until the player comes within a defined range.
  • CHASING: The enemy moves toward the player when they get close enough.
  • RETURNING: If the player escapes too far, the enemy returns to its original position.

Here’s the Lua function that defines this behavior:

 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
31
32
33
34
35
36
37
38
39
function update_enemy(player, enemy, home_x, home_y, delta_time)
    local distance_to_player = math.sqrt((player.x - enemy.x)^2 + (player.y - enemy.y)^2)
    local distance_to_home = math.sqrt((home_x - enemy.x)^2 + (home_y - enemy.y)^2)

    if enemy_state.state == STATE_IDLE then
        -- Transition to chasing if the player is within the follow distance
        local player_near = distance_to_player < enemy_config.follow_distance
        if player_near and enemy_state.cooldown_timer <= 0 then
            enemy_state.state = STATE_CHASING
        end

    elseif enemy_state.state == STATE_CHASING then
        -- Stop chasing and return if the player is too far
        if distance_to_player > enemy_config.max_distance then
            enemy_state.state = STATE_RETURNING
        end

        -- Chase the player
        return player.x, player.y

    elseif enemy_state.state == STATE_RETURNING then
        -- Transition back to IDLE if the enemy is close to home
        if distance_to_home < 5 then
            enemy_state.state = STATE_IDLE
            enemy_state.cooldown_timer = enemy_config.cooldown
        end

        -- Return to home position
        return home_x, home_y
    end

    -- Handle cooldown timer in IDLE state
    if enemy_state.state == STATE_IDLE and enemy_state.cooldown_timer > 0 then
        enemy_state.cooldown_timer = enemy_state.cooldown_timer - delta_time
    end

    -- Default: Stay in current position
    return enemy.x, enemy.y
end

This logic, defined in Lua, is called every frame by the C code, ensuring smooth, real-time enemy behavior.

We also have a global configuration for the enemy, you can change them and reload with R:

1
2
3
4
5
6
enemy_config = {
    speed = 2,
    cooldown = 0.5,
    max_distance = 200, -- Distance to stop chasing
    follow_distance = 400 -- Distance to start chasing
}

Dynamic Script Reloading

Pressing R, calls reload_lua_script function, which reloads the Lua script, allowing developers to test changes immediately. This feature is invaluable for tweaking AI behavior, such as adjusting speed or proximity thresholds, without restarting the game.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void reload_lua_script(const char *script_path, Enemy *enemy) {
    if (luaL_dofile(L, script_path) != LUA_OK) {
        printf("Error loading script: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
        return;
    }

    lua_getglobal(L, "enemy_config");
    if (lua_istable(L, -1)) {
        lua_getfield(L, -1, "speed");
        enemy->speed = lua_tonumber(L, -1);
        lua_pop(L, 1);

        lua_getfield(L, -1, "cooldown");
        enemy->cooldown = lua_tonumber(L, -1);
        lua_pop(L, 1);

        lua_getfield(L, -1, "max_distance");
        enemy->max_distance = lua_tonumber(L, -1);
        lua_pop(L, 1);
    }
    lua_pop(L, 1); // Pop the table
}

Smooth Movement with Lerp

To ensure natural movement, the enemy’s position is interpolated using linear interpolation (lerp):

1
2
enemy->rect.x = lerp(enemy->rect.x, lua_tonumber(L, -2), delta_time * enemy->speed);
enemy->rect.y = lerp(enemy->rect.y, lua_tonumber(L, -1), delta_time * enemy->speed);

Linear interpolation smoothly transitions between two values based on a ratio, making it essential in game development for animations, movement, and blending, ensuring smooth and realistic changes. Lerp More information on Wikipedia.

1
2
3
4
// Utility function for linear interpolation
float lerp(float a, float b, float t) {
    return a + t * (b - a);
}
Core C Code Overview

The C code handles the game’s main loop, integrates Lua, and communicates data between the player, enemy, and Lua script. Key components include:

Lua State Initialization

The Lua interpreter is initialized in C, and the enemy script is loaded using the following code:

1
2
3
L = luaL_newstate();
luaL_openlibs(L);
reload_lua_script("enemy_ai.lua", &enemy);
Data Exchange with Lua

Data is passed between C and Lua using Lua’s stack. For example, the player’s and enemy’s positions are pushed to Lua as tables, and the updated positions are retrieved as return values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
lua_newtable(L);
lua_pushnumber(L, player->x);
lua_setfield(L, -2, "x");
lua_pushnumber(L, player->y);
lua_setfield(L, -2, "y");

lua_newtable(L);
lua_pushnumber(L, enemy->rect.x);
lua_setfield(L, -2, "x");
lua_pushnumber(L, enemy->rect.y);
lua_setfield(L, -2, "y");

lua_pcall(L, 6, 2, 0);
enemy->rect.x = lerp(enemy->rect.x, lua_tonumber(L, -2), delta_time * enemy->speed);
enemy->rect.y = lerp(enemy->rect.y, lua_tonumber(L, -1), delta_time * enemy->speed);
lua_pop(L, 2);
Script Reloading

The reload_lua_script function enables dynamic reloading of the Lua script when the R key is pressed. This is achieved by calling:

1
2
3
4
if (luaL_dofile(L, script_path) != LUA_OK) {
    printf("Error loading script: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}
Game Loop

The main loop handles player input, updates enemy behavior, and renders the game:

1
2
3
4
5
6
7
8
while (running) {
    // Input Handling

    // Update Enemy
    update_enemy(&enemy, &player, delta_time);

    // Render Game
}

These components demonstrate how Lua and C work together seamlessly, with Lua focusing on AI behavior and C managing performance-critical tasks like rendering and input.

The complete source code can be found in the repository.

Why Lua? The Benefits for C Applications

Lua’s lightweight design and easy embedding make it ideal for extending C applications, especially games. Here are key benefits demonstrated by this project:

  • Flexibility: Lua scripts can be edited independently of the core C code, enabling rapid prototyping and iteration.

  • Modularity: Separating game logic (Lua) from the engine (C) makes the codebase easier to maintain and extend.

  • Dynamic Updates: Features like script reloading allow real-time adjustments, essential for fine-tuning gameplay.

  • Performance: Lua’s small runtime ensures minimal overhead, even when called every frame. And you can achieve even better performance with LuaJIT.

Practical Uses and Extensions

With the techniques shown in this example, you can:

  • Expand AI Behaviors: Add states like attacking or fleeing to make enemies more complex.

  • Introduce Multiple Enemies: Assign different Lua scripts to various enemies for unique behaviors.

  • Create Moddable Games: Allow players to modify or extend game logic by editing Lua scripts.

  • Enhance Interactivity: Use Lua to define dynamic environments, such as traps or NPCs with dialogue systems.

Conclusion

This project showcases the incredible potential of Lua in C applications, particularly for games. By combining C’s performance with Lua’s flexibility, developers can create dynamic, modular systems that are easy to modify and extend.

Check the complete code in the repository.

Whether you're building AI systems, dynamic environments, or moddable games, the possibilities are endless. So, why stop here? Experiment with this example, expand on it, and let your creativity flow.

Want to explore more about Lua integration, game development, or others Lua applications? Follow this blog for deep dives, hands-on examples, and some programming techniques that will take your projects to the next level!

Thank you!

Extra Resources