Scripting iOS Games With Lua - Part III

| Comments

In part I of this post, we created a simple iOS game in which to embed Lua scripting. In part II we embedded the Lua interpreter and demonstrated calling Lua scripts from our game. We also added some simple interaction by enabling our Lua scripts to access the state of our player ship (x,y position).

In this final part, we are going to build on this functionality to expose the entire ship control to Lua so that our scripts can control our NPC ships.

In part I of this post we created the ShipController class to allow our code access to the state of a ship (position) as well as to manipulate the ships controls. In part II we used a Lua script to access the ship’s state. Now we are going to make the full ShipController interface available to our Lua scripts.

We will begin by creating delagate functions for the rest of the methods in our ShipController. Add the following code to ViewController.m:

ViewController.m
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
static int press_right_button(lua_State *L){
    ShipController *sc = (ShipController *)lua_touserdata(L, 1);
    [sc pressRightButton];

    return 0;
}

static int press_left_button(lua_State *L){
    ShipController *sc = (ShipController *)lua_touserdata(L, 1);
    [sc pressLeftButton];

    return 0;
}

static int press_top_button(lua_State *L){
    ShipController *sc = (ShipController *)lua_touserdata(L, 1);
    [sc pressTopButton];

    return 0;
}

static int press_bottom_button(lua_State *L){
    ShipController *sc = (ShipController *)lua_touserdata(L, 1);
    [sc pressBottomButton];

    return 0;
}

static int get_ship_position(lua_State *L){
    ShipController *sc = (ShipController *)lua_touserdata(L, 1);

    // push x and y position on the stack
    lua_pushnumber(L, sc.x);
    lua_pushnumber(L, sc.y);

    // let the caller know how many results are available on the stack
    return 2;
}

These five functions are our delegates that implement the standard Lua C function prototype mentioned in part II. Each of these follows a standard format. First they pull a pointer off the stack and assign it to a local variable. This pointer is a ShipController pointer for the ship that is to be controlled. After that they use the local variable to perform an action, or, in the case of get_ship_position, to retrieve information. Finally, they push results on the stack where necessary and return the number of results pushed. The only really new thing here is the call to lua_touserdata at the beginning of each method. So what does this do?

There are essentially two different ways to pass non-primitive data to Lua. The first way lets Lua manage the memory for this data. We can define a struct to hold our data elements (including pointers) and ask Lua to allocate and manage the memory for instances of this struct, which we then fill in with our data. This approach is called user data. User data has the advantage of having Lua manage the memory, but its implementation is slightly more complicated.

The second approach is to simply pass pointers to Lua for the objects we create and manage in the main game code. This approach is known as light user data, and is the one we will use here.

Next we need to add these functions to our Lua library. This is simply a matter of adding entries for each to our library array.

ViewController.m
1
2
3
4
5
6
7
8
9
static const struct luaL_Reg shiplib_f [] = {
    {"player_ship_position", player_ship_position},
    {"press_right_button", press_right_button},
    {"press_left_button", press_left_button},
    {"press_top_button", press_top_button},
    {"press_bottom_button", press_bottom_button},
    {"get_ship_position", get_ship_position},
    {NULL, NULL}
};

Now all of these methods are available to our lua code. Before we can use them, we need to create another ship. Add the following code at the top of ViewController.m just below the allocation of our player ship:

ViewController.m Enemy Ship Declaration
1
ShipController *enemyShipController;

And add an allocation in viewDidLoad just below the one for the player ship:

ViewController.m Enemy Ship Allocation
1
enemyShipController = [[ShipController alloc] initWithX:105 Y:102 Speed:1.0 Name:@"enemy_ship"];

Our enemy ship will originate at (x,y) = (105, 102), or slightly offset from our player ship.

Next we need to create a simple AI script for our enemy ship. Create a new file called enemy_ship.lua and add the following:

enemy_ship.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function process(npc_ship)

 -- get the location of the player's ship
    player_x, player_y = ship.player_ship_position()

    -- get the location npc ship
    x, y = ship.get_ship_position(npc_ship)

    -- move this ship toward the player
    if player_x < x then
          ship.press_left_button(npc_ship)
    elseif player_x > x then
    ship.press_right_button(npc_ship)
    end

    if player_y < y then
        ship.press_bottom_button(npc_ship)
    elseif player_y > y then
        ship.press_top_button(npc_ship)
    end

end

This script declares a sigle function, process, which takes a single argument, a pointer to a ShipController. The function does nothing with this pointer directly, it simply passes it to the functions we bound to our ship library. The “AI” here is about as primitive as can be; it simply causes the enemy ship to move a bit closer to the player ship by calling the appropriate “press_*_button” functions.

The only things left to do are to load the new script and modify our game loop to call this lua function. The first is simply a matter of changing the name of the loaded file.

ViewController.m Loading Enemy Ship Lua Script
1
NSString *luaFilePath = [[NSBundle mainBundle] pathForResource:@"enemy_ship" ofType:@"lua"];

`

While we are at it, lets modify our output to make use of the name property we added to ShipController.

ViewController.m Adding output to run loop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(void)runLoop:(id)sender {


    // put the pointer to the lua function we want on top of the stack
    lua_getglobal(L,"process");

    lua_pushlightuserdata(L, enemyShipController);

    // call the function on the stack
    int err = lua_pcall(L, 1, 0, 0);
  if (0 != err) {
      luaL_error(L, "cannot run lua file: %s",
                   lua_tostring(L, -1));
      return;
  }

    NSLog(@"%@ at (%f,%f)", playerShipController.name, playerShipController.x, playerShipController.y);
    NSLog(@"%@ ship at (%f,%f)", enemyShipController.name, enemyShipController.x, enemyShipController.y);
}

This method uses the lua_getglobal function, which simply retrieves the address for the process symbol (our Lua function name) and puts it on the stack. Then it pushes our pointer to our ShipController for our enemy ship on the stack. Finally, it tells lua to call the function on the stack, using one argument – the pointer we pushed on the stack.

Now if we run our game we get the following output:

2012-02-06 20:57:30.609 TestLua[37090:f803] Left button pressed for ship enemy_ship
2012-02-06 20:57:30.609 TestLua[37090:f803] Bottom button pressed for ship enemy_ship
2012-02-06 20:57:30.610 TestLua[37090:f803] player_ship at (100.000000,100.000000)
2012-02-06 20:57:30.610 TestLua[37090:f803] enemy_ship ship at (104.000000,101.000000)
2012-02-06 20:57:31.608 TestLua[37090:f803] Left button pressed for ship enemy_ship
2012-02-06 20:57:31.609 TestLua[37090:f803] Bottom button pressed for ship enemy_ship
2012-02-06 20:57:31.609 TestLua[37090:f803] player_ship at (100.000000,100.000000)
2012-02-06 20:57:31.609 TestLua[37090:f803] enemy_ship ship at (103.000000,100.000000)
2012-02-06 20:57:32.608 TestLua[37090:f803] Left button pressed for ship enemy_ship
2012-02-06 20:57:32.609 TestLua[37090:f803] player_ship at (100.000000,100.000000)
2012-02-06 20:57:32.609 TestLua[37090:f803] enemy_ship ship at (102.000000,100.000000)
2012-02-06 20:57:33.608 TestLua[37090:f803] Left button pressed for ship enemy_ship
2012-02-06 20:57:33.609 TestLua[37090:f803] player_ship at (100.000000,100.000000)
2012-02-06 20:57:33.609 TestLua[37090:f803] enemy_ship ship at (101.000000,100.000000)
2012-02-06 20:57:34.608 TestLua[37090:f803] Left button pressed for ship enemy_ship
2012-02-06 20:57:34.609 TestLua[37090:f803] player_ship at (100.000000,100.000000)
2012-02-06 20:57:34.609 TestLua[37090:f803] enemy_ship ship at (100.000000,100.000000)
2012-02-06 20:57:35.609 TestLua[37090:f803] player_ship at (100.000000,100.000000)
2012-02-06 20:57:35.609 TestLua[37090:f803] enemy_ship ship at (100.000000,100.000000)

So there you have it. Our enemy ship controlled by a Lua script. We see the enemy ship script frantically pressing buttons to try to move closer to our ship. Not exactly the brightest AI in the world, but enough to get started. Along with better AI, there are some refinements that could be added. For one thing, constantly passing in the pointer to our enemy ship’s ShipController seems a bit cheesy. It would be better if we could wrap it up in a nice Lua object that our scripts could use, and we can. But that is the subject for another post.

Comments