This final chapter draws on all previous chapters to demonstrate a sample game using the tools and source code developed up to this point, with special attention paid to Lua script programming. My goal is to make it as easy as possible for you to create your own RPG, by giving just enough information in the example to show how things work, but without going so far into the gameplay that it’s difficult to understand how the sample game works. We will build a character creation screen and learn how to save the game, and then use script code to create a simple scenario with a few quests. If questing is your thing, then you can create your own set of quests for your own game. If “hack & slash” is your thing, then you can use simple quests to direct the player toward a goal while primarily just throwing hordes of monsters at them! Along the way, many small but significant improvements have been made to the classes (especially the Game
class) to accommodate the requirements of a complete game. We have to pull the sample code from all of the prior chapters into a single project that required these changes—using a custom Dialogue
for each component (treasure, combat, quests, etc.), among the usual improvements made to code while a game is being developed. All of the editors are in the run-time folder for the game in this chapter’s resource files (www.courseptr.com/downloads) for easy access, so if you want to make changes to the game just fire up the editors and start editing!
Here’s what we’ll cover in this chapter:
I had to make a few design decisions in order to get this game finished, again, without making it too complex (at which point, it is no longer very useful as a learning tool). One such decision is related to saving and loading the game. The game save revolves around the Player
class and the current state of the player, without much concern for the rest of the game data (NPC states, treasure drops, etc.). Basically, the levels are pretty much self-contained modules that can simply be re-run at any time, based on the script code of course. Getting into a load/save dialog can be messy. We do have the Dialogue
class, and it is absolutely possible to use it to display a list of saved game files for the player to load, but I didn’t want to get into all the code needed to do that (reading the directory for existing savegame files, for example). So, in the interest of time and brevity, only one savegame file will be used. This game is simple enough that I do not feel this detracts at all from the experience. You can use the load/save code to implement your own load/save system. Figure 20.1 shows the character generator for the game.
The great thing about the source code for Celtic Crusader is that all of the “big pieces” are in classes that are initialized in Form1_Load
. Furthermore, all of the real work is being done in the doUpdate()
function, which calls doScrolling()
, doMonsters()
, doHero()
, etc. This is a very clean way to build a game, because it is easy to add new things to it, and we can see easily what’s happening in this game at a glance without wading through hundreds of lines of code muddying up these main functions. The way this code is structured also makes it possible to script much of it with Lua! We haven’t touched Lua since way back in Chapter 13, “Using Portals to Expand the World.” Now we’ll dig up that Lua code again and add it to the final game project so we can script portions of the game, such as loading levels.
Refer back to Chapter 13 if you need help adding the LuaInterface library to the project, which dll files are needed, etc.
In order to give the script something to work on, we have to call a function in the script file. When a script is opened, if there is no function, then it is simply parsed and no more processing takes place. You can continue to use the script global variables but no work takes place in the script unless you tell it to do something from the Basic game code. In our Form1
source code, we have a function called doUpdate()
which does timing, calls all of the update functions, displays info on the screen, etc.—in other words, this is our workhorse function, the core loop of the game. We’re going to plug the Lua update into Game.Update()
, which is already functioning. It is called from doUpdate()
:
game.Update()
The script.lua file must be in the folder Celtic CrusaderProjectinDebug or else the LuaInterface library won’t be able to find it. If you add the .lua file to the project with “Add Existing Item,” unfortunately, Visual Studio will copy the file to the project folder which is the wrong location, so that won’t work. It’s probably best to just open the .lua file with Notepad or another text editor outside of Visual Studio.
The loop in Form1.doUpdate()
calls a function to update Lua. The only thing Game.Update()
really does is refresh the game window (technically, the PictureBox
on the form), so it doesn’t matter whether we update Lua before or after Game.Update()
, it will work either way. I’ve added a new function call to a ScriptUpdate()
function which we will write.
Private Sub doUpdate()
Dim frameRate As Integer = game.FrameRate()
Dim ticks As Integer = Environment.TickCount()
Static drawLast As Integer = 0
If ticks > drawLast + 16 Then
drawLast = ticks
doScrolling()
doMonsters()
doTreasure()
doHero()
doDialogue()
doInventory()
doQuests()
doCombat()
game.ScriptUpdate()
game.Update()
Application.DoEvents()
Else
REM throttle the cpu
Threading.Thread.Sleep(1)
End If
End Sub
Below is a sample Lua script for the final game demo in this chapter. All of the major components of the game can be selectively loaded with script functions, and almost total control is given to the script to override what happens in the default Basic code (in Form1_Load()
and doUpdate()
). Let’s briefly review the properties available. Note that most are all read-only properties. Making changes to them does not affect the game, as they are just intended to supply information to the script. The exception is ScrollX
and ScrollY
, which are both sent to the script and back to Basic, so you can change the player’s current location in the world with these properties.
WindowTitle
ScrollX
ScrollY
PortalFlag
CollidableFlag
Health
HP
QuestNumber
QuestSummary
QuestCompleteFlag
MessageResult
Here are the functions available to our Lua script, which are tied in to the Basic code.
LoadLevel()
LoadItems()
LoadQuests()
LoadHero()
DropGold()
AddCharacter()
Write()
Message()
This code assumes the reader has a basic understanding of the Lua language, since we aren’t learning the language in this book. Lua is not difficult, but it is more “C-like” than Basic, so it may come as a bit of a surprise. Just study the example and modify it to see what happens in the game.
--Celtic Crusader Lua Script state = 0 --called once when game starts up function startup() WindowTitle = "Celtic Crusader" ScrollX = 1000 ScrollY = 100 LoadLevel("kildare.level") LoadItems("default.item") LoadQuests("default.quest") LoadHero("default.char") DropGold(2, 900, 420) DropGold(4, 940, 440) DropGold(3, 920, 430) DropGold(1, 910, 420) DropGold(5, 930, 420) DropItem("Silver Key", 700, 450) AddCharacter("skeleton unarmed.char", 880, 400, 380, 150) for n = 1,20 do AddCharacter("skeleton unarmed.char", 1750, 100, 2000, 800) end end --called regularly every frame function update() Write(480, 580, "Controls: (Space) Action, (I) Inventory, (Q) Quests") Write(0, 540, "Portal: " .. tostring(PortalFlag)) Write(0, 560, "Collidable: " .. tostring(CollidableFlag)) Write(650, 0, tostring(Health) .. "/" .. tostring(HP) .. " HP") text = "Current quest: " .. tostring(QuestNumber) .. ": " .. QuestSummary if QuestCompleteFlag == true then text = text .. " (COMPLETE)" else text = text .. " (incomplete)" end Write(0, 580, text) if state == 0 then Message("Welcome!", "Welcome to the land of Celtic Crusader, " .. "brave adventurer! " .. "Make your way to the room nearby and search for a key." ,"Continue") end if MessageResult == 1 then state = state + 1 end end
With this script code working, we can see in Figure 20.2 that the 20 skeleton NPCs have been added and are roaming within the specified range.
The only thing that is saved is the player character, not the game state. Think of the player’s stats, inventory, gold, experience, level, and so on, as the persistent data, while the game levels are replayable with simple goals and repeatable logic.
Here is the Player.SaveGame()
function. This is an expandable function that can handle future needs for player data. Presently, all of the player’s stats, gold, inventory items, current world position, and current quest are saved. Just remember, the levels are intended to be stateless, meaning they reset when they are loaded, so treat the levels as replayable when designing your game. An alternative is to save all of the data for a game level, such as which items have been picked up, which monsters have been killed, etc.—a daunting amount of information to keep track of! And not to mention, that messes up the data-driven nature of the game, making it difficult to change anything in the editors when some things are stored in the save file and others are not.
Public Sub SaveGame(ByVal filename As String) Try REM create data type templates Dim typeInt As System.Type = System.Type.GetType("System.Int32") Dim typeBool As System.Type = System.Type.GetType("System.Boolean") Dim typeStr As System.Type = System.Type.GetType("System.String") REM create xml schema Dim table As New DataTable("gamestate") table.Columns.Add(New DataColumn("name", typeStr)) table.Columns.Add(New DataColumn("class", typeStr)) table.Columns.Add(New DataColumn("race", typeStr)) table.Columns.Add(New DataColumn("level", typeInt)) table.Columns.Add(New DataColumn("xp", typeInt)) table.Columns.Add(New DataColumn("hp", typeInt)) table.Columns.Add(New DataColumn("str", typeInt)) table.Columns.Add(New DataColumn("dex", typeInt)) table.Columns.Add(New DataColumn("sta", typeInt)) table.Columns.Add(New DataColumn("int", typeInt)) table.Columns.Add(New DataColumn("cha", typeInt)) table.Columns.Add(New DataColumn("quest", typeInt)) table.Columns.Add(New DataColumn("scrollx", typeInt)) table.Columns.Add(New DataColumn("scrolly", typeInt)) For n = 1 To 9 table.Columns.Add(New DataColumn("item0" + n.ToString(), typeStr)) Next For n = 10 To 30 table.Columns.Add(New DataColumn("item" + n.ToString(), typeStr)) Next REM copy data into datatable Dim row As DataRow = table.NewRow() row("name") = Name row("class") = PlayerClass row("race") = Race row("level") = Level row("xp") = Experience row("hp") = HitPoints row("str") = STR row("dex") = DEX row("sta") = STA row("int") = INT row("cha") = CHA row("quest") = p_game.quests.QuestNumber row("scrollx") = p_game.world.ScrollPos.X row("scrolly") = p_game.world.ScrollPos.Y Dim itm As Item For n = 1 To 9 itm = p_game.inven.GetItem(n) row("item0" + n.ToString()) = itm.Name Next For n = 10 To 30 itm = p_game.inven.GetItem(n) row("item" + n.ToString()) = itm.Name Next table.Rows.Add(row) REM save xml file table.WriteXml(filename) table.Dispose() Catch es As Exception MessageBox.Show(es.Message) End Try End Sub
Loading a game with Player.LoadGame()
involves a minor workaround regarding the character artwork. First, the “class” property is read from the save file, and then an appropriate animation set is loaded based on the class (remember, the .char files for the player are templates, not actual characters). Here are the files loaded for the four classes:
Warrior | hero sword.char |
Paladin | hero axe shield.char |
Hunter | hero bow.char |
Priest | hero staff.char |
After one of the four .char files have been loaded—just to get the animations going—then the save file data is loaded over the top of the template data that was just loaded. So, fields like Name, Race, STR, and so on, replace the template values stored in the .char file. These properties are stored in the .save file, and will override the defaults.
Public Sub LoadGame(ByVal filename As String) Try REM open the xml file Dim doc As New XmlDocument() doc.Load(filename) Dim list As XmlNodeList = doc.GetElementsByTagName("gamestate") Dim element As XmlElement = list(0) REM load default animations based on class p_game.hero.PlayerClass = GetElement("class", element) Select Case p_game.hero.PlayerClass Case "Warrior" p_game.hero.Load("hero sword.char") Case "Paladin" p_game.hero.Load("hero axe shield.char") Case "Hunter" p_game.hero.Load("hero bow.char") Case "Priest" p_game.hero.Load("hero staff.char") End Select REM read data fields p_game.hero.Name = getElement("name", element) p_game.hero.PlayerClass = getElement("class", element) p_game.hero.Race = getElement("race", element) p_game.hero.Level = Convert.ToInt32(GetElement("level", element)) p_game.hero.Experience = Convert.ToInt32(GetElement("xp", element)) p_game.hero.HitPoints = Convert.ToInt32(GetElement("hp", element)) p_game.hero.STR = Convert.ToInt32(GetElement("str", element)) p_game.hero.DEX = Convert.ToInt32(GetElement("dex", element)) p_game.hero.STA = Convert.ToInt32(GetElement("sta", element)) p_game.hero.INT = Convert.ToInt32(GetElement("int", element)) p_game.hero.CHA = Convert.ToInt32(GetElement("cha", element)) p_game.quests.QuestNumber = Convert.ToInt32(GetElement("quest", element)) p_game.world.X = Convert.ToInt32(GetElement("scrollx", element)) p_game.world.Y = Convert.ToInt32(GetElement("scrolly", element)) Dim itm As Item For n = 1 To 9 itm = p_game.items.GetItem(GetElement("item0" + _ n.ToString(), element)) If itm IsNot Nothing Then p_game.inven.SetItem(itm, n) End If Next For n = 10 To 30 itm = p_game.items.GetItem(GetElement("item" + _ n.ToString(), element)) If itm IsNot Nothing Then p_game.inven.SetItem(itm, n) End If Next Catch ex As Exception MessageBox.Show(ex.Message) End Try End Sub
Unfortunately, we’re out of time and space to go any further with the game within the pages of this book. So, despite there being much more we could do with the game, this will have to conclude the Celtic Crusader game and the book. The adventure will continue in Visual C# Game Programming for Teens, which delves into dungeon-based RPGs! I hope you have enjoyed the journey we have taken together while learning how to build a custom role-playing game. It has been enjoyable for me to share my thoughts about RPG programming. I would welcome your comments and game ideas as well! Be sure to stop by my forum to say hello at http://www.jharbour.com/forum. Safe journeys ahead.
3.138.37.20