Coding self contained reusable plots
Published January 31st, 2007 in Article, Lua, Plot, Procedural, Role Playing Games, Scripting, csharpTo test the theory of creating a random plot we need to build 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!
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
-
{
-
///
-
/// 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 and 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 functions 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!
-
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 your 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.
Source Code
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.)
Technorati Tags: plot, Lua, C sharp, Lua
Related Posts




1 Response to “Coding self contained reusable plots”