• entries
    206
  • comments
    217
  • views
    224514

RPG Anvil: The Lua Tutorial Part 5

Sign in to follow this  

559 views



Today, how to access C++ objects when Lua calls your C++ function.

The previous installments of this tutorial are here:



The goal of the tutorial is for our C++ game code to call into a Lua script to determine whether the player can open a door or not. Last time, we called the Lua script to make the check, and the Lua script called back into C++ code to get the player's Lockpicking Skill Level. However, in a cliffhanger of dramatic proportions, I pointed out that just calling into C++ wasn't enough; we also need to have access to the game's C++ objects. Today, I show how to get access to the game's data from the C++ code that Lua calls.

To frame the problem, we have this Lua script:

char *szLua =
"x = GetLockpickLevel() "
"return ( x > 7 ) ";


And, in C++, we have some game data. For this demo, we'll make the data as simple as possible, though the method I'll describe for getting at the data is universal; we can get any class (or classes) into the Lua-called code. Anyways, a super-simple class that describes the player's Lockpick Level:

class CGameData
{
public:
CGameData( int i ) { m_iLockpickLevel = i; }
int m_iLockpickLevel;
};


The problem is: How can the C++ function GetLockLevel access our game's CGameData object?

You could simple store a pointer to the CGameData in a global (or static) variable. However, unless you are writing the simplest of toy game demos, I advise against it. I don't want to launch into a screed against global variables in C++ here, but suffice to say there are good reasons against using global variables in most cases, both stylistic and practical. I'm sure a Google search can turn up more reading than you'd care to do.

A much better approach is to associate the CGameData pointer with the lua_State; every function called by Lua has access to the lua_State (which basically represents the Lua virtual computer that runs Lua code). Lua even has a nice type for this exact purpose; "light userdata". The functions lua_touserdata and lua_pushlightuserdata support this type, and allow our C++ code to put "light userdata" in and out of the Lua state.

One particular place you could store the CGameData pointer (as "light userdata") is the "Registry". The Registry is a Lua concept for storing data for C code, and is distinct from the Windows Registry or anything else like that. Lookup Registry in the Lua documentation, or the LUA_REGISTRYINDEX constant in the headers. However, I've actually chosen a different mechanism; the Lua Registry is OK, but there are some issues. If you end up using reentrant scripts (where one Lua script ends up calling another Lua script through C++), or re-using Lua states, it's possible you could have conflicts in the Registry, where you end up overwriting one CGameData pointer with another. Even if you aren't doing that now, I'd go a different way, just to keep my code future-safe.

Instead, I prefer the "context" paradigm, common in C code, particularly pseudo-object-oriented C code. Basically, we'll pass a context object pointer to the Lua script on the stack, and when the Lua script calls a C++ function, it will pass the context back to us unchanged. The context contains any data (or pointers to data) that our C++ function will need.

In the case of this demo, I'll just pass the CGameData pointer as the context. However, you can easily imagine more data in the context; if we extended the demo a little further, we might pass a pointer to a CDoor object to the script, with data about the door the player is trying to open. If we had to call Lua reentrantly, we could use a separate context object, with different data in it, so the C++ functions called from Lua can do their work properly.

OK enough chatter. To make the context concept work, we actually have to change our Lua script slightly; here's the new one:

char *szLua =
"function game_door_check( context ) "
" local x = GetLockpickLevel( context ) "
" return ( x > 7 ) "
"end ";


As you see, the Lua script now takes a context parameter, and passes it on to the GetLockpickLevel function. Variables are loosely typed in Lua, so there is no problem passing light userdata around like that, even though the Lua script doesn't understand the type at all.

Here's the simplest possible GetLockpickLevel implementation:

int lua_GetLockpickLevel( lua_State *luaState )
{
CGameData *pData = (CGameData *) lua_touserdata( luaState, 1 );
lua_pushinteger( luaState, pData->m_iLockpickLevel );
return( 1 );
}


In this case, we get the light userdata from the stack (with lua_touserdata) in position 1. Then we push the return value, from pData->m_iLockpickLevel, onto the Lua stack, and return the number of return values; 1 in this case.

However, that is actually a bad implementation. A simple typo in your script will crash your program. I highly recommend that you carefully check for errors, so you can quickly diagnose scripting issues (plus my pet peeve is tutorials that ignore error conditions). Something like this is much better:

int lua_GetLockpickLevel( lua_State *luaState )
{
int nArgs = lua_gettop( luaState );
if( nArgs != 1 )
{
lua_pushstring( luaState, "Script Error 1" );
lua_error( luaState );
}
CGameData *pData = (CGameData *) lua_touserdata( luaState, 1 );
if( !pData )
{
lua_pushstring( luaState, "Script Error 2" );
lua_error( luaState );
}
lua_pushinteger( luaState, pData->m_iLockpickLevel );
return( 1 );
}


Now, astute readers will have noticed that my new Lua script has a function statement that wasn't there before. You can read all about functions in the Lua documentation; in this case I needed to do this so I could pass the context variable (the light userdata) in to Lua. This adds a complication that, from C++ code, I need to first run the script to create the function, and then, as a separate step, call the function. To run the script, after the old call to luaL_loadstring:

    iStatus = lua_pcall( lState, 0, 0, 0 );
if( iStatus )
{
std::cout << "Error: " << lua_tostring( lState, -1 );
return 1;
}


Basically, we call the script, with no parameters or return values.

Now, to call a function, we first have to get the function and put it onto the Lua stack. Note that in Lua, functions are just another kind of object, so we retrieve the function, based on its name, from the Lua global namespace, like so (note that lua_getfield retrieves the function and puts it on top of the stack):

    lua_getfield( lState, LUA_GLOBALSINDEX, "game_door_check" );


Next, we need to put our CGameData pointer on the Lua stack as light userdata. (This will be the context parameter in the Lua script):

    CGameData Data( 8 );
lua_pushlightuserdata( lState, (void *) &Data );


Then, we tell Lua to run. Since the stack is configured with the parameter on the top, and the function next, we just tell Lua to run with one parameter, and it is smart enough to do things properly:

    iStatus = lua_pcall( lState, 1, 1, 0 );


The Lua documentation for lua_call has a more thorough description of how and why this works.

The remainder of the code, retrieving the return value, remains the same. Here is the complete code listing to this point:

// luatest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include
#include

class CGameData
{
public:
CGameData( int i ) { m_iLockpickLevel = i; }
int m_iLockpickLevel;
};

char *szLua =
"function game_door_check( context ) "
" local x = GetLockpickLevel( context ) "
" return ( x > 7 ) "
"end ";

int lua_GetLockpickLevel( lua_State *luaState )
{
int nArgs = lua_gettop( luaState );
if( nArgs != 1 )
{
lua_pushstring( luaState, "Script Error 1" );
lua_error( luaState );
}
CGameData *pData = (CGameData *) lua_touserdata( luaState, 1 );
if( !pData )
{
lua_pushstring( luaState, "Script Error 2" );
lua_error( luaState );
}
lua_pushinteger( luaState, pData->m_iLockpickLevel );
return( 1 );
}

int _tmain(int argc, _TCHAR* argv[])
{
lua_State *lState;

lState = luaL_newstate();
luaL_openlibs( lState );

lua_register( lState, "GetLockpickLevel", lua_GetLockpickLevel );

int iStatus = luaL_loadstring( lState, szLua );
if( iStatus )
{
std::cout << "Error: " << lua_tostring( lState, -1 );
return 1;
}

iStatus = lua_pcall( lState, 0, 0, 0 );
if( iStatus )
{
std::cout << "Error: " << lua_tostring( lState, -1 );
return 1;
}

lua_getfield( lState, LUA_GLOBALSINDEX, "game_door_check" );
CGameData Data( 8 );
lua_pushlightuserdata( lState, (void *) &Data );
iStatus = lua_pcall( lState, 1, 1, 0 );
if( iStatus )
{
std::cout << "Error: " << lua_tostring( lState, -1 );
return 1;
}

int iRet = (int) lua_toboolean( lState, -1 );
if( iRet )
{
std::cout << "Door opened!" << std::endl;
}
else
{
std::cout << "Door still closed." << std::endl;
}

lua_close( lState );

return 0;
}



(Sorry, the code is getting pretty long at this point. I do think properly handling errors is important, which adds some verbosity to the code).

Well, at this point, we have fully functional code. Yay! There is one more thing I want to cover next time, and that is handling C++ exceptions properly. And after that I'll have some last notes and thoughts.
Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now