Chapter 11. Rendering a Game Level

We’re going to take our first steps into a new world in this chapter: we will be learning how to load a game level created with the custom level editor developed in Chapter 10. This editor gives us considerable leeway in how the game will play, and we will explore some of the gameplay possibilities related to the game world in this chapter. You will learn the techniques used in tile-based scrolling, gaining an understanding of how a scrolling display is created using a “partial-tile” buffered scrolling algorithm. We’ll be using level data from our custom level editor program to construct the game world. By using a small surface about the same size as the screen, the tiles are drawn to the buffer in order to produce a smooth-scrolling game world. The resulting tile scrolling engine is the foundation for the Celtic Crusader game.

Here’s what we’ll cover in this chapter:

Mapping the Game World

The first thing I discovered when attempting to create ninth-century Ireland using the level editor was that this game world has the potential of being huge. To manage the memory requirements of the game world, it should be divided up into many level files and linked together with portals. We’ll learn more about portals in Chapter 13, “Using Portals to Expand the World.” In the meantime, we’ll focus on just rendering the tilemap for one level and begin to interact with the level. The goal is to put as much functionality into the rendering and interaction of one level as possible, because that very same functionality (such as preventing the characters from passing through solid objects) extends to any level. Getting one small portion of the game world up and running means that the entire game world can be rendered by the game engine based solely on the data files.

This is what we call data-driven programming—where the data describes what should happen, and the source code processes the data according to known rules. So, what we’re doing here is applying professional software engineering methodology to our role-playing game engine. When you want to add a new feature to the game, and that feature is added to the engine based on properties in the level file, then suddenly every level you create has that feature. For example, let’s consider collision tiles, because that is a feature we will be addressing shortly. The level editor lets us specify which tiles are collidable, and which tiles may be passed through. The collidable tiles should block the player from moving through them. Or, put differently: the game should prevent the player from moving through collidable—or let us say solid—tiles on the map. Any tile can be made solid by setting the property.

Tile-Based Ireland

Take a look at Figure 11.1, which shows the original map of ninth-century Ireland first introduced back in Chapter 9. As you may recall, this map was drawn by hand, scanned, and then cleaned up and enhanced. Why does the game need such a large game world? Actually, we don’t need to re-create the entire land mass in the game, but having a good story up front helps with the gameplay since there are essentially endless places we can take the player in the game with such a large game world available. The gameplay we focus on just revolves around one area, but we could model any area of this game world using the level editor.

The original digitized and enhanced map of ninth-century Ireland.

Figure 11.1. The original digitized and enhanced map of ninth-century Ireland.

The huge size of the game world in Celtic Crusader should be very encouraging to you, especially if you have a good imagination and you would like to create a massive RPG with a long, complex storyline and intense, varied character-development features. But this is just the beginning. By using portals, we could even open a “doorway” on the game world map into a dungeon, then after looting and pillaging (in classic RPG style), your character can return to the regular map again.

Trying to render the entire game world in one giant tile scroller tends to reduce the gameplay, and that was certainly the case with the game in the previous edition of this book. Now, we’re taking an entirely different approach. Instead of editing one gigantic tilemap, we’re going to create many smaller tilemaps with our own level editor. As you learned in the previous chapter, our tilemaps have a maximum size of 128 tiles across, and 128 tiles down, for a total pixel resolution of 4096 × 4096. Figure 11.2 shows the level editor enlarged so that more of this particular level is visible in the editor window. However, we don’t have to actually use that full size for every map. By defining regions with the collidable property, we can limit the player’s movements to a smaller area surrounded by trees or mountains or anything you want. Although the tilemap will remain fixed at 128 × 128, we can use much smaller areas, as well as combine several tilemaps (via portals) to create larger areas. The gameplay possibilities are endless!

The level editor window is resizable for large widescreen displays.

Figure 11.2. The level editor window is resizable for large widescreen displays.

Trick

I want to emphasize again that the game world truly has no limit when using level files with portal tiles. When a portal is entered, the game teleports the player to a new location. By using a portal file as well as the coordinates, we can even load up a new level file entirely and position the player at any location in that new file with the same technique. Furthermore, teleporting the player will be almost instantaneous. Which means, you could create a huge level with seamless edges by creating a strip of portal tiles along one edge so that when the player reaches that edge, the player continues to walk in the same direction. If the two level files are seamless in their edges, the player will never know they have just entered a new level file!

Loading and Drawing Level Files

Our custom level editor that was developed in Chapter 10 produces .xml files containing information about a game level. We can load the .xml file using .NET classes in the System.Xml namespace—so loading is not a problem. Rendering the level is where we’ll focus most of our attention. First, let’s just look at loading the data from a level file and render one screen full of tiles with it as a starting point. Until now, we have only seen game levels inside the editor, but now we’ll be able to render the level with Basic code. To render a level, we need two things: 1) The tilemap data from the .xml file; and 2) The source tiles stored in a bitmap file. The level file describes the tile number that should be represented at each location in the game level. Here is the source code for the Level Viewer, shown in Figure 11.3. This example does not know how to scroll, but it’s a good start.

The Level Viewer demo displays just the upper-left corner of the game world.

Figure 11.3. The Level Viewer demo displays just the upper-left corner of the game world.

Imports System.Xml
Public Class Form1
    Public Structure tilemapStruct
        Public tilenum As Integer
        Public data1 As String
        Public collidable As Boolean
    End Structure

    Const COLUMNS As Integer = 5
    Private bmpTiles As Bitmap
    Private bmpSurface As Bitmap
    Private pbSurface As PictureBox
    Private gfxSurface As Graphics
    Private fontArial As Font
    Private tilemap() As tilemapStruct

    Private Sub Form1_FormClosed(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.FormClosedEventArgs) _
            Handles Me.FormClosed
        bmpSurface.Dispose()
        pbSurface.Dispose()
        gfxSurface.Dispose()
    End Sub

    Private Sub Form1_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        Me.Text = "Level Viewer"
        Me.Size = New Point(800 + 16, 600 + 38)
        REM create tilemap
        ReDim tilemap(128 * 128)
        REM set up level drawing surface
        bmpSurface = New Bitmap(800, 600)
        pbSurface = New PictureBox()
        pbSurface.Parent = Me
        pbSurface.BackColor = Color.Black
        pbSurface.Dock = DockStyle.Fill
        pbSurface.Image = bmpSurface
        gfxSurface = Graphics.FromImage(bmpSurface)
        REM create font
        fontArial = New Font("Arial Narrow", 8)
        REM load tiles bitmap
        bmpTiles = New Bitmap("palette.bmp")

    REM load the tilemap
    loadTilemapFile("tilemap.xml")
    drawTilemap()
End Sub

Private Sub loadTilemapFile(ByVal filename As String)
    Try
        Dim doc As XmlDocument = New XmlDocument()
        doc.Load(filename)
        Dim nodelist As XmlNodeList = doc.GetElementsByTagName("tiles")
        For Each node As XmlNode In nodelist
            Dim element As XmlElement = node
            Dim index As Integer = 0
            Dim value As Integer = 0
            Dim data1 As String = ""
            Dim collidable As Boolean = False
            REM read tile index #
            index = Convert.ToInt32( _
                element.GetElementsByTagName("tile")(0).InnerText)
            REM read tilenum
            value = Convert.ToInt32( _
                element.GetElementsByTagName("value")(0).InnerText)
            REM read data1
            data1 = Convert.ToString( _
                element.GetElementsByTagName("data1")(0).InnerText)
            REM read collidable
            collidable = Convert.ToBoolean( _
                element.GetElementsByTagName("collidable")(0).InnerText)
            tilemap(index).tilenum = value
            tilemap(index).data1 = data1
            tilemap(index).collidable = collidable
        Next
    Catch es As Exception
        MessageBox.Show(es.Message)
    End Try
End Sub

Private Sub drawTilemap()
    For x = 0 To 24
        For y = 0 To 18
            drawTileNumber(x, y, tilemap(y * 128 + x).tilenum)

            Next
        Next
    End Sub

    Public Sub drawTileNumber(ByVal x As Integer, ByVal y As Integer, _
           ByVal tile As Integer)
        REM draw tile
        Dim sx As Integer = (tile Mod COLUMNS) * 33
        Dim sy As Integer = (tile  COLUMNS) * 33
        Dim src As New Rectangle(sx, sy, 32, 32)
        Dim dx As Integer = x * 32
        Dim dy As Integer = y * 32
        gfxSurface.DrawImage(bmpTiles, dx, dy, src, GraphicsUnit.Pixel)
        REM save changes
        pbSurface.Image = bmpSurface
    End Sub

    Private Sub Form1_KeyUp(ByVal sender As System.Object, _
            ByVal e As System.Windows.Forms.KeyEventArgs) _
            Handles MyBase.KeyUp
        If e.KeyCode = Keys.Escape Then End
    End Sub
End Class

Introduction to Tiled Scrolling

What is scrolling? In today’s gaming world, where 3D is the focus of everyone’s attention, it’s not surprising to find gamers and programmers who have never heard of scrolling. What a shame! The heritage of modern games is a long and fascinating one that is still relevant today, even if it is not understood or appreciated. The console industry puts great effort and value into scrolling, particularly on handheld systems such as the Nintendo DS. Scrolling is the process of displaying a small window of a larger virtual game world. There are three basic ways to scroll the display:

  • Loading a large tiled bitmap image

  • Creating a large bitmap out of tiles at runtime

  • Drawing tiles directly on the screen

Figure 11.4 illustrates the concept of scrolling, which, in essence, involves the use of a large game world of which only a small portion is visible through the screen at a time.

The scroll window shows a small part of a larger game world.

Figure 11.4. The scroll window shows a small part of a larger game world.

The key to scrolling is having something in the virtual game world to display in the scroll window (or the screen). Also, I should point out that the entire screen need not be used as the scroll window. It is common to use the entire screen in scrolling-shooter games, but role-playing games (RPGs) often use a smaller window on the screen for scrolling, using the rest of the screen for gameplay (combat, inventory, and so on) and player/party information, as shown in Figure 11.5.

Some games use a smaller portion of the game screen for a scrolling window.

Figure 11.5. Some games use a smaller portion of the game screen for a scrolling window.

You could display one huge bitmap image in the virtual game world representing the current level of the game (or the map), and then copy a portion of that virtual world onto the screen. This is the simplest form of scrolling. Another method uses tiles to create the game world at runtime. Suppose we had a large bitmap file containing a pre-rendered image of the game world. You would then load up that large bitmap and copy a portion of it to the screen, and that portion would represent the current scroll position.

Constructing the Tiled Image

This theory of using a single large bitmap seems reasonable at first glance, but that method of scrolling has a very serious limitation. When you create a game world, the whole point is to interact with that game world. A single, large bitmap used to render the game world prevents you from actually tracking where the player is located on the map, as well as what other objects are on the map. In a tile-based game world, each tile is represented by a number, and that number has meaning. A tile containing a tree is impassable, whereas a tile of grass can be walked on. Of course, you could create a new array or some other method to keep track of the player, various enemies, and objects in the game world, but that requires a lot of extra work. There’s a better way to do it. A high-speed scrolling arcade game automatically scrolls horizontally or vertically, displaying ground-, air-, or space-based terrain below the player’s vehicle (usually represented by an airplane or spaceship). The point of such games is to keep the action moving so fast that the player doesn’t have a chance to rest from one wave of enemies to the next.

Tile Buffer

Tiling is a process in which there really is no background, just an array of small images that make up the background as it is drawn. In other words, it is a virtual background and takes up very little memory compared to a full bitmapped background. You are already familiar with how tiling works after learning about the level editor, but you are probably wondering: How can I load tiles and make a scrolling game world out of a level file?

Most levels in a scrolling arcade game are quite large, comprised of thousands of tiles in one orientation or the other (usually just scrolling up and down—vertically—or left to right—horizontally). These types of games are called shooters for the most part, although the horizontally scrolling games are usually platformers (such as the original Mario games). Not only does your average Mario game have large scrolling levels, but those levels have parallax layers that make the background in the distance scroll by more slowly than the layer on which the player’s character is walking.

When working on a new game, I find it helpful to start storing my tiles in a new image one by one as I need them, so that I can construct a new set of tiles for the game while I’m working on the game. This also helps to keep the tile numbers down to a smaller number. If you have a huge tile map with hundreds of tiles in it and you only need a few of them during the early stages of development, then you have to figure out where each tile must be drawn, and you have to work with a texture in memory.

Stepping Stones of the World

The process of drawing tiles to fill the game world reminds me of laying down stepping stones, and tiling is a perfect analogy for how this works. Basically, tiles of a larger pattern are laid down from left to right, top to bottom, in that order. The first row is added, one tile at a time, all the way across; then the next row down is filled in from left to right, and so on until the entire map is filled. A single large bitmap is just not used—that’s amateur. Another possibility is that you could continue to use a single large bitmap, but create that bitmap at runtime, and fill it with tiles according to the map file and tile images. Although this solution would generate the game world on the fly, the resulting texture representing the game world would require several gigabytes of memory, which is not feasible.

Tile Rendering Theory

Now that you have a good understanding of what scrolling is and how we can edit tile maps and export them, let’s take a look at the basic theory and code to actually draw a scrolling tile map on the screen. The problem with scrolling a large game world is that too much memory is required to create a texture in memory to contain an entire game world (even if you break the map into sections). You cannot create a single texture to hold the entire game world because most levels are far too large! It would consume so much memory that the program would not even run, and even if it did, that would be a horrible way to write a game. We’re talking about old-school scrolling here, after all, from an era when video game systems had tiny amounts of memory—like 64kb! Surely we can figure out how they did it back then.

Let’s examine a method of tile rendering that supports giant maps using very little memory. In fact, all the memory required for the tiled scroller developed here is a bitmap for the tiles and an array with all of the tile values, plus a screen-sized texture. (In other words, no more memory that would be needed for a background image.) A map comprised of several million tiles can be rendered by this tile engine and will require only a small memory footprint.

Figure 11.6 shows the game world with the scroll window superimposed, so you can see how the screen represents a portion of the game world. While viewing this figure, imagine there is no image containing this game world map, just a virtual array of tile numbers. Those tiles are drawn just to the screen, based on what is visible in the darkened part of the figure. Do you see the rock border around the map? The border is helpful when you are developing a new tile scroller, because it shows the boundaries, allowing you to determine whether the scrolling view is being displayed at the correct location based on the scroll position. (In other words, it should stop scrolling when it reaches the edge of the “world,” but should not skip any tiles.)

An example of a small game level.

Figure 11.6. An example of a small game level.

Now let’s assume that you’re using a screen resolution of 800 × 600, because this is a good resolution to use; it’s relatively small so the screen updates quickly, but it is large enough to display a lot of details on the screen without crowding. We may even want to move up to 1024 × 768 at some point.

There is a simple calculation that gives you the tile number as well as the partial tile values relatively easily. Are you familiar with modulus? This is a mathematical operation that produces the remainder of a division operation. Let me give you a simple example:

10/5 = 2

This is simple enough to understand, right? What happens when you are using numbers that are not evenly divisible?

10/3 = 3.33333333

This is a problem, because the remainder is not an even number, and we’re talking about pixels here. You can’t deal with parts of a pixel! However, you can work with parts of a tile, because tiles are made up of many pixels. Thinking in terms of complete tiles here, let’s take a look at that division again:

10/3 = 3, with a remainder of 0.33333333

Let me now use numbers more relevant to the problem at hand:

800/64 = 12.5

This represents a calculation that returns the number of tiles that fit on a screen with a width of 800 pixels (assuming the tiles are 64 pixels wide). What does 12.5 tiles mean when you are writing a scroller? The .5 represents a part of a tile that must be drawn; hence, I call it partial-tile scrolling. Switching to 32 × 32 pixel tiles results in an evenly divisible screen, at least horizontally (32 × 32 results in 25 tiles across, 18.75 tiles down).

Here is where it gets really interesting! After you have drawn your tiles across the screen, and you want to fill in the remaining .5 of a tile, you can calculate the size of the tile like so:

64 × 0.5 = 32

That is, 32 pixels of the partial tile must be drawn to handle the scrolling edge that was not lined up with a tile edge on the map. Rather than keeping track of the remainder at all, there is a simpler way to calculate the portion of the tile that must be drawn, in the measurement of pixels:

800 Mod 64 = 32

Hint

The modulus operator ("Mod" in Basic) is similar to operators like multiply, divide, add, and subtract, but it simply returns the remainder of a division, which works great for our purposes here.

Try not to think of scrolling in screen terms, because the whole discussion revolves around the tile map in memory (the tile data itself). The tile data is expanded to full tiles when drawn to the screen, but until that happens, these tiles might be thought of as a huge virtual game world from which the scrolling window is drawn.

Try another problem so you get the hang of calculating partial tiles before we get into the source code. Suppose the scroll position is at (700,0) on the map, and the tiles are again 64 × 6432. Which would be the starting tile, and what is the value of the partial tile (in pixels)? To calculate first the tile position in the map data array, just drop the decimal part, which represents the remainder:

700/64 = 10.9375 (10 whole tiles plus a partial tile)

Next, you do want to keep the remainder, and actually drop the tile position itself, because now you’re interested in pixels.

700 Mod 64 = 60

To verify that this calculation is correct, you can do the following:

64 × 0.9375 = 60

The modulus operator greatly helps with this calculation by skipping that middle step. It simply provides the remainder value directly, giving the exact number of pixels that must be drawn from the partial tile to fill in the top and left edges of the screen. I have shown the calculation in Figure 11.7, which is based on 64 × 64-pixel tiles.

An example of how the partial tile calculation is performed at position (700,0).

Figure 11.7. An example of how the partial tile calculation is performed at position (700,0).

Ready for another try at it? This time, calculate the tile numbers and partial-tile values for both the X and Y position of the scroll window at (372, 489). Below is the answer, but see if you can figure it out before looking....

First the X value:

372/64 = 5.8125 (tile X = 5)

64 × 0.8125 = 52 (pixels)

Now for the Y value:

489/64 = 7.640625 (tile Y = 7)

64 × 0.640625 = 41 (pixels)

The same calculations are used for any size of tile, from 16 × 16 to 32 × 32 or any other size.

Per-Tile Scrolling

Scrolling the level one tile at a time produces fairly good results, especially if you need extremely high performance in a very fast-paced game. Of course, a role-playing game is not a hectic, fast-paced scroller, but we do still need good performance. For an RPG, we need slower, more precise scrolling that is not possible with a full-tile scroller. What we need is sub-tile scrolling at a per-pixel rate. Let’s learn the full-time method first, since that may come in handy for another game or two, and then we’ll look at the sub-tile method.

Full Tile Scrolling

For the full tile-based scroller, we’ll be keeping track of the scroll position as it relates to entire tiles with a width and height of 32 × 32, which is the effective scroll rate (since each tile is 32 pixels across). The Level Scroller demo shown in Figure 11.8 and listed below does let you move around and look at the whole level but only one step at a time. There is something appealing about this scroller. I like how precise it is, moving one whole tile at a time, and think this would work great for a turn-based war game or a Civilization type game. We’ll peruse just the important code for the Level Scroller demo. The loadTilemapFile() function was already presented in the previous example, so we’ll just skip any functions like this that have already been shown.

The Level Scroller demo scrolls the game world in one-tile increments.

Figure 11.8. The Level Scroller demo scrolls the game world in one-tile increments.

Private Sub Form1_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
    Me.Text = "Level Scroller"
    Me.Size = New Point(800 + 16, 600 + 38)
    REM create tilemap
    ReDim tilemap(128 * 128)
    REM set up level drawing surface
    bmpSurface = New Bitmap(800, 600)
    pbSurface = New PictureBox()
    pbSurface.Parent = Me
    pbSurface.BackColor = Color.Black
    pbSurface.Dock = DockStyle.Fill
    pbSurface.Image = bmpSurface
    gfxSurface = Graphics.FromImage(bmpSurface)
    REM create font

    fontArial = New Font("Arial Bold", 18)
    REM load the tilemap
    bmpTiles = New Bitmap("palette.bmp")
    loadTilemapFile("tilemap.xml")
    drawTilemap()
End Sub

The drawTilemap() function assumes we have an 800 × 600 display (800/32 = 25 tiles across, and 600 / 32 = 19 tiles down).

Private Sub drawTilemap()
    Dim tilenum, sx, sy As Integer
    For x = 0 To 24
        For y = 0 To 18
            sx = scrollPos.X + x
            sy = scrollPos.Y + y
            tilenum = tilemap(sy * 128 + sx).tilenum
            drawTileNumber(x, y, tilenum)
        Next
    Next
End Sub

The drawTileNumber() function uses the modulus operator to draw a tile from the tile palette image (which looks like a vertical strip of five tiles across, shown in the previous chapter). This function does not handle partial-tile scrolling as discussed, but does use the same modulus operator for a similar purpose of drawing a tile out of a source image. The same function can be found in the level editor’s source code.

Public Sub drawTileNumber(ByVal x As Integer, ByVal y As Integer, _
        ByVal tile As Integer)
    REM draw tile
    Dim sx As Integer = (tile Mod COLUMNS) * 33
    Dim sy As Integer = (tile  COLUMNS) * 33
    Dim src As New Rectangle(sx, sy, 32, 32)
    Dim dx As Integer = x * 32
    Dim dy As Integer = y * 32
    gfxSurface.DrawImage(bmpTiles, dx, dy, src, GraphicsUnit.Pixel)

    REM save changes
    pbSurface.Image = bmpSurface
End Sub

The Form1_KeyUp() event is really the part of this program that causes things to happen. Based on user input, the tilemap is redrawn at a new scroll position. The drawTilemap() function does the work of filling in the window with tiles at the correct location of the tilemap.

Private Sub Form1_KeyUp(ByVal sender As System.Object, _
        ByVal e As System.Windows.Forms.KeyEventArgs) _
        Handles MyBase.KeyUp
    Select Case (e.KeyCode)
        Case Keys.Escape
             End
        Case Keys.Up, Keys.W
             scrollPos.Y -= 1
             If scrollPos.Y < 0 Then scrollPos.Y = 0
             drawTilemap()
        Case Keys.Down, Keys.S
             scrollPos.Y += 1
             If scrollPos.Y > 127 - 18 Then scrollPos.Y = 127 - 18
             drawTilemap()
        Case Keys.Left, Keys.A
             scrollPos.X -= 1
             If scrollPos.X < 0 Then scrollPos.X = 0
             drawTilemap()
        Case Keys.Right, Keys.D
             scrollPos.X += 1
             If scrollPos.X > 127 - 24 Then scrollPos.X = 127 - 24
             drawTilemap()
    End Select
    Dim text As String = "Scroll " + scrollPos.ToString()
    gfxSurface.DrawString(text, fontArial, Brushes.White, 10, 10)
End Sub

Full-Tile Smooth Scrolling

The preceding example showed how to scroll the level one tile per keypress, which would work for a turn-based game but is otherwise too slow. We’ll now take a look at how to scroll while a key is pressed without requiring the user to hit the key repeatedly. The main difference between this and the preceding example is that a flag is used to track the key press and release states for the keys Up, Down, Left, and Right. As long as a key is being held, the map will continue to scroll in that direction. The Smooth Scroller demo is shown in Figure 11.9. Its code follows. Since only a few changes have been made, only the modified code is shown.

The Smooth Scroller demo scrolls the game world quickly and smoothly.

Figure 11.9. The Smooth Scroller demo scrolls the game world quickly and smoothly.

Public Structure keyStates
    Public up, down, left, right As Boolean
End Structure

Const COLUMNS As Integer = 5
Private bmpTiles As Bitmap
Private bmpSurface As Bitmap
Private pbSurface As PictureBox
Private gfxSurface As Graphics
Private fontArial As Font
Private tilemap() As tilemapStruct

Private scrollPos As New PointF(0, 0)
Private oldScrollPos As New PointF(-1, -1)
Private keyState As keyStates
Private WithEvents timer1 As Timer

Private Sub Form1_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
    Me.Text = "Smooth Scroller"
    Me.Size = New Point(800 + 16, 600 + 38)
    REM create tilemap
    ReDim tilemap(128 * 128)
    REM set up level drawing surface
    bmpSurface = New Bitmap(800, 600)
    pbSurface = New PictureBox()
    pbSurface.Parent = Me
    pbSurface.BackColor = Color.Black
    pbSurface.Dock = DockStyle.Fill
    pbSurface.Image = bmpSurface
    gfxSurface = Graphics.FromImage(bmpSurface)
    REM create font
    fontArial = New Font("Arial Bold", 18)
    REM load the tilemap
    bmpTiles = New Bitmap("palette.bmp")
    loadTilemapFile("tilemap.xml")
    REM start the timer
    timer1 = New Timer()
    timer1.Interval = 20
    timer1.Enabled = True
End Sub

Private Sub Form1_KeyDown(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.KeyEventArgs) _
        Handles Me.KeyDown
    Select Case (e.KeyCode)
        Case Keys.Up, Keys.W : keyState.up = True
        Case Keys.Down, Keys.S : keyState.down = True
        Case Keys.Left, Keys.A : keyState.left = True
        Case Keys.Right, Keys.D : keyState.right = True
    End Select
End Sub

Private Sub Form1_KeyUp(ByVal sender As System.Object, _
        ByVal e As System.Windows.Forms.KeyEventArgs) _
        Handles MyBase.KeyUp
    Select Case (e.KeyCode)
        Case Keys.Escape : End
        Case Keys.Up, Keys.W : keyState.up = False
        Case Keys.Down, Keys.S : keyState.down = False
        Case Keys.Left, Keys.A : keyState.left = False
        Case Keys.Right, Keys.D : keyState.right = False
    End Select
End Sub

The “engine” behind this example is based on a Timer control called timer1, and the timer1_tick() function fires off regularly, which is what makes this a real-time program. Even if no scrolling is taking place, this function still causes the tilemap to redraw at a fast pace.

    Private Sub timer1_tick() Handles timer1.Tick
        If keyState.up Then
            scrollPos.Y -= 1
            If scrollPos.Y < 0 Then scrollPos.Y = 0
        End If
        If keyState.down Then
            scrollPos.Y += 1
            If scrollPos.Y > 127 - 18 Then scrollPos.Y = 127 - 18
        End If
        If keyState.left Then
            scrollPos.X -= 1
            If scrollPos.X < 0 Then scrollPos.X = 0
        End If
        If keyState.right Then
            scrollPos.X += 1
            If scrollPos.X > 127 - 24 Then scrollPos.X = 127 - 24
        End If

        drawTilemap()

        Dim text As String = "Scroll " + scrollPos.ToString()
        gfxSurface.DrawString(text, fontArial, Brushes.White, 10, 10)
    End Sub
End Class

Per-Pixel Scrolling

Finally, we come to per-pixel, sub-tile scrolling. In the preceding example, the level was moved one tile at a time—that is, one whole row or column at a time. This is a good and fast way to move around the game world, and is the way I would recommend when you need to warp or jump from one location in the level to another very quickly. But for individual character movement in the game world, we need a slower, more precise form of scrolling, where only a few pixels at a time are shifted in the scroll direction. In order to accomplish this, we need a new feature—a scroll buffer. This buffer will be slightly larger than the screen, with a border around it equal to the size of the tiles. So, if our tiles are 32 × 32 pixels, then we need a 32-pixel border around the scroll buffer.

Sub-Tile Scrolling

The key to implementing a dynamic sub-tile scrolling engine is a third buffer in memory (so called because the screen and back buffer are the first two), upon which the tiles are drawn at the current scroll position. The word dynamic here refers to the way the tile engine draws what is needed at that particular point in the game world, while sub-tile refers to the way it draws full tiles and partial tiles to fill the borders. If you think about it, the tiles are 32 × 32 pixels in size so without the partial-tile capability, drawing tiles directly to the screen one portion at a time results in very jumpy scrolling, where the screen is only updated whenever complete tiles can be drawn (as was the case in the preceding example).

To make this technique work, we start with a Point variable called scrollPos to keep track of the scroll position. When drawing tiles directly, these variables give a precise position at which the tiles should start drawing in a left-to-right, top-to-bottom orientation. If the scroll position is at (500,500), what does this mean, exactly? It means that the tiles specified in the map should be drawn at the upper-left corner of the screen, from the position of the 500 × 500 point in the game world. Try to keep this concept in mind when you are working on scrolling, because the screen position is always the same: the scrolling view is rendered onto the screen at the upper left, 0×0. While the scroll position changes all the time, the destination location on the screen never changes. We’re drawing one screen worth of the game world at a time, from any location in that game world. At the same time, we want to render the tiles that make up that portion of the game world dynamically, in order to keep the scroll engine efficient.

Drawing the Scroll Buffer

After you have filled the scroll buffer with tiles for the current scroll position within the game world, the next thing you must do is actually draw the scroll buffer to the screen. This is where things get a little interesting. The scroll buffer is filled only with complete tiles, but it is from here that the partial tiles are taken into account. This is interesting because the whole tiles were drawn onto the scroll buffer, but the partial tiles are handled when drawing the scroll buffer to the screen. The Point variable called subtile is given the result of the modulus calculation, and these values are then used as the upper-left corner of the scroll buffer that is copied to the screen.

Remember, the scrolling window is just the beginning. The rest of the game still has to be developed, and that includes a lot of animated sprites for the player’s character, non-player characters (NPCs), plus buildings, animals, and any other objects that appear in the game. The bottom line is that the scroller needs to be as efficient as possible. (Yes, even with today’s fast PCs, the scroller needs to be fast—never use the argument that PCs are fast to excuse poorly written code!).

Aligning Tiles to the Scroll Buffer

There is one factor that you must take into consideration while designing the screen layout of your game with a scrolling window. The size of the scrolling window must be evenly divisible by the size of the tiles, or you end up with a floating overlap at the uneven edge. This is an issue that I considered solving in the scrolling code itself. But it turns out that this is unnecessary because you can just change the destination rectangle when drawing the scroll buffer to the screen (something we’ll explore later in this chapter with the “Scrolling View-port” program).

If using a screen resolution of 800 × 600 with 32 × 32 tiles, your width is fine, but height doesn’t quite line up evenly. Cut off the bottom of the scroll window at 576 (which is 18 tiles high), leaving the remaining 24 pixels unused at the bottom. This shouldn’t be a problem because you can use that screen real estate for things like an in-game menu system, player status information, or perhaps in-game dialog (not to be confused with the discussion earlier about partial tiles.

We may want to limit the scrolling window to a portion of the screen as it makes more sense than displaying game information over the top of the scrolling window. This holds true unless we are doing something cool like drawing transparent windows over the top of the background. Two more options occur to me: we could just scale the buffer to fill the screen, or we could just draw the extra tile at the bottom and crop it.

Sub-Tile Scroll Buffering

Now we come to sub-tile scrolling, the type we need for a slow-paced RPG, in which a character walks around in the game world. This type of game requires a scroller with per-pixel granularity. In other words, scrolling at the pixel level rather than at the full tile level (which was 32 pixels at a time). I’ve called this method “sub-tile scroll buffering” because the game world needs to scroll slowly in any direction one pixel at a time. Some familiar techniques can be used again, but we need to modify the code quite a bit to support this more advanced form of scrolling.

To help you understand this technique better, I’ve created two examples. The first example (shown in Figure 11.10) just demonstrates how the scroll buffer works by letting you move the scroll buffer around on the screen. The final example coming up in the next section demonstrates scrolling a game level with this technique. Again, only the key code is shown here for reference, not the complete code listing (with wasteful repeating of code). This project is called Sub-Tile Buffer Demo in the chapter’s resources (www.courseptr.com/downloads).

The key to dynamic sub-tile scrolling is the buffer border.

Figure 11.10. The key to dynamic sub-tile scrolling is the buffer border.

Private Sub Form1_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
    Me.Text = "Sub-Tile Buffer Demo"
    Me.Size = New Point(800 + 16, 600 + 38)
    REM set up level drawing surface
    bmpSurface = New Bitmap(800, 600)
    pbSurface = New PictureBox()
    pbSurface.Parent = Me
    pbSurface.BackColor = Color.Black
    pbSurface.Dock = DockStyle.Fill
    pbSurface.Image = bmpSurface
    gfxSurface = Graphics.FromImage(bmpSurface)
    REM create fonts
    fontArial12 = New Font("Arial", 12)
    fontArial18 = New Font("Arial", 18)
    REM create scroll buffer
    bmpScrollBuffer = New Bitmap(25 * 32 + 64, 19 * 32 + 64)
    gfxScrollBuffer = Graphics.FromImage(bmpScrollBuffer)
    REM fill buffer "border" area
    gfxScrollBuffer.FillRectangle(Brushes.Gray, _
        New Rectangle(0, 0, bmpScrollBuffer.Width, bmpScrollBuffer.Height))
    REM fill "screen" buffer area
    gfxScrollBuffer.FillRectangle(Brushes.BlueViolet, _
        New Rectangle(32, 32, 25 * 32, 19 * 32))

    For y = 0 To 18
        For x = 0 To 24
            gfxScrollBuffer.DrawRectangle(Pens.White, _
                32 + x * 32, 32 + y * 32, 32, 32)
        Next
    Next

    gfxScrollBuffer.DrawString("SCROLL BUFFER BORDER", fontArial12, _
        Brushes.White, 0, 0)

    REM get this thing running
    timer1 = New Timer()
    timer1.Interval = 16
    timer1.Enabled = True
End Sub

Private Sub timer1_tick() Handles timer1.Tick
    If keyState.down Then
        scrollPos.Y -= 2
        If scrollPos.Y < -64 Then scrollPos.Y = -64
    End If
    If keyState.up Then
        scrollPos.Y += 2
        If scrollPos.Y > 0 Then scrollPos.Y = 0
    End If
    If keyState.right Then
        scrollPos.X -= 2
        If scrollPos.X < -64 Then scrollPos.X = -64
    End If
    If keyState.left Then
        scrollPos.X += 2
        If scrollPos.X > 0 Then scrollPos.X = 0
    End If

    gfxSurface.DrawImage(bmpScrollBuffer, scrollPos)
    gfxSurface.DrawString(scrollPos.ToString(), fontArial18, _
        Brushes.White, 650, 0)
    pbSurface.Image = bmpSurface
End Sub

Sub-Tile Smooth Scrolling

Now for the final example of the chapter—the sub-tile smooth scrolling. In the preceding example, you could see how the scroll buffer works with a border around the edges of the buffer to take into account the partial tiles. This produces smooth per-pixel scrolling in any direction. Figure 11.11 shows the example program that displays the right and bottom edges of the buffer so you can see how the sub-tile scroller works. (Note: there is a commented-out line of code that will render the scroll buffer smoothly without showing the partial tiles if you wish to see it, but this is more interesting as a learning experience with the tiles left in.)

Smooth sub-tile scrolling is accomplished using an image buffer.

Figure 11.11. Smooth sub-tile scrolling is accomplished using an image buffer.

This tile scroller is now finished. We have data specific to each tile for use in gameplay (such as the collidable property), and a moving game world. However, there are optimizations that can still be made to this scroller—plus, we might want to create a reusable class to consolidate the code a bit. I think it would be nice if we could just call one function to load the tilemap, and another to draw it at any given position in the game level. This source code is found in the project called Sub-Tile Smooth Scroller in the chapter’s resources.

Private Sub Form1_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
    Me.Text = "Sub-Tile Smooth Scroller"
    Me.Size = New Point(900, 700)

    REM set up level drawing surface
    bmpSurface = New Bitmap(1024, 768)

    pbSurface = New PictureBox()
    pbSurface.Parent = Me
    pbSurface.BackColor = Color.Black
    pbSurface.Dock = DockStyle.Fill
    pbSurface.Image = bmpSurface
    gfxSurface = Graphics.FromImage(bmpSurface)

    REM create font
    fontArial = New Font("Arial Bold", 18)

    REM create tilemap
    ReDim tilemap(128 * 128)
    bmpTiles = New Bitmap("palette.bmp")
    loadTilemapFile("tilemap.xml")

    REM create scroll buffer
    bmpScrollBuffer = New Bitmap(25 * 32 + 32, 19 * 32 + 32)
    gfxScrollBuffer = Graphics.FromImage(bmpScrollBuffer)

    REM get this thing running
    timer1 = New Timer()
    timer1.Interval = 10
    timer1.Enabled = True
End Sub

Private Sub updateScrollBuffer()
    REM fill scroll buffer with tiles
    Dim tilenum, sx, sy As Integer
    For x = 0 To 25
        For y = 0 To 19
            sx = scrollPos.X  32 + x
            sy = scrollPos.Y  32 + y
            tilenum = tilemap(sy * 128 + sx).tilenum
            drawTileNumber(x, y, tilenum)
        Next
    Next
End Sub

Public Sub drawScrollBuffer()
    REM fill the scroll buffer only when moving

    If scrollPos <> oldScrollPos Then
        updateScrollBuffer()
        oldScrollPos = scrollPos
    End If

    REM calculate sub-tile size
    subtile.X = scrollPos.X Mod 32
    subtile.Y = scrollPos.Y Mod 32

    REM create the source rect
    REM Note that this example shows the edges of the scroll buffer
    REM but for production, use the commented-out line instead
    Dim source As New Rectangle(subtile.X, subtile.Y, _
        bmpScrollBuffer.Width, bmpScrollBuffer.Height)
    'Dim source As New Rectangle(subtile.X, subtile.Y, 800, 600)

    REM draw the scroll viewport
    gfxSurface.DrawImage(bmpScrollBuffer, 1, 1, source, GraphicsUnit.Pixel)
End Sub

Private Sub timer1_tick() Handles timer1.Tick
    REM respond to user input
    Dim steps As Integer = 4
    If keyState.up Then
        scrollPos.Y -= steps
        If scrollPos.Y < 0 Then scrollPos.Y = 0
    End If
    If keyState.down Then
        scrollPos.Y += steps
        If scrollPos.Y > (127 - 19) * 32 Then
            scrollPos.Y = (127 - 19) * 32
        End If
    End If
    If keyState.left Then
        scrollPos.X -= steps
        If scrollPos.X < 0 Then scrollPos.X = 0
    End If
    If keyState.right Then
        scrollPos.X += steps
        If scrollPos.X > (127 - 25) * 32 Then

                scrollPos.X = (127 - 25) * 32
            End If
        End If

        gfxSurface.Clear(Color.Black)

        drawScrollBuffer()

        gfxSurface.DrawString("Scroll " + scrollPos.ToString(), _
            fontArial, Brushes.White, 0, 0)
        gfxSurface.DrawString("Sub-tile " + subtile.ToString(), _
            fontArial, Brushes.White, 300, 0)

        gfxSurface.DrawRectangle(Pens.Blue, 0, 0, 801, 601)
        gfxSurface.DrawRectangle(Pens.Blue, 1, 1, 801, 601)
        pbSurface.Image = bmpSurface
    End Sub
End Class

Level Up!

Wow, that was a ton of great information and some killer source code! This gives us enough information to begin working on the game world of Celtic Crusader! I don’t know about you, but after this long wait, it feels good to have reached this point. Now that we have a level editor and a working level renderer, we can begin working on gameplay. Although the tilemap is drawing, we aren’t using any of the extended data fields (such as collidable), which is the topic of the next two chapters!

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

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