Lua Game Design (Guide)

Posted by aeron on Nov. 9, 2012, 12:17 a.m.

We've all probably heard that Lua (and scripting languages in general) can come in handy when writing games. And if you haven't heard that, you just have. In this guided example I will walk you through the process of binding a C++ class to Lua and manipulating it from a script.

But why, if you're already going through the trouble of writing a game in a compiled language such as C++, would you introduce an interpreted layer of potential slowness? Well when it comes down to it, the performance of the program isn't as important as the performance of the programmer. Lua, when used strategically, can be a real timesaver for development with negligible effect on framerate. And with the right amount of game logic implemented in Lua, you can develop large chunks of your game without ever having to recompile! Hell you could even reload your scripts during runtime and develop without ever closing the game. This can do wonders for your flow, allowing you to work and test lots of small changes without regular interruption caused by compilation.

Scope

Of course finding this sweet spot takes careful consideration. Ultimately it's up to you to figure out what will be implemented in C++ and what will be offloaded to scripts. Be aware that it might take you several iterations (read: attempts that go to shit) before you get "good" at defining that line. Fortunately it gets easier the more you practice, so even if you screw up it will still benefit you.

As for me personally, I'm still in the early stages of understanding, so bear with me. The main purpose of this guide is to spark your interest rather than show you the "right" or "best" way to do things.

Code

The full source code for this example can be downloaded here. For brevity I'll just refer to relevant snippets as we go along.

SLB

There are plenty of solutions available for binding your code to Lua, but I'm going to be focusing on Simple Lua Binder, since it's quite easy to jump into. I used the SLB-2.00_amalgamation, which contains a minimal number of files that can be added directly into your project. This bundle includes lua itself, so no need to worry about getting that separately. Note that everything is included in the example zip above, so you don't need to download SLB again unless you really want to.

Dialogue class

For my example, I am going to have two important classes, the first of which is Dialogue. Dialogue is a simple class that holds some information about a part of a conversation. The definition looks like this:

class Dialogue {
	public:
		Dialogue(std::string message_text, std::string next_function);
		void addChoice(Dialogue* choice);
		std::string getChoice();
		
		//Message text
		std::string message;
		
		//Name of function to call for the next message
		std::string next;
	protected:
		std::vector<Dialogue*> choices;
};

For now just ignore the next variable and getChoice(), we'll go over those later. The rest you can probably assume how it works, if not just peek at the implementation and you'll see it's all very cut and dry.

Binding

SLB is used to bind some of this functionality to Lua. The code to generate the bindings is:

SLB::Manager m;
SLB::Class< Dialogue >("Dialogue",m)
	.constructor<std::string,std::string>()
	.set("addChoice", &Dialogue::addChoice);

What this does is create a table in Lua that exposes part of our Dialogue class. If we were to create an SLB::Script we could access the class from Lua like in the following:

local d = SLB.Dialogue('Whatcha doing in my waters?','')
d:addChoice(SLB.Dialogue('Nothing..', ''))
d:addChoice(SLB.Dialogue('Not fishing...', ''))

More in-depth Scripting

Now being able to modify a class from Lua is cool, but how can we get that data back into our C++ runtime? My solution is to create functions in Lua that simply return an instance of Dialogue. These can be called from C++ by creating a subclass of SLB::Script.

Here's the definition for my second important class, DialogueScript:

class DialogueScript : public SLB::Script {
	public:
		DialogueScript(SLB::Manager* m);
		Dialogue* getDialogue(std::string s);
};

Now, the SLB::Script class already takes care of managing the lua_State for us, along with providing some convenient functions for loading code into the script. For the simplicity of the example we will just load our script from a file default.lua in the constructor:

DialogueScript::DialogueScript(SLB::Manager* m) : Script(m) {
	doFile("default.lua");
}

The more important part of this class is the getDialogue function:

Dialogue* DialogueScript::getDialogue(std::string lua_function_name) {
	lua_State *L = getState();
	SLB::LuaCall<Dialogue*(void)> call(L,lua_function_name.c_str());

	Dialogue* d=NULL;
	d = call();
	return d;
}

This takes a function name and finds it in the lua_State, calls it and returns the value. Now, theoretically we should be able to define a function in default.lua similar to:

function test()
	local d = SLB.Dialogue('Are you playing those love games with me?','')
	d:addChoice(SLB.Dialogue('Wow I didn\'t realize I liked watercolors so much', ''))
	d:addChoice(SLB.Dialogue('Baileys... so creamy', ''))
	return d
end

And back in C++ we could use the following to get ahold of said Dialogue node:

DialogueScript s(&m);
Dialogue* d = s.getDialogue("test");

Back to Dialogue: next and getChoice

So now we're ready to add a layer of interactivity. For this I use Dialogue::getChoice(). This method first prints the current Dialogue node's message. If choices have been added, they are listed and the program prompts the user for a response. Finally, this function will return the name of the function to execute next if there is one. This is deduced based on the value of the next variable in either the current node or in the selected choice node.

To clarify, the variable next is used to hold the name of a function that will return an associated Dialogue node. If the node has no choices added onto it, next will name the next function in the conversation if there is one. If the node is itself stored as a choice, it will instead name the function that gets called when the user picks that choice.

Gluing it together

Now all we have to do is combine the use of DialogueScript with Dialogue::getChoice. It's as simple as:

DialogueScript s(&m);
Dialogue* d = s.getDialogue("main");
while(d!=NULL) {
	std::string next = d->getChoice();
	if(next.length()>0)
		d = s.getDialogue(next);
	else d = NULL;
}

This creates a loop that starts off with the main dialogue node. This node gets prompted and then the next node is evaluated if there is one. This is repeated until no more nodes are returned. Simple as that! So now we can define our Dialogue in a scriptable fashion:

function yourThoughtsPlease()
	local d = SLB.Dialogue('What do you think of me?','')
	d:addChoice(SLB.Dialogue('You\'re alright I guess...', 'alright'))
	d:addChoice(SLB.Dialogue('I don\'t know, sir...', 'idk'))
	return d
end

function alright()
	return SLB.Dialogue('Do you love me?','')
end

function idk()
	return SLB.Dialogue('Make an assessment.','')
end

But you can also do much more than just that, after all you have the power of an entire scripting language in your hands! Seen in action, some sample output of the included default script looks like:

What do you get when you multiply six times nine?
1. Fifteen
2. Fifty four
3. Fourty two
4. I suck at math...
> 2
What are you, in base 10?
Okay I'll ask you again... What do you get when you multiply six times nine?
1. Fifteen
2. Fourty two
3. I suck at math...
> 2
You are enlightened. Are you a wizard?
1. I think I am!
2. No way!
> 2
Don't be modest now, only a wizard would say he's not a wizard. I know from experience.
Would you like to turn into a whale?
1. Yeah buddy!
2. WHAT?! No!!
> 1
Poof! You turn yourself into a whale! Wowee, that was crazy!

So you can already see there's a little bitta flexibility at our fingertips ;)

Onward

Obviously for application to a real game this will take some modification, but again the point of this wasn't to be the end all be all. Every use case for Lua will be different so you don't necessarily have to stick to anything I just wrote about. Explore what's out there, try something new, hack some shit together. Hopefully I've inspired you enough at least to go look up more info on game scripting in general. And with that, I leave you. Happy coding!

Comments

DFortun81 11 years, 5 months ago

You can even export the full load of the lua game and save it for later and then load it back up later assuming that no external resources were required. [That can be quickly bypassed with some extra effort.]

ludamad 11 years, 5 months ago

Cool. I recently started to make my project lanarts use this (SLB) a few days ago. One nifty thing I found was that if you hack onto SLB::Private namespace and provide overloads for Type<> you can get nifty results such as eg wrapping a colour structure as a lua array and vice versa (eg passing {255,0,0} as a colour.)

SLB is definitely a good middle-ground sensible option for lua bindings (without using Boost).