Chapter 12. Adding Objects to the World

In this chapter we will learn how to add objects to the game world in such a way that they will show up when the viewport scrolls. This will require some coding trickery that goes a bit beyond the usual fare that we’ve needed so far, so if your experience with the Basic language is somewhat on the light side, you will want to pay close attention to the explanations here. We will go back to using the Game class that was first introduced back in Chapter 5, “Bitmaps: Our First Building Block”, which handles most of the “framework” code needed for a game that has been put on hold while building the level editor and testing out game levels. But now we can return to the Game class, as well as the Sprite class from Chapter 6, “Sprites and Real-Time Animation”.

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

Adding Scenery to the Game World

Our game level editor works great for creating tilemaps, and it has support for additional data fields and a collision property. But, there comes a point when you need more than just the tilemap data to make a real game—you need interactive objects in the game world as well. So, the first thing we’re going to learn in this chapter is how to add some scenery objects, using the tilemap scrolling code developed in the previous chapter. At the same time, we need to address performance. The scrolling code takes up 100% of the processor when the scroll buffer is being refilled continuously. Even if you move the scroll position one pixel, the entire buffer is rebuilt. That is consuming huge amounts of processor time! It might not even be noticeable on a typical multi-core system today, but a laptop user would definitely notice because that tends to use up the battery very quickly. In addition to adding scenery, we’ll work on a new core game loop that is more efficient.

A New Game Loop

If you open up the Sub-Tile Smooth Scroller project from the previous chapter, watch it run while looking at your processor’s performance in Task Manager. To open Task Manager, you can right-click the Windows toolbar and choose Start Task Manager, or you can press Ctrl+Alt+Delete to bring up the switch user screen to find Task Manager. Figure 12.1 shows Task Manager while the aforementioned demo is running. Note how one of the cores is pretty much maxed out while the others are idle—that’s because the program is running in just one thread, and it’s pushing the processor pretty hard for such a seemingly simple graphics demo.

Observing processor utilization in Task Manager.

Figure 12.1. Observing processor utilization in Task Manager.

The reason for this processor abuse is the use of a timer for rendering. For reference, here is a cropped version of the timer1_tick() function from Chapter 11.

Private Sub timer1_tick() Handles timer1.Tick
    REM respond to user input
    Dim steps As Integer = 4
    . . .
    REM refresh window
    pbSurface.Image = bmpSurface
End Sub

The timer event began firing when the timer1 object was created via this code in Form1_Load:

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

The Timer class was never really intended to be used as the engine for a high-speed game loop! Timers are more often used to fire off signals at regular intervals for hardware devices, to monitor a database for changes, that sort of thing. It does not have very good granularity, which means precision at high speed. So, we need to replace the timer with our own real-time loop. I’ve got just the thing—a While loop. But, Visual Basic programs are graphical and forms-based, so we can’t just make a loop and do what we want, because that will freeze up the form. Fortunately, there’s a function that will do all of the events: Application.DoEvents(). This code can be added to the end of Form1_Load so it’s the last thing that runs after everything has been loaded for the game:

Dim gameover as Boolean = False
While Not gameover
    doUpdate()
End While

Modifying Game.vb

Somewhere in that doUpdate() function, we have to call Application.DoEvents() so the form can be refreshed. If we call it every frame, that will also be wasteful because Application.DoEvents() processes the event messages for form controls (like the Timer as well as for drawing the controls). If we call it every frame, then our game loop will be even more limited than it was with the timer! No, we need to learn just when and where to use this function and that calls for a knowledge of frame-rate timing.

Public Function FrameRate() As Integer
    Static count As Integer = 0
    Static lastTime As Integer = 0
    Static frames As Integer = 0
    REM calculate core frame rate
    Dim ticks As Integer = Environment.TickCount()
    count += 1
    If ticks > lastTime + 1000 Then
        lastTime = ticks
        frames = count
        count = 0
    End If
    Return frames
End Function

Do you recall the Game class from way back in Chapter 5? We will be using the Game class again in this chapter. The Game class is a good home for the FrameRate() function. While we’re at it, let’s add two more helper functions that we need for calculating random numbers (which is good for simulating the rolling of dice—absolutely essential when rolling new RPG characters!).

Private p_random As Random

Public Function Random(ByVal max As Integer)
    Return Random(0, max)
End Function

Public Function Random(ByVal min As Integer, ByVal max As Integer)
    Return p_random.Next(min, max)
End Function

A New Game Loop

Now that those dependencies are added, let’s take a look at the new doUpdate() function, which is called from within the main While loop that will drive our game. I’ll stick with just the bare minimum for now, leaving out any code specific to one example or another, and just show you a skeleton version of the function.

Private Sub doUpdate()
    Dim frameRate As Integer = game.FrameRate()
    Dim ticks As Integer = Environment.TickCount()
    Static drawLast As Integer = 0
    If ticks > drawLast + 16 Then
        drawLast = ticks
        game.Print(0, 0, "Frame rate " + frameRate.ToString())
        game.Update()
        Application.DoEvents()
    Else
        Threading.Thread.Sleep(1)
    End If
End Sub

Resolutions

One problem with a game based on Windows Forms and GDI+ is the lack of a fullscreen mode. Although we could extend the resolution of the game window to any desired size, it would be scaled necessarily to that target resolution, not rendered with higher detail. We could, for example, run the game at 1600 × 1200 by scaling the Output of 800 x 600 by a factor of two. This would work, and the result might look pretty good since it’s an even factor (odd factors tend to produce bad results when scaling graphics).

This bare minimum version of doUpdate() handles its own timing and is more action packed than it at first appears. First, we need to get the frame rate from the Game class, and this needs to happen before the If statement, because it needs to run as fast as possible. Everything within the If statement block of code is slowed down code for rendering. Anything we need to draw in the game goes inside that If block.

If ticks > drawLast + 16 Then

The If statement will be true once every 16 milliseconds. Where does that value come from? That is approximately 60 frames per second—a desirable goal for a game.

1 second = 1000 ms

delay = 1000 ms / 60 fps

delay = 16.66667 ms

Truncating the decimal part gives our code a few free frames per second, causing the actual frame rate to clock in at around 64 fps, but that depends on the processor—it might be less than 60 on some systems. The point is, we need this uber-vital code in the game loop to keep slow stuff from bottlenecking the whole game! That’s exactly what was happening in Chapter 11’s demos, which had no time-limiting code.

So, we first get the current system timer value in milliseconds with Environment. TickCount(), which will be some large millisecond number like 3828394918. That doesn’t matter. What matters is how many milliseconds transpire from one frame to the next. Keeping track of that tick value in the drawLast variable allows our code to use it for comparison in the next frame. If at least 16 ms have gone by since the last time drawLast was set, then it’s time to draw!

The real frame rate of a game is not the 60 fps draw rate, it’s the rate at which the game is updated every frame. That includes any math and physics calculations, collision detection (which can be very time consuming!), A.I. for enemy movements, and so on. If we tried to do all of these things inside the 60 fps game loop, it would immediately drop to below that desired refresh rate, all the while many frames are going to waste outside the If statement.

Now to address the processor throttling: In Chapter 11, one thread would max out one processor core just to draw the tilemap, which seems silly for such a simple 2D graphic. The problem was not the drawing code but the timer. We’ll correct that now. If 16 ms have not transpired so that it’s time to draw, then we tell the current thread to sleep for 1 ms. This has the effect of allowing the processor core to rest if the game is idling for that short time period. 16 ms is an extremely small amount of time in human terms, but for the computer it’s enough time to read a whole book! The Else statement in the code below kicks in if 16 ms have not yet transpired.

Else
    Threading.Thread.Sleep(1)
End If

New Level Class

The tilemap scrolling code has reached a level of critical mass where it’s no longer possible to manage it all with global variables and functions—it’s time to move all of this code into a class. This will clean up the main source code file for our projects significantly! The new Level class will have quite a few private variables, public properties, and public functions. All of the complex code will be hidden and the scroller will function in a turn-key fashion: simply load up a level file, and then call Update() and Draw() regularly. You will recognize all of the variables and functions present in the previous chapter’s example projects, but now they are packaged nicely into the Level.vb file. There is no new code here—this is all just the same code we’ve already seen, organized into a class.

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

    Private p_game As Game
    Private p_mapSize As New Point(0, 0)
    Private p_windowSize As New Point(0, 0)
    Private p_tileSize As Integer
    Private p_bmpTiles As Bitmap
    Private p_columns As Integer
    Private p_bmpScrollBuffer As Bitmap
    Private p_gfxScrollBuffer As Graphics

    Private p_tilemap() As tilemapStruct
    Private p_scrollPos As New PointF(0, 0)
    Private p_subtile As New PointF(0, 0)
    Private p_oldScrollPos As New PointF(-1, -1)

    Public Property ScrollPos() As PointF
        Get
            Return p_scrollPos
        End Get
        Set(ByVal value As PointF)
            REM save new scroll position
            p_scrollPos = value
        End Set
    End Property

    Public Sub New(ByRef gameObject As Game, ByVal width As Integer, _
            ByVal height As Integer, ByVal tileSize As Integer)
        p_game = gameObject
        p_windowSize = New Point(width, height)
        p_mapSize = New Point(width * tileSize, height * tileSize)
        p_tileSize = tileSize
        REM create scroll buffer
        p_bmpScrollBuffer = New Bitmap(p_mapSize.X + p_tileSize, _
            p_mapSize.Y + p_tileSize)
        p_gfxScrollBuffer = Graphics.FromImage(p_bmpScrollBuffer)
        REM create tilemap
        ReDim p_tilemap(128 * 128)
    End Sub

    Public Function loadTilemap(ByVal filename As String) As Boolean
        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 #

                Dim data As String
                data = element.GetElementsByTagName("tile")(0).InnerText
                index = Convert.ToInt32(data)
                REM read tilenum
                data = element.GetElementsByTagName("value")(0).InnerText
                value = Convert.ToInt32(data)
                REM read data1
                data = element.GetElementsByTagName("data1")(0).InnerText
                data1 = Convert.ToString(data)
                REM read collidable
                data = element.GetElementsByTagName("collidable")(0). _
                     InnerText
                collidable = Convert.ToBoolean(data)
                p_tilemap(index).tilenum = value
                p_tilemap(index).data1 = data1
                p_tilemap(index).collidable = collidable
            Next
        Catch es As Exception
            MessageBox.Show(es.Message)
            Return False
        End Try
        Return True
    End Function

    Public Function loadPalette(ByVal filename As String, _
            ByVal columns As Integer) As Boolean
        p_columns = columns
        Try
            p_bmpTiles = New Bitmap(filename)
        Catch ex As Exception
            Return False
        End Try
        Return True
    End Function

    Public Sub Update()
        REM fill the scroll buffer only when moving
        If p_scrollPos <> p_oldScrollPos Then
            p_oldScrollPos = p_scrollPos
            REM validate X range
            If p_scrollPos.X < 0 Then p_scrollPos.X = 0

            If p_scrollPos.X > (127 - p_windowSize.X) * p_tileSize Then
                p_scrollPos.X = (127 - p_windowSize.X) * p_tileSize
            End If
            REM validate Y range
            If p_scrollPos.Y < 0 Then p_scrollPos.Y = 0
            If p_scrollPos.Y > (127 - p_windowSize.Y) * p_tileSize Then
                p_scrollPos.Y = (127 - p_windowSize.Y) * p_tileSize
            End If
            REM calculate sub-tile size
            p_subtile.X = p_scrollPos.X Mod p_tileSize
            p_subtile.Y = p_scrollPos.Y Mod p_tileSize
            REM fill scroll buffer with tiles
            Dim tilenum, sx, sy As Integer
            For x = 0 To p_windowSize.X
                For y = 0 To p_windowSize.Y
                    sx = p_scrollPos.X  p_tileSize + x
                    sy = p_scrollPos.Y  p_tileSize + y
                    tilenum = p_tilemap(sy * 128 + sx).tilenum
                    drawTileNumber(x, y, tilenum)
                Next
            Next
        End If
   End Sub

   Public Sub drawTileNumber(ByVal x As Integer, _
           ByVal y As Integer, ByVal tile As Integer)
       Dim sx As Integer = (tile Mod p_columns) * (p_tileSize + 1)
       Dim sy As Integer = (tile  p_columns) * (p_tileSize + 1)
       Dim src As New Rectangle(sx, sy, p_tileSize, p_tileSize)
       Dim dx As Integer = x * p_tileSize
       Dim dy As Integer = y * p_tileSize
       p_gfxScrollBuffer.DrawImage(p_bmpTiles, dx, dy, src, _
           GraphicsUnit.Pixel)
   End Sub

   Public Sub Draw(ByVal rect As Rectangle)
       Draw(rect.X, rect.Y, rect.Width, rect.Height)
   End Sub

   Public Sub Draw(ByVal width As Integer, ByVal height As Integer)
       Draw(0, 0, width, height)

    End Sub

    Public Sub Draw(ByVal x As Integer, ByVal y As Integer, _
            ByVal width As Integer, ByVal height As Integer)
        REM draw the scroll viewport
        Dim source As New Rectangle(p_subtile.X, p_subtile.Y, _
            width, height)
        p_game.Device.DrawImage(p_bmpScrollBuffer, x, y, source, _
            GraphicsUnit.Pixel)
    End Sub
End Class

Adding Trees

The first example project in this chapter will add random trees to the game level—or, at least, make it seem that trees have been added. Actually, the trees are just drawn over the top of the tilemap scroller at a specific location meant to appear to be in the level. The first step to adding interactive objects to the game world involves moving them realistically with the scroller, and drawing those objects that are in view while not drawing any object that is outside the current viewport (which is the scroll position in the level plus the width and height of the window). First, we need to make some improvements to the Sprite class, then we’ll get to the random trees afterward.

Modifying Sprite.vb

It turns out that we need to make some new improvements to the Sprite class introduced back in Chapter 6, “Sprites and Real-Time Animation”. The changes are needed not because of a lack of foresight back then, but because of changing needs as work on Celtic Crusader progresses. Expect future needs and the changes they will require—versatility is important in software development! First, we need to gain access to the animation system in the Sprite class. It was originally designed with a very useful Animate() function, but I want to be able to set the starting frame of animation to a random value for the random trees example, and presently there is no way to specifically set the sprite frame. Here’s a new property to do that:

Public Property CurrentFrame() As Integer
    Get

        Return p_currentFrame
    End Get
    Set(ByVal value As Integer)
        p_currentFrame = value
    End Set
End Property

Next, we need a new Draw() function. Adding a second version of the function will overloadDrawin the class, giving it more features, but we must be careful not to disrupt any existing code in the process. Specifically, I need to be able to draw a copy of a sprite, based on its current animation frame, to any location on the screen, without changing the sprite’s position. That calls for a new Draw() function that accepts screen coordinates. For reference, here is the existing Draw() function:

REM draw sprite frame at current position
Public Sub Draw()
    Dim frame As New Rectangle
    frame.X = (p_currentFrame Mod p_columns) * p_size.Width
    frame.Y = (p_currentFrame  p_columns) * p_size.Height
    frame.Width = p_size.Width
    frame.Height = p_size.Height
    p_game.Device.DrawImage(p_bitmap, Bounds(), frame, GraphicsUnit.Pixel)
End Sub

And here is the new overloaded function:

REM draw sprite at specified position without changing original position
Public Sub Draw(ByVal x As Integer, ByVal y As Integer)
    REM source image
    Dim frame As New Rectangle
    frame.X = (p_currentFrame Mod p_columns) * p_size.Width
    frame.Y = (p_currentFrame  p_columns) * p_size.Height
    frame.Width = p_size.Width
    frame.Height = p_size.Height
    REM target location
    Dim target As New Rectangle(x, y, p_size.Width, p_size.Height)
    REM draw sprite
    p_game.Device.DrawImage(p_bitmap, target, frame, GraphicsUnit.Pixel)
End Sub

Adding the Trees

Now that we have a new Level class and modified versions of the Game and Sprite classes, we can finally go over a new example involving interactive objects in the game world. In this example, the objects won’t exactly be interactive—yet! The random trees will be visible and will seem to scroll with the tiles. The Random Tree Demo program includes optimizations to the game loop, with the addition of the game level renderer (via the Level class), and a linked list of tree sprites that are scattered randomly around the upper-left corner of the game level (so we don’t have to move very far to see them all—but it is very easy to scatter the trees throughout the entire level). The source image for the tree scenery objects is shown in Figure 12.2. The images used in the demo are each 64 × 64 pixels in size.

The tree sprite sheet has 32 unique trees and bushes that can be used for scenery. Courtesy of Reiner Prokein.

Figure 12.2. The tree sprite sheet has 32 unique trees and bushes that can be used for scenery. Courtesy of Reiner Prokein.

Figure 12.3 shows the Random Trees demo program running. Note the frame rate value! As you can see in the source code listing below, the trees are only randomly placed within the first 1000 pixels, in both the horizontal and vertical directions. Feel free to experiment with the code, extending the range of the trees to the entire level if you wish. Just be mindful of the number of objects being added. Although only the visible tree sprites are drawn, the entire list is looked at every frame, which can slow down the program quite a bit if there are too many objects. Why don’t you perform a little experiment? See how many trees you can add before the frame rate drops too low to be playable?

Random trees are added to the game world.

Figure 12.3. Random trees are added to the game world.

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

  Private game As Game
  Private level As Level
  Private keyState As keyStates
  Private gameover As Boolean = False
  Private treeImage As Bitmap
  Private tree As Sprite
  Private trees As List(Of Sprite)
  Private treesVisible As Integer = 0

  Private Sub Form1_Load(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles MyBase.Load
      Me.Text = "Random Tree Demo"
      REM create game object
      game = New Game(Me, 800, 600)
      REM create tilemap
      level = New Level(game, 25, 19, 32)
      level.loadTilemap("tilemap.xml")
      level.loadPalette("palette.bmp", 5)
      REM load trees
      treeImage = game.LoadBitmap("trees64.png")
      trees = New List(Of Sprite)
      For n = 0 To 100
          Dim tree As New Sprite(game)
          tree.Image = treeImage
          tree.Columns = 4
          tree.TotalFrames = 32
          tree.CurrentFrame = game.Random(31)
          tree.Size = New Point(64, 64)
          tree.Position = New PointF(game.Random(1000), game.Random(1000))
          trees.Add(tree)
     Next n
     REM game loop
     While Not gameover
         doUpdate()
     End While
 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

    Private Sub DrawTrees()
        Dim sx As Integer
        Dim sy As Integer
        treesVisible = 0
        For Each tree As Sprite In trees
            sx = level.ScrollPos.X
            sy = level.ScrollPos.Y
            If tree.X > sx And tree.X < sx + 23 * 32 _
            And tree.Y > sy And tree.Y < sy + 17 * 32 Then
                Dim rx As Integer = Math.Abs(sx - tree.X)
                Dim ry As Integer = Math.Abs(sy - tree.Y)
                tree.Draw(rx, ry)
                treesVisible += 1
            End If
        Next
    End Sub

    Private Sub doUpdate()
        REM respond to user input
        Dim steps As Integer = 8
        Dim pos As PointF = level.ScrollPos
        If keyState.up Then pos.Y -= steps

        If keyState.down Then pos.Y += steps
        If keyState.left Then pos.X -= steps
        If keyState.right Then pos.X += steps
        level.ScrollPos = pos
        REM refresh level renderer
        level.Update()
        REM get the untimed core frame rate
        Dim frameRate As Integer = game.FrameRate()
        REM drawing code should be limited to 60 fps
        Dim ticks As Integer = Environment.TickCount()
        Static drawLast As Integer = 0
        If ticks > drawLast + 16 Then '1000/60 = 16 ms
            drawLast = ticks
            REM draw the tilemap
            level.Draw(0, 0, 800, 600)
            REM draw the trees in view
            drawTrees()
            REM print da stats
            game.Print(0, 0, "Scroll " + level.ScrollPos.ToString())
            game.Print(250, 0, "Frame rate " + frameRate.ToString())
            game.Print(500, 0, "Visible trees " + treesVisible.ToString() + "/
100")
            REM refresh window
            game.Update()
            Application.DoEvents()
        Else
            REM throttle the cpu
            Threading.Thread.Sleep(1)
        End If
    End Sub
End Class

Adding an Animated Character

Random trees are pretty interesting, but what is not all that fascinating any more is a scrolling game world without any characters in it. We’re a little ahead of that subject at this point, which will not be covered until Chapter 14, “Creating the Character Editor”. But, we do need an animated character to walk around and appear to begin interacting with the game world. Figure 12.4 shows the sprite sheet used for the character.

This sprite sheet is used for the animated walking character in the demo.

Figure 12.4. This sprite sheet is used for the animated walking character in the demo.

There are nine frames for each animation set and eight directions for a total of 72 frames of animation. Since the diagonal directions require two key presses (such as Up and Left), the diagonals are handled first, and then the four cardinal directions are handled with Else statements. The core code for this demo is in the doUpdate() function again, as it was in the previous demo. First, we draw the level that has the effect of also erasing the window, which saves processor cycles normally needed to clear the screen. Next, the trees are drawn if they are found within the current scroll region of the level. Finally, the hero character sprite is drawn. There is no sprite z-buffering in this demo—that is, no priority drawing of some sprites over the top of others, something that will need to be addressed in a fully featured game. Figure 12.5 shows the Walk About demo in action.

An animated character now walks in the direction that the scroller is moving.

Figure 12.5. An animated character now walks in the direction that the scroller is moving.

Exaggerated Dimensions

The characters in the game are clearly out of scale with the environment. In some cases the player is taller than a full-size tree! This is fairly common in games of this type, drawing attention to the important items on the screen by highlighting them in some way. The characters will have to be somewhat larger than their surroundings so they’re visible! If the game screen were rendered with all objects to scale, then characters would be only a few pixels tall compared to a tree. The game screen is a representation of the real world—or, at least, of some other world, anyway.

Regarding the tree artwork in this chapter: the trees are just a representation of any environment object you want in your game world. Just change the image and draw houses, signs, rocks, walls, or anything else you can imagine! Think of the trees as just placeholders for any object!

Since this program is very similar to the previous one, only the important code is shown here, not the complete listing. Refer to the chapter’s resources for the complete Walk About demo.

Private game As Game
Private level As Level
Private keyState As keyStates
Private gameover As Boolean = False
Private treeImage As Bitmap
Private tree As Sprite
Private trees As List(Of Sprite)
Private treesVisible As Integer = 0
Private hero As Sprite
Private heroDir As Integer = 0

Private Sub Form1_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
    Me.Text = "Walk About Demo"
    REM create game object
    game = New Game(Me, 800, 600)
    REM create tilemap
    level = New Level(game, 25, 19, 32)
    level.loadTilemap("tilemap.xml")
    level.loadPalette("palette.bmp", 5)
    REM load hero
    hero = New Sprite(game)
    hero.Image = game.LoadBitmap("hero_sword_walk.png")
    hero.Columns = 9
    hero.TotalFrames = 9 * 8
    hero.Size = New Point(96, 96)
    hero.Position = New Point(400 - 32, 300 - 32)
    hero.AnimateWrapMode = Sprite.AnimateWrap.WRAP
    hero.AnimationRate = 20
    REM load trees
    treeImage = game.LoadBitmap("trees64.png")
    trees = New List(Of Sprite)
    For n = 0 To 100
        Dim tree As New Sprite(game)
        tree.Image = treeImage
        tree.Columns = 4
        tree.TotalFrames = 32

        tree.CurrentFrame = game.Random(31)
        tree.Size = New Point(64, 64)
        tree.Position = New PointF(game.Random(1000), game.Random(1000))
        trees.Add(tree)
    Next n
    REM game loop
    While Not gameover
        doUpdate()
    End While
End Sub


Private Sub doUpdate()
    REM move the tilemap scroll position
    Dim steps As Integer = 4
    Dim pos As PointF = level.ScrollPos
    If keyState.up Then pos.Y -= steps
    If keyState.down Then pos.Y += steps
    If keyState.left Then pos.X -= steps
    If keyState.right Then pos.X += steps
    level.ScrollPos = pos
    REM orient the player in the right direction
    If keyState.up And keyState.right Then
        heroDir = 1
    ElseIf keyState.right And keyState.down Then
        heroDir = 3
    ElseIf keyState.down And keyState.left Then
        heroDir = 5
    ElseIf keyState.left And keyState.up Then
        heroDir = 7
    ElseIf keyState.up Then
        heroDir = 0
    ElseIf keyState.right Then
        heroDir = 2
    ElseIf keyState.down Then
        heroDir = 4
    ElseIf keyState.left Then
        heroDir = 6
    Else
        heroDir = -1

    End If

    REM refresh level renderer
    level.Update()
    REM get the untimed core frame rate
    Dim frameRate As Integer = game.FrameRate()
    REM drawing code should be limited to 60 fps
    Dim ticks As Integer = Environment.TickCount()
    Static drawLast As Integer = 0
    If ticks > drawLast + 16 Then
        drawLast = ticks
        REM draw the tilemap
        level.Draw(0, 0, 800, 600)
        REM draw the trees in view
        DrawTrees()
        REM draw the hero
        Dim startFrame As Integer = heroDir * 9
        Dim endFrame As Integer = startFrame + 8
        If heroDir > -1 Then
            hero.Animate(startFrame, endFrame)
        End If
        hero.Draw()
        REM print da stats
        game.Print(0, 0, "Scroll " + level.ScrollPos.ToString())
        game.Print(250, 0, "Frame rate " + frameRate.ToString())
        game.Print(500, 0, "Visible trees " + _
            treesVisible.ToString() + "/100")
        REM refresh window
        game.Update()
        Application.DoEvents()
    Else
        REM throttle the cpu
        Threading.Thread.Sleep(1)
    End If
End Sub

There is one limitation to this first attempt at adding a playable character—due to the way in which the scroller works, we can’t move the character all the way into the corner of the game world. The character sprite is fixed to the center of the screen. This will be remedied in the next chapter with some clever code!

Level Up!

We have made quite a bit of positive progress in this chapter with some key features needed for a full-blown RPG. Although a few trees and an animated character don’t seem like much to go on so far, we have laid the foundation for the interactive aspects of the game with the meager code in this chapter. Soon, we will be tapping into the data fields of the level data and positioning objects in the game world based on data entered in the level editor, which really makes for the start of a solid data-driven game. This is really the core of the game that will be expanded upon even further in the next chapter.

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

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