Chapter 8. Gemstone Hunter - Put on your Platform Shoes

In the Gemstone Hunter game, the player is a treasure-hunting archaeologist roaming the zombie-infested wilderness. Unlike the other games presented in this book, Gemstone Hunter will not automatically generate levels. For this reason, the game can be viewed more along the lines of a project starter to explore topics, such as combining Windows Forms and XNA, to produce a level editor, and an introduction to the basics of building a platform-style game.

The XNA Creator's Club website provides a number of Starter Kits that contain sample code and images to get you started on developing specific types of games. The Platform Starter Kit was introduced with XNA 3.1 and included the XNA Game Studio distribution. We are going to make use of some of the graphics from this Starter Kit in Gemstone Hunter. We will not use the actual starter kit itself, however, for a couple of reasons. First, all of the code in the Starter Kit is in C#, and second, we want to build on some of the code that we have established in our prior games and focus on key aspects of the platform genre.

In this chapter, we will begin the development of Gemstone Hunter, by building a map editor for the game. In order to do so, we will cover:

  • Expanding our existing tile map engine

  • Adding multiple projects to a Visual Studio solution

  • Adding a Windows Form to an XNA game project

  • Communicating between Windows Forms and our game code

  • Loading and saving map files through serialization

Borrowing graphics

While we may not be directly using XNA's Platform Starter Kit, we will be borrowing the graphical resources for the player's character and enemy monsters from the project. We will begin by creating the project that will eventually house our completed game:

Borrowing graphics

Time for action - creating projects

  1. Download 2403_08_GRAPHICPACK.zip from the book's website, and extract the contents to a temporary folder. Leave this folder open in Windows Explorer.

  2. Inside Visual Studio, select New Project... from the File menu.

  3. Create a new Windows Game (4.0) project called Gemstone Hunter.

  4. Right-click on the Gemstone Hunter Content project, and add a new folder called Textures.

  5. Switch back to the Windows Explorer window, and highlight the Sprites folder, as well as the three .PNG files (Gem.png, PlatformTiles.png, and TitleScreen.png). Right-click on one of the highlighted files and select Copy.

  6. Switch back to the Visual Studio window, right-click on the Textures folder in the content project, and select Paste. This should result in a Sprites folder under Textures folder and all three images being added to your project.

  7. Right-click on the Gemstone HunterContent project, and add a new folder called Fonts.

  8. Create a new SpriteFont object called Pericles8 in the Fonts folder. Set the font name to Pericles and the size to 8.

  9. At the top of the Solution Explorer window, right-click on the solution (Solution'Gemstone Hunter') and select Add | New Project....

  10. From the new project window, select the Windows Game Library (4.0) template. Name the project Tile Engine, and add it to the solution:

    Time for action - creating projects

What just happened?

We now have three projects in our solution—The Gemstone Hunter game project, the associated content project, and the Tile Engine game library project. The game project itself will be detailed in the next chapter. The Tile Engine project will contain the code for, not surprisingly, the game's tile engine, which will be shared with yet another project, the Level Editor, which we will create shortly.

A more advanced tile engine

In Robot Rampage, we built a simple tile engine that displayed a single layer of tiles from a two-dimensional array of integers, which represented the tiles associated with each map square. For Gemstone Hunter, we will construct a new tile engine that handles multiple tile layers, including a layer of tiles that are drawn in the foreground, appearing in front of the player:

A more advanced tile engine

Since we need to store more information about an individual map square, we will begin by defining a class that will contain all of the information we need about a particular square.

Time for action - the MapSquare class

  1. Add a new class called MapSquare to the Tile Engine project.

  2. Modify the declaration of the MapSquare class by adding the<Serializable()> attribute before the class declaration. The class declaration should read:

    <Serializable()> Public Class MapSquare
    
  3. Add the declarations region to the MapSquare class:

    #Region "Declarations"
    Public LayerTiles(3) As Integer
    Public CodeValue As String = "
    Public Passable As Boolean = True
    #End Region
    
  4. Add a constructor to the MapSquare class:

    #Region "Constructor"
    Public Sub New(
    background As Integer,
    interactive As Integer,
    foreground As Integer,
    code As String,
    passable As Boolean)
    LayerTiles(0) = background
    LayerTiles(1) = interactive
    LayerTiles(2) = foreground
    CodeValue = code
    Me.Passable = passable
    End Sub
    #End Region
    
  5. Add the TogglePassable() method to the MapSquare class:

    #Region "Public Methods"
    Public Sub TogglePassable()
    Passable = Not Passable
    End Sub
    #End Region
    
    

What just happened?

In step 2, we added a new type of information to our code file called an attribute. Attributes indicate to the Visual Basic compiler that the item following it should be treated differently in some way compared to the norm for that item type.

In our case, we want to be able to save the tile map that we will be creating to a file and then load that file in both the editor and the game projects. To do this, we need to indicate to Visual Basic that the object is serializable&mdash;that is, the object can be converted into a byte-stream, which can be stored and reloaded at a later point.

Note

Modifying serializable classes

We need to be fairly sure we have covered all of the bases with our MapSquare class, because once we have used the class to save map files, any changes made (even renaming variables) will cause reloading the saved data to either fail completely, or return corrupted results. This is because the binary serialization method that we will be using essentially grabs the in-memory representation of the data and saves it to disk, reloading it in the same manner. If the definition of the structure changes in any way, the binary format that was output will not match the new in-memory format, resulting in unpredictable problems with our loaded maps.

The LayerTiles array stores three integer values representing the tile images on the background, interactive, and foreground layers. Depending on the needs of your game, you could create any number of layers. When we draw them, we will use the index of the layer to determine the depth at which the sprite is drawn, ensuring that they appear in the correct order on the screen.

While building our map, we can associate a string value with each individual map square by setting the CodeValue variable. This variable will allow us to create special features, such as map transitions, traps, and invisible barriers to enemy movement.

Finally, to determine if the map square blocks player movement, we can check the Passable member variable. If it is false, the player cannot move into this square. The default for all MapSquares is to be open to movement (Passable set to true).

Rebuilding the camera

Just as we did in Robot Rampage, we will use a Camera module to represent the player's view of our game world and track all object positions in world-based coordinates.

Our Camera module is very similar to the module from Robot Rampage, with only minor changes which are detailed next.

Time for action - the Camera module

  1. Create a new module Camera in the Tile Engine project.

  2. Modify the declaration of the Camera module to make it public:

    Public Module Camera
    
  3. Add declarations to the Camera module:

    #Region "Declarations"
    Private _position As Vector2 = Vector2.Zero
    Private _viewPortSize As Vector2 = Vector2.Zero
    #End Region
    
  4. Add properties to the Camera module:

    #Region "Properties"
    Public Property WorldRectangle As Rectangle = New Rectangle(0, 0, 0, 0)
    Public Property ViewPortWidth As Integer
    Get
    Return CInt(Int(_viewPortSize.X))
    End Get
    Set(ByVal value As Integer)
    _viewPortSize.X = value
    End Set
    End Property
    Public Property ViewPortHeight As Integer
    Get
    Return CInt(Int(_viewPortSize.Y))
    End Get
    Set(ByVal value As Integer)
    _viewPortSize.Y = value
    End Set
    End Property
    Public Property Position As Vector2
    Get
    Return _position
    End Get
    Set(ByVal value As Vector2)
    _position = New Vector2(
    MathHelper.Clamp(value.X, WorldRectangle.X, WorldRectangle.Width - ViewPortWidth),
    MathHelper.Clamp(value.Y, WorldRectangle.Y, WorldRectangle.Height - ViewPortHeight))
    End Set
    End Property
    Public ReadOnly Property ViewPort As Rectangle
    Get
    Return New Rectangle(
    CInt(Int(Position.X)), CInt(Int(Position.Y)),
    ViewPortWidth, ViewPortHeight)
    End Get
    End Property
    #End Region
    
  5. Add methods to the Camera module:

    #Region "Public Methods"
    Public Sub Move(offset As Vector2)
    Position += offset
    End Sub
    Public Function ObjectIsVisible(bounds As Rectangle) As Boolean
    Return ViewPort.Intersects(bounds)
    End Function
    Public Function WorldToScreen(worldLocation As Vector2) As Vector2
    Return worldLocation - Position
    End Function
    Public Function WorldToScreen(worldRect As Rectangle) As Rectangle
    Return New Rectangle(
    worldRect.Left - CInt(Int(Position.X)),
    worldRect.Top - CInt(Int(position.Y)),
    worldRect.Width,
    worldRect.Height)
    End Function
    Public Function ScreenToWorld(screenLocation As Vector2) As Vector2
    advanced tile enginecamera, rebuildingReturn screenLocation + Position
    End Function
    Public Function ScreenToWorld(screenRect As Rectangle) As Rectangle
    Return New Rectangle(
    screenRect.Left + CInt(Int(Position.X)),
    screenRect.Top + CInt(Int(Position.Y)),
    screenRect.Width,
    screenRect.Height)
    End Function
    #End Region
    

What just happened?

The Transform() methods from the Robot Rampage Camera have been renamed to WorldToScreen(), and a new pair of methods called ScreenToWorld() have been added. We will need to respond to mouse events in the map editor, and the mouse position is reported in screen coordinates. These new methods will assist in determining the map square underneath the mouse cursor.

We saw in Robot Rampage that converting world coordinates to screen coordinates was a simple matter of subtracting the position of the camera from the coordinate. The reverse is also true. To convert from a screen coordinate to a world map coordinate, we add the position of the camera.

Constructing the Tile Engine

The basic concepts behind our original tile-based map engine are unchanged, and indeed most of the code for our new tile engine can be brought over from Robot Rampage. We need to make modifications to the way the map itself is stored, and the way we access (for both reading and setting) map tiles to accommodate the MapSquare class, instead of a simple array of integers.

Time for action - the TileMap module part 1

  1. In the Tile Engine project, rename the Class1.vb file that was generated by the Game Library project template to TileMap.vb by right-clicking on Class1.vb and selecting Rename. Visual Studio will ask if you wish to rename all references to the class as well. Go ahead and click on Yes. We have not referenced our new class anywhere, so no other code is actually updated.

  2. Double-click on the newly renamed TileMap.vb file to open it in the editor.

  3. Modify the declaration of the TileMap class to add two Imports directives and to change the class into a Public Module. The entire text of the file should now be:

    Imports System.Runtime.Serialization.Formatters.Binary
    Imports System.IO
    Public Module TileMap
    End Module
    
  4. Add declarations to the TileMap module:

    #Region "Declarations"
    Public Const TileWidth As Integer = 48
    Public Const TileHeight As Integer = 48
    Public Const MapWidth As Integer = 160
    Public Const MapHeight As Integer = 12
    Public Const MapLayers As Integer = 3
    Private Const skyTile As Integer = 2
    Private mapCells(mapWidth,mapHeight) As MapSquare
    Public EditorMode As Boolean = False
    Public spriteFont As SpriteFont
    Private tileSheet As Texture2D
    #End Region
    
  5. Add the TilesPerRow property to the TileMap module:

    #Region "Properties"
    Public ReadOnly Property TilesPerRow As Integer
    Get
    Return CInt(Int(tileSheet.Width / TileWidth))
    End Get
    End Property
    #End Region
    
    
  6. Add the Initialize() method to the TileMap module:

    #Region "Initialization"
    Public Sub Initialize(tileTexture As Texture2D)
    tileSheet = tileTexture
    For x As Integer = 0 To MapWidth - 1
    For y As Integer = 0 To MapHeight - 1
    For z As Integer = 0 To MapLayers - 1
    mapCells(x,y) = New MapSquare(skyTile, 0, 0, "", True)
    Next
    Next
    Next
    End Sub
    #End Region
    
  7. Add the TileSourceRectangle() function to the TileMap module:

    #Region "Tile Handling Methods"
    Public Function TileSourceRectangle(index As Integer) As Rectangle
    Return New Rectangle(
    (index Mod TilesPerRow) * TileWidth,
    CInt(Int(index / TilesPerRow)) * TileHeight,
    TileWidth,
    TileHeight)
    End Function
    #End Region
    

What just happened?

Before we continue with the implementation of the TileMap class, let's look at the difference we can see so far from the same class in Robot Rampage.

Right from the start, we have a directive that we have not used before: Imports. There are many, many namespaces (pre-defined pieces of code) supplied with the .Net Framework, and everything we have used up until this point has been accessible without using the Imports directive. All Imports does is tell the Visual Basic compiler that we will be using objects from the specified namespace and lets us avoid typing the entire namespace name whenever we want to use one of its components. The System.Runtime.Serialization.Formatters.Binary assembly provides a simple way for us to write our level files out to the disk (and read them back in again), by converting the array of MapSquare objects in memory into a format that can be written to a file.

Note

Importing namespaces

Imports directives provide us with a shorthand way of using classes in namespaces that we would not normally access. Without it, we must enter the full name (System.Runtime.Serialization.Formatters.Binary.BinaryFormatter) of the class we wish to use, instead of simply using the class name (BinaryFormatter). Both approaches will work, but if we are accessing objects from a long namespace more than a couple of times in a code file, the Imports directive saves quite a bit of typing.

If you examine C# XNA code, you will often see several using directives at the top of each code file, with namespaces, such as Microsoft.XNA.Framework.Graphics. We do not need to import these in Visual Basic, as the project templates take care of this for us.

You may notice that both the declaration and initialization regions are quite a bit shorter than they were in Robot Rampage. In Gemstone Hunter, we will define a tile sheet image that contains the tiles we will use in the game. Unlike our previous games, we will not simply store all of our game's graphics on the same sprite sheet, but break it up into multiple sheets. We have a texture dedicated to the tile images for our game, and each type of game object has a texture file of its own:

What just happened?

By defining a single sprite sheet to hold only tiles for the tile map, we can treat the sheet in a special manner and decide that the sheet will be evenly divided into as many tiles as will fit on the image. Our tile sheet image is 480x480 pixels, and with a 48x48 tile size, we have 10 rows of 10 tiles, for 100 total tiles available to our game. We could always increase the size of the image to add more tiles, though we would want to keep it to increments of 48 pixels in each direction, to make the math easier.

We will number the tiles starting with zero in the upper-left corner of the tile sheet and progressing across a row. When we reach the end of the row, we return to the left side of the image and start a new row. The TilesPerRow property and the TileSourceRectangle method replace the array of pre-defined tiles, by providing a way to locate the source rectangle for any tile we wish to draw on the tile sheet image.

Our map is still represented by an array, but around this time, it is a two-dimensional array of MapSquare objects, instead of simple integers. We have also rearranged our terminology to reflect the more complex nature of our tile map. What we referred to as squares in Robot Rampage, we now call cells. Any of our code that dealt with getting or setting information about map tiles in Robot Rampage will need to be updated to handle the entire MapSquare object in each cell, instead of a simple integer value.

The TileMap itself will include support for being used in editing mode, which can be toggled by setting the EditorMode member variable. While in editor mode, we will draw the contents of the CodeValue member of the MapSquare class on top of each square, so the TileMap class needs a SpriteFont for use with SpriteBatch.DrawString().

Our Initialize() method is greatly simplified by the removal of the tiles array, allowing us to establish all of the mapCells as MapSquares, with empty tiles on each layer. Our tile sheet contains a fully transparent tile in the upper-left corner (tile zero) and a blue sky tile in the third position (tile two), so by filling the map with squares containing tile two on the background layer, and tile zero on the other two layers, we end up with an empty map with a blue sky background. This will simply save time when creating a new map with the map editor, by letting us skip drawing the sky on each map.

Time for action - the TileMap module part 2

  1. Add methods dealing with locating map cells to the TileMap module:

    #Region "Information about Map Cells"
    Public Function GetCellByPixelX(pixelX As Integer) As Integer
    Return CInt(Int(pixelX / TileWidth))
    End Function
    Public Function GetCellByPixelY(pixelY As Integer) As Integer
    Return CInt(Int(pixelY / TileHeight))
    End Function
    Public Function GetCellByPixel(pixelLocation As Vector2) As Vector2
    Return New Vector2(
    GetCellByPixelX(CInt(Int(pixelLocation.X))),
    GetCellByPixelY(CInt(Int(pixelLocation.Y))))
    End Function
    Public Function GetCellCenter(
    cellX As Integer,
    cellY As Integer) As Vector2
    Return New Vector2(
    tile engineTileMap moduleCSng((cellX * TileWidth) + (TileWidth / 2)),
    CSng((cellY * TileHeight) + (TileHeight / 2)))
    End Function
    Public Function GetCellCenter(cell As Vector2) As Vector2
    Return GetCellCenter(
    CInt(Int(cell.X)),
    CInt(Int(cell.Y)))
    End Function
    Public Function CellWorldRectangle(
    cellX As Integer,
    cellY As Integer) As Rectangle
    Return New Rectangle(
    cellX * TileWidth,
    cellY * TileHeight,
    TileWidth,
    TileHeight)
    End Function
    Public Function CellWorldRectangle(cell As Vector2) As Rectangle
    Return CellWorldRectangle(
    CInt(Int(cell.X)), CInt(Int(cell.Y)))
    End Function
    Public Function CellScreenRectangle(
    cellX As Integer,
    cellY As Integer) As Rectangle
    Return Camera.WorldToScreen(CellWorldRectangle(cellX, cellY))
    End Function
    Public Function CellSreenRectangle(cell As Vector2) As Rectangle
    Return CellScreenRectangle(CInt(Int(cell.X)), CInt(Int(cell.Y)))
    End Function
    Public Function CellIsPassable(
    cellX As Integer,
    cellY As Integer) As Boolean
    tile engineTileMap moduleDim square As MapSquare = GetMapSquareAtCell(cellX, cellY)
    If IsNothing(square) Then
    Return True
    Else
    Return square.Passable
    End If
    End Function
    Public Function CellIsPassable(cell As Vector2) As Boolean
    Return CellIsPassable(CInt(Int(cell.X)), CInt(Int(cell.Y)))
    End Function
    Public Function CellIsPassableByPixel(
    pixelLocation As Vector2) As Boolean
    Return CellIsPassable(
    GetCellByPixelX(CInt(Int(pixelLocation.X))),
    GetCellByPixelY(CInt(Int(pixelLocation.Y))))
    End Function
    Public Function CellCodeValue(
    cellX As Integer,
    cellY As Integer) As String
    Dim square As MapSquare = GetMapSquareAtCell(cellX, cellY)
    If IsNothing(square) Then
    Return ""
    Else
    Return square.CodeValue
    End If
    End Function
    Public Function CellCodeValue(cell As Vector2) As String
    Return CellCodeValue(CInt(Int(cell.X)), CInt(Int(cell.Y)))
    End Function
    #End Region
    
  2. Add methods for manipulating MapSquares to the TileMap module:

    #Region "Information about MapSquare objects"
    Public Function GetMapSquareAtCell(
    tileX As Integer,
    tileY As Integer) As MapSquare
    If ((tileX >= 0) And (tileX < MapWidth) And
    (tileY >= 0) And (tileY < MapHeight)) Then
    Return mapCells(tileX, tileY)
    Else
    Return Nothing
    End If
    End Function
    Public Sub SetMapSquareAtCell(
    tileX As Integer,
    tileY As Integer,
    square As MapSquare)
    If ((tileX >= 0) And (tileX < MapWidth) And
    (tileY >= 0) And (tileY < MapHeight)) Then
    mapCells(tileX, tileY) = square
    End If
    End Sub
    Public Sub SetTileAtCell(
    tileX As Integer,
    tileY As Integer,
    layer As Integer,
    tileIndex As Integer)
    If ((tileX >= 0) And (tileX < MapWidth) And (tileY >= 0) And (tileY < MapHeight)) Then
    mapCells(tileX, tileY).LayerTiles(layer) = tileIndex
    End If
    End Sub
    Public Function GetMapSquareAtPixel(
    pixelX As Integer, pixelY As Integer) As MapSquare
    Return GetMapSquareAtCell(
    GetCellByPixelX(pixelX), GetCellByPixelY(pixelY))
    End Function
    Public Function GetMapSquareAtPixel( pixelLocation As Vector2) As MapSquare
    Return GetMapSquareAtPixel(
    CInt(Int(pixelLocation.X)),
    CInt(Int(pixelLocation.Y)))
    End Function
    #End Region
    

What just happened?

Much of the code we need for dealing with the tile map is unchanged from our simpler tile engine, aside from the changes to accommodate our new cell and MapSquare terminology. We also use the MapSquare type when getting and setting the contents of map cells instead of integers.

The SetTileAtCell() method may seem out of place among the methods dealing with MapSquare objects. Its purpose is to provide a way to change the tile index of a single layer in a cell, without repackaging the cell's entire MapSquare object. By passing SetTileAtCell() a cell location, layer number, and tile index, we can change the content of a single layer&mdash;exactly what we will need to do when building the map editor.

Because the game engine will need easy access to the Passable and CodeValue members of a cell (without the need to deal with the tile layer values), we have created the shortcut methods CellIsPassable() and CellCodeValue(). When the time comes to move the player and enemy objects during game play, we will make extensive use of these methods to determine what map squares are accessible to game entities.

Drawing the Tile Map

We are now ready to assemble the code necessary to draw the enhanced tile map to the screen. We need to account for all three layers of the map, ensuring that each will be drawn in the proper relationship to the others&mdash;the background layer appearing furthest away, the interactive layer drawn above it, and finally the foreground layer drawn nearest to the screen.

Time for action - the TileMap module part 3

  1. Add the Draw() method to the TileMap module:

    #Region "Drawing"
    Public Sub Draw(spriteBatch As SpriteBatch)
    Dim startX As Integer
    startX = GetCellByPixelX(CInt(Int(Camera.Position.X)))
    Dim endX As Integer
    endX = GetCellByPixelX(CInt(Int(Camera.Position.X)) + Camera.ViewPortWidth)
    Dim startY As Integer
    startY = GetCellByPixelY(CInt(Int(Camera.Position.Y)))
    Dim endY As Integer
    endY = GetCellByPixelY(CInt(Int(Camera.Position.Y)) + Camera.ViewPortHeight)
    For x As Integer = startX To endX
    For y As Integer = startY To endY
    For z As Integer = 0 To MapLayers - 1
    If ((x >= 0) And (y >= 0) And (x < MapWidth) And (y < MapHeight)) Then
    spriteBatch.Draw(
    tileSheet,
    CellScreenRectangle(x,y),
    TileSourceRectangle( mapCells(x,y).LayerTiles(z)),
    Color.White,
    0.0,
    Vector2.Zero,
    SpriteEffects.None,
    1F - (CSng(z) * 0.1F))
    End If
    Next
    If EditorMode Then
    DrawEditModeItems(spriteBatch, x, y)
    End If
    Next
    Next
    End Sub
    Public Sub DrawEditModeItems(
    spriteBatch As SpriteBatch,
    x As Integer,
    y As Integer)
    If ((x < 0) Or (x >= MapWidth) Or (y < 0) Or (y >= MapHeight)) Then
    Return
    End If
    tile mapdrawingIf Not CellIsPassable(x,y) Then
    spriteBatch.Draw(
    tileSheet,
    CellScreenRectangle(x, y),
    TileSourceRectangle(1),
    New Color(255, 0, 0) * 0.4F,
    0.0,
    Vector2.Zero,
    SpriteEffects.None,
    0.0)
    End If
    If mapCells(x, y).CodeValue <> "" Then
    Dim screenRect As Rectangle = CellScreenRectangle(x, y)
    spriteBatch.DrawString(
    spriteFont,
    mapCells(x, y).CodeValue,
    New Vector2(screenRect.X, screenRect.Y),
    Color.White,
    0.0,
    Vector2.Zero,
    1.0,
    SpriteEffects.None,
    0.0)
    End If
    End Sub
    #End Region
    

What just happened?

Once again, the Draw() method is very familiar. We still use the position of the camera to determine the range of cells that need to be drawn to the screen, but this time we nest a third loop inside the horizontal and vertical loops, which draws the tiles from each of the three layers.

This SpriteBatch.Draw() call is unlike any of the others we have made, because this time we are specifying a layer depth as the last parameter of the call. In the past, when we have used the advanced form of the Draw() method, we have always left this parameter at a value of 0.

Note

Layer depths

By specifying the layer depth at which to draw the sprite, we can execute the draw calls in any order and allow the graphics card to place them properly according to their sorted depth. In order for this to work, we need to specify some additional parameters to the SpriteBatch.Begin() method call we will use in the Game1 class' Draw() method. We need to specify SpriteSortMode.BackToFront as the first parameter of the call. This tells the SpriteBatch object to pay attention to the layer depth information, otherwise the order in which the sprites are drawn will still be used for sorting, and the layer depths will be ignored. The FrontToBack reverses the meaning of the layer depth parameter, making items at 1.0 closer to the camera than items at 0.0. None of the other modes (Immediate, Deferred, and Texture) will allow us to properly sort sprites, because they either rely on the drawing order (Immediate and Deferred) or group the drawn sprites in order by the source texture they come from (Texture).

If our tile engine is currently in editor mode, the DrawEditModeItems() method is called after each tile is drawn. This uses the white tile at tile index one to draw a semi-transparent red block over any square that is not passable by the player. Additionally, the method uses SpriteBatch.DrawString() to display the content of the CodeValue variable associated with each map cell, if it is not empty.

Time for action - adding the tile map to the game project

  1. Right-click on the Gemstone Hunter project in Solution Explorer, and click on Add Reference....

  2. Click on the Projects tab in the Add Reference window, and ensure that the Tile Engine project is selected. Click on OK:

    Time for action - adding the tile map to the game project
  3. Open the Game1.vb file in the Gemstone Hunter project, and add the following Imports directive to the top of the file:

    Imports Tile_Engine
    
  4. Add the following to the LoadContent() method of the Game1 class:

    TileMap.Initialize( Content.Load(Of Texture2D)("TexturesPlatformTiles"))
    TileMap.SetTileAtCell(3, 3, 1, 10)
    Camera.WorldRectangle = new Rectangle(0, 0, 160 * 48, 12 * 48)
    Camera.Position = Vector2.Zero
    Camera.ViewPortWidth = 800
    Camera.ViewPortHeight = 600
    
  5. Replace the current Draw() method of the Game1 class with the following:

    Protected Overrides Sub Draw(gameTime As GameTime)
    GraphicsDevice.Clear(Color.Black)
    spriteBatch.Begin(
    SpriteSortMode.BackToFront,
    BlendState.AlphaBlend)
    TileMap.Draw(spriteBatch)
    spriteBatch.End()
    MyBase.Draw(gameTime)
    End Sub
    
  6. Execute the project.

What just happened?

By referencing the Tile Engine project from the Gemstone Hunter project, we can utilize the code from the Tile Engine project, by including an Imports directive referencing the Tile_Engine namespace.

During the LoadContent() method, we initialize the TileMap and Camera classes, and add a single tile to the map, so that we will see it when we run the application.

Finally, we see the special form of the SpriteBatch.Begin() call that we need to make, to display the different tile layers sorted in the proper order. In addition to specifying the sort mode, we also need to specify the way transparent sprites are blended together. The default is BlendState.AlphaBlend, which is what we want to keep. Since there is no SpriteBatch.Begin() call that allows us to specify only the sort mode, we must supply the blend mode, even though it is normally the default.

Why go to all of this trouble creating multiple projects and referencing them instead of including the MapSquare, Camera, and TileMap code directly into the Gemstone Hunter project? Since we are going to need to display the tile engine in both the game and the level editor, splitting it out into its own project and referencing it allows us to create only one set of source files for the map components. If we were to include the code directly in the game, we would need to make another copy of those three items for our map editor project. Any time we need to update the entities, we would need to remember to update them in both places, increasing the chance of introducing errors and inconsistencies into our project.

The map editor project

With the tile engine in place, we are now ready to begin building the map editor that we will use to create levels for the Gemstone Hunter game. The map editor will combine both an XNA Game and a Windows Forms form to take advantage of the Windows Forms controls (menus, buttons, checkboxes, and so on) to save us the time of recreating all of these controls within XNA.

Creating the map editor project

Since we know that we want to create a Windows Forms application for our level editor, it is tempting to use the Windows Forms application template that is included with Visual Studio. However, it is much easier to add a Windows Forms object to an XNA game project than to work the other way around, and try to incorporate all of the components of an XNA project into the Windows Forms template.

Time for action - creating the Level Editor project

  1. In the Solution Explorer window, right-click on the top-most item that reads Solution 'Gemstone Hunter' (3 Projects), and select Add | New Project....

  2. Select the Windows Game (4.0) project template.

  3. Name the project Level Editor, and click on OK.

  4. Right-click on the Level Editor project, and select Add Reference....

  5. On the Projects tab of the Add Reference window, select Tile Engine, and click on OK:

    Time for action - creating the Level Editor project
  6. Right-click on the Level Editor project again, and select Add Content Reference....

  7. Select the Gemstone HunterContent (Content) project, and click OK.

  8. Right-click on the Level Editor project in the Solution Explorer window, and click on Set as StartUp Project.

What just happened?

Your solution now has five separate projects: the game project (simply called Gemstone Hunter), the game's Content project, the Tile Engine game library, the Level Editor, and the Level EditorContent project. Because we have created a content reference in the Level Editor project to the game's content project, all of the game's content will be available in the level editor as well. We will leave the Level Editor Content project empty, but if we needed content items available in the Level Editor that were not needed by the game, we could place them here.

By setting the Level Editor as the startup project, whenever we execute our code from the development environment, the Level Editor will be the application that starts (as opposed to starting the actual game). This setting is just a convenience for us while we work on the editor:

What just happened?

Just like our Gemstone Hunter project, the Level Editor project contains a reference to the Tile Engine project, allowing it to make use of the tile engine code without duplicating it.

Adding a form

We will begin the construction of the Level Editor by adding a Windows Form to our project and linking it to the XNA Game, to allow the output of the game to be displayed on a PictureBox control on the form.

Time for action - adding a form

  1. Right-click on the Level Editor project in Solution Explorer, and select Add | Windows Form.

  2. Name the form MapEditor.vb, and click on the Add button.

  3. The MapEditor form will automatically open in Design mode as a blank window:

    Time for action - adding a form
  4. In the properties window (right-click on the form and select Properties if you have hidden the properties window), set the Size property to 700, 670 pixels.

  5. On the left edge of the screen, open the Toolbox panel (View | Other Windows | Toolbox if it is hidden) and expand the All Windows Forms section. Locate the MenuStrip control and drag an instance of it onto the MapEditor form. Leave the menu items empty for now.

  6. Drag a new PictureBox control from the Toolbox panel onto the form.

  7. Click on the newly created PictureBox, and set the following properties in the Properties window:

    • Name:pctSurface

    • Anchor:Top, Bottom, Left, Right

    • Location:184, 27

    • Modifiers : Public

    • Size:471, 576

  8. Right-click on the MapEditor.vb file in Solution Explorer, and select View Code to open the source code for the MapEditor form.

  9. Add the following Imports directives to the MapEditor class file:

    Imports System.IO
    Imports System.Drawing
    Imports System.Drawing.Imaging
    Imports System.Windows.Forms
    Imports Tile_Engine
    
  10. Add the following declaration to the MapEditor class:

    Public game As Game1
    
  11. In the Level Editor project, double-click on the Program.vb file, and replace the Main() method with the following:

    Sub Main(ByVal args As String())
    Dim form As MapEditor = New MapEditor()
    form.Show()
    form.game = New Game1(
    form.pctSurface.Handle,
    form,
    form.pctSurface)
    form.game.Run()
    End Sub
    
  12. Still in the Level Editor project, open the Game1.vb class file, and add the following declarations to the declarations area:

    Private drawSurface As IntPtr
    Private parentForm As System.Windows.Forms.Form
    Private pictureBox As System.Windows.Forms.PictureBox
    
  13. Replace the Game1 constructor with the following:

    Public Sub New(drawSurface As IntPtr, parentForm As System.Windows.Forms.Form, surfacePictureBox As System.Windows.Forms.PictureBox)
    graphics = New GraphicsDeviceManager(Me)
    Content.RootDirectory = "Content"
    Me.drawSurface = drawSurface
    Me.parentForm = parentForm
    Me.pictureBox = surfacePictureBox
    AddHandler graphics.PreparingDeviceSettings,
    AddressOf graphics_PreparingDeviceSettings
    Mouse.WindowHandle = drawSurface
    End Sub
    
  14. Add the graphics_PreparingDeviceSettings() event handler to the Game1 class:

    Private Sub graphics_PreparingDeviceSettings( sender As object, e As PreparingDeviceSettingsEventArgs)
    With e.GraphicsDeviceInformation.PresentationParameters .DeviceWindowHandle = drawSurface
    End With
    End Sub
    
  15. Execute the project:

    Time for action - adding a form

What just happened?

As it turns out, we can add a form to an XNA Game project in the same way we would add a form to a standard Windows application. The form will not show up by default, however. The Program.vb file is the driver behind the XNA project, and the Main() method gets executed when the application starts.

Note

Other methods

This is not the only way to integrate Windows Forms and XNA. Check out the Winforms samples at the XNA Creators Club website (http://creators.xna.com/en-US/sample/winforms_series1) for additional information. The Creators Club website has many samples on other topics as well.

For a normal XNA Game, the Main() method creates an instance of the Game1 class and calls its Run() method. In order to combine our game with a Windows Form, we have altered this startup process.

Instead of creating an instance of the Game1 class in the Program module (where Main() lives), we create an instance of our MapEditor form class, and then create an instance of Game1 inside the form.

This will simplify addressing components of the Game1 class from the MapEditor form, allowing us to change properties in the game object in response to user interaction with form controls.

We pass the window handle of the PictureBox to the Game1 constructor. The window handle uniquely identifies the display area of PictureBox, allowing our code to redirect XNA's drawing commands from the original game window to the area defined by the PictureBox.

In order to tell XNA that the graphics we draw should be displayed on the PictureBox, we add an event handler to the PreparingDeviceSettings event of the GraphicsDeviceManager class. In this event handler, we simply set the DeviceWindowHandle associated with the graphics device to drawSurface, the value that we passed in when the instance of the Game1 class was created inside Program.vb.

The last thing the constructor does is tell XNA's Mouse class that it should report coordinates relative to the PictureBox that the game will be drawn onto. If we do not make this setting, we will not be able to determine where on our game display the mouse cursor is located.

Alas! We still have a few problems. The most obvious is that there is a big, empty window that shows up on top of our level editor window. This is the window that would normally contain the XNA game. We have moved the output of the XNA drawing commands to a new drawing surface, but the old window still gets created.

The second problem will not be apparent right away, but will cause us trouble when we resize the map editor form while it is running. When the size of the PictureBox changes, we need to let XNA know that the back-buffer size has changed and let our Camera module know that the view port on the display has changed as well.

To address both of these problems, we will add additional event handlers to the system events that occur when the visibility of the game's empty window changes and when the picture box is resized.

Note

Event handlers

Just about everything that happens in Windows happens in response to events. When the user clicks on a button, resizes a window, selects an item from a menu, or any number of other actions, Windows notifies everything that might be impacted by that action that the event has taken place. All Windows Forms controls have event handlers that determine how they respond to those events. In Visual Basic, we can easily add an event handler to an object, even if it already has event handlers, using the AddHandler statement.

Time for action - adding event handlers

  1. Add the following directive to the top of the Level Editor project's Game1 class:

    Imports Tile_Engine
    
  2. Add the following declaration to the declarations area of the Game1 class of the Level Editor project:

    Private gameForm As System.Windows.Forms.Control
    
  3. Add the following code to the constructor of the Game1 class:

    gameForm = System.Windows.Forms.Control. FromHandle(Me.Window.Handle)
    AddHandler gameForm.VisibleChanged, AddressOf gameForm_VisibleChanged
    AddHandler pictureBox.SizeChanged, AddressOf pictureBox_SizeChanged
    
  4. Add the two event handlers to the Game1 class:

    Private Sub gameForm_VisibleChanged(sender As object, e as EventArgs)
    If gameForm.Visible Then
    gameForm.Visible = False
    End If
    End Sub
    event handlersaddingPrivate Sub pictureBox_SizeChanged(sender As object, e As EventArgs)
    If (parentForm.WindowState <>
    System.Windows.Forms.FormWindowState.Minimized) Then
    graphics.PreferredBackBufferWidth = pictureBox.Width
    graphics.PreferredBackBufferHeight = pictureBox.Height
    Camera.ViewPortWidth = pictureBox.Width
    Camera.ViewPortHeight = pictureBox.Height
    graphics.ApplyChanges()
    End If
    End Sub
    
  5. Execute the application.

  6. To end the application, you will need to return to the Visual Studio interface and select Stop Debugging from the Debug menu.

What just happened?

At first, the gameForm_VisibleChanged() method may look odd. It may seem that it would prevent the game from ever being displayed, as it sets the gameForm.Visible property to false, if it ever ends up as true.

Remember, though, that the form (or window), which is automatically generated by XNA, will always be empty. Our game's display is now being redirected to the PictureBox on our MapEditor form. This means that we really do want to make sure that the game's form is never visible. Whenever its visibility changes, we ensure that Visible is set to false, to keep it from appearing.

When the size of the PictureBox changes&mdash;since it is anchored to the sides of the MapEditor form, resizing the form will resize the PictureBox&mdash;we want to update the game's GraphicsDeviceManager with the new size of our display area, and pass those updates along to the Camera module. We need to be careful to check the WindowState of the parent form when processing the resize event. Because the back-buffer width and height must be greater than zero, if we attempt to set them when the form has been minimized, the application will crash.

Clicking on the window close button or pressing Alt + F4 to end the application will no longer work, because the hidden window that Game1 would normally run in will not close automatically.

Filling out our form

Right now, we just have the familiar blue XNA window being displayed on our form. We need to add a number of other controls to the form to build a functional level editor.

Time for action - creating the menu bar

  1. Double-click on the MapEditor.vb file in Solution Explorer to open the MapEditor form in the design window.

  2. Click on the empty MenuStrip that you previously added to the form, and add menu entries for the following items (including the& symbols, which will create keyboard shortcuts for the menu entries):

    &File

    • &Load Map

    • &Save Map

    • : A single dash, creating a separator line

    • E&xit

    &Tools

    • &Clear Map

    &Layer

    • &Background

    • &Interactive

    • &Foreground

    Time for action - creating the menu bar
  3. Double-click on the Exit item under the File menu to have Visual Studio automatically generate an event handler for the Exit menu item.

  4. Enter the following code into the ExitToolStripMenuItem_Click() event handler:

    game.Exit()
    Application.Exit()
    

What just happened?

We now have a standard Windows menu attached to our form with a few entries for our level editor. In order to add code to menu items other than the Exit command, we need to make modifications to our Game1 class, so we will come back to them after we have laid out all of the items on our display.

Note

What are all those ampersands?

In step two, each of the menu item entries contains an ampersand (&) character, usually as the first character in the entry. When the MenuStrip control sees these characters, instead of displaying them in the menu, it causes the next character in the name to be underlined and treated as a shortcut key. By labeling the File menu as &File, the item will be displayed as File, and pressing Alt + F will open the File menu. Items within the File menu can then be accessed by pressing their own shortcut keys (L for Load, S for Save, and so on).

Time for action - tile selection controls

  1. Expand the Textures folder in the Gemstone Hunter Content project.

  2. Click on the PlatformTiles.png file. The Properties window below Solution Explorer will update to display the properties of the image file.

  3. Change the Copy to Output Directory property to Copy if newer.

  4. Switch back to the Design mode view of the MapEditor form.

  5. Add an ImageList control to the MapEditor form by double-clicking on the control in the Toolbox window. It will show up in the gray area below the form, as it is a non-visible control. Set the following properties on the ImageList:

    • Name:imgListTiles

    • ColorDepth:Depth32Bit

    • ImageSize: 48, 48

    Time for action - tile selection controls
  6. Add a ListView control to the MapEditor form, and give it the following properties:

    • Name:listTiles

    • HideSelection:False

    • LargeImageList:imgListTiles

    • Location:10, 27

    • MultiSelect:False

    • Size:173, 315

    • TileSize:48, 48

    • View:Tile

    Time for action - tile selection controls
  7. Right-click on MapEditor.vb in Solution Explorer, and select View Code.

  8. Add the following helper method to the MapEditor class:

    Private Sub LoadImageList()
    Dim filepath As String
    Filepath = Application.StartupPath + "ContentTexturesPlatformTiles.png"
    Dim tileSheet As Bitmap = New Bitmap(filepath)
    Dim tilecount As Integer = 0
    Dim y As Integer
    Dim x As Integer
    Dim tilesAcross As Integer
    Dim tilesDown As Integer
    tilesAcross = CInt(Int(tileSheet.Width / TileMap.TileWidth))
    tilesDown = CInt(Int(tileSheet.Height / TileMap.TileHeight))
    For y = 0 To tilesDown - 1
    For x = 0 To tilesAcross - 1
    Dim newBitmap As Bitmap
    Dim rect As Rectangle
    Rect = New Rectangle(
    x * TileMap.TileWidth,
    y * TileMap.TileHeight,
    TileMap.TileWidth,
    TileMap.TileHeight)
    newBitmap = tileSheet.Clone(
    rect,
    System.Drawing.Imaging.PixelFormat.DontCare)
    imgListTiles.Images.Add(newBitmap)
    Dim itemName As String = ""
    If tilecount = 0 Then
    formtile section controlsitemName = "Empty"
    End If
    If tilecount = 1 Then
    itemName = "White"
    End If
    listTiles.Items.Add(new ListViewItem(itemName, tilecount))
    tileCount += 1
    Next
    Next
    End Sub
    
  9. Double-click on the MapEditor.vb file to reopen the Design mode view of the form.

  10. Double-click on the title bar for the MapEditor window, causing Visual Studio to automatically generate an event handler for the MapEditor_Load event.

  11. Add the following line to the MapEditor_Load() event handler:

    LoadImageList()
    
    
  12. Execute the application:

    Time for action - tile selection controls
  13. End the application by selecting Exit from the File menu.

What just happened?

After executing the project, you should have a scrollable view of the tiles in the tile set in the ListView control in the upper-left corner of the editor window. The LoadImageList() helper method reads the PlatformTiles.png file, and splits it up into tile-sized chunks, which it stores inside the imgListTiles ImageList control.

Notice that we needed to modify the properties of the PlatformTiles.png file so that it gets copied to the output directory, because normally the content pipeline would convert the PNG file into an XNB file, which is unreadable by the standard Windows Bitmap class. The XNB file will still be created and copied, but the PNG file will also be placed in the output folder where our MapEditor code can find it.

As each image is added to the imgListTiles control, entries are also added to the listTiles ListView control. The first and second tiles are given special labels in the ListView (Empty and White), as they will both appear to be empty white squares since the background of the ListView itself is white.

The MapEditor_Load() event handler runs when the form is loaded, as its name implies. We will be expanding on this event handler as we add more controls to the form, since it gives us a convenient place to perform initialization.

Time for action - scroll bars

  1. Double click on the MapEditor.vb file in Solution Explorer to reopen the map editor in the design view if it is not already open.

  2. In the Toolbox window, double-click on the VScrollBar control, to add it to the form. Give it the following properties:

    • Name:VScrollBar1

    • Anchor:Top, Bottom, Right

    • LargeChange:48

    • Location:658, 27

    • Size:17, 576

  3. In the Toolbox window, double-click on the HScrollBar control to add it to the form. Give it the following properties:

    • Name:HScrollBar1

    • Anchor:Bottom, Left, Right

    • LargeChange:48

    • Location:184, 606

    • Size:474, 17

  4. Switch back to the code view for the MapEditor.vb file, and add the FixScrollBarScales() helper method to the MapEditor class:

    Private Sub FixScrollBarScales()
    Camera.ViewPortWidth = pctSurface.Width
    Camera.ViewPortHeight = pctSurface.Height
    Camera.Move(Vector2.Zero)
    VScrollBar1.Minimum = 0
    VScrollBar1.Maximum = Camera.WorldRectangle.Height - Camera.ViewPortHeight
    HScrollBar1.Minimum = 0
    HScrollBar1.Maximum = Camera.WorldRectangle.Width - Camera.ViewPortWidth
    formscroll barsEnd Sub
    
  5. Edit the MapEditor_Load() method to include a call to FixScrollBarScales():

    FixScrollBarScales()
    
  6. Double-click on MapEditor.vb in Solution Explorer to reopen the Design mode view of the MapEditor form.

  7. Click on the title bar of the MapEditor window to select the form as the active control.

  8. In the Properties window, ensure that the drop-down box at the top of the window reads MapEditor System.Windows.Forms.Form.

  9. Still in the Properties widow, click on the yellow lightning bolt button in the toolbar to switch the view from properties to event handlers:

    Time for action - scroll bars
  10. Scroll down to locate the Resize event, and double-click on the empty box to the right of the event name, causing Visual Studio to automatically generate an event handler for the MapEditor_Resize event.

  11. Add the following to the MapEditor_Resize() method:

    FixScrollBarScales()
    
  12. Switch back to properties view in the Properties window by going back to the Design mode view of the form and clicking on the small page icon to the left of the lightning bolt icon in the Properties window toolbar.

What just happened?

We now have scroll bars attached to the sides of the game's display area. When the form is initially displayed, and then again whenever it is resized, the scroll bars will be rescaled so that they cover the entire area of the game's tile map.

We will use these scroll bars to move around on the map while editing, though their actual implementation will again be tied to changes to the Game1 class.

Time for action - final controls

  1. Add a GroupBox to the MapEditor form, and give it the following properties:

    • Name:groupBoxRightClick

    • Location:10, 346

    • Size:173, 103

    • Text:Right Click Mode

  2. Add a RadioButton control inside the groupBoxRightClick GroupBox by dragging it from the Toolbox window and dropping it inside the Groupbox. Give it the following properties:

    • Name:radioPassable

    • Checked:True

    • Location:6, 17

    • Text:Toggle Passable

  3. Add another RadioButton control inside the groupBoxRightClick control with the following properties:

    • Name:radioCode

    • Location:6, 35

    • Text:Code

  4. Add a TextBox control inside the groupBoxRightClick control, with the following properties:

    • Name:txtNewCode

    • Location:62, 36

    • Size:103, 20

  5. Add a Label control inside the groupBoxRightClick, with the following properties:

    • Name:lblCurrentCode

    • Location:60, 59

    • Text:---

  6. Add a ComboBox control inside the groupBoxRightClick, with the following properties:

    • Name:cboCodeValues

    • DropDownStyle:DropDownList

    • Location:5, 75

    • Size:160, 21

  7. Add a Label control below the group box, with the following properties:

    • Name:lblMapNumber

    • Location:12, 452

    • Text:Map Number

  8. Add a ComboBox control below the group box, with the following properties:

    • Name:cboMapNumber

    • DropDownStyle:DropDownList

    • Location:81, 452

    • Size:94, 21

  9. Modify the MapEditor_Load() method of the MapEditor class, by adding the following to the existing code:

    cboCodeValues.Items.Clear()
    cboCodeValues.Items.Add("Gemstone")
    cboCodeValues.Items.Add("Enemy")
    cboCodeValues.Items.Add("Lethal")
    cboCodeValues.Items.Add("EnemyBlocking")
    cboCodeValues.Items.Add("Start")
    cboCodeValues.Items.Add("Clear")
    cboCodeValues.Items.Add("Custom")
    For x As Integer = 0 To 99
    cboMapNumber.Items.Add(x.ToString().PadLeft(3, CChar("0")))
    Next
    cboMapNumber.SelectedIndex = 0
    TileMap.EditorMode = True
    
  10. Return to the Design view of the MapEditor form, and double-click on the cboCodeValues combo box (the combo box inside the group box). Update the automatically generated event handler to read:

    Private Sub cboCodeValues_SelectedIndexChanged(
    sender As System.Object, e As System.EventArgs
    ) Handles cboCodeValues.SelectedIndexChanged
    formfinal controlstxtNewCode.Enabled = False
    Select Case ( cboCodeValues.Items(cboCodeValues.SelectedIndex).ToString())
    Case "Gemstone"
    txtNewCode.Text = "GEM"
    Case "Enemy"
    txtNewCode.Text = "ENEMY"
    Case "Lethal"
    txtNewCode.Text = "DEAD"
    Case "EnemyBlocking"
    txtNewCode.Text = "BLOCK"
    Case "Start"
    txtNewCode.Text = "START"
    Case "Clear"
    txtNewCode.Text = ""
    Case "Custom"
    txtNewCode.Text = ""
    txtNewCode.Enabled = True
    End Select
    End Sub
    
  11. Execute the application:

    Time for action - final controls

What just happened?

We now have all of the interactive controls that we need for our level editor. The codes drop-down box provides a number of standard code values that we will use during level creation, with the ability to add custom codes, which are entered in the textbox above it.

Updating the Game1 class

We currently have a Windows form that contains our XNA Game display, but the two pieces of the level editor application do not yet share any information, or allow the user to actually edit maps. It is time to begin updating our game to support level editing.

Time for action - updating Game1

  1. Double-click on the Game1.vb file in the Level Editor project to open it in the editor.

  2. Add the following declarations to the Game1 declarations area:

    Public DrawLayer As Integer = 0
    Public DrawTile As Integer = 0
    Public EditingCode As Boolean = False
    Public CurrentCodeValue As String = ""
    Public HoverCodeValue As String = ""
    Public lastMouseState As MouseState
    Private vscroll As System.Windows.Forms.VScrollBar
    Private hscroll As System.Windows.Forms.HScrollBar
    
  3. Add the following lines to the Game1 constructor:

    vscroll = CType(parentForm.Controls("VScrollBar1"), System.Windows.Forms.VScrollBar)
    hscroll =CType(parentForm.Controls("HScrollBar1"), System.Windows.Forms.HScrollBar)
    
  4. Modify the LoadContent() method of the Game1 class to read:

    Protected Overrides Sub LoadContent()
    spriteBatch = new SpriteBatch(GraphicsDevice)
    Camera.ViewPortWidth = pictureBox.Width
    Camera.ViewPortHeight = pictureBox.Height
    Camera.WorldRectangle = New Rectangle(
    0,
    0,
    TileMap.TileWidth * TileMap.MapWidth,
    TileMap.TileHeight * TileMap.MapHeight
    )
    TileMap.Initialize( Content.Load(Of Texture2D)("TexturesPlatformTiles"))
    TileMap.spriteFont = Content.Load(Of SpriteFont)("FontsPericles8")
    lastMouseState = Mouse.GetState()
    pictureBox_SizeChanged(Nothing, Nothing)
    End Sub
    
  5. Modify the Draw() method of the Game1 class to read:

    Protected Overrides Sub Draw(gameTime As GameTime)
    GraphicsDevice.Clear(Color.Black)
    spriteBatch.Begin(
    SpriteSortMode.BackToFront,
    BlendState.AlphaBlend)
    TileMap.Draw(spriteBatch)
    spriteBatch.End()
    MyBase.Draw(gameTime)
    End Sub
    

What just happened?

To simplify communications between the Windows Form and the XNA Game, we have declared a number of public member variables that our Windows Form code will be able to update in response to user-generated events. We have also loaded a SpriteFont to draw code values with, and a MouseState variable to hold the state of the mouse between frames.

Finally, we declare two objects that reference the scroll bars on the level editor form. We will use these to sync-up the display of the tile map to the location of the scroll bars.

The LoadContent() method is fairly standard, setting the size of the tile map, and loading the tile images and sprite font. The TileMap class spriteFont member is set to the font we loaded, and the lastMouseState member is initialized. Right before exiting, the LoadContent() method calls the pictureBox_SizeChanged() method, to make sure that the graphics device has the proper dimensions for the display window.

In our Draw() method, we have again used the expanded form of the SpriteBatch.Begin() call in order to specify the SpriteSortMode.BackToFront parameter.

Time for action - the Game1 Update method

  1. Replace the current Update() method in the Game1 class with the following:

    Protected Overrides Sub Update(gameTime As GameTime)
    Camera.Position = new Vector2(hscroll.Value, vscroll.Value)
    Dim ms As MouseState = Mouse.GetState()
    if ((ms.X > 0) And (ms.Y > 0) And (ms.X < Camera.ViewPortWidth) And (ms.Y < Camera.ViewPortHeight)) Then
    Dim mouseLoc As Vector2 = Camera.ScreenToWorld( new Vector2(ms.X, ms.Y))
    If Camera.WorldRectangle.Contains( CInt(mouseLoc.X), CInt(mouseLoc.Y)) Then
    If ms.LeftButton = ButtonState.Pressed Then
    TileMap.SetTileAtCell( TileMap.GetCellByPixelX(CInt(mouseLoc.X)),
    TileMap.GetCellByPixelY(CInt(mouseLoc.Y)),
    DrawLayer,
    DrawTile)
    End If
    If (ms.RightButton = ButtonState.Pressed) And (lastMouseState.RightButton = ButtonState.Released) Then
    If EditingCode Then
    TileMap.GetMapSquareAtCell( TileMap.GetCellByPixelX(CInt(mouseLoc.X)),
    TileMap.GetCellByPixelY(CInt(mouseLoc.Y))
    ).CodeValue = CurrentCodeValue
    Else
    TileMap.GetMapSquareAtCell(
    TileMap.GetCellByPixelX(CInt(mouseLoc.X)),
    TileMap.GetCellByPixelY(CInt(mouseLoc.Y))
    ).TogglePassable()
    End If
    End If
    HoverCodeValue = TileMap.GetMapSquareAtCell(
    TileMap.GetCellByPixelX(
    CInt(mouseLoc.X)),
    TileMap.GetCellByPixelY(
    CInt(mouseLoc.Y))).CodeValue
    End If
    End If
    lastMouseState = ms
    MyBase.Update(gameTime)
    End Sub
    
  2. Execute the application. Attempting to draw tiles at this point results in black holes being punched in the all-blue map:

    Time for action - the Game1 Update method

What just happened?

The first thing our Update() method does is to set the game's camera position, based on the current values of the horizontal and vertical scroll bars on the level editor form. When we scroll the scroll bars, the game map will scroll as well.

Next, we verify that the mouse coordinates are within the view port of the camera and thus within the PictureBox control on the editor form. If they are, we determine the world-based coordinates of the mouse cursor.

If the left mouse button has been pressed, we update the tile under the cursor on the current DrawLayer with the current DrawTile. The right mouse button is a bit more complicated.

We will use the right mouse button to set two different types of information&mdash;either toggling on and off the Passable property of the MapSquare, or setting its CodeValue property, depending on the mode selected on the MapEditor form.

In either case, we will only make a change to the underlying tile if the right mouse button is pressed during this frame and was not pressed during the previous frame. This eliminates updating the same square multiple times on a single button press (which would make toggling passability on and off difficult!) During the first frame after the button is pressed, the MapSquare will be updated. Until the button is released, no other updates will occur.

The HoverCodeValue member is set to the CodeValue of the MapSquare under the mouse cursor. This value will be used by the MapEditor form and displayed on a label on the screen to provide an alternative method of viewing the code for an individual square.

Finally, lastMouseState is updated to the current mouse state, and the Update() method is completed.

Connecting the form to the game

As we saw at the end of the last section, attempting to draw tiles to the map at this point only leaves empty spaces. This is because the DrawLayer and DrawTile integers both default to zero, so we are drawing a fully-transparent tile onto the background layer.

Now that we have made the necessary updates to the XNA side of the level editor, we need to flesh out the event handlers for the controls we placed on the MapEditor form to update the control variables inside the Game1 class.

Time for action - completing the editor part 1

  1. Open the MapEditor form in Design mode.

  2. Double-click on the listTiles ListView control to automatically generate an event handler for the SelectedIndexChanged event. Update the event code to read:

    Private Sub listTiles_SelectedIndexChanged( sender As System.Object, e As System.EventArgs) Handles listTiles.SelectedIndexChanged
    If listTiles.SelectedIndices.Count > 0 Then
    game.DrawTile = listTiles.SelectedIndices(0)
    End If
    End Sub
    
  3. Return to the Design view of the MapEditor form, and double-click on the radio button labeled Toggle Passable to generate a handler for the CheckChanged event. Update the event handler to read:

    Private Sub radioPassable_CheckedChanged( sender As System.Object, e As System.EventArgs) Handles radioPassable.CheckedChanged
    If Not IsNothing(game) Then
    If radioPassable.Checked Then
    game.EditingCode = False
    Else
    game.EditingCode = True
    End If
    End If
    End Sub
    
  4. Return to the Design view, and double-click on the Code radio button. Update the CheckChanged event handler to read:

    Private Sub radioCode_CheckedChanged( sender As System.Object, e As System.EventArgs) Handles radioCode.CheckedChanged
    If Not IsNothing(game) Then
    If radioPassable.Checked Then
    game.EditingCode = False
    Else
    game.EditingCode = True
    End If
    End If
    End Sub
    
  5. Return to the Design view, and double-click on the txtNewCode textbox. Update the TextChanged event handler to read:

    Private Sub txtNewCode_TextChanged( sender As System.Object, e As System.EventArgs) Handles txtNewCode.TextChanged
    game.CurrentCodeValue = txtNewCode.Text
    End Sub
    
  6. Return to the Design view, and double-click on the Background menu item under the Layer menu. Update the Click event handler to read:

    Private Sub BackgroundToolStripMenuItem_Click( sender As System.Object, e As System.EventArgs) Handles BackgroundToolStripMenuItem.Click
    game.DrawLayer = 0
    backgroundToolStripMenuItem.Checked = True
    interactiveToolStripMenuItem.Checked = False
    foregroundToolStripMenuItem.Checked = False
    End Sub
    
  7. Generate a Click event handler for the Interactive layer menu item, and update it to read:

    Private Sub InteractiveToolStripMenuItem_Click( sender As System.Object, e As System.EventArgs) Handles InteractiveToolStripMenuItem.Click
    game.DrawLayer = 1
    backgroundToolStripMenuItem.Checked = False
    interactiveToolStripMenuItem.Checked = True
    foregroundToolStripMenuItem.Checked = False
    End Sub
    
  8. Generate a Click event handler for the Foreground layer menu item, and update it to read:

    Private Sub ForegroundToolStripMenuItem_Click( sender As System.Object, e As System.EventArgs) Handles ForegroundToolStripMenuItem.Click
    game.DrawLayer = 2
    backgroundToolStripMenuItem.Checked = False
    interactiveToolStripMenuItem.Checked = False
    foregroundToolStripMenuItem.Checked = True
    End Sub
    
  9. In the MapEditor_Load() method, add the following to the end of the method to indicate the starting layer for the editor:

    backgroundToolStripMenuItem.Checked = True
    
  10. Execute the application, and use the editor to draw tiles to the map:

    Time for action - completing the editor part 1
  11. Maximize the display window, and attempt to use the scroll bars to scroll around the map display.

What just happened?

All of our event handlers for the form controls simply pass information along to the appropriate variables in the Game1 class. Selecting a layer from the menu bar updates the DrawLayer value, while the radio buttons toggle the EditingCode variable between true and false. We need to wrap these statements in "If Not IsNothing(game) Then" statements, because the first time these events will be executed is before the game object has been created.

Any time the txtNewCode control's Text property is changed, the current contents of the TextBox are copied to the CurrentCodeValue member of the Game1 class.

In step 11, you probably noticed that the game window does not update the current position when you are scrolling through the scroll bars until you release the mouse button, even though the scroll bar's marker moves the whole time. This happens because the movement of the scroll bar is preventing the game loop from executing while the user is in the process of manipulating the scroll bars.

In addition, the scroll bars do not match up to the map window until we resize the display at least once. This is because the form's Load event happens prior to the initialization of the Game1 instance (if you go back and look at Program.vb, the form is created and shown first, and then the game is initialized), so the WorldRectangle property of the Camera module is {0, 0, 0, 0}. This results in FixScrollBarScales() setting negative numbers for the maximum values of the scroll bars.

We can fix both of these problems by adding a timer to the form that initially calls FixScrollBarScales() and repeatedly calls the game's Tick() method.

Time for action - fixing the scrolling delay

  1. Reopen the Design mode view of the MapEditor window.

  2. Double-click on the timer control in the Toolbox window to add a new instance to the MapEditor form. As with the ImageList control, the timer is not visible and will appear in the editor as an icon and label below the design window. Give the timer control the following properties:

    • Name:timerGameUpdate

    • Enabled:True

    • Interval:20

  3. Double-click on the timerGameUpdate control to generate a Tick event handler, and add the following code to it:

    Private Sub timerGameUpdate_Tick( sender As System.Object, e As System.EventArgs) Handles timerGameUpdate.Tick
    If hScrollBar1.Maximum < 0 Then
    FixScrollBarScales()
    End If
    game.Tick()
    If game.HoverCodeValue <> lblCurrentCode.Text Then
    lblCurrentCode.Text = game.HoverCodeValue
    End If
    End Sub
    
  4. Execute the application. Draw a few tiles on the map, and use the scroll bars to verify that they function as expected.

What just happened?

Using the scroll bars does not prevent the timer control from firing its Tick event, so by executing the game's Tick() method from within the timerGameUpdate_Tick() event handler, we can force the game's Update() and Draw() methods to run even when they normally would not.

The last item in the timerGameUpdate_Tick() handler checks to see if the HoverCodeValue inside the Game1 class has been updated, since it was last copied to the label displaying it on the Windows Form. If it has, the form label is updated as well.

Loading and saving maps

The last thing we need to address to complete the Gemstone Hunter Level Editor is how we will load and save our map files. There are a number of ways we could store our level maps, but we will implement a very simple method that does not require parsing XML or creating a textfile with a special format to store the map.

Time for action - implementing loading and saving

  1. In the Tile Engine project, open the TileMap.vb module file.

  2. Add the Loading and Saving Maps region to the TileMap module:

    #Region "Loading and Saving Maps"
    Public Sub SaveMap(fileToSave As FileStream)
    Dim formatter As BinaryFormatter = new BinaryFormatter()
    formatter.Serialize(fileToSave, mapCells)
    fileToSave.Close()
    End Sub
    Public Sub LoadMap(fileToLoad As FileStream)
    Try
    Dim formatter As BinaryFormatter = new BinaryFormatter()
    mapCells = CType(formatter.Deserialize(fileToLoad), MapSquare(,))
    fileToLoad.Close()
    Catch
    ClearMap()
    End Try
    End Sub
    Public Sub ClearMap()
    For x As Integer = 0 To MapWidth - 1
    For y As Integer = 0 To MapHeight - 1
    For z As Integer = 0 To MapLayers - 1
    mapCells(x, y) = New MapSquare(2, 0, 0, "", True)
    Next
    Next
    Next
    End Sub
    #End Region
    
  3. Back in the Level Editor project, open the MapEditor form in Design mode.

  4. Double-click on the Load Map item in the File menu to create the Click event handler, and update its code to read:

    Private Sub LoadMapToolStripMenuItem_Click( sender As System.Object, e As System.EventArgs) Handles LoadMapToolStripMenuItem.Click
    Try
    TileMap.LoadMap(New FileStream( Application.StartupPath + "MAP" + cboMapNumber.Items(cboMapNumber.SelectedIndex).ToString() + ".MAP", FileMode.Open))
    Catch
    System.Diagnostics.Debug.Print("Unable to load map file")
    End Try
    End Sub
    
  5. Double-click on the Save Map item in the File menu, and update its Click handler to read:

    Private Sub SaveMapToolStripMenuItem_Click( sender As System.Object, e As System.EventArgs) Handles SaveMapToolStripMenuItem.Click
    TileMap.SaveMap(New FileStream( Application.StartupPath + "MAP" + cboMapNumber.Items(cboMapNumber.SelectedIndex).ToString() + ".MAP", FileMode.Create))
    End Sub
    
  6. Double-click on the Clear Map item in the Tools menu, and update its Click handler to read:

    Private Sub ClearMapToolStripMenuItem_Click( sender As System.Object, e As System.EventArgs) Handles ClearMapToolStripMenuItem.Click
    TileMap.ClearMap()
    End Sub
    
  7. Execute the application and create a simple map. Save it to disk, update the map, and reload it.

What just happened?

When a map is saved, we create a FileStream object, which represents our file on disk. We then create an instance of the BinaryFormatter class and call its Serialize() method, passing in the stream to serialize the data from the object we wish to serialize. In our case, it is the array containing the MapSquare objects that represent our game map.

When loading the map, the process is exactly the same, except that we use the Deserialize() method of the BinaryFormatter class to reverse the process, converting the binary data on disk back into its in-memory representation. By surrounding the attempt to load the map with a Try...End Try block, we can take action (clearing the map to an empty blue sky) instead of simply crashing the level editor.

From the Windows Form side of the system, we call the new LoadMap() and SaveMap() methods, passing in the FileStream object that is created based on the cboMapNumber drop-down list. Our maps will be saved in files named "MAP###.MAP", with the three-digit number taken from the cboMapNumber list. While using our maps in the Level Editor, they will be stored in the same directory our Level Editor executable is running from (normally, the Visual Studio 2010ProjectsGemstone HunterLevel EditorLevel Editorinx86debug folder inside your Documents folder). In the following screenshot, we have created a sample map using the level editor. This map currently has no passability or code information on it, and it contains only tile information for now:

What just happened?

Passability

When building maps, we will use the right mouse button to toggle each individual map square as either passable or impassable. When a square is marked as impassable, it will be tinted red by the editor, indicating that both the monster and the player will treat the square as a solid wall, no matter what visual representation the square has.

Without this information, the player would fall straight through the level and off the map&mdash;something we will need to account for in the game engine when we build it in the next chapter.

Map codes

Each MapSquare can be assigned a code value that will allow the game to implement special behavior for that square. We have pre-defined a handful of code values including:

  • Gemstone (GEM): A gem will be spawned at this location for the player to collect.

  • Enemy (ENEMY): An enemy will be spawned at this location.

  • Lethal (DEAD): Contacting this square will kill the player.

  • Enemy Blocking (BLOCK): Players can move through these squares, but enemies will treat them as walls. This allows us to confine enemies to an elevated platform, for example.

  • Start (START): If no position is set because of a map transition, the player will start the map in this square.

In addition, we will define a special code for map transitions. In the following image, we have a code value of T-001-03-10 on the MapSquare containing the door into the brick building. At runtime, we will interpret this code value to mean: Transition (T) to map 001, at location 03, 10. In this way, we can link maps together and allow the player to move between them:

Map codes

This image shows the map from the previous section with both passability and code information filled in. In Editor mode, the TileEngine class displays the code values as text blocks on each map square. These codes mean nothing to the editor, so just like passability information, we will need to account for it in the game engine.

One last issue

Remember way back when we hid the empty Game1 form, the one where we added code to the Exit menu item to properly terminate the application? We can clear up what happens when the user clicks on the X button in the upper-right corner of the window to close the application.

Time for action - handling the FormClosed event

  1. Open the MapEditor form in Design mode.

  2. Select the form as the current object by clicking on the form's title bar in the Design window.

  3. Switch to Event editing mode in the Properties window by clicking on the lightning bolt button.

  4. Scroll down to the FormClosed event, and double-click in the empty box to the right of the event name to create the MapEditor_FormClosed() event handler.

  5. Add the following code to the MapEditor_FormClosed() event handler:

    game.Exit()
    Application.Exit()
    

What just happened?

When the form closes, we need to shut down both the XNA game and the overall application, otherwise the system will not release the resources, and the program will still be running invisibly in the background.

Have a go hero

The Gemstone Hunter Level Editor project is fairly rough around the edges. It is not exactly a model example of Windows Forms development, but then few purpose-built internal game development tools are.

If you feel like diving further into Windows Forms development, here are a few suggestions for improving on the level editor:

  • Currently, the level editor does not alert you if you try to load a map after you have made changes to the current map. By adding checks to the Update() method of the Game1 class, you could flag the map as having changed and issue the appropriate warnings to the user when they try to load a new map.

  • Marking squares as impassable requires an individual click on each square. You could expand the number of radio buttons to include marking squares as passable and impassable as separate tasks, thus allowing the user to hold down the mouse button and draw large blocks of impassable squares.

  • On the more game-focused side of things, try creating a few levels! The level editor supports up to 100 levels (000 through 099), so there is plenty of room to experiment.

Summary

We now have a working&mdash;if not pretty&mdash;level editor. In this chapter, we:

  • Added multiple layers and other types of data to the TileMap class that we built for Robot Rampage

  • Created a multi-project Visual Studio solution that shares code between projects

  • Added a Windows Forms form to our XNA Game Studio project and modified the program's startup process to render the form to a PictureBox control on the form

  • Implemented methods to allow communication between the Windows Form and the XNA game, including synchronized scroll bars, and updating member variables in the Game1 class in response to Windows Forms controls events

  • Implemented methods to load and save map files through the BinaryFormatter class

In the next chapter, we will flesh out the Gemstone Hunter project and cover the basics of building a platform-style game using the maps that we created with the level editor from this chapter.

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

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