Chapter 8. Adding Objects to the Dungeon

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 C# 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 2, “Drawing Shapes and Bitmaps with GDI+,” 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 3, “Sprites and Real-Time Animation.”

Here are the goods:

  • Adding scenery to the game world

  • A new game loop

  • Level class

  • Adding trees

  • Adding an animated character

Adding Objects 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 8.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 8.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 the previous chapter.

private void timer1_tick(object sender, EventArgs e)
{
    int steps = 4;
    if (keyState.up)
    {
        scrollPos.Y -= steps;
        if (scrollPos.Y < 0) scrollPos.Y = 0;
    }
    ...
}

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

//create the timer
timer1 = new Timer();
timer1.Interval = 20;
timer1.Enabled = true;
timer1.Tick += new EventHandler(timer1_tick);

The Timer class was never really intended to be used as the engine for a highspeed 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 C# 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:

while (!gameover)
{
    doUpdate();
}
Application.Exit();

Reviewing Game.cs

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. Do you recall the Game class from way back in Chapter 3? We will be using the Game class again in this chapter. The Game class contains the FrameRate() method. The Game.FrameRate() method gives us that value.

public int FrameRate()
{
    int ticks = Environment.TickCount;
    p_count += 1;
    if (ticks > p_lastTime + 1000)
    {
        p_lastTime = ticks;
        p_frames = p_count;
        p_count = 0;
    }
    return p_frames;
}

This function assumes that the following global variables are defined:

int p_count, p_lastTime, p_frames;

A New Game Loop

So, we want to start using the Game class again. Game is just a helper class, not an engine of sorts, so we do need to supply our own pump or motor in the form of a loop. Let’s take a look at a new doUpdate() function, which is called from the while loop that powers 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 void doUpdate()
{
    //drawing code should be set to 60 fps
    int ticks = Environment.TickCount;
    if (ticks > drawLast + 16)
    {
        drawLast = ticks;
        game.Update();
        Application.DoEvents();
    }
    else
    {
        //throttle the cpu
        Thread.Sleep(1);
    }
}

This bare minimum version of doUpdate() handles its own timing and is more action packed than it 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)

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 the projects in the previous chapter that 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 the previous chapter, one thread would max out one processor core just to draw the tilemap, which seems silly for such a simple 2D demo. 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
{
    Thread.Sleep(1);
}

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 methods—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 methods. 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.cs file. There is no new code here—this is all just the same code we’ve already seen, organized into a class.

public class Level
{
    public struct tilemapStruct
    {
        public int tilenum;
        public string data1;
        public string data2;
        public string data3;
        public string data4;
        public bool collidable;
        public bool portal;
        public int portalx;
        public int portaly;
        public string portalfile;
    }

    private Game p_game;
    private Size p_mapSize = new Size(0, 0);
    private Size p_windowSize = new Size(0, 0);
    private int p_tileSize;
    private Bitmap p_bmpTiles;
    private int p_columns;
    private Bitmap p_bmpScrollBuffer;
    private Graphics p_gfxScrollBuffer;
    private tilemapStruct[] p_tilemap;
    private PointF p_scrollPos = new PointF(0, 0);
    private PointF p_subtile = new PointF(0, 0);
    private PointF p_oldScrollPos = new PointF(-1, -1);

    public Level(ref Game game, int width, int height, int tileSize)
    {
        p_game = game;
        p_windowSize = new Size(width, height);
        p_mapSize = new Size(width * tileSize, height * tileSize);
        p_tileSize = tileSize;

        //create scroll buffer
        p_bmpScrollBuffer = new Bitmap(p_mapSize.Width + p_tileSize,
            p_mapSize.Height + p_tileSize);
        p_gfxScrollBuffer = Graphics.FromImage(p_bmpScrollBuffer);

        //create tilemap
        p_tilemap = new tilemapStruct[128 * 128];
    }

    public tilemapStruct getTile(PointF p)
    {
        return getTile((int)(p.Y * 128 + p.X));
    }

    public tilemapStruct getTile(int pixelx, int pixely)
    {
        return getTile(pixely * 128 + pixelx);
    }
    public tilemapStruct getTile(int index)
    {
        return p_tilemap[index];
    }

    //get/set scroll position by whole tile position
    public Point GridPos
    {
        get {
            int x = (int)p_scrollPos.X / p_tileSize;
            int y = (int)p_scrollPos.Y / p_tileSize;
            return new Point(x, y);
        }
        set {
            float x = value.X * p_tileSize;
            float y = value.Y * p_tileSize;
            p_scrollPos = new PointF(x, y);
        }
    }

    //get/set scroll position by pixel position
    public PointF ScrollPos
    {
        get { return p_scrollPos; }
        set { p_scrollPos = value; }
    }


    public bool loadTilemap(string filename)
    {
        try {
            XmlDocument doc = new XmlDocument();
            doc.Load(filename);
            XmlNodeList nodelist = doc.GetElementsByTagName("tiles");
            foreach (XmlNode node in nodelist)
            {
                XmlElement element = (XmlElement)node;
                int index = 0;
                tilemapStruct ts;
                string data;
                //read data fields from xml
                data = element.GetElementsByTagName("tile")[0].
                    InnerText;
                index = Convert.ToInt32(data);
                data = element.GetElementsByTagName("value")[0].
                    InnerText;
                ts.tilenum = Convert.ToInt32(data);
                data = element.GetElementsByTagName("data1")[0].
                    InnerText;
                ts.data1 = Convert.ToString(data);
                data = element.GetElementsByTagName("data2")[0].
                    InnerText;
                ts.data2 = Convert.ToString(data);
                data = element.GetElementsByTagName("data3")[0].
                    InnerText;
                ts.data3 = Convert.ToString(data);
                data = element.GetElementsByTagName("data4")[0].
                    InnerText;
                ts.data4 = Convert.ToString(data);
                data = element.GetElementsByTagName("collidable")[0].
                    InnerText;
                ts.collidable = Convert.ToBoolean(data);
                data = element.GetElementsByTagName("portal")[0].
                    InnerText;
                ts.portal = Convert.ToBoolean(data);
                data = element.GetElementsByTagName("portalx")[0].
                    InnerText;
                ts.portalx = Convert.ToInt32(data);
                data = element.GetElementsByTagName("portaly")[0].
                    InnerText;
                ts.portaly = Convert.ToInt32(data);
                data = element.GetElementsByTagName("portalfile")[0].
                    InnerText;
                ts.portalfile = Convert.ToString(data);

                //store data in tilemap
                p_tilemap[index] = ts;
            }
        }
        catch (Exception es)
        {
            MessageBox.Show(es.Message);
            return false;
        }
        return true;
    }

    public bool loadPalette(string filename, int columns)
    {
        p_columns = columns;
        try {
            p_bmpTiles = new Bitmap(filename);
        }
        catch (Exception ex)
        {
            return false;
        }
        return true;
    }

    public void Update()
    {
        //fill the scroll buffer only when moving
        if (p_scrollPos != p_oldScrollPos)
        {
            p_oldScrollPos = p_scrollPos;

            //validate X range
            if (p_scrollPos.X < 0) p_scrollPos.X = 0;
            if (p_scrollPos.X > (127 - p_windowSize.Width) * p_tileSize)
                p_scrollPos.X = (127 - p_windowSize.Width) * p_tileSize;

            //validate Y range
            if (p_scrollPos.Y < 0) p_scrollPos.Y = 0;
            if (p_scrollPos.Y > (127 - p_windowSize.Height) * p_tileSize)
                p_scrollPos.Y = (127 - p_windowSize.Height) * p_tileSize;

            //calculate sub-tile size
            p_subtile.X = p_scrollPos.X % p_tileSize;
            p_subtile.Y = p_scrollPos.Y % p_tileSize;

            //fill scroll buffer with tiles
            int tilenum, sx, sy;
            for (int x = 0; x < p_windowSize.Width + 1; x++)
                for (int y = 0; y < p_windowSize.Height + 1; y++)
                {
                    sx = (int)p_scrollPos.X / p_tileSize + x;
                    sy = (int)p_scrollPos.Y / p_tileSize + y;
                    tilenum = p_tilemap[sy * 128 + sx].tilenum;
                    drawTileNumber(x, y, tilenum);
                }
        }
    }

    public void drawTileNumber(int x, int y, int tile)
    {
        int sx = (tile % p_columns) * (p_tileSize + 1);
        int sy = (tile / p_columns) * (p_tileSize + 1);
        Rectangle src = new Rectangle(sx, sy, p_tileSize, p_tileSize);
        int dx = x * p_tileSize;
        int dy = y * p_tileSize;
        p_gfxScrollBuffer.DrawImage(p_bmpTiles, dx, dy, src,
            GraphicsUnit.Pixel);
    }

    public void Draw(Rectangle rect)
    {
        Draw(rect.X, rect.Y, rect.Width, rect.Height);
    }

    public void Draw(int width, int height)
    {
        Draw(0, 0, width, height);
    }

    public void Draw(int x, int y, int width, int height)
    {
        Rectangle source = new Rectangle((int)p_subtile.X,
            (int)p_subtile.Y, width, height);
        p_game.Device.DrawImage(p_bmpScrollBuffer, x, y, source,
            GraphicsUnit.Pixel);
    }
}

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. We can use this code to add any object to the game world or dungeon at a random location. But, more importantly, this experiment teaches us an invaluable skill: adding objects at runtime using a list. It’s easy to add a treasure chest to a dungeon level, using tile data in the level editor, or by manually placing it, but adding treasure at runtime is another matter! That’s what we need to learn how to do.

Modifying Sprite.cs

It turns out that we need to make some new improvements to the Sprite class introduced back in Chapter 3. The changes are needed not because of a lack of foresight back then, but because of changing needs as work on our game code progresses. Expect future needs and the changes they will require—versatility is important in software development! The Sprite class needs a new version of the Draw() method. Adding a second version of the method will overload Draw in 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:

public void Draw()
{
    Rectangle frame = new Rectangle();
    frame.X = (p_currentFrame % 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);
}

And here is the new addition:

public void Draw(int x, int y)
{
    //source image
    Rectangle frame = new Rectangle();
    frame.X = (p_currentFrame % p_columns) * p_size.Width;
    frame.Y = (p_currentFrame / p_columns) * p_size.Height;
    frame.Width = p_size.Width;
    frame.Height = p_size.Height;

    //target location
    Rectangle target = new Rectangle(x, y, p_size.Width, p_size.Height);

    //draw sprite
    p_game.Device.DrawImage(p_bitmap, target, frame, GraphicsUnit.Pixel);
}

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 8.2. The images used in the demo are each 64x64 pixels in size. For this demo, I’ve switched to a grassy level with water and some trails to take a short break from the usual dungeon walls. For the dungeon motif, we could replace the trees with stones, rubble, used torches, broken weapons, and so on.

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

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

We are going to work on an over-world level for this example because it’s easier to move around on wide-open terrain and the trees illustrate the concept well—although it might have been fun to fill a walled dungeon level with crates and barrels (staples of the typical dungeon crawler game genre!).

Figure 8.3 shows the Random Trees demo program running. Note the frame rate value! As you can see in the following source code listing, 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 8.3. Random trees are added to the game world.

This example requires a new function in the Game class called Random, so let’s take a look at it in advance:

public int Random(int max)
{
    return Random(0, max);
}

public int Random(int min, int max)
{
    return p_random.Next(min, max);
}
using System;
using System.Threading;
using System.Collections.Generic;
using System.Drawing;
using System.Windows;
using System.Windows.Forms;
using System.Xml;

namespace RPG
{
    public partial class Form1 : Form
    {
        public struct keyStates
        {
            public bool up, down, left, right;
        }

        Game game;
        Level level;
        keyStates keyState;
        bool gameover;
        Bitmap treeImage;
        List<Sprite> trees;
        int treesVisible;
        int drawLast;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            gameover = true;
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            gameover = false;
            treesVisible = 0;
            drawLast = 0;
            this.Text = "Random Tree Demo";

            //create game object
            Form form = (Form)this;
            game = new Game(ref form, 800, 600);

            //create tilemap
            level = new Level(ref game, 25, 19, 32);
            level.loadTilemap("sample.level");
            level.loadPalette("palette.bmp", 5);

            //load trees
            treeImage = game.LoadBitmap("trees64.png");
            trees = new List<Sprite>();
            for (int n = 0; n< 100;n++)
            {
                Sprite tree = new Sprite(ref game);
                tree.Image = treeImage;
                tree.Columns = 4;
                tree.TotalFrames = 32;
                tree.CurrentFrame = game.Random(31);
                tree.Size = new Size(64, 64);
                tree.Position = new PointF(game.Random(1000),
                    game.Random(1000));
                trees.Add(tree);
            }

            while (!gameover)
            {
                doUpdate();
            }
            Application.Exit();
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Escape: gameover = true; break;
                case Keys.Up:
                case Keys.W: keyState.up = true; break;
                case Keys.Down:
                case Keys.S: keyState.down = true; break;
                case Keys.Left:
                case Keys.A: keyState.left = true; break;
                case Keys.Right:
                case Keys.D: keyState.right = true; break;
            }
        }

        private void Form1_KeyUp(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Up:
                case Keys.W: keyState.up = false; break;
                case Keys.Down:
                case Keys.S: keyState.down = false; break;
                case Keys.Left:
                case Keys.A: keyState.left = false; break;
                case Keys.Right:
                case Keys.D: keyState.right = false; break;
            }
        }

        private void drawTrees()
        {
            int sx,sy;
            treesVisible = 0;
            foreach (Sprite tree in trees)
            {
                sx = (int)level.ScrollPos.X;
                sy = (int)level.ScrollPos.Y;
                if (tree.X > sx && tree.X < sx + 24 * 32 && tree.Y > sy &&
                    tree.Y < sy + 18 * 32)
                {
                    int rx = Math.Abs(sx - (int)tree.X);
                    int ry = Math.Abs(sy - (int)tree.Y);
                    tree.Draw(rx, ry);
                    treesVisible += 1;
                }
            }
        }
        private void doUpdate()
        {
            //respond to user input
            int steps = 4;
            PointF pos = level.ScrollPos;
            if (keyState.up) pos.Y -= steps;
            if (keyState.down) pos.Y += steps;
            if (keyState.left) pos.X -= steps;
            if (keyState.right) pos.X += steps;
            level.ScrollPos = pos;

            //refresh level renderer
            level.Update();

            //get the untimed core frame rate
            int frameRate = game.FrameRate();

            //drawing code should be limited to 60 fps
            int ticks = Environment.TickCount;
            if (ticks > drawLast + 16)
            {
                drawLast = ticks;

                //draw the tilemap
                level.Draw(0, 0, 800, 600);

                //draw the trees in view
                drawTrees();

                //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");

                //refresh window
                game.Update();
                Application.DoEvents();
            }
            else
            {
                //throttle the cpu
                Thread.Sleep(1);
            }
        }
    }
}

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 Part III, “Exploring the Dungeon,” which begins with Chapter 10, “Creating Characters and Monsters.” But, we do need an animated character to walk around and appear to begin interacting with the game world. Figure 8.4 shows the sprite sheet used for the character.

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

Figure 8.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 8.5 shows the Walk About demo in action.

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

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

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.

using System;
using System.Threading;
using System.Collections.Generic;
using System.Drawing;
using System.Windows;
using System.Windows.Forms;
using System.Xml;

namespace RPG
{
    public partial class Form1 : Form
    {
        public struct keyStates
        {
            public bool up, down, left, right;
        }

        Game game;
        Level level;
        keyStates keyState;
        bool gameover = false;
        Bitmap treeImage;
        List<Sprite> trees;
        int treesVisible = 0;
        int drawLast = 0;
        Sprite hero;
        int heroDir = 0;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            gameover = true;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Text = "Walk About Demo";

            //create game object
            Form form = (Form)this;
            game = new Game(ref form, 800, 600);

            //create tilemap
            level = new Level(ref game, 25, 19, 32);
            level.loadTilemap("sample.level");
            level.loadPalette("palette.bmp", 5);

            //load trees
            treeImage = game.LoadBitmap("trees64.png");
            trees = new List<Sprite>();
            for (int n = 0; n< 100;n++)
            {
                 Sprite tree = new Sprite(ref game);
                 tree.Image = treeImage;
                 tree.Columns = 4;
                 tree.TotalFrames = 32;
                 tree.CurrentFrame = game.Random(31);
                 tree.Size = new Size(64, 64);
                 tree.Position = new PointF(game.Random(1000),
                     game.Random(1000));
                 trees.Add(tree);
            }

            //load hero
            hero = new Sprite(ref game);
            hero.Image = game.LoadBitmap("hero_sword_walk.png");
            hero.Columns = 9;
            hero.TotalFrames = 9 * 8;
            hero.Size = new Size(96, 96);
            hero.Position = new Point(400 - 32, 300 - 32);
            hero.AnimateWrapMode = Sprite.AnimateWrap.WRAP;
            hero.AnimationRate = 20;

            while (!gameover)
            {
                doUpdate();
            }
            Application.Exit();
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Escape: gameover = true; break;
                case Keys.Up:
                case Keys.W: keyState.up = true; break;
                case Keys.Down:
                case Keys.S: keyState.down = true; break;
                case Keys.Left:
                case Keys.A: keyState.left = true; break;
                case Keys.Right:
                case Keys.D: keyState.right = true; break;
            }
        }

        private void Form1_KeyUp(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Up:
                case Keys.W: keyState.up = false; break;
                case Keys.Down:
                case Keys.S: keyState.down = false; break;
                case Keys.Left:
                case Keys.A: keyState.left = false; break;
                case Keys.Right:
                case Keys.D: keyState.right = false; break;
            }
        }

        private void drawTrees()
        {
            int sx,sy;
            treesVisible = 0;
            foreach (Sprite tree in trees)
            {
                sx = (int)level.ScrollPos.X;
                sy = (int)level.ScrollPos.Y;
                if (tree.X > sx && tree.X < sx + 24 * 32 && tree.Y > sy &&
                    tree.Y < sy + 18 * 32)
                {
                    int rx = Math.Abs(sx - (int)tree.X);
                    int ry = Math.Abs(sy - (int)tree.Y);
                    tree.Draw(rx, ry);
                    treesVisible += 1;
                }
            }
        }

        private void doUpdate()
        {
            //respond to user input
            int steps = 4;
            PointF pos = level.ScrollPos;
            if (keyState.up) pos.Y -= steps;
            if (keyState.down) pos.Y += steps;
            if (keyState.left) pos.X -= steps;
            if (keyState.right) pos.X += steps;
            level.ScrollPos = pos;

            //orient the player in the right direction
            if (keyState.up && keyState.right) heroDir = 1;
            else if (keyState.right && keyState.down) heroDir = 3;
            else if (keyState.down && keyState.left) heroDir = 5;
            else if (keyState.left && keyState.up) heroDir = 7;
            else if (keyState.up) heroDir = 0;
            else if (keyState.right) heroDir = 2;
            else if (keyState.down) heroDir = 4;
            else if (keyState.left) heroDir = 6;
            else heroDir = -1;

            //refresh level renderer
            level.Update();

            //get the untimed core frame rate
            int frameRate = game.FrameRate();

            //drawing code should be limited to 60 fps
            int ticks = Environment.TickCount;
            if (ticks > drawLast + 16)
            {
                drawLast = ticks;

                //draw the tilemap
                level.Draw(0, 0, 800, 600);

                //draw the trees in view
                drawTrees();

                //draw the hero
                int startFrame = heroDir * 9;
                int endFrame = startFrame + 8;
                if (heroDir > -1)
                    hero.Animate(startFrame, endFrame);
                hero.Draw();
                //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");

                //refresh window
                game.Update();
                Application.DoEvents();
            }
            else
            {
                //throttle the cpu
                Thread.Sleep(1);
            }
        }
    }
}

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. The subjects we have covered here are 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
13.59.114.92