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.)
No comments:
Post a Comment