This chapter focuses on the development of an item editor to make creating and managing game items (such as swords, armor pieces, rings, and other gear found in a typical RPG) easier for a designer. If you have ever tried to create an RPG without an editor like this, I’m sure you ran into the same roadblock that I have when it comes to working with items. Just giving the player a sword tends to be a hard-coded, manual process, with less than favorable results. Even just storing items in a text file and reading them in is better than manually creating arrays of items in code, unless your game is on the extremely simple side with just “hack & slash” gameplay without much depth. Since our goal is to send the player on quests before we finish this book, having items that satisfy the quests is essential! For an example of how the item data is used, we’ll put that on hold until the next chapter and just focus on editing in this one.
Here’s what we’ll cover in this chapter:
The item editor is a bit different from the character editor that we developed back in Chapter 14. While characters are stored one per .char file, many items are stored in a .item file. The editor has a File menu, which allows you to start a new item database, load an existing file, or save the current file to a new file. The item editor is shown in Figure 17.1.
The editor was designed to work with groups of items in a file. The example shown here contains some weapons and armor items, but it would be better to organize the items into separate groups (weapons, armor, rings, etc.) so that it’s easier to manage and search for specific items. Another approach is to just store everything in a single item database, which is perfectly okay but it’s just harder to find and edit items when there are so many in a single file.
This editor does make it possible for players to cheat on your game. By giving the filenames a different extension besides .xml, we can at least hide the data from casual wanna-be hackers, but anyone with bare minimum skills will try opening the .item file in a text editor and immediately see that it is an editable text file containing .xml tables. There are ways to get around this problem, but that subject is beyond the scope of this book. Experience has shown me that for players who want to hack a game file, whether it’s a core database or a saved game, no effort short of hard encryption will prevent them from doing so. If someone wants to hack your game that badly, take it as a compliment.
The item editor works with just single-frame bitmaps, with support for two versions for each item: a drop image and an inventory image. The drop image is usually smaller and oriented with the ground tiles, while the inventory image is often oriented straight-on so it looks better in an inventory screen (the player’s equipment). Neither image is suitable for both purposes. If you try to use the inventory image as a drop item, it will look funny on the ground at the wrong angle. However, one alternative is to bring up a “loot window” showing the contents of an item rather than dropping items directly onto the ground. A loot window does eliminate quite a bit of work since we do not need to keep track of two different images in addition to the added code to allow the player to pick up items on the ground.
An advantage to the ground image is that you can place individual items anywhere in the game world for the player to find. But, due to the extra work required, I think most designers would rather have their artists working on new gameplay art rather than extra drop images. This is why in most games you’ll most often find crates and boxes rather than usable items in the game world. Since we have both versions of the artwork for every item from Reiner, we can use them with the item editor. Just note that both are not absolutely necessary and it’s acceptable to just use the inventory version, as long as you have a loot window come up in the game whenever the player opens a container. I kind of like the realism added when items drop to the ground when an NPC falls in combat (maybe even add the dramatic effect of having the items scatter around the body randomly). Since it’s fairly common to “loot the corpse” in most computer RPGs, use whichever method you want in your own game since the artwork is available.
What if we were to add multiple image frame support to the item editor, so that batches of item artwork could be stored on a sprite sheet? That’s a distinct possibility. At the very least, we could store both the drop and inventory image together in a two-frame image. The problem is, the artwork is not all uniform in size, with the drop items being somewhat smaller (in Reiner’s). Sure, you could enlarge the images to a fixed size all around, but will that save time in the long run versus just adding both image filenames into the item editor fields?
There is the additional problem of having literally hundreds of asset files in the game’s folder. The limited set of item images used so far already accounts for a large number of files cluttering the game’s main folder. A definite improvement would be to store the images in a sub-folder like .assets under the main folder, and then prefix all of the image filenames stored in the item editor database with .assets. So, a filename field such as “drop plate 1.png” would become “.assetsdrop plate 1.png.” You can do any such manipulation in the game code while loading these assets.
I wanted to use an auto-increment identifier for each item in the item editor database and then use the ID in quests and in the player’s inventory. But, though an identifier-based database is required for a professional project, it’s not the best choice for an amateur game with multi-purpose tools like what we have for Celtic Crusader. Instead of using an ID, the Item.Name
property will be used to look up the data for an item. All that is required to make this work effectively is to ensure that your items each have a unique name. If you want to have three rings called “Magic Ring,” be sure to add a qualifier to the name like “Magic Ring +1” or something to uniquely identify each item. Since the name is the lookup field, the first item matching the name will be used in a lookup.
As with the previous level editor and character editor, the new item editor includes a class (called Item
) that makes the data available to our game. The Item
class is meant to handle a single item from the editor database (which is stored in an XML file). In our game code, we will need to load the .item file with additional code and then use the Item
class for each item that is loaded. In other words, there is no overall “Items” class that loads an entire .item file, is saved by the editor, and makes those items available to the game. Perhaps there should be?
Public Class Item Private p_name As String Private p_desc As String Private p_dropfile As String Private p_invfile As String Private p_category As String Private p_weight As Single Private p_value As Single Private p_attacknumdice As Integer Private p_attackdie As Integer Private p_defense As Integer Private p_buffStr As Integer Private p_buffDex As Integer Private p_buffSta As Integer Private p_buffInt As Integer Private p_buffCha As Integer Public Sub New() p_name = "new item" p_desc = "" p_dropfile = "" p_invfile = "" p_category = "" p_weight = 0.0 p_value = 0.0 p_attacknumdice = 0 p_attackdie = 0 p_defense = 0 p_buffStr = 0 p_buffDex = 0 p_buffSta = 0 p_buffInt = 0 p_buffCha = 0 End Sub Public Property Name() As String Get Return p_name End Get Set(ByVal value As String) p_name = value End Set End Property Public Property Description() As String Get Return p_desc End Get Set(ByVal value As String) p_desc = value End Set End Property Public Property DropImageFilename() As String Get Return p_dropfile End Get Set(ByVal value As String) p_dropfile = value End Set End Property Public Property InvImageFilename() As String Get Return p_invfile End Get Set(ByVal value As String) p_invfile = value End Set End Property Public Property Category() As String Get Return p_category End Get Set(ByVal value As String) p_category = value End Set End Property Public Property Weight() As Single Get Return p_weight End Get Set(ByVal value As Single) p_weight = value End Set End Property Public Property Value() As Single Get Return p_value End Get Set(ByVal value As Single) p_value = value End Set End Property Public Property AttackNumDice() As Integer Get Return p_attacknumdice End Get Set(ByVal value As Integer) p_attacknumdice = value End Set End Property Public Property AttackDie() As Integer Get Return p_attackdie End Get Set(ByVal value As Integer) p_attackdie = value End Set End Property Public Property Defense() As Integer Get Return p_defense End Get Set(ByVal value As Integer) p_defense = value End Set End Property Public Property STR() As Integer Get Return p_buffStr End Get Set(ByVal value As Integer) p_buffStr = value End Set End Property Public Property DEX() As Integer Get Return p_buffDex End Get Set(ByVal value As Integer) p_buffDex = value End Set End Property Public Property STA() As Integer Get Return p_buffSta End Get Set(ByVal value As Integer) p_buffSta = value End Set End Property Public Property INT() As Integer Get Return p_buffInt End Get Set(ByVal value As Integer) p_buffInt = value End Set End Property Public Property CHA() As Integer Get Return p_buffCha End Get Set(ByVal value As Integer) p_buffCha = value End Set End Property Public ReadOnly Property Summary() As String Get Dim text As String text = "This '" + p_name + "', " Dim weight As String = "" Select Case p_weight Case Is > 50 : weight = "a very heavy " Case Is > 25 : weight = "a heavy " Case Is > 15 : weight = "a " Case Is > 7 : weight = "a light " Case Is > 0 : weight = "a very light " End Select text += weight Select Case p_category Case "Weapon" : text += "weapon" Case "Armor" : text += "armor item" Case "Necklace" : text += "necklace" Case "Ring" : text += "ring" Case Else : text += p_category.ToLower() + " item" End Select If p_attacknumdice <> 0 Then text += ", attacks at " + p_attacknumdice.ToString() _ + "D" + p_attackdie.ToString() _ + " (" + p_attacknumdice.ToString() + " - " _ + (p_attackdie * p_attacknumdice).ToString() _ + " damage)" End If If p_defense <> 0 Then text += ", adds " + p_defense.ToString() + " armor points" End If Dim fmt As String = "+#;-#" If p_buffStr <> 0 Then text += ", " + p_buffStr.ToString(fmt) + " STR" End If If p_buffDex <> 0 Then text += ", " + p_buffDex.ToString(fmt) + " DEX" End If If p_buffSta <> 0 Then text += ", " + p_buffSta.ToString(fmt) + " STA" End If If p_buffInt <> 0 Then text += ", " + p_buffInt.ToString(fmt) + " INT" End If If p_buffCha <> 0 Then text += ", " + p_buffCha.ToString(fmt) + " CHA" End If Return text + "." End Get End Property Public Overrides Function ToString() As String Return p_name End Function End Class
Like the character editor from a few chapters back, the item editor is all Visual Basic source code (the only editor written in C# is the level editor). The item editor is completely self-contained and it can create new item files from scratch as well as edit existing files. One nice feature is auto-save: while editing items, if you click on a different item in the list or close the editor, the current item is automatically saved. This takes out some of the tedium from editing a large number of items—just point, click, and edit, without concern for saving at every step.
Obviously, there is a form filled with controls that are not listed here, because the user interface is too complex to build from scratch (as in a tutorial-style walkthrough). The sources here are familiar because the XML code is similar to the code in the other editors. Some of the source code for the editor has been omitted to save space. To see the complete source code listing, please open the project (www.courseptr.com/downloads).
Public Class Form1 Dim device As Graphics Dim surface As Bitmap Dim g_filename As String = "items.item" Dim currentIndex As Integer Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load surface = New Bitmap(Size.Width, Size.Height) picDrop.Image = surface device = Graphics.FromImage(surface) clearFields() loadFile(g_filename) End Sub Private Sub showItem(ByVal index As Integer) clearFields() Dim item As Item = lstItems.Items(index) txtName.Text = item.Name txtDesc.Text = item.Description txtDropImageFilename.Text = item.DropImageFilename txtInventoryImageFilename.Text = item.InvImageFilename cboCategory.Text = item.Category txtWeight.Text = item.Weight.ToString() txtValue.Text = item.Value.ToString() cboAttackNumDice.Text = item.AttackNumDice.ToString() cboAttackDie.Text = "D" + item.AttackDie.ToString() txtDefense.Text = item.Defense.ToString() txtSTR.Text = item.STR.ToString() txtDEX.Text = item.DEX.ToString() txtSTA.Text = item.STA.ToString() txtINT.Text = item.INT.ToString() txtCHA.Text = item.CHA.ToString() txtSummary.Text = item.Summary End Sub Private Function getElement(ByVal field As String, _ ByRef element As XmlElement) As String Dim value As String = "" Try value = element.GetElementsByTagName(field)(0).InnerText Catch ex As Exception REM ignore error, just return empty Console.WriteLine(ex.Message) End Try Return value End Function Private Sub loadFile(ByVal filename As String) Try REM open the xml file Dim doc As New XmlDocument() doc.Load(filename) Dim list As XmlNodeList = doc.GetElementsByTagName("item") For Each node As XmlNode In list Dim element As XmlElement = node Dim item As New Item() item.Name = getElement("name", element) item.Description = getElement("description", element) item.DropImageFilename = getElement("dropimagefilename", _ element) item.InvImageFilename = getElement("invimagefilename", _ element) item.Category = getElement("category", element) item.Weight = Convert.ToSingle(getElement("weight", element)) item.Value = Convert.ToSingle(getElement("value", element)) item.AttackNumDice = Convert.ToInt32( _ getElement("attacknumdice", element)) item.AttackDie = Convert.ToInt32( _ getElement("attackdie", element)) item.Defense = Convert.ToInt32(getElement("defense", _ element)) item.STR = Convert.ToInt32(getElement("STR", element)) item.DEX = Convert.ToInt32(getElement("DEX", element)) item.STA = Convert.ToInt32(getElement("STA", element)) item.INT = Convert.ToInt32(getElement("INT", element)) item.CHA = Convert.ToInt32(getElement("CHA", element)) lstItems.Items.Add(item) Next Catch ex As Exception MessageBox.Show(ex.Message) Return End Try End Sub Private Sub saveFile(ByVal filename As String) Try REM create data type templates Dim typeInt As System.Type Dim typeSingle As System.Type Dim typeStr As System.Type typeInt = System.Type.GetType("System.Int32") typeStr = System.Type.GetType("System.String") typeSingle = System.Type.GetType("System.Single") REM create xml schema Dim table As New DataTable("item") table.Columns.Add(New DataColumn("name", typeStr)) table.Columns.Add(New DataColumn("description", typeStr)) table.Columns.Add(New DataColumn("dropimagefilename", typeStr)) table.Columns.Add(New DataColumn("invimagefilename", typeStr)) table.Columns.Add(New DataColumn("category", typeStr)) table.Columns.Add(New DataColumn("weight", typeSingle)) table.Columns.Add(New DataColumn("value", typeSingle)) table.Columns.Add(New DataColumn("attacknumdice", typeInt)) table.Columns.Add(New DataColumn("attackdie", typeInt)) table.Columns.Add(New DataColumn("defense", 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)) REM copy character data into datatable For Each item As Item In lstItems.Items Dim row As DataRow = table.NewRow() row("name") = item.Name row("description") = item.Description row("dropimagefilename") = item.DropImageFilename row("invimagefilename") = item.InvImageFilename row("category") = item.Category row("weight") = item.Weight row("value") = item.Value row("attacknumdice") = item.AttackNumDice row("attackdie") = item.AttackDie row("defense") = item.Defense row("STR") = item.STR row("DEX") = item.DEX row("STA") = item.STA row("INT") = item.INT row("CHA") = item.CHA table.Rows.Add(row) Next REM save xml file table.WriteXml(filename) table.Dispose() Catch es As Exception MessageBox.Show(es.Message) End Try End Sub Private Sub saveCurrentItem() Dim item As Item If currentIndex < 0 Or txtName.Text = "" Then Return End If Try item = lstItems.Items(currentIndex) item.Name = txtName.Text item.Description = txtDesc.Text item.DropImageFilename = txtDropImageFilename.Text item.InvImageFilename = txtInventoryImageFilename.Text item.Category = cboCategory.Text item.Weight = Convert.ToSingle(txtWeight.Text) item.Value = Convert.ToSingle(txtValue.Text) item.AttackNumDice = Convert.ToInt32(cboAttackNumDice.Text) item.AttackDie = Convert.ToInt32(cboAttackDie.Text.Substring(1)) item.Defense = Convert.ToInt32(txtDefense.Text) item.STR = Convert.ToInt32(txtSTR.Text) item.DEX = Convert.ToInt32(txtDEX.Text) item.STA = Convert.ToInt32(txtSTA.Text) item.INT = Convert.ToInt32(txtINT.Text) item.CHA = Convert.ToInt32(txtCHA.Text) lstItems.Items(currentIndex) = item Catch ex As Exception MessageBox.Show(ex.Message) End Try End Sub REM portions of this code have been omitted to conserve space
That concludes our work on the item editor. This editor has been developed with slightly different goals than the other editors we’ve seen so far, in that many items are stored in a single xml file rather than one item per file. This makes it quite easy to edit many items quickly by simply clicking each item in the list and making changes to it. Since the editor automatically saves changes made to items, this works quite well. Just be sure to save the file when you’re done editing.
3.17.58.199