Chapter 20. So You Want to Be a Hero?

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:

Rolling Your Player Character

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.

Scripting

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.

Hint

Refer back to Chapter 13 if you need help adding the LuaInterface library to the project, which dll files are needed, etc.

Rolling the stats for a new character.

Figure 20.1. Rolling the stats for a new character.

Binding Basic Functions

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()

Hint

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

Binding Lua Functions

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()

Hint

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.

Attacking enemy characters that were added to the level with script code.

Figure 20.2. Attacking enemy characters that were added to the level with script code.

Loading and Saving the Game

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.

Saving

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

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

Level Up!

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.174.183