Using XML as a resource in your code –one more series on gamewriting (Matt Gertz)
This is actually going to be part one of a two-part blog. In this post, I’m going to cover some basic usage of XML, as well as a few other interesting coding points such as debug-only code. In the second part, I’ll cover the creation and handling of on-the-fly controls.
As has probably become apparent over my posts so far, I have a certain fondness for creating small computer games, and while I promise not to make every post about writing them, nevertheless this post does cover the creation of a different type of game than I’ve discussed previously. Some background…
The first computer that my wife and I bought together was a Macintosh Centris 650, a really nice machine for the time and quite a change from my old Commodore 128. Being avid gamers, we quickly discovered all sorts of fun titles, such as the SSI Gold Box games, Civilization, SimLife, and so on. However, there was one title that absolutely blew me away; a watershed title that proved to me that the world had changed. In a word: Myst.
Myst was an immersive game where you could walk around, look around, and interact with your environment. The actual navigation was actually not too dissimilar to that of the Gold Box games, but the photorealism combined with the real-time interaction with the scene was taking it to the next level. This weekend, while sitting and trying to think of a topic for this blog, I was wondering if I could build something like Myst from scratch using Visual Basic. I could (for example) create a virtual walkthrough of my new house to mail to my relatives or something. So, I sat down to figure out how to do it.
The first thing I would need was a form. I created a new windows app and set the form size to be 800 x 600, keeping everything in the 4:3 ratio used by most monitors and digital cameras. The next thing I would need is a way to show the current image, and you might think that I’d use a PictureBox for this. However, after careful thought, I realized that it would be best just to assign the image to the background of the form itself – that way, I could use transparency if I chose to overlay other items onto the form. (Recall from my last post that transparency is only enabled relative to the form itself!)
My next assumption would be that I would map the virtual area into a 3-dimensional grid, with x and y giving the coordinates for each “cell” on the floor, and z indicating which floor I was on. In each cell, I should be able to turn left or left, maybe look up or down, and maybe move forward. My direction would be important to know, as well as how high or low I was looking. I came up with this:
Dim Cells(maxX, maxY, maxZ) As Cell
Dim currentX As Integer
Dim currentY As Integer
Dim currentZ As Integer
Dim currentOrientation As Orientation
Dim currentAttitude As Attitude
Using these enums:
Down = 1
Neutral = 2
Up = 3
TowardsLargerY = 0 ‘ i.e., North
TowardsLargerX = 1 ‘ i.e., East
TowardsSmallerY = 2 ‘ i.e., South
TowardsSmallerX = 3 ‘ i.e., West
Each “Cell” is just a structure owned by the form containing information on what you can do in the cell: can you step into that cell (i.e., is it active – that’s important since not all areas are perfect rectangles and you may want to “cut out” some sections of the grid as not relevant), can you look up or down, etc:
Protected Structure Cell
Dim IsActive As Boolean
Dim CanWalkTowardLargerY As Boolean
Dim CanWalkTowardLargerX As Boolean
Dim CanWalkTowardSmallerY As Boolean
Dim CanWalkTowardSmallerX As Boolean
Dim CanWalkUpTowardLargerY As Boolean
Dim CanWalkUpTowardLargerX As Boolean
Dim CanWalkUpTowardSmallerY As Boolean
Dim CanWalkUpTowardSmallerX As Boolean
Dim CanWalkDownTowardLargerY As Boolean
Dim CanWalkDownTowardLargerX As Boolean
Dim CanWalkDownTowardSmallerY As Boolean
Dim CanWalkDownTowardSmallerX As Boolean
Dim CanLookUp As Boolean
Dim CanLookDown As Boolean
On the form, I laid out five labels – one on the left, one on the right, one towards the top, one towards the bottom, and one in the middle. By setting their Autosize properties to False, I stretch them so that, all together, they covered the entire form. I then cleared their text and set their background color to “Transparent” so that the image on the form could show through them. I also changed their cursor properties to show a cursor pointing left, right, etc. as appropriate, and set the default form’s cursor to the “no smoking” sign.
Why do this? Well, now I can use these labels to (a) show the user what would happen if they clicked there to navigate relative to the current image and (b) handle the click event in that area rather easily. I can also enable or disable the labels as necessary – for example, if the user cannot move forward in their current cell, I simply disable the central label, therefore ignoring all clicks there and allowing the “no smoking” cursor of the base form to now show through. That gives valuable feedback to the player. A very simple example of the event handler for one of the labels (PanLeft) follows:
Private Sub PanLeft_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles PanLeft.Click
TransitTo(currentX, currentY, currentZ, _
- TransitTo() updates the current player position/orientation/attitude, shows the indicated new image, and enables/disables labels as necessary based on info from the new adjacent cells.
- LeftOrientation() is a helper function which just returns the orientation 90 degrees to the left – for example, TowardsLargerY (north) if we were currently aimed TowardsLargerX (east).
The logic for the central label (PanForward) is actually slightly more complex, since we need to see if we’re changing nor only X or Y but possibly Z as well, as might happen if their were stairs or a ladder in front of the user, but otherwiseis pretty much the same sort of thing as the other labels.
To test it out, I created a 2x2x1 cell “house” to walk around in – that is, four cells total. (Yes, I didn’t test the Z-axis transitions yet, and no doubt that will come back to bite me later – maybe I’ll have to talk about debugging in the next post. J) Now, each cell could have up to 4 orientations * 3 attitudes = 12 views, and since I didn’t block any up/down views in any of my four cells, that meant I needed 48 images total. I generated some basic GIFs using Paint.exe, and added them into the resource manager, creating some helper functions to assist me in loading them from My.Resources based on the x/y/z/orientation/attitude. (In the “real world,” I might use digital camera shots and load those instead.) However, note that in the game logic as written, not all of the cells would necessarily have to have look up or look down capability, and not all cells can necessarily be traversed to. How is all of that information stored and acted upon? I didn’t want to hard-code the actual layout into the code, so I needed to store this information in a separate file instead. XML is a good way to store it, being human-readable (unlike a binary file). Here’s a bit of the XML file I created to detail the logic for one cell, created as file MAZE.XML:
<?xml version=“1.0“ encoding=“utf-8“ ?>
<cell active=“true“ x-pos=“0“ y-pos=“0“ z-pos=“0“
[… etc …]
Again, in the “real world,” a separate application would likely generate the file from graphs that the developer drew, but I wrote this one by hand. Anyway, each <cell> element contains all of the information I want to load into the structure representing a given location, including which cell the information pertains to. I added the file into the resource manager, noting that My.Resources.Maze will now return a string containing the contents of the XML file – cool. Using the XML resource to initialize the cells during the game is extremely easy, and here is an excerpt from my code for accessing that file resource:
Public Sub LoadMazeRules()
‘ Load the maze rules from the built-in XML file
Dim x As Integer = 0
Dim y As Integer = 0
Dim z As Integer = 0
Dim xmlDoc As New XmlDocument
Dim outerNode As XmlNode = _
For Each node As XmlNode In outerNode.ChildNodes
x = node.Attributes.GetNamedItem(“x-pos”).Value
y = node.Attributes.GetNamedItem(“y-pos”).Value
z = node.Attributes.GetNamedItem(“z-pos”).Value
With Cells(x, y, z)
.CanLookDown = node.Attributes.GetNamedItem(“lookdown”).Value
.CanLookUp = node.Attributes.GetNamedItem(“lookup”).Value
‘ (Etc… keep loading in attributes for this cell)
As you can see, I start out by declaring a new XmlDocument instance. The method LoadXml can take a string which contains valid XML , and since that’s exactly what I can provide from calling My.Resources.Maze(), life is good. At any element level, I can query for child elements by name – I know that “cells” is a child of the XmlDocument node, so I get the first (and only) instance of that, and then I know that “cell” elements are children of that node, so I query for those. Having that list, I can then enumerate over it and pick out all of the information I want from the attributes via GetNamedItem(), supplying the desired attribute as the argument. It’s very important to note that XML elements, tags, etc are all case-sensitive, so querying for “Cell” instead of “cell” will not work! Also note that I need not make any assumptions about the ordering of the cell elements with respect to my internal array, since I’m taking the coordinates directly from the XML itself. If, for some reason, the XML is missing a cell, that cell structure’s members will all default to zero or false (including IsActive), so I could just leave out any inactive cells if I wished to. When I’m done with the XmlDocument, it just goes out of scope and I don’t worry about it anymore.
There are other ways to retrieve XML as well, and you can see some of these from the snippets provided in Visual Basic 2005. To see these, in the editor type “?” and press the TAB key, and then navigate down to the XML item and select it. There are a lot of snippets here, but two of them are particularly interesting. The first one is “Read XML from a String,” which looks like this:
Dim reader As XmlReader = XmlReader.Create( _
New StringReader( “<book/>” ))
and where you replace the argument to StringReader with your own XML string. This is analogous to reading in a normal stream serially, and it’s particularly useful when you need to find something in a XML string but you don’t want to duplicate everything into an XmlDocument. It’s fast and doesn’t cache anything. In the “while” loop, you would add code to check reader.NodeType, see what it is (e.g., XmlNodeType.Element), and act appropriately to that to collect information from the members reader.Name, reader.Value, etc.
The second interesting snippet is actually two snippets: “Write class data to an XML file” and the corresponding “Read class data from an XML file.” Essentially, the former just writes out all of the data from a class instance into the XML file, and the latter reads it back in. If I had created a tool to automatically generate my XML file for my virtual walk-through, I might have used these snippets to write out the cell values to an XML file and then read them back in later, putting the logic (and the structure) in a class library to be shared by both.
I’ve included the code written thus far in a ZIP file attached to this post. You’ll note when you look through it that I’ve added a routine to check the validity of the XML information – it’s pretty straightforward, and the only interesting thing is that I only run the validation when debugging (i.e., the code is in an #if DEBUG block). For my next post, we’ll add some state to this game, and add some controls on the fly to give the player something to do other than walk around.–Matt–*