Chapter 3. Sprites and Real-Time Animation

This chapter will show how to create a sprite using the code developed in the previous chapter for working with bitmaps. We have a lot of ground to cover here, and we’ll be going through it thoroughly because this is the foundation of the Dungeon Crawler game. You will finish this chapter with a solid grasp of sprite programming knowledge, with the ability to load a sprite sheet and draw a sprite with timed animation. Because we want a sprite to draw transparently over any background image in a game, we’ll also learn how to work with an alpha channel in a bitmap image to render an image with transparency. This chapter moves along at a pretty good clip, so you don’t want to skip ahead or you might miss some important detail.

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

  • What is a sprite?

  • Sprite animation theory

  • Creating a Sprite class

  • Improving the Game class

  • Adding a real-time game loop

  • Gameplay functions

What Is a Sprite?

The first question that often arises when the discussion of sprites comes up is, “What is a sprite?” To answer this question simply, a sprite is a small, transparent, animated game object that usually moves on the screen and interacts with other sprites. You might have trees or rocks or buildings in your game that don’t move at all, but because those objects are loaded from a bitmap file when the game starts running, and drawn in the game separately from the background, it is reasonable to call them sprites. There are two basic types of sprites. One type of sprite is the “normal” sprite that I just described, which I refer to as a dynamic sprite. This type of sprite is often called an actor in game design theory. The other type of sprite might be called a static sprite; it is the sort that doesn’t move or animate. A static sprite is used for scenery or objects that the player uses (such as items that might be picked up in the game world). This type of sprite is often called a prop.

Definition

A sprite is a small, transparent, animated game object that usually moves on the screen and interacts with other sprites. There are two types of sprites: actors and props.

I’m going to treat any game entity that is loaded and drawn separately from the background as a sprite. So, I might have a whole house, which normally would be considered part of the background, as a sprite. I use that concept in the sample program later in this chapter.

Figure 3.1 shows an example sprite of a dragon. The sprite is really just the detailed pixels that you see at the center of the image, showing the dragon flying. The sprite itself only takes up about half of the actual size of the image because the computer only sees sprites in the shape of a rectangle. It is physically impossible to even store a sprite without the rectangular boundary because bitmap images are themselves rectangular. The real problem with a sprite is what to do about all the transparent pixels that should not be shown when the image is displayed on the screen (or rather, on the back buffer surface).

The sprite boundary is a rectangle that encloses the sprite with transparent pixels.

Figure 3.1. The sprite boundary is a rectangle that encloses the sprite with transparent pixels.

The amateur game programmer will try to draw a sprite using two loops that go through each pixel of the sprite’s bitmap image, drawing only the solid pixels. Here is some pseudo-code for how one might do this:

For Y = 1 To Sprite_Height
  For X = 1 to Sprite_Width
    If Pixel At X,Y Is Solid Then
      Draw Pixel At X,Y
    End
  Next X
Next Y

This pseudo-code algorithm (so named because it would not compile) goes through each pixel of the sprite image, checking for solid pixels, which are then drawn while transparent pixels are ignored. This draws a transparent sprite, but it runs so slowly that the game probably won’t be playable (even on a top-of-the-line PC).

And yet, this is the only way to draw a transparent sprite! By one method or another, some process must check the pixels that are solid and render them. The key here is understanding how drawing works, because this very critical and time-consuming algorithm is quite old and has been built into the silicon of video cards for many years now. The process of copying a transparent image from one surface to another has been provided by video cards for decades now, dating back to the old Windows 3.1 and “video accelerator” cards. The process is called bit block transfer or just blit for short. Because this important process is handled by an extremely optimized and custom video chip, you don’t need to worry about writing your own blitter for a game any longer. (Even older systems like the Nintendo Game Boy Advance have a hardware blitter.)

The video card uses alpha blending to draw textures with a translucent effect (which means you can see through them like a window) or with full transparency. Fifty-percent translucency means that half of the light rays are blocked and you can only see about half of the image. Zero-percent translucency is called opaque, which is completely solid. The opposite is 100-percent translucency, or fully transparent, which lets all light pass through. Figure 3.2 illustrates the difference between an opaque and transparent sprite image.

The sprite on the right is drawn without the transparent pixels.

Figure 3.2. The sprite on the right is drawn without the transparent pixels.

When an image needs to be drawn with transparency, we call the transparent color a color key, and the process of alpha blending causes that particular pixel color to be completely blended with the background. At the same time, no other pixels in the texture are affected by alpha blending, and the result is a transparent sprite. Color key transparency is not often used today because it’s a pain. A better way to handle transparency is with an alpha channel and a file format that supports it such as tga or png. (Note: bmp files do not support an alpha channel.)

Animating a Sprite

After you have written a few games, you’ll most likely find that many of the sprites in your games have similar behaviors, to the point of predictability. For instance, if you have sprites that just move around within the boundaries of the screen and wrap from one edge to the other, you can create a subroutine to produce this sprite behavior on call. Simply use that subroutine when you update the sprite’s position. If you find that a lot of your sprites are doing other predictable movements, it is really helpful to create many different behavioral subroutines to control their actions.

This is just one simple example of a very primitive behavior (staying within the boundary of the screen), but you can create very complex behaviors by writing subroutines that cause sprites to react to other sprites or to the player, for instance, in different ways. You might have some behavior subroutines that cause a sprite to chase the player, or run away from the player, or attack the player. The possibilities are truly limited only by your imagination, and, generally, the most enjoyable games use movement patterns that the player can learn while playing. The Sprite demo program in this chapter demonstrates sprite movement as well as animation, so you may refer to that program for an example of how the sprite movement code is used.

Sprite Animation Theory

Sprite animation goes back about three decades, when the first video game systems were being built for arcades. The earliest arcade games include classics such as Asteroids that used vector-based graphics rather than bitmap-based graphics. A vector-based graphics system uses lines connecting two points as the basis for all of the graphics on the screen. Although a rotating vector-based spaceship might not be considered a sprite by today’s standards, it is basically the same thing. Any game object on the screen that uses more than one small image to represent itself might be considered a sprite. However, to be an animated sprite, the image must simulate a sequence of images that are cycled while the sprite is being displayed.

Animation is a fascinating subject because it brings life to a game and makes objects seem more realistic. An important concept to grasp at this point is that every frame of an animation sequence must be treated as a distinct image that is stored in a bitmap file; as an alternative, some animation might be created on the fly if a technique such as rotation or alpha cycling is used. (For instance, causing a sprite to fade in and out could be done at runtime.) In the past, professional game developers did not often use rotation of a sprite at runtime due to quality concerns, but we can do that today with pretty good results.

Animation is done with the use of a sprite sheet. A sprite sheet is a bitmap containing columns and rows of tiles, with each tile containing one frame of animation. It is not uncommon for a sprite with eight directions of movement to have 64 or more frames of animation just for one activity (such as walking, attacking, or dying).

Figure 3.3 shows a dragon sprite with 64 frames of animation. The dragon can move in any of eight directions of travel, and each direction has eight frames of animation. We’ll learn to load this sprite sheet and then draw it transparently on the screen with animation later in this chapter. The source artwork (from Reiner Prokein) comes in individual bitmap files—so that a 64-frame dragon sprite starts out with 64 individual bitmap files.

A dragon sprite sheet with an 8 × 8 layout of animation frames.

Figure 3.3. A dragon sprite sheet with an 8 × 8 layout of animation frames.

Tip

This dragon sprite was provided courtesy of Reiner “Tiles” Prokein at www.reinerstileset.de. Most of the other sprite artwork in this book is also from Reiner’s sprite collection, which includes a royalty-free license.

The trick to animating a sprite is keeping track of the current frame of animation along with the total animation frames in the animation sequence. This dragon sprite is stored in a single, large bitmap image and was actually stored in 64 individual bitmaps before I converted it to a single bitmap using Pro Motion.

Trick

Cosmigo’s Pro Motion is an excellent sprite animation editor available for download at www.cosmigo.com/promotion. All of the sprite sheets featured in this book were converted using this great, useful tool.

After you have exported an animation sequence as a sprite sheet image, the trick is to get a handle on animating the sprite in source code. Storing all the frames of animation inside a single bitmap file makes it easier to use the animation in your program. However, it doesn’t necessarily make it easier to set up; you have to deal with the animation looping around at a specific point, rather than looping through all 64 frames. Now we’ll start to see where all of those odd properties and subroutines in the Sprite class will be used. I have animated the dragon sprite by passing a range to the Animate function that represents one of the four directions (up, down, left, right), which is determined by the user’s keyboard input. Although the sprite sheet has frames for all eight directions, including diagonals, the example program in this chapter sticks to the four main directions to keep the code simpler.

To get the current frame, we need to find out where that frame is located inside the sprite sheet in the least amount of code possible. To get the Y position of a frame, you take the current frame and divide by the columns to get the appropriate row (and then multiply that by the frame height, or height of each tile).

To get the X position of the frame, perform that same division as before, but get the remainder (modulus result) from the division rather than the quotient, and then multiply by the sprite’s width. At this point, the rest of the rectangle is set up using the sprite’s width and height. The destination rectangle is configured to the sprite’s current position, and then a call to the existing Draw subroutine takes care of business. Figure 3.4 shows the numbered columns and rows of a sprite sheet. Note that the numbering starts at 0 instead of 1. That is a little harder to follow when reading the code, but using a base of 0 makes the calculations much simpler. See if you can choose a frame number and calculate where it is located on the sprite sheet on your own!

The numbered columns and rows of the dragon sprite sheet.

Figure 3.4. The numbered columns and rows of the dragon sprite sheet.

Creating a Sprite Class

We could get by with a couple of reusable functions and a Bitmap. But, that would involve a lot of duplicated code that could very easily be put into a class. So, that is what we will do. There aren’t very many classes in this book, in the interest of making source code easier to understand, but in some cases it’s more difficult to not use a class—as is the case with sprite programming. The first thing you will notice is the use of a new namespace called RPG. This helps to organize things in our project now that we have some classes as well as the Form code. You can call the namespace what you wish, as long as every source code file uses the same namespace name (in order for them to “see” each other). “RPG” is sort of the name of the engine for Dungeon Crawler—it has no real meaning beyond organizing the code.

I have some goals for our new Sprite class. First, it will be self-contained, with the exception that it needs the rendering device in our Game class (Game.Device) for drawing. We can pass a reference to the game object to a sprite’s constructor at runtime and that should take care of it.

Second, the class should handle both drawing and animation with enough variation to support any needs we’ll have in Dungeon Crawler, with numerous properties to keep the code clean and tidy. This is a pretty good start, but we will make small changes to Sprite over time to meet any new needs as the game begins to take shape.

using System;
using System.Drawing;
namespace RPG
{
    public class Sprite
    {
        public enum AnimateDir
        {
            NONE = 0,
            FORWARD = 1,
            BACKWARD = -1
        }
        public enum AnimateWrap
        {
            WRAP = 0,
            BOUNCE = 1
        }
        private Game p_game;
        private PointF p_position;
        private PointF p_velocity;
        private Size p_size;
        private Bitmap p_bitmap;
        private bool p_alive;
        private int p_columns;
        private int p_totalFrames;
        private int p_currentFrame;
        private AnimateDir p_animationDir;
        private AnimateWrap p_animationWrap;
        private int p_lastTime;
        private int p_animationRate;

The Sprite constructor method is next. The variables and references are initialized at this point. It’s good programming practice to set the initial values for the properties on our own.

public Sprite(ref Game game)
  {
      p_game = game;
      p_position = new PointF(0, 0);
      p_velocity = new PointF(0, 0);
      p_size = new Size(0, 0);
      p_bitmap = null;
      p_alive = true;
      p_columns = 1;
      p_totalFrames = 1;
      p_currentFrame = 0;
      p_animationDir = AnimateDir.FORWARD;
      p_animationWrap = AnimateWrap.WRAP;
      p_lastTime = 0;
      p_animationRate = 30;
  }

The Sprite class includes numerous properties to give access to its private variables. In most cases this is a direct get/set association with no real benefit to hiding the variables internally, but in some cases (such as AnimationRate) the values are manipulated within the property’s set.

public bool Alive
 {
     get { return p_alive; }
     set { p_alive = value; }
 }

 public Bitmap Image
 {
     get { return p_bitmap; }
     set { p_bitmap = value; }
 }

 public PointF Position
 {
     get { return p_position; }
     set { p_position = value; }
 }

 public PointF Velocity
 {
     get { return p_velocity; }
     set { p_velocity = value; }
 }
 public float X
 {
     get { return p_position.X; }
     set { p_position.X = value; }
 }

 public float Y
 {
     get { return p_position.Y; }
     set { p_position.Y = value; }
 }

 public Size Size
 {
     get { return p_size; }
     set { p_size = value; }
 }

 public int Width
 {
     get { return p_size.Width; }
     set { p_size.Width = value; }
 }

 public int Height
 {
     get { return p_size.Height; }
     set { p_size.Height = value; }
 }

 public int Columns
 {
     get { return p_columns; }
     set { p_columns = value; }
 }

 public int TotalFrames
 {
     get { return p_totalFrames; }
     set { p_totalFrames = value; }

 }

 public int CurrentFrame
 {
     get { return p_currentFrame; }
     set { p_currentFrame = value; }
 }

 public AnimateDir AnimateDirection
 {
     get { return p_animationDir; }
     set { p_animationDir = value; }
 }

 public AnimateWrap AnimateWrapMode
 {
     get { return p_animationWrap; }
     set { p_animationWrap = value; }
 }

 public int AnimationRate
 {
     get { return 1000 / p_animationRate; }
     set
     {
         if (value == 0) value = 1;
         p_animationRate = 1000 / value;
     }
 }

Sprite animation is handled by the single Animate() method, which should be called from the gameplay functions Game_Update() or Game_Draw(). Animation timing is handled automatically in this function using a millisecond timer, so it can be called from the extremely fast-running Game_Update() without concern for animation speed being in sync with the drawing of the sprite. Without this built-in timing, the Animate() function would have to be called from Game_Draw(), which is timed at 60 Hz (or frames per second). Code such as this Animate() function really should be run from the fastest part of the game loop whenever possible, and only real drawing should take place in Game_Draw() due to timing considerations. If you were to put all of the gameplay code in Game_Draw() and hardly anything in Game_Update(), which is the fast-running function, then the game would slow down quite a bit. We will also need the default Animate() function which defaults to animating the whole range of animation automatically.

public void Animate()
{
    Animate(0, p_totalFrames - 1);
}

public void Animate(int startFrame, int endFrame)
{
    //do we even need to animate?
    if (p_totalFrames <= 0) return;

    //check animation timing
    int time = Environment.TickCount;
    if (time > p_lastTime + p_animationRate)
    {
        p_lastTime = time;

        //go to next frame
        p_currentFrame += (int)p_animationDir;
        switch (p_animationWrap)
        {
            case AnimateWrap.WRAP:
                if (p_currentFrame < startFrame)
                    p_currentFrame = endFrame;
                else if (p_currentFrame > endFrame)
                    p_currentFrame = startFrame;
                break;

            case AnimateWrap.BOUNCE:
                if (p_currentFrame < startFrame)
                {
                    p_currentFrame = startFrame;
                    p_animationDir = AnimateDir.FORWARD;
                }
                else if (p_currentFrame > endFrame)
                {
                    p_currentFrame = endFrame;
                    p_animationDir = AnimateDir.BACKWARD;
                }
                break;
        }
    }
}

The single Draw() method can handle all of our sprite drawing needs, including animation! However, there is an optimization that can be made for sprites that do not animate (i.e., “props”): the modulus and division calculations being done in this function make sprite sheet animation possible, but this code can slow down a game if quite a few sprites are being drawn without any animation. The Game.DrawBitmap() function can be used in those cases, because it does not take up any processor cycles to calculate animation frames.

        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);
         }

Oddly enough, even though we have not discussed the subject yet, this class already has collision detection included. We have a chapter dedicated to the subject: the very next chapter. So, let’s just briefly take a look at this as-yet-unused code with plans to dig into it soon. There is one very useful property here called Bounds, which returns a Rectangle representing the bounding box of the sprite at its current position on the screen. This is used both for drawing and collision testing. When drawing in the Draw() method, Bounds provides the destination rectangle which defines where the sprite will be drawn on the screen, and with scaling of the image if you want. The IsColliding() method below also uses Bounds. One very handy function in the Rectangle class is IntersectsWith(). This function will return true if a passed rectangle is intersecting with it. In other words, if two sprites are touching, then we will know by using this function that is built in to the Rectangle class. We don’t have to even write our own collision code! Nevertheless, we’ll explore advanced collision techniques in the next chapter (including distance or circular collision).

        public Rectangle Bounds
        {
            get {
                Rectangle rect = new Rectangle(
                    (int)p_position.X, (int)p_position.Y,
                    p_size.Width, p_size.Height);
                return rect;
            }
        }

        public bool IsColliding(ref Sprite other)
        {
            //test for bounding rectangle collision
            bool collision = Bounds.IntersectsWith(other.Bounds);
            return collision;
        }
    }
}

Sprite Drawing Demo

The Sprite demo program shows how to use the new Sprite class, the improved Game class (coming up), and the new Form/Module code presented in this chapter to draw an animated sprite. The result is shown in Figure 3.5. The dragon sprite is actually comprised of animation frames that are each 128 × 128 pixels in size, but I have enlarged the sprite sheet so the dragon is twice as large as normal. This isn’t a great idea for a game, because we can resize the sprite at runtime (with the Bitmap.DrawBitmap() method), but it was a simple solution to make it appear bigger for the sake of illustration.

The user controls an animated dragon sprite in the Sprite demo program.

Figure 3.5. The user controls an animated dragon sprite in the Sprite demo program.

Improving the Game Class

It is completely possible to make a game within the source code file of the Form, without any support or helper code or external libraries. But, there will come a point where the complexity of the source code (in a single file) will exceed our ability to manage it effectively, and progress on the game will grind to a halt with new and frustrating bugs cropping up every time one is apparently fixed.

We have already seen an early first attempt at a Game class in the previous chapter. Now that we have added a new Sprite class to our toolbox, we will revise Game to give it new features too. In the next section we will build a real-time game loop with new gameplay method calls that will enable us to write code that runs extremely fast, and that is detached from the Forms architecture. The new and improved Game class still has the primary responsibility of creating the rendering device (i.e., the PictureBox/Graphics/Bitmap concoction), but added to that is support for printing text in various fonts and loading and drawing bitmaps. At one point I had considered putting the game loop in the Game class, but it proved to be too complex and we’re going for simple, fast, and practical, keeping the source code flexible and easy to understand so changes can be made if needed. This is my best guess at this early stage, and I’m sure changes will be made later.

using System;
using System.Drawing;
using System.Diagnostics;
using System.Windows;
using System.Windows.Forms;

namespace RPG
{
    public class Game
    {
        private Graphics p_device;
        private Bitmap p_surface;
        private PictureBox p_pb;
        private Form p_frm;
        private Font p_font;
        private bool p_gameOver;

        public Game(ref Form form, int width, int height)
        {
            Trace.WriteLine("Game class constructor");

            //set form properties
            p_frm = form;
            p_frm.FormBorderStyle = FormBorderStyle.FixedSingle;
            p_frm.MaximizeBox = false;
            //adjust size for window border
            p_frm.Size = new Size(width + 6, height + 28);

            //create a picturebox
            p_pb = new PictureBox();
            p_pb.Parent = p_frm;
            //p_pb.Dock = DockStyle.Fill;
            p_pb.Location = new Point(0, 0);
            p_pb.Size = new Size(width, height);
            p_pb.BackColor = Color.Black;

            //create graphics device
            p_surface = new Bitmap(p_frm.Size.Width, p_frm.Size.Height);
            p_pb.Image = p_surface;
            p_device = Graphics.FromImage(p_surface);

            //set the default font
            SetFont("Arial", 18, FontStyle.Regular);
        }
        ~Game()
        {
            Trace.WriteLine("Game class destructor");
            p_device.Dispose();
            p_surface.Dispose();
            p_pb.Dispose();
            p_font.Dispose();
        }

        public Graphics Device
        {
            get { return p_device; }
        }

        public void Update()
        {
            //refresh the drawing surface
            p_pb.Image = p_surface;
        }

We studied rudimentary text printing in the previous chapter, which showed how to use Graphics.DrawString() to print text using any TrueType font installed on the system. Now, it’s possible to use just Font and Graphics. DrawString() for our text output needs, but I propose a simpler, more convenient approach. Instead of recreating the font object in each game, let’s add some text printing code to the Game class. This will handle most text output needs, while giving us the freedom to still create a custom font in the game if we want. Below is the new printing support in the Game class. You can change the default font using the SetFont() function and then use Print() to print text anywhere on the screen. A word of warning, though: changing the font several times per frame will slow down a game, so if you need more than one font, I recommend creating another one in your gameplay code and leaving the built-in one at a fixed type and size.

        /*
          * font support with several Print variations
          */
         public void SetFont(string name, int size, FontStyle style)
         {
             p_font = new Font(name, size, style, GraphicsUnit.Pixel);
         }
        
         public void Print(int x, int y, string text, Brush color)
         {
             Device.DrawString(text, p_font, color, (float)x, (float)y);
         }
        
         public void Print(Point pos, string text, Brush color)
         {
             Print(pos.X, pos.Y, text, color);
         }
         
         public void Print(int x, int y, string text)
         {
             Print(x, y, text, Brushes.White);
         }
        
         public void Print(Point pos, string text)
         {
             Print(pos.X, pos.Y, text);
         }

Next is the new Bitmap support code in our Game class. We will still need the old LoadBitmap() method, but will add several versions of the DrawBitmap() method. When a method name is repeated with different sets of parameters, we call that an overload—one of the fundamentals of object-oriented programming, or OOP. While still in the source code for the Game class, here is the final portion of code for the new and improved Game class:

        /*
         * Bitmap support functions
         */
        public Bitmap LoadBitmap(string filename)
        {
            Bitmap bmp = null;
            try
            {
                bmp = new Bitmap(filename);
            }
            catch (Exception ex) { }
            return bmp;
        }

        public void DrawBitmap(ref Bitmap bmp, float x, float y)
        {
            p_device.DrawImageUnscaled(bmp, (int)x, (int)y);
        }

        public void DrawBitmap(ref Bitmap bmp, float x, float y, int width,
            int height)
        {
            p_device.DrawImageUnscaled(bmp, (int)x, (int)y, width, height);
        }

        public void DrawBitmap(ref Bitmap bmp, Point pos)
        {
            p_device.DrawImageUnscaled(bmp, pos);
        }

        public void DrawBitmap(ref Bitmap bmp, Point pos, Size size)
        {
            p_device.DrawImageUnscaled(bmp, pos.X, pos.Y, size.Width,
                size.Height);
        }
    }
}

Form1 Source Code

The Form source code will be on the short side because its job is now only to pass control to the Main() method and pass along events to the gameplay methods. This code is found in the Form1.cs file in the project. Note that each event calls only one method, and we haven’t seen them before. The main gameplay code will also be located in the Form1.cs file.

using System;
using System.Drawing;
using System.Windows.Forms;
using RPG;

namespace Sprite_Demo
{
    public partial class Form1 : Form
    {
        private bool p_gameOver = false;
        private int p_startTime = 0;
        private int p_currentTime = 0;
        public Game game;
        public Bitmap dragonImage;
        public Sprite dragonSprite;
        public Bitmap grass;
        public int frameCount = 0;
        public int frameTimer = 0;
        public float frameRate = 0;
        public PointF velocity;
        public int direction = 2;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Main();
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            Game_KeyPressed(e.KeyCode);
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            Shutdown();
        }

Adding a Real-Time Game Loop

As you’ll recall, in past chapters we used a Timer control to make things happen. In those cases, the Timer was sort of the engine for the program, causing something to happen automatically. Otherwise, the only thing we can do in our code is respond to events from the controls on a Form. The Timer control works pretty well for this, but we need to dig a bit deeper to get more performance out of our code, and to do that we have to use our own timed loop. The function below is called Main(), which makes it somewhat resemble the main() function of a C++ program, or the WinMain() function of a Windows program. Before the While loop gets started, we create the game object and call Game_Init(), which is sort of the gameplay loading function where you can load game assets before the timed loop begins. After the loop exits, then the gameplay function Game_End() is called, followed by End.

      public void Main()
      {
          Form form = (Form)this;
          game = new Game(ref form, 800, 600);
      
          //load and initialize game assets
          Game_Init();
      
          while (!p_gameOver)
          {
              //update timer
              p_currentTime = Environment.TickCount;
      
              //let gameplay code update
              Game_Update(p_currentTime - p_startTime);
      
              //refresh at 60 FPS
              if (p_currentTime > p_startTime + 16)
              {
                  //update timing
                  p_startTime = p_currentTime;
      
                  //let gameplay code draw
                  Game_Draw();
      
                  //give the form some cycles
                  Application.DoEvents();
      
                  //let the game object update
                  game.Update();
                }
      
              frameCount += 1;
              if (p_currentTime > frameTimer + 1000)
              {
                  frameTimer = p_currentTime;
                  frameRate = frameCount;
                  frameCount = 0;
              }
          }
      
          //free memory and shut down
          Game_End();
          Application.Exit();
      }

Calling the Shutdown() function from anywhere in the program causes it to end. No other code is needed besides setting p_gameOver to true, because that variable controls the real-time game loop, and when that ends, then two things will happen: 1) Game_End() is called, allowing the gameplay code to clean up; 2) Application.Exit() is called, which closes the program.

      public void Shutdown()
      {
          p_gameOver = true;
      }

Gameplay Functions

We’re continuing to work on the Form1.cs source code file, now moving into the gameplay methods. I call them by that name because the Main() method and everything else might be thought of as the game engine code, and now we’re dealing with just gameplay code (that is, code that directly interacts with the player). While the engine code seldom changes, the gameplay code changes frequently and certainly will be different from one game to the next. There is no rule that we must use these particular method names—you are welcome to change them if you wish.

  1. The first method to run is Game_Init(), and this is where you can load game assets.

  2. The Game_Update() method is called repeatedly in the untimed portion of the game loop, so it will be running code as fast as the processor can handle it.

  3. The Game_Draw() method is called from the timed portion of the game loop, running at 60 FPS.

  4. The Game_End() method is called after the game loop exits, allowing for cleanup code such as removing gameplay assets from memory.

  5. The Game_KeyPressed() method is called from Form1_KeyDown(), and receives the code of any key being pressed. This is a bit of a workaround, when we could have just responded to the key press directly in Form1_KeyDown(), but we want the gameplay code to be separated. Eventually we’ll have mouse input as well.

       public bool Game_Init()
        {
            this.Text = "Sprite Drawing Demo";
            grass = game.LoadBitmap("grass.bmp");
            dragonImage = game.LoadBitmap("dragon.png");
            dragonSprite = new Sprite(ref game);
            dragonSprite.Image = dragonImage;
            dragonSprite.Width = 256;
            dragonSprite.Height = 256;
            dragonSprite.Columns = 8;
            dragonSprite.TotalFrames = 64;
            dragonSprite.AnimationRate = 20;
            dragonSprite.X = 250;
            dragonSprite.Y = 150;
            return true;
        }

        //not currently needed
        public void Game_Update(int time) { }

        public void Game_Draw()
        {
            //draw background
            game.DrawBitmap(ref grass, 0, 0, 800, 600);
            //move the dragon sprite
            switch (direction)
            {
                case 0: velocity = new Point(0, -1); break;
                case 2: velocity = new Point(1, 0); break;
                case 4: velocity = new Point(0, 1); break;
                case 6: velocity = new Point(-1, 0); break;
            }
            dragonSprite.X += velocity.X;
            dragonSprite.Y += velocity.Y;

            //animate and draw dragon sprite
            dragonSprite.Animate(direction * 8 + 1, direction * 8 + 7);
            dragonSprite.Draw();

            game.Print(0, 0, "Press Arrow Keys to change direction");
        }

        public void Game_End()
        {
            dragonImage = null;
            dragonSprite = null;
            grass = null;
        }

        public void Game_KeyPressed(System.Windows.Forms.Keys key)
        {
            switch (key)
            {
                case Keys.Escape: Shutdown(); break;
                case Keys.Up: direction = 0; break;
                case Keys.Right: direction = 2; break;
                case Keys.Down: direction = 4; break;
                case Keys.Left: direction = 6; break;
            }
        }

    }
}

Level Up!

The most remarkable accomplishment in this chapter is the creation of a robust Sprite class. Any time we need to give our sprites some new feature or behavior, it will be possible with this class. But no less significant is the start of a reusable game engine in C#! From the new real-time game loop to the new sprite animation code to the new gameplay functions, it’s been quite a romp in just a few pages! But we’ve set a foundation now for the Dungeon Crawler game, and in a very short time we will begin discussing the design of the game and begin working on the editors. But first, there are a couple more essential topics that must be covered—collision detection and audio.

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

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