Thursday, July 28, 2005

Using Lua Scripting For Games

So you know a bit about Lua and how to hook it up to C#. Now you want to create a cool computer role playing game with cool Lua scripting but don't know where to start.

Wonder no further! In this article I'll attempt to stuff Lua scripting into every possible place Lua scripting could be stuffed. Now this article might a tad on the lengthy side but it has a lot of repeated code listings so don't worry!

In the beginning



To use Lua scripting in a game, you first need a game. For our purposes it will be very small and text based.

It's going to a two "room" game with one NPC.

The first room will be a dark forest. The second room a dank dungeon. In the forest we'll have a knight looking for some treasure that's in the second room.

Yep that's all the game plan we need :D

CODE!



Okay set up Lua, you should know how by now.

The base code should look a little like this:


using System;
using LuaInterface;

namespace RPGLUA
{

///
/// The main class for our superb RPG
///

class RPG
{
Lua lua =
new Lua();



So that's the Lua setup just waiting to be used for scripting goodness. Next up we need the basic game loop and basic game interface. Time to throw together a simple skeleton program!


class RPG
{
Lua lua =
new Lua();
bool quit = false;
///
/// The main entry point for the application.
///

[STAThread]
static void Main(string[] args)
{
RPG rpg =
new RPG();
rpg.Go();
}

public void Go()
{
Console.WriteLine("Welcome to the cool RPG Game");
Console.WriteLine("\tPress q to quit!");
while(quit == false)
{
Console.Write(">");
string ans = Console.ReadLine();

if(ans == "q")
quit =
true;
}

Console.WriteLine("Goodbye");
}
}




As you can probably guess - on running the above, we create an instance of the RPG class imaginatively called rpg. Then we tell it to go with the Go function. At this point it enters a big loop that won't end until we quit. Each time it loops it waits for player input, in the form of a read line. Currently the only recognized input is "q" which will let us exit the game.

I believe I said something about rooms. Let's start making one.


        class Room
{
private Lua scriptPower;
private string scriptName;
private string name;

public Room(string name, Lua lua)
{
this.name = name;
scriptName = name + ".lua";
scriptPower = lua;
scriptPower.DoFile(scriptName);
}
}



Yah we're going for a generic room object. Basically you pass in the Lua script name (without the .lua). Then the room loads up the script file. We also keep a local copy of the name. That's it. How about a Lua script to give us an idea of what we're doing?

forest.lua



----
--Our Forest Room
---

--Create a forest Table--
forest = {};

--What to do on entering the forest--
function onEnter()
print("You enter the forest");
end;

--Add some text--
forest.description = "A deep dark foreboding forest. A bit scary.";
forest.name = "Dark Forest";

--Add a function pointer--
forest.OnEnter = onEnter;


Type that into a file called forest.lua and save it to wherever your exe is created (same place you put the lua dlls). Now we have a carefully crafted forest let's update the room code, with two getters.


        public string Name
{
get
{
return(string) scriptPower[name + ".name"];
}
}

public string Description
{
get
{
return (string)scriptPower[name + ".description"];
}
}




This is getting a bit exciting isn't it? Well I'm not one of those people who likes to write out reems and reems of code and then pull it all together at the end. I wanna start using the above getters right now! For this we need to upgrade our interface to version Alpha2009. No problem.

Here it is all at once.


           public void Go()
{
Console.WriteLine("Welcome to the cool RPG Game");
Console.WriteLine("\tPress q to quit!\n\r");
while(quit == false)
{

Explore(currentRoom);
Console.Write(">");
string ans = Console.ReadLine();
ClearScreen();

if(ans == "q")
quit =
true;
}

Console.WriteLine("Goodbye");
}

public void Explore(Room r)
{
Console.WriteLine("You are in: " + r.Name);
Console.WriteLine(r.Description);
}

//Write 50 blank lines should clear the terminal
public void ClearScreen()
{
for(int i = 0; i < 50; i++)
Console.WriteLine();
}



Pretty self explanatory. My clear screen is a bit crap but what are you going to do?

Run it and see how cool it is! Wooo! We've setup the dark forest as the first room. Next we're going to add an NPC in the middle of the forest : - the knight. No doubt while reading this you are thinking "Oh you could do this too or write it this way instead". Yeah there are many ways to do things and a lot of choice between what you want in the script and what you want in the C# code. Generally knowing what you're creating will make these choices easier. (Do you want the game to be extended or are you just using scripts for easier content management)

Lets create the NPC class it will be based very closely on our room class. In fact it is going to be the room class but renamed to NPC. Object Orientaness suggests we should have a parent class possibly a scriptable object and a lookable interface? ILookable and Scriptable. I'm not going to put this in though! :o I want to keep things pretty simple and direct.



class NPC
{
private Lua scriptPower;
private string scriptName;
private string name;

public NPC(string name, Lua lua)
{
this.name = name;
scriptName = name + ".lua";
scriptPower = lua;
scriptPower.DoFile(scriptName);
}


public string Name
{
get
{
return (string)scriptPower[name + ".name"];
}
}

public string Description
{
get
{
return (string)scriptPower[name + ".description"];
}
}
}




All quite familiar. Time to stir things up a bit. We're going to be able to add occupants to rooms and we'll add a way to interact with NPCs in said rooms. This calls for funktions.


     public void DescribeOccupants()
{
foreach(NPC n in npcList)
{
Console.WriteLine("There is a " + n.Name + " here.");
}
}

public void AddNPC(NPC npc)
{
npcList.Add(npc);
}



Looks like we also need an array list called npcList. Therefore remember to include using System.Collections;. Add the npcList to the top of the room class, like so


private ArrayList npcList = new ArrayList();


We can now add NPCs to rooms! Rather than hard code this. Lets use another script that will have the power to add NPCs to rooms among other things. We'll call this script setup. The script will need one or two helper functions so let's write them and hook them into lua. Here's the code:

class RPG
{
Lua lua =
new Lua();
bool quit = false;
Room currentRoom;

public Room CreateRoom(string roomName)
{
return new Room(roomName, lua);
}

public NPC CreateNPC(string npcName)
{
return new NPC(npcName, lua);
}

public void SetStartRoom(Room r)
{
currentRoom = r;
}

public void AddNPCToRoom(Room r, NPC n)
{
r.AddNPC(n);
}

public RPG()
{
lua.RegisterFunction("CreateRoom",
this, this.GetType().GetMethod("CreateRoom"));
lua.RegisterFunction("CreateNPC",
this, this.GetType().GetMethod("CreateNPC"));
lua.RegisterFunction("AddNPCToRoom",
this, this.GetType().GetMethod("AddNPCToRoom"));
lua.RegisterFunction("SetStartRoom",
this, this.GetType().GetMethod("SetStartRoom"));
lua.DoFile("setup.lua");


Okay let's see the script file. It uses the simple functions, we wrote, that allow easy creation of NPCs and rooms and ways to add NPCs to rooms.

Setup.lua




---
--- Setup Script
---


start = CreateRoom("forest");
SetStartRoom(start);
AddNPCToRoom(start, CreateNPC("knight"));


After this I bet you'd quite like to see the knight.lua file too yeah? It's basically the same as room.

knight.lua




----
--Knight NPC
---

--Create a knight Table--
knight = {};

knight.description = "A tall knight wrapped in bulky armour. He seems to be shaking.";
knight.name = "A Knight";

function knight.OnTalk()
end;


Our code creates a forest room and puts a knight in the forest. It also determines where we the player start (in the forest). Let's get the functionality of this room and npc quite complete before we add the next room. First we'll want to see (in the game) the knight. This isn't a problem.



public void Explore(Room r)
{
Console.WriteLine("You are in: " + r.Name);
Console.WriteLine(r.Description);
r.DescribeOccupants()
}



Pretty easy. But now we need to interact with the the npc. I'm going to give the NPCs an interact function and a interact key. In the NPC class do the following:


public string interactKey;

public NPC(string name, Lua lua)
{
this.name = name;
this.interactKey = name[0].ToString();


What's the interact key? Well it's so the program can say "Press K to interact with the knight". Then we'll have an interact function where the interactions actually take place. All NPCs will have Talk and Look. Basic but that's all we'll need. Currently I've added the code for what happens when you look.



public void Interact()
{
string answer = "";
while(answer != "x")
{
Console.WriteLine("You are interacting with " + Name);
Console.WriteLine("\tx to exit");
Console.WriteLine("\tt to talk");
Console.WriteLine("\tl to look");

Console.Write(">");
answer = Console.ReadLine();

if(answer == "l")
{
Console.WriteLine(
this.Description);
}
}
}



Pretty standard stuff. Now we need to have a way to interact with these NPCs when we, the plater, are in the same room. We'll make two functions. Display Interactions and CheckInteractionKeys. I'm aware that there could be a problem where you had say a knight and king both K's :o at this point the computer could ask you to clarify. This is more of an example though, I assume most people will be going the graphical route and interaction will be a bit more straightforward.

So in the room class add the following two functions.


public void DisplayInteractions()
{
foreach(NPC n in npcList)
{
Console.WriteLine("Press " + n.interactKey +
" to interact with " + n.Name);
}
}

public void CheckInteractions(string keypress)
{
foreach(NPC n in npcList)
{
if(n.interactKey == keypress)
{
n.Interact();
return;
}
}
}



Now we have to link these functions into the game loop. After which, we should be able to interact with NPCs! Yay for us!

DisplayInteractions looks at all the NPCs in the room. It then gets the interaction key from each NPC and says if you want to interact with npc X you must press his interaction key Y.

The second function takes the key that the user has pressed and asks each NPC in the room - is this your key? If the npc say's yes that's my key then his or her interaction is run and they engage the player. After that's finished the user is dropped back to the room he/she was in.

That said let's add some calls to the functions.


public void Go()
{
Console.WriteLine("Welcome to the cool RPG Game");
Console.WriteLine("\tPress q to quit!\n\r");
while(quit == false)
{
Explore(currentRoom);
Console.Write(">");
string ans = Console.ReadLine();
ClearScreen();

currentRoom.CheckInteractions(ans);
if(ans == "q")
quit =
true;
}

Console.WriteLine("Goodbye");
}

public void Explore(Room r)
{
Console.WriteLine("You are in: " + r.Name);
Console.WriteLine(r.Description);
r.DescribeOccupants();
Console.WriteLine();
r.DisplayInteractions();
}



On exploring a room we tell all the npcs to tell the user what key needs to be pressed to interact with them. On the user pressing a key we call the check each NPC.

No problem, give it a whirl and interact with the knight.

Looking is good but there's something else I desire in my knights and that's talking! Let's make the knight talk.

Very important - Make sure to add the following two lines of code (if you're ever getting Metatable errors see if you have added these two lines).


public RPG()
{
lua.OpenTableLib();
lua.OpenBaseLib();



With that done we can move to the setup file. We want the knight script to have access to the knight object. This will be done through the setup file.

Setup.lua




---
--- Setup Script
---

load_assembly("RPGLUA");
NPC = import_type("RPGLUA.NPC");

start = CreateRoom("forest");
SetStartRoom(start);
npc = CreateNPC("knight");
knight.this = npc;
AddNPCToRoom(start, npc);


We import the NPC class so we can use in our Lua scripts. When we create a NPC called knight we know it creates a table called knight (which we're using as a namespace). So into knight we add a entry called this which refers to the C# npc object. From this we can access everything to do with knight (we don't get rights to protected or private stuff though :( )

Next we'll alter the C# NPC class in two places. This will set us up for being able to talk. First the OnTalk function.


public void OnTalk()
{
scriptPower.DoString("knight:OnTalk()");
}



Next the call to the OnTalk function. This call is made when t is pressed.


           public void Interact()
{
string answer = "";
while(answer != "x")
{
Console.WriteLine("You are interacting with " + Name);
Console.WriteLine("\tx to exit");
Console.WriteLine("\tt to talk");
Console.WriteLine("\tl to look");

Console.Write(">");
answer = Console.ReadLine();

if(answer == "l")
{
Console.WriteLine(
this.Description);
}
else if(answer == "t")
{
this.OnTalk();
}
}
}




These two functions mean when we press T we're going to called the ontalk function in the knight script. Before we go to the on knight script though we want to add one last function to the NPC class.



public void Say(string say)
{
Console.WriteLine(Name + " says \"" + say +"\"");
}



Our script will use the Say function to make our NPC talk. Here's the script in action.

knight.lua



----
--Knight NPC
---

--load_assembly("RPGLUA");
--NPC = import_type("RPGLUA.NPC");



--Create a knight Table--
if(knight == nil) then
knight = {};
end



knight.description = "A tall knight wrapped in bulky armour. He seems to be shaking.";
knight.name = "A Knight";

function knight.OnTalk()
knight.this:Say("Ah I'm so scared! I have to find the kings treasure but I'm paralyzed with fear! Please help me!");
end;


Okay finally we're getting to some kind of story! Scripting seems to be working fine and I'm sure you're getting the jist of it. We're going to add rooms and pasages ways then allow the quest to be finished. A passage way is quite simple here's the code.



class PassageWay
{
public string description;
public Room destination;
public string key = "p";

public PassageWay(string describe, Room leadsto)
{
destination = leadsto;
description = describe;
}
}




Righty-o that's a passage way done. Of course really we need a room for a passage way to go to! And we need rooms to "own" passageways. Okay first let's give rooms passageways much in the same way they were given NPCs. Here's all the code at once woo:



public void AddPassageWay(PassageWay pw)
{
wayList.Add(pw);
}

public void DescribeOccupants()
{

foreach(PassageWay pw in wayList)
{
Console.WriteLine(pw.description);
}

foreach(NPC n in npcList)
{
Console.WriteLine("There is a " + n.Name + " here.");
}


}

public void AddNPC(NPC npc)
{
npcList.Add(npc);
}

public void DisplayInteractions()
{
foreach(NPC n in npcList)
{
Console.WriteLine("Press " + n.interactKey +
" to interact with " + n.Name);
}

foreach(PassageWay pw in wayList)
{
Console.WriteLine("Press " + pw.key + " to use the "
+ pw.description);
}
}

public void CheckInteractions(ref Room currentRoom, string keypress)
{
foreach(NPC n in npcList)
{
if(n.interactKey == keypress)
{
n.Interact();
return;
}
}

foreach(PassageWay pw in wayList)
{
if(pw.key == keypress)
{
currentRoom = pw.destination;
}
}
}



Prertty straight forward. The important thing to note is that ref. Yes we need to change rooms on a keypress (to simulate the player moving from one room to another) therefore we need to have access to the currentRoom. Hence the new argument. All this code requires a new arrayList too (to store all the passageways a room might have).


private ArrayList wayList = new ArrayList();


Now we must ammend the code so we pass a current room reference in.

This ammendmant all goes down In the Go function.



while(quit == false)
{
Explore(currentRoom);
Console.Write(">");
string ans = Console.ReadLine();
ClearScreen();

currentRoom.CheckInteractions(
ref currentRoom, ans);
if(ans == "q")




Now interactions can change the room the player is in - all very cool. Let's give the scripts a few more functions to play with.



public PassageWay CreatePassageWay(string describe, Room r)
{
return new PassageWay(describe, r);
}



The above goes in the RPG class and we register it like so:


public RPG()
{
lua.OpenTableLib();
lua.OpenBaseLib();
lua.RegisterFunction("CreateRoom",
this, this.GetType().GetMethod("CreateRoom"));
lua.RegisterFunction("CreatePassageWay",
this, this.GetType().GetMethod("CreatePassageWay"));

...


There it is all registered. Ready to see the new setup file? It's a beast!

setup.lua



---
--- Setup Script
---

load_assembly("RPGLUA");
NPC = import_type("RPGLUA.NPC");
Room = import_type("RPGLUA.Room");
Way = import_type("RPGLUA.PassageWay");

--Create the forest
start = CreateRoom("forest");
SetStartRoom(start);

--Create the Knight
npc = CreateNPC("knight");
knight.this = npc;

AddNPCToRoom(start, npc);

--Create the dungeon
room2 = CreateRoom("dungeon");


--Create the passage from the forest to the dungeon
underground = CreatePassageWay("Steps surrounded by leaf litter", room2);
start:AddPassageWay(underground);


Okay and here's the dungeon file. I wrote it at the start but here's a good time to introduce it!

dungeon.lua




----
--Our Dungeon Room
---

--Create a forest Table--
dungeon = {};

function onEnter()
print("You enter the dungeon.");
end;

dungeon.description = "A small room with irregular stone walls. It's damp and dark. Also there are piles of glittering treasure.";
dungeon.name = "Dank Dungeon";

dungeon.OnEnter = onEnter;


If you actually play the game you'll find you can see a passage and use it and end up in a new room (trapped by the way!). I was suprised at this as it worked immediately without me having to fix bugs :D We are practically done! We need to improve the setup script so all rooms-scripts have a this method. While we're at we'll add a passage way back.

(I was going to have the treasure in a box you had to open it with a key from the knight but I want to keep this short. In fact right at the start I was going to have four rooms, a knight AND a witch - crazy!)

setup.lua




---
--- Setup Script
---

load_assembly("RPGLUA");
NPC = import_type("RPGLUA.NPC");
Room = import_type("RPGLUA.Room");
Way = import_type("RPGLUA.PassageWay");

--Create the forest
start = CreateRoom("forest");
forest.this = start;
SetStartRoom(start);

--Create the Knight
npc = CreateNPC("knight");
knight.this = npc;

AddNPCToRoom(start, npc);

--Create the dungeon
room2 = CreateRoom("dungeon");
dungeon.this = room2;

--Create the passage from the forest to the dungeon
underground = CreatePassageWay("Steps surrounded by leaf litter", room2);
start:AddPassageWay(underground);

--Create the passage from the dungeon to the forest
overground = CreatePassageWay("a stone staircase", start);
room2:AddPassageWay(overground);


To finish the game we need a have-you-seen-the-treasure-flag and we need to set up the OnEnter function for rooms. Because I'm amazing I know the potential problem of seeing the treasure before you get the quest but do you know what? I don't care, I guess this is how the people who made Lionheart felt.

The flag isn't a problem:


           public RPG()
{
lua["seenTreasure"] = 0;



Neithers the call to the OnEnter command.


           foreach(PassageWay pw in wayList)
{
if(pw.key == keypress)
{
currentRoom = pw.destination;
currentRoom.OnEnter();
}
}



We're jumping around a bit here but first next let's go to knight.lua.


function knight.OnTalk()

if(seenTreasure == 0) then
knight.this:Say("Ah I'm so scared! I have to find the kings treasure but I'm paralyzed "

.. "with fear! Please help me!");
else
knight.this:Say("What you found my treasure that's great. I'm so happy. I'll get it in a "

.. "bit! By the way game over");
end;
end;


The two dots are concatinate by the by. As you can see above we vary what the knight says depending if you've seen the treasure or not. All we have left to do is set the flag when entering the dungeon-room and we've written a cool scripted game. It's possibly quite extensible too I haven't checked. I know you can add an arbitary number of rooms.

In the Room class add


           public void OnEnter()
{
scriptPower.DoString(name +":OnEnter()");
}



Woooooooooo finally add the following to the dungeon.lua file:


function onEnter()
seenTreasure = 1;
end;


And that's it game over, a fully scripted RPGish style game we can all enjoy. If you're still raring for more I suggest you refactor the code so its more OO friendly. Then maybe try and a more complicated quest. You might want to have things like:
OnLeave
OnPush
OnGive
OnStealFrom
OnDeath
etc etc

Also as I went along a lot of the helper functions registered with Lua became redundant -- they can be cleared out.

Go wild. Try a graphical front end too! Patch it into your latest game! Let me know how it goes.


Tags:
,
,
,
,
,
,
,
,
,

Wednesday, July 27, 2005

Fixing problems

So I've removed the white lines and the speed jumped a bit. There's still a noticable speed drop on changing maps but I know why this, currently it's not a urgent fix. A major problem is that unless shut down properly the save game corrupts. I'm going to delay all these problems by giving the maps "dirty" flags to see if they need saving or not.

There are no random black lines in full screen mode so I assume it's a problem with windowed mode and probably to do with using the entire width and height of the window rather than the part that's doing the displaying. (I can now confirm that this is not the problem :D . . . next is ensure everything is using these values. Notably lines only appear on the vertical - suggesting something wrong the Height value. Which makes sense because i suspect it's valued a 600 but it's really 598! Maybe I'm on to something!

Note to self: Consider replacing halfViewDimension with two seperate values based on window size rather than tile dimensions!)

Why console isn't working is currently a bit of mystery and I've yet to investigate. Well I just did a quick test where I started the game with the console down - it dissapeared. So it's being rendered behind the scene or something else is happen. (not the camera class? Investigation is called for!!)

Also I suspect the Anchor code I put in is messed up. I need to strip it down this weekend a create a debug world. Gah I hope I get enough free time.

Todo:
-Disable anchor code
-Create a new one map world
-Have an obvious border
-Fiddle until black lines go away (yes I know very scientific)
-Add dirty flag
-Add anchor code
-Try and draw something at the anchor
-Catch improper save and have a corruption warning.

Interestingly I can still use the console even if I can't see it.
Also I think I can see more of the map in full screen mode and this has implications for how many tiles should be loaded into memory and shown at once.

In summary it works best is full screen at the moment. In windowed not too good.

Tuesday, July 26, 2005

Steps

Recently I finished the menu system to my safisfaction. That is you can, from the menu, continue a game of one of several users. There I've left the code hanging because I know it's time to tie the game back into my code base. If I don't do it now (and god knows I've been putting it off long enough) then the two pieces aren't going to fit together easily.

I'm about half way through. The minor things are namespaces changes (and there seem to be lots but hopefully everything will be better organized from here on out). The non-minor thing is going to be the revision of how time works. Getting to the compiling-again stage will be a big step. The codes a monster as well. There's just so much of it. I'm going to have to start developing a lot more seperately and then patching it in.

Of course I suffer from ADD like everyone else so I can't do just one thing at once. I've also been unsuccesfully trying to get a scroll bar to work. Currently it renders correctly but doesn't function, so that's something to look into.

And I've been playing with various name and conversation generators. I've currently got something complicated in Lua along these lines but I'm not satisfied with it and want it more complicated still. I couldn't find an IDE I'm even vaguely impressed with therefore I've been using notepad and a dofile instruction in the lua interpretter . . . there has to be a better way.

I've also been playing with python but I haven't done anything of note. I'm thinking of writing out nearly all my combat enginey thing as a text based program first, possibly in Python using curses. Then I can just slap the logic on top of my game.

I've had several ideas about how to expand my random quest engine thing but haven't touched it in a while.

The goal for the week is recompiling. I don't think this will be a problem but you never know. After that it will be smoothing the interface between menu and game.

Gah computer crashed.
Anyhoo it's compiling and the menu link is in :o I was amazed that it worked. Took 260 odd build errors to be killed off. Also for some reason it's really slow and there are gaps in the tiles. And there are also big white lines that I remember using for debugging the camera but it's cool and I'm happy. It almost feels like a real game. Now I just need to spend time messing with it so that's it's beautiful again :D

Interesting links at the moment include:

Using Cellular Automata To Build Random Dungeons on rec.games.roguelike.development

and

Automated storytelling and interactive plot in games on GameDev.net (which is rather wishy-washy but interesting all the same)

Wednesday, July 20, 2005

Osaka

I'm off to Osaka for a trip. I will be back monday but ... no work will get done this weekend (;_;)

Though it's summer and I won't be teaching so I'll get to work during the week. Yay!

Tuesday, July 19, 2005

Self Contained Resuable Plots


The most simple method of implementing random plots is discussed here. It allows plot reuse and the plot may change slightly each time it's seeded into the world. Self contained, here, means the plot data cannot "chain", one plot will not call another and so on, plots are self contained in a single data file. I'll dicuss random plot generation in general and then write a simple framework using Lua and C#.

(My terminology is a bit off so if anyone has any better ideas I'd like to hear them. I quite like the idea of growing a quest. It suggests something organic and non-rigid.)

If you want to skip my waffle and get straight to the implemenation starting reading from section 7.

1. What



We don't want hard coded plots. We want to abstract this. Instead of

"We the village of Moo are being attacked by the Necromancer Gah, he lives over there, kill him we'll give you this nice shiny sword."


We modularize this particular type of quest. So we have some thing that might look like:

"We [small_settlement] are being attacked by [enemy] he lives in [enemy_lair] kill him and we'll give you [reward]


Where the variables are constrained:


small_settlement = village.population < 100;
enemy = necromancer | troll | mad wizard | dark knight
enemy_lair = cave | encampment | tunnel | ruined tower
reward = shiny sword | ...


And you could fit many many more things to the above template. Lots of different constraints mean lots of different flavours of the base quest. Of course the above syntax is somewhat idealized but hopefully you can see how doable such a thing is.

2. Why



If you want randomly generated content. Random generated content increases replayability (ha a new word?). It can also make you're world seem more alive especially if you're clever about tying your quests into the current state of the world. For instance if you have data in your game about relations, say mother, father, son, daughter. Then on killing an innocent NPC you might randomly attempt to seed a revenge quest.

3. How



Possibly the most important part (ideas are all well and good but they're nothing until they're implemented). Let's try think of our quest / plots as scripts or objects (seperate from our main game code).

The Basic Method

First we assume we have big list of abstract plots. So we have some object called PlotList


PlotList = {
FindMyMissingItem,
KillTheMonsterInMyHouse,
DefendTheVillageFromTheMonster
}


Before a plot can be used it's need to be checked against the state of the world. You can't use the DragonAttacksVillage plot if the world has no villages. This we'll call verifying the plot. If we can't verify a plot we can still potentially use by seeding everything - creating the village for the dragon to attack for instance. I don't cover this in my framework though.

Farming?

Imagine the world to be your lush lush field. Adding a quest to this field involves both harvesting and seeding. Once the plot is verified and we decide to use it we farm out the plot. That is we harvest some resources from the world (a suitable village) and seed some plot elements (an angry dragon). This gives us some idea of the processes required in a plot file lets create something a little more specific.

The Structure Of A Plot File


  • Resource List

  • Plot States



The first thing a plot file will have is a list of resources (stuff the quest will be making use of Dragons, Villages . . . that sort of thing). Then we have some state information about how the quest is progressing.


  • Has the dragon attacked the village?

  • Has the player killed the dragon?

  • Has the played seen the dragon?

  • etc



That's the basics of what makes a quest. There will also be some functions that need to be defined. These functions are particular to the plot and can usually take the form of events (thinking event based programming.).


  • OnDragonDeath

  • OnVillageDestroyed

  • OnSeeingDragon

  • etc



Enough vaguarity let's imagine an example quest and then let's codify it as a plot file.

A man has rats in his cellar. He wants rid of them. As a reward he'll give you gold.


Okay that's pretty specific. For a more general quest file we'll change rat to small monster and gold to small reward. We could define both these catergories in some kind of look up table in our game code (or inanother script file if you really want to). Bit confused? Well if you've ever flicked through any D&D books, (and that's the farest I've ever got) you'll see tables were you roll a dice and the look up the dice number in the table. So let's image the small monster table takes a D20 (20-sided die).


Small Monster Table

1 - 3: Rats
3 - 6: Bats
7 - 9: Nightling
9 - 12: Ferrel Hedgehogs
13 - 15: Mad Dog
15 - 17: Giant Frogs
18 - 19: Wyrm Lavae
20: Possum


Hmm probably best not to directly implement my table :D but I hope you get the idea. You roll the die, check the score and you have your small monster ready to seed into the world. There'd a similar small treasure table. You could also link the tables to the current player level so the quests are never too easy.

Anyway let's look at the script - I'm using psuedo code here.


#MonsterInMyHouse.Plot

#Resources
Monster[] smallMonsters;
Npc victim;
Region storage_room;
Item smallTreasure;


The above resources must be harvested from the game or created and then seeded. How you choose which to create and seed and which to harvest is very strongly related to the architecture of your game.

(God could you imagine coding this in C? Nightmare.)

To my eye I'd generate and seed the monsters and the small treasure. I'd harvest the man and the storage room from the game itself.

You could, of course, check the man's possesions and get him to reward you with something he owns. That way you wouldn't have to create the treasure. If he had no money he might give some nice velvet trousers or something entertaining, like his house! (I guess one should be careful writing such functions.)

On being a farmer

A few words on matching the requirements of the plot to the resources of the world.

As I said this depends on your particular architecture. For my purposes I assume there's a super overloaded function called query world. Let's also assume all plot files come with a function called Farm(maybe plant or grow would be better words?) . What would the farm function look like for the "monsters in my house quest"? (Not how I skip the verify function! If you're greating an actual game you should probably stick one in.) Well it might look something like below.


void Farm(World theWorld)
{
#we've already verifed and know everything we need exists

ArrayList eligableList = new ArrayList();

foreach(NPC p in theWord)
{
if(p.house != null && p.house.hasType("storage room")
eligableList.Add(p);
}

victim = eligableList.chooseRandom();

#we should pick at random but we're geting the first we see.
foreach(Region r in victim.house.rooms)
{
if(r.type = "storage")
{
region = r;
break;
}
}

smallTreasure = GenerateSmallTreasure();
victim.Inventory += smallTreasure;

monsterNo = random(10);
monster = RandomSmallMonster();
monsters = #add 10 copies of the monster

}


And that's pretty much the seed function. In an actual implementation you might have to set a few conversation datastructures too.

Speaking of conversation -- that's how we'll be sparking off this plot. Let's assume we can use conversation like this.


Conversation IHaveMonsters;
Conversation IStillHaveMonsters;
Conversation ThankYou;


These could possibly be wrapped up into a single conversation type depending on your game. I'm going with the following syntax.


victim.conversation += IHaveMonsters


This IHaveMonsters conversation has embbed scripts and probably looks a little like:


NPC: I have %script.monsters[0].name.plural% in my %region.name%;
PC: That sounds bad.
NPC: It is. You look like the sort who could help me! I'll give you a little something if you help me out.
1.PC: YES {script.advance("begin")};
2.PC: No Dice {script.advance("bad ending")};
NPC: Great, please come back when you're done.


So depending on conversation choices script flag are set. This of course requires a pretty flexible conversation engine! We're going to go for a much more basic approach.

There needs to be another flag set up for the rats dying. In the script file you need:


monster.OnDie += script.OneMonsterDown();


Then when all the monsters have been dispatched you can move the quest along.

4. Issues



It's not easy to program in an easily extendable way. We need scripts that can use classes from our compiled code. Even when we write these plot files they're still hard coded. The plots all have a certain flavour and culture to them that's hard to remove. Could we write reusable plots that could be shared between games? These are areas for investigation. Can the rats in my basement plot be written in such a way that it could be turned into a "space bats in my space-ship problem?". On the face level it seems so but what about when we really start to flesh out the random script we'll need to draw on elements from the game world -- there's no doubt!

The conversation is a very important part of the quest. I'm not writing a conversation engine here - in the framework we'll use very very simple strings of text. A powerful conversation engine will allow your scripts to be far more detailed. The conversation itself should be partially generated or flavoured by the game engine. This improves the quests replatability and prevents them from quickly going stale.

If you're a little confused about what I'm taking about here. Imagine you add a list of hates to NPCs (or moods in general) you could check the small_creature against the NPCs hate list and if there was a match maybe she would mention her hate.

We don't always want npc's saying I have X in my Y please kill it.. Rather we want to flavour it. I hate X. I have a load of X in my favourite Y. I do all my reading in Y.. That sounds better it's still robotic but it's much better. It's add extra information draw from the NPC the quest is using.


There is one extremely important concern that the framework will entirely ignore - the ability to query things efficently! It is very important.

To improve querying efficency I'd suggest drawing quest generation out over a number of frames. If you know what you're doing possibly sticking it in it's own thread! The more richly detailed your game engine the wider scope you have for making your quests more interesting. For instance imagine for one quest you want a NPC below half health wearing a blue hat - that's going to take some searching!

Other issues include keeping any data stored in the array in-sync with data in the game world. This won't usually be a subtle problem and therefore shouldn't be too hard to work with (in other words if your datas out of sync things are going to work so poorly that you're going to know about it!) Generally you'll be working with references too, so no problem!

5. Expansion



Procedural generated plots are the holy grail - lofty and somewhat unobtainable. Though, there a few other ideas that come to my mind some of which are similar to those floating around the noosphere.

Once we have the basic frameowork in place perhaps we can extend it in one of the following directions. ( I have plenty of ideas about this but this isn't the place to get into the technical details.)

Plot chains - where plots are strung together according to features and constraints (imagine the teeth and holes of a jigsaw for an idea of how to get started but unlike a jigsaw the teeth and holes can be cumulative or not linearly order. So the next piece of the jigsaw might be required to fit a hole on the other side of the board. Like building a jigsaw in non-Euclidian space! Best to start simple though!) The features and constraint s enusre that from piece to piece the plot makes sense. Taken as a whole the plot might make less sense.

Story Arcs - sound like plot chains don't they? Well they're similar. The Story Arc object is a like a giant plot piece itself. In the most inaccurate terms possible it can be through of as an AI telling a story by picking and choosing the correct plot pieces to go along with it's prewritten plots pieces. Story arcs usually have a goal state, that once completed it doesn't really matter how it happened.

Procedural Plots - I cannot imagine how without a remarkable AI with a knowledge of general human culture and the current culture of your game. I'd suggest in order to approach procedural plots we using chaining but at a much finer granularity and a net like nature. For instance the basement plot the first thing that happens is a man giving you a task. This could be the first in a chain - next would be the task itself. Let's develop that first block and see how much variety there is in and what it's teeth and holes might look like.

A man gives you a task


There's more here than it seems. We could create lots of these with different jigsaw constraints.

A poor man gives you a simple task.
A leper gives you a task.
A knight gives you a quest.
A townsman has a problem.
A rich woman gives you a demand.

The last one demand might require two blocks one for not accepting - the stick so to speak and one for what happens when you do accept (finally leading to a carrot)

DEMAND - do TASK
or HARM

HARM - tell guards you killed my husband
tell guards you raped me.

The demand is a tooth, the next block must be a demand or it won't fit.

As you can see this is going to be a lot or work and there are plenty of issues I don't touch on here. Rich and woman are holes that allow other blocks to fit. More teeth and holes will be added by the next block. For instance that woman was once married etc etc. To make this work you need a very rich and detailed world.

Emergent Plot - a more general solution. A rabbit eats all a farmers crop, he identifies the rabbit as the problem the abscene of the rabbit as the solution. He's willing to achieve this in a number of ways including hiring some one to solve his problem. For this to work really the NPCs have to act more like agents in the AI sense of the word. There is some (academic) work in this area like the paper A Behavior Language: Joint Action and Behavioral Idioms by Michael Mateas and Andrew Stern. Also at the time of writing Oblivion seems to promise something in this direction under the name Radient AI.

6. Scriptable



We really want plot scripts. The rogue-like Gearhead has managed this. After reading this tutorial you maybe able to understand the text files that contain Gearheads plots. They're written in a custom made scripting language so they're still pretty cryptic! (all the games source code is available though so feel free to get your hands dirty)

Scripts are a good idea as you can share them between projects and keep adding more to the project without updating the binary and needing to compile. If the game searches the script directory well then users can also add scripts and you can easily do content updates.

7. Hermatically Programmable



So that's all theory and it seems pretty solid but the only way to test it is going to be some type of mini-program or framework. As I said before it's very hard to seperate plot from game engine so this might be far from what you envision as ideal.

We need at least some type of dummy game engine or dummy classes to demonstrate a running plot. We can use interfaces in an attempt to make it somewhat more modular. By creating something simple we can test out ideas and see if they're feasible and hack out rough scripts.

Here I'm going to use C# and Lua. C# is wonderful and can be used to create games with ease and efficency. Lua is extremely simple to embed and is widely used.

As you should know C# doesn't support multiple inhertitance but it does support multiple interfaces! So potentially ... (You'd be very lucky) ... you can take the code we write, implement the correct interfaces and it will magically work in your game. Yay!

7.1 A Framework



We're going to build the framework around the "There are rats in my basement quest", so we need objects like basements and rats (see how hard it is to create a completely seperate-from-a-game-engine program!).

The framework can be thought to represent the game world. Then we'll add a lua interface and finally a lua script. We can't do anything without the framework though.

To start we'll create a simple LUA enabled program. Please read my tutorials on this if you don't know where to start. Here's some beginning code that compiles:


using System;
using LuaInterface;

namespace PlotFrameWork
{
///
/// Summary description for Class1.
///
class Class1
{
static Lua lua = new Lua();
///
/// The main entry point for the application.
///
[STAThread]
static void Main(string[] args)
{
Console.WriteLine("Plot Framework");
//magic goes here
Console.ReadLine();
}
}
}



This is where we're going to start. I'm going to try and keep these brief which means it won't be as clean or as expandable as it could be and it's going to be reasonably tailored to our single rat-in-the-basement-quest.

So what's the very minimum we need?

      One NPC

      One Cellar

      One small creature

      One reward



Still a lot of work!

Let's create everything Lua's going to use in it's own namespace. This is important for how we register classes with Lua.


namespace LuaPlots
{
}


Just waiting to be filled with the objects and actors and plots that we'll weave into an epic story. For now we're going to have one room of one type - the cellar.

From this we'll create one NPC who owns said cellar. When it comes round to harvesting he'll immediately be found to be compatible, and being the only NPC he'll be selected. We're going to start with a class that might come in handy (bottom up programmnig style). The name class. It contains a name and it's plural, useful for conversations to prevent things like "There are mouses in my cellar."

For some reason with all the getters and setters it's turned out massive. We can expand this so we know whether to use "a" or "an" or "the" or "they" and all that. We don't want sentences like "Here's your reward its a gems!".


public Name( string singular, string plural)
{
this.singular = singular;
this.plural = plural;
}

private readonly string singular;
private readonly string plural;

public string Singular
{
get { return singular; }
}

public string Plural
{
get { return plural; }
}
public override string ToString()
{
return singular;
}


(As an aside I was recently reading the Angbang source and there's a function in there that can change most singluars to plurals with out a problem. It may be worth using something similar to that code)

Now we can name things quite well - why not build some things to actually name? Let's start with a room and - We'll describe what we need from a room object with an interface.


public interface IRoom
{
string Type
{
get;
set;
}

Name Name
{
get;
set;
}

void AddMonsters(IMonster[] m);
}


The interface specifies how we intend to use a room object. The next interface we want is that of an NPC. This is the person who's going to own the room. Again an NPC should have a name - and this time a method for discovering what rooms they own.


public interface INpc
{
Name Name
{
get;
set;
}

IRoom FindOwnedRoom(string type);
}


There are a few more interfaces to go. One is the monster interface the other is the world interface. Let's do the monster one first.


public interface IMonster
{
Name Name
{
get;
set;
}

void Kill();

}


Very simple it has a name a function where you kill it. What more could you want from a monster. Last interface is the gameworld this will store the C-sharp classes and have few function that are used directly with Lua.


public interface IGameWorld
{
INpc[] AllNpcs();

IMonster[] GetSmallMonsters();
string GetSmallReward();

}


That's the game world nice and simple. Notice how we're just using text for rewards. There's one last layer of detail required - delegates / callbacks. They're very important to create smooth running quests.

The types of delegates you'll want to use though are definetly game engine related. Some are pretty general like things dying. But getting super-secret-power-Z by eating a PowerStone is a feature that's only ever going to happen in your own engine.

There are two events that concern us for our quest. Conversation events and Death events.

Delegates are great and are supposedly supported by Lua but I could not get them to work in the documented way. Supposedly you define a delegate.


public delegate void Death(string howDeathHappened);


Fair enough. Then if you make events in your interface like so:


public event Death OnDeath


Well then in Lua you should be able to add to the event by using the code.


someObj.OnDeath:Add(luaFunctionThatMatchesDelegate);

function luaFunctionThatMatchesDelegate(howDied)
--stuff
end


Well this told me Add didn't exist. When I messed things around so that it supposedly did exist (by explicitly defining Add and Remove in C#) it crashed with a StackOverFlow error.) To say the least this was both frustrating and upsetting. After a couple of failed hackish work arounds I came up with a reasonably painless hack to do the job.

If you don't experience the above problems then you can do it the *correct* way but I somehow doubt it's just me alone on my computer.

Well let's begin the Voodoo and get death in their. When a rat dies we're going to give it a death event. We'll start by defining death events in their most general form - a delegate.


namespace LuaPlots
{
public delegate void Death(string how);


Right at the top. So a death event comes with a string describing how the death occured - this could easily be extended to class with lots of details. Imagine writing plots about posion or multi-murders and you may start to feel you want detailed death info.

We want all monsters to be able to die. So we throw in a death event. In the monster interface.


event Death OnDeath;

Death AddDeathEvent
{
set;
}

Death RemoveDeathEvent
{
set;
}


So there's the event and oh look two oddly named functions - could this be the nasty hack? Why yes, yes it is! By assigning delegates to those to set functions we'll add and remove Death Events. Doesn't quite seem to make sense at this stage and you can ignore it for now. I'll explain the details when we implement a monster and write Lua code.

The next one is a talking event. If you have a full conversation engine you can add this in yourself. Here it will be called everytime the NPC speaks.

Once again we start with delegate.


namespace LuaPlots
{
public delegate void Death(string how); //used for when something dies
public delegate void Speak(string speech); //used for when something speaks


Look at those comments! Almost looks like someone knows how to program. Quite similar to death but this time we're going to add it to the INpc interface. So NPCs can talk and Monsters can die - that's how you make games. Here's the code to give NPCs speech.


event Speak OnSpeak;

Speak AddSpeakEvent
{
set;
}

Speak RemoveSpeakEvent
{
set;
}


There's the code so we can add Lua functions as reponses to C# events such as death. Say if a rat dies then the Lua function OnARatDying can be called. Yes, yes a little bit cryptic but you'll be glad to hear thats the end of the top-level framework. The next bit is to create objects for all these lovely interfaces. First the world!

public class TestWorld : IGameWorld
{


INpc[] npcList;
IRoom[] roomList;

public TestWorld()
{
roomList =
new IRoom[]
{
new NormalRoom(new Name("cellar", "cellars"), "storage")
};

npcList =
new INpc[]
{
new NPC(new Name("Pete","Petes"), roomList[0])

};
}
public INpc[] AllNpcs()
{
return npcList;
}


public IMonster[] GetSmallMonsters()
{
return new Rat[] {new Rat(), new Rat() };
}

public string GetSmallReward()
{
return "10 pieces of Gold";
}
}


Thats the world. It's assumed to come with a few standard generation and search functions. So we can search for monsters, objects, npcs and places according to various specifications. For instance "Find a man who owns a pet rat". In the "TestWorld" I've merely added the basics required for one single rat-hunting quest. It's easy to expand if we want to test out different quests (the code will probably need changing a little in some places :D)

In our idealized world we have an NPC list of everyone who lives in the world. And perhaps slightly oddly a list of all the different rooms that exist in the world.

In the constructor we create one NPC who owns one room - a cellar. This is not a very populated world. Then we have functions to get all the npcs to get all the rooms and generator functions to generate a group of small monsters and also a small treasure.

Notice that we're only generating one type of monster and one reward. This is called laziness - also it's only a framework! Feel free to expand upon this.

Next up we'll create a room. We're going to call this room object "Normal Room".


public class NormalRoom : IRoom
{
private string type;
private Name name;
private IMonster[] monsters;

public IMonster[] Monsters
{
get { return monsters; }
set { monsters = value; }
}

public Name Name
{
get { return name; }
set { name = value; }
}

public string Type
{
get { return type; }
set { type = value; }
}

public NormalRoom(Name roomName, string roomType)
{
name = roomName;
type = roomType;
}

public void AddMonsters(IMonster[] m)
{
monsters = m;
}

}


Notable things here include the AddMonsters function. This is quite specific to our needs. In a real game there might be an Add Entity function. Or possibly you'd get the room co-ordinates and then throw the rats in. You might also require some elaborate means to keep the rats in the room. These are all problems we don't care about for this demonstration.

So basically rooms can store monsters and you can query a rooms type. Next up let's add the monster. Now here things are a little different because of the event.

public class Rat : IMonster
{
static Name name = new Name("rat", "rodents");

//private event Death onDeath;
public event Death OnDeath;




public Rat()
{

}


#region IMonster Members

public Name Name
{
get
{
return name;
}
set
{
name =
value;
}
}

public void Kill()
{
// TODO: Add Rat.Kill implementation
this.OnDeath("How? Well it was killed by a player.");
}

//This is mangled C# code so it can interface with Lua
public Death AddDeathEvent
{
set{ OnDeath += value;}
}

//Same as AddDeathEvent
public Death RemoveDeathEvent
{
set{ OnDeath -= value; }
}



#endregion

}


You can see the round about way we're modifying delegates here. I'm still not all together sure if the remove one even works! But onwards and upwards. Important to thing to note here is that when the rat has it's Kill() function called it in turn activates it's Death event. We can hook this event up to our scripts no problem (well there where are few problems getting Lua to play well with the delegates but it's okay now)!

So we have a room, a world, a suitably small enemy - now we need the NPC. Here's the code.

public class NPC : INpc
{
Name name;
IRoom ownedRoom;
String conversation = "I have nothing to say";


public NPC(Name NPCname, IRoom room)
{
name = NPCname;
ownedRoom = room;
}

public event Speak OnSpeak;

public void Spoke()
{
if(OnSpeak != null)
OnSpeak("Spoke");
}

public Speak AddSpeakEvent
{
set { OnSpeak += value; }
}

public Speak RemoveSpeakEvent
{
set { OnSpeak -= value; }
}


#region INpc Members

public Name Name
{
get
{
return name;
}
set
{
name =
value;
}
}

public string Conversation
{
get { return conversation; }
set { conversation = value; }
}

//If it can't find a room null is returned
public IRoom FindOwnedRoom(string type)
{
if(ownedRoom.Type == type)
return ownedRoom;
else
return null;
}


#endregion

}

NPCs can own things and them talking is an event. Firing this event with the current conversation system is very hard. (we don't want to fire when the conversation string is accessed we want to fire after it. Because if we had code:



fireTalkEvent
return talkObject;


Well the event might change the talk object and all sort of problems would occur.)

Therefore I've added a extra function Spoke we'll manually call this after the NPC says anything. Bit hackish but this isn't the most ideal conversation system really.

That's all the objects we need!

7.2 A user interface for the Framework



I promise soon we'll get to Lua and the script but first we need some way to interact with the world or we won't be able to test our script. We're using a console program so all interaction will be done through ReadLine yay like a very simple text game.

The plan is simple we, the player, will see the one NPC and we'll have T for Talk and C for go to cellar. Then from the cellar we can return to the NPC. The cellar will show it's contents to us. If there's something there we can kill then we can press the K key to kill. Truely the finest in interactive technology. So here we go let's create a simple game.

This is the least essential bit so I'm going to be throwing the code down in two big chunks. One for each "room" though one "room" can be thought to be the void as we're not going to have a room object there.


class Class1
{
static Lua lua = new Lua();
static TestWorld testWorld = new TestWorld();


Here a Lua interface is created, as is a world object. In the constructor we want to add the NPC to the world and the cellar.


//The Start Point of The User Interface
public void UIStart()
{
string answer = "";
while(answer.ToLower() != "t" && answer.ToLower() != "c"
&& answer.ToLower() != "q")
{
Console.WriteLine("\t\t---PLOTS GENERATION TEST------\n");
Console.WriteLine("Test World Has Only One Occupant");
Console.WriteLine("Here stands " + testWorld.AllNpcs()[0].Name.Singular);
Console.WriteLine("\tPress \"T\" to talk to " + testWorld.AllNpcs()[0].Name.Singular);
Console.WriteLine("\r\tPress \"C\" to go to the cellar the only room in existance");
Console.WriteLine("\r\tPress \"Q\" to quit");
answer = Console.ReadLine();


}

if(answer.ToLower() == "t")
{
Console.WriteLine(testWorld.AllNpcs()[0].Name + " says \"" +
testWorld.AllNpcs()[0].Conversation + "\"");
((NPC)testWorld.AllNpcs()[0]).Spoke();
UIStart();
}

if(answer.ToLower() == "c")
{
UIRoom(testWorld.AllNpcs()[0].FindOwnedRoom("storage"));
}
}



It's very simple if you press a wrong key then the whole thing will just be reshown. Notable we cast the INpc interface to NPC so we can call the Spoke method. The second rooms a little more complicated. We want to alter the description based on the contents are


  • no monsters

  • one monster

  • multiple monsters



Here it is in all it's glory.


public void UIRoom(IRoom r)
{
NormalRoom n = r
as NormalRoom;
if(r != null)
{

string answer = "";
while(answer.ToLower() != "x" && answer.ToLower() != "k")
{

if(n.Monsters == null)
{
Console.WriteLine("An empty " + n.Name);
}
else
{
if(n.Monsters.Length == 0)
Console.WriteLine("An empty " + n.Name);
else
{
if(n.Monsters.Length == 1)
{
Console.WriteLine("There is one " + n.Monsters[0].Name.Singular + " here!");
}
else
{
Console.WriteLine("There are " + n.Monsters[0].Name.Plural + " here!");
}

Console.WriteLine("Press K to kill a " + n.Monsters[0].Name.Singular);
}

}
Console.WriteLine("Press x to exit");
answer = Console.ReadLine();
}

if(answer == "x")
UIStart();

if(answer == "k")
{
if(n.Monsters.Length == 0)
{
Console.WriteLine("There's nothing to kill.");
UIRoom(r);
return;

}

n.Monsters[n.Monsters.Length-1].Kill();

//Reduce the array by one
IMonster[] newArray = new IMonster[n.Monsters.Length-1];
for(int i = 0; i < newArray.Length; i++)
{
newArray[i] = n.Monsters[1];
}
n.Monsters = newArray;
UIRoom(r);
}

}
}



The recursion bugs me a bit - if you were willing you could cause a stack overflow by always pressing k. It would take a long long time though. Anyway that's the code. Press K and the first monster in the room is killed. The rooms monster array is then taken and reduced by one.

That's the user interface done. Next up is hooking up Lua and the game script we'll be having. We can acutally play "the game" without the script. All we'll see is a single NPC and be able to move to the cellar and back to the NPC.

7.3 Registering Lua



Let's check out the main function. We want to register a few helper classes with Lua. We also want to run the quest we've created.


static void Main(string[] args)
{
Class1 c =
new Class1();

c.RegisterGameFrameWork();
Console.WriteLine("Plot Framework");
Console.WriteLine("Calling Lua File");

try
{
lua.DoFile("infest.txt");
}
catch(Exception error)
{
Console.WriteLine("Lua Problem: " + error.Message.ToString());
}

c.UIStart();
Console.WriteLine("Finished Calling Lua File");
}



Yeah I've called the script "infest.txt". You can comment that line out and the c.RegisterGameFrameWork(); and it should work. So what's in the RegisterGameFrameWork? Let's fine out!


public void RegisterGameFrameWork()
{
lua.OpenBaseLib();
lua.OpenMathLib();
lua.OpenTableLib();


lua.RegisterFunction("GetNPCs",testWorld,testWorld.GetType().GetMethod("AllNpcs"));
lua.RegisterFunction("GetSmallReward", testWorld, testWorld.GetType().GetMethod("GetSmallReward"));
lua.RegisterFunction("GetSmallMonsters", testWorld, testWorld.GetType().GetMethod("GetSmallMonsters"));

lua.RegisterFunction("Output",
this, this.GetType().GetMethod("Output"));
lua.RegisterFunction("ID",
this, this.GetType().GetMethod("ID"));
}



Pretty straight forward. You might be curious about the Output and ID functions. These are debug functions that I use for Lua. Here's the code for them.



public void ID(Object o)
{
if(o == null)
Console.WriteLine("Null value");
else
{
Console.WriteLine(o.ToString());
}
}

public void Output(string output)
{
Console.WriteLine(output);
}



And that's it Lua is linked up. We've registered the important functions. Everything should work great! The last thing is the script itself.

7.4 Our quest script



All scripts need to be aware of certain C# classes. To become aware of C# classes a few lines of code need writing. We should create a small startup to be run once at the start of the game that would add all required classes. But, because we only have a single script I'm just including that registration info directly.


---
-- Basic Setup Code
-- This would probably be done once somewhere else and only ever done once.
---

load_assembly("LuaPlots");
IRoom = import_type("LuaPlots.IRoom"); -- import the room interface
INpc = import_type("LuaPlots.INpc"); -- import npc interface


I've split the script up into Resources there are some resources we seed :- reward for example and the conversations and there are some we harvest the npc and the room. The harvesting, seeding process I called Farming and we use a farm function.

So we have some resources. I put a lot of these together in table that sits in the script and can be accessed from anywhere - they're global.

The resource table also includes information on the current state of the quest. This includes on number called questState, telling us what state we're at and another counter telling us how many monsters remain. Here it is:


---
-- The Farm Function (it harvests and seeds)
---

function farm()
-- Get The NPC
Resources.npc = GetNPCs();
Resources.npc = Resources.npc[0];

-- Get The Room Where The Monsters Are
Resources.room = Resources.npc:FindOwnedRoom("storage");

-- Get some monsters
Resources.monsters = GetSmallMonsters();

-- Get a small reward
Resources.reward = "10gp";

---
--Link Everything Up
---

--Record The Number of Monsters
Resources.monsterCurrent = Resources.monsters.Length;

--Put the monsters in the cellar (could do this AFTER accepting quest)
Resources.room:AddMonsters(Resources.monsters);


--Put reward on NPC (I'm not going to do this but just mentioning that we could)

-- Fill out the non-harvest stuff
SetStartConversation();

---
-- Hook Up Quest Events
---
for i = 0, Resources.monsters.Length-1 do
Resources.monsters[i].AddDeathEvent = monsterKilled;
end

Resources.npc.AddSpeakEvent = onNPCSpeak;


end


Cool yeah! The new code at the bottom is attaching a lua function to each and every monster's Death event. Also the npc gets an event attached to it's Speak event.

A function is also called to assign conversation to the NPC. So we need to look at the event handling functions and the conversation writing functions. Here one of the conversation functions.


function SetStartConversation()

IHaveMonsters = "You seem to be a brave adventurer. It seems my "
.. Resources.room.Name.singular .. " "
.. "has been infested by ";

if Resources.monsters.Length == 1 then
IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Singular;
else
IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Plural;
end

IHaveMonsters = IHaveMonsters .. "\n\rPlease kill them, there's a good a chap.";

Resources.npc.Conversation = IHaveMonsters;
end


Okay here is the first thing the NPC will say to you if you talk to him before killing all his rats.

If he only has one creature he'll refer to it in the singular otherwise he'll use the plura. In our case he refers to it in plural terms. The script generates the conversation and then assigns the conversation to the NPC. That's it. All the other conversations work the same way.

Now let's look at how we handle an event. I'm going to fill in what happens when you talk to the NPC, because it's simpler.


function onNPCSpeak(speech)


if(Resources.questState == 3 or Resources.questState == 2 ) then
Resources.questState = 4;
Resources.npc.Conversation = "I've nothing to say";
Resources.npc.RemoveSpeakEvent = onNPCSpeak;
end


if(Resources.questState == 0) then
Resources.questState = 1;
SetMidConversation();
end


end


If you've never talked to him before (and haven't killed all his rats) then the quest auto-intiates. Then if you go kill all his rats and talk to to him then again - the quest is set to over. On the quest ending we also set the conversation string to something else so next time we talk to him he'll say this. That way we don't have a random NPC constantly thanking us for saving his basement from rats. Finally we unregister the event. There should be more clean up stuff here to. If we used a lua table to contain the entire quest (like a namespace in C#) we could now set it to nil.

When all the rats die. Then we set the conversation to some kind of thank you speech depending how you proceeded through the quest. Did you kill the rats before you were asked too and so on.

Okay here's the entire script. It should be pretty self explantory (i guess :D)


--[[
Plot Name: Remove an Infestation
Author: Daniel Schuller
Version: 1

Rundown
=======

Some small creature have infested some one's store room.
They'd like you to clean it.

We don't consider duration. His 212th son is going to have the same problem if you don't help.
(all quests should have some resolution that doesn't involve the player)

Seed Stage: Monsters are planted as is conversation. Conversation is not as easy to edit as it could be.

--]]

---
-- Basic Setup Code
-- This would probably be done once somewhere else and only ever done once.
---

load_assembly("LuaPlots");
IRoom = import_type("LuaPlots.IRoom"); -- import the room interface
INpc = import_type("LuaPlots.INpc"); -- import npc interface

---
-- Resources
---

Resources =
{
npc,
room,
monsters,
reward,

---
--PlotCounters
---

monsterCurrent, --no of monster left alive

questState = 0
--[[
0: Not Talked To NPC
1: OnQuest
2: RatsDead + OnQuest
3: Rats Dead Not On quest
4: Rewarded Quest Over
--]]
}

---
-- *Functions*
---

---
-- Conversation Setters
---

function SetStartConversation()

IHaveMonsters = "You seem to be a brave adventurer. It seems my "
.. Resources.room.Name.singular .. " "
.. "has been infested by ";

if Resources.monsters.Length == 1 then
IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Singular;
else
IHaveMonsters = IHaveMonsters .. Resources.monsters[0].Name.Plural;
end

IHaveMonsters = IHaveMonsters .. "\n\rPlease kill them, there's a good a chap.";

Resources.npc.Conversation = IHaveMonsters;
end

function SetMidConversation()

IStillHaveMonsters = "Why are talking to me? I thought you were cleaning out my "
.. Resources.room.Name.Singular .. ". Well get to it!"

Resources.npc.Conversation = IStillHaveMonsters;

end

function SetEndConversationOne()

Thanks = "Thanks for solving my problem. Here's a little something for you trouble. It's "
.. Resources.reward;
Resources.npc.Conversation = Thanks;
end

function SetEndConversationTwo()

ThanksIGuess = "Oh hello. You're the chap who was making all the noise in the cellar, what?"
.. "\n\r Well I did find those rats a damn nusiance, so I'd like to compensate you!" ..
"\n\rEven if you did come into my private property and enter my rooms unannounced!" ..
"Here's a " .. Resources.reward .. ". Enjoy!";

Resources.npc.Conversation = ThanksIGuess;

end

---
--Event Handlers
---

function onNPCSpeak(speech)


if(Resources.questState == 3 or Resources.questState == 2 ) then
Resources.questState = 4;
Resources.npc.Conversation = "I've nothing to say";
Resources.npc.RemoveSpeakEvent = onNPCSpeak;
end


if(Resources.questState == 0) then
Resources.questState = 1;
SetMidConversation();
end


end



function monsterKilled(how)

monsterPlural = Resources.monsters[0].Name.Plural;

if(Resources.questState == 1) then
Output("There's nothing more fun than killing " .. monsterPlural ..
" for " .. Resources.npc.Name.Singular);
end

Resources.monsterCurrent = Resources.monsterCurrent - 1;

if(Resources.monsterCurrent == 0) then

--Advance the quest
if(Resources.questState == 0) then
Resources.questState = 3;
SetEndConversationTwo();
elseif(Resources.questState == 1) then
Output("Old big nose should be happy, that's all his " .. monsterPlural ..
" killed!");
Resources.questState = 2;
SetEndConversationOne();
else
Output("There's needs to be a serious bug to get here. Quest state is "
.. Resources.questState);
end
end
end

---
-- The Farm Function (it harvests and seeds)
---

function farm()
-- Get The NPC
Resources.npc = GetNPCs();
Resources.npc = Resources.npc[0];

-- Get The Room Where The Monsters Are
Resources.room = Resources.npc:FindOwnedRoom("storage");

-- Get some monsters
Resources.monsters = GetSmallMonsters();

-- Get a small reward
Resources.reward = "10gp";

---
--Link Everything Up
---

--Record The Number of Monsters
Resources.monsterCurrent = Resources.monsters.Length;

--Put the monsters in the cellar (could do this AFTER accepting quest)
Resources.room:AddMonsters(Resources.monsters);


--Put reward on NPC (I'm not going to do this but just mentioning that we could)

-- Fill out the non-harvest stuff
SetStartConversation();

---
-- Hook Up Quest Events
---
for i = 0, Resources.monsters.Length-1 do
Resources.monsters[i].AddDeathEvent = monsterKilled;
end

Resources.npc.AddSpeakEvent = onNPCSpeak;


end



--This is the actually running code! :o

farm();


8. Final Words



Well there it is. A nice reusable random plot thingy, just what every game needs. The next thing you should probably try is creating a new plot here are some ideas:


  • I seem to have lost my [small precious item] could you find it?

  • You must prove yourself to me by [doing small task] before i'll give you [reward]

  • Please kill the person sleeping with my siginifcant other



etc etc The plots can be quite complicated and multistaged.
Try and implement a story arc. Consider how to save plots mid way through! We'd possibly want to store the Resource table is there some way to do this without writing it into the plot file?

There are plenty of interesting problems to tackle! Good luck tell me what you make!

(Saving wise you'd want to seperate out you event linking and then save the resource block using hash codes for the big objects. Then load the resource block and call the link function. The link function should be error tolerant. i.e. if there are monsters and they're dead they're going to be nill values don't try and link events in!)

Speaking of which, it can't happen in our game but what if the quest giver dies. These things should be accounted for.


Further Reading



Little, as far as I can find, has been written on plot generation. (outside hard to access academic papers! Google Scholar is a step in the right direction though.)