.NET Compact Framework
Gaming with the .NET Compact Framework: A Simple Example
 

Geoff Schwab
Excell Data Corporation

April 2004

Applies to:
   
Microsoft® .NET Compact Framework 1.0
   Microsoft Visual Studio® .NET 2003
   Microsoft eMbedded Visual C++® 3.0

Art provided by:
   Douglas Albright III
   www.bittertrain.com

Summary: This sample expands upon the GAPI series of articles by creating a simple playable game demo. (37 printed pages)

Download Bust This Game Sample
Evaluate or Purchase GapiDraw
Download GAPI
Download eMbedded Visual Tools 2002 Edition


Contents

Introduction
GX Libraries
Technical Design
   Game Data: XML and Datasets
   Level
   Block Definitions and Instances
   Paddle
   Ball
Test Application
   The GameMain Class
Conclusion

Introduction

The Bust This game demo was developed as a simple demonstration of the power of the .NET Compact Framework to support a managed game library. This simple game demo is implemented in managed code and utilizes XML to provide game level information.

The game plays much like the old game Break Out. It supports variable sized playing fields, a static background and unique block, ball, and paddle definitions on a per-level basis.

Figure 1 shows a screenshot of the GDI version of the game in action.

Figure 1. Screenshot of Bust This gameplay.

GX Libraries

The GX libraries should be familiar to anyone who has followed the GAPI series of articles. These libraries are exactly the same in this sample as in the more complex "Ultimate G Man" game demo. For more information on these libraries and the method by which GXGraphics supports three separate low-level graphics implementations, refer to the G Man article.

Note: The GXGraphics library supports three separate graphics implementations: GX, GapiDraw, and GDI. To build and run the GapiDraw version, follow the instructions in the "Ultimage G Man" game article.

Technical Design

The data for each level of the game is defined in each level XML file. The game will progress through each level in sequential order (starting with level1.xml) until there are no more leveln.xml files remaining. At that point, the last level will be repeated indefinitely. Each level file allows the level to be completely customized.

Game Data: XML and DataSets

XML DataSets were chosen as the data format due to their flexibility and ease of modification. I was able to hand off the level data to an artist and he created the art and updated the level files himself.

Each game object in the world is represented by a DataRow within the DataSet.

The game area defines the background appearance and the size of the playing field. This data is maintained by the Level class. If ImageIndex is less than 0 then there is no background. Similarly, if BorderWidth is 0 or less then no border is drawn.

  <GameArea>
    <X>23</X>
    <Y>32</Y>
    <Width>197</Width>
    <Height>250</Height>
    <ImageIndex>3</ImageIndex>
    <BorderWidth>0</BorderWidth>
    <BorderColor>255,0,0</BorderColor>
    <TextColor>255,255,255</TextColor>
  </GameArea>

The player is represented in the game by the Paddle class. The Paddle class is derived from BlockInst and is represented in data by Paddle.

  • YOffset is the distance from the bottom of the game area at which the paddle should reside.
  • MoveRate is the rate in pixels/second that the paddle moves when the player presses a move button.
  • MaxVelocityModifier is the maximum effect the paddle can have on the ball's x velocity.
  • NoEffectWidth defines the width of the center area of the paddle at which the ball's velocity will not be affected by the paddle.
  • ImageIndex is the index into the level's image list that represents the paddle.
  <Paddle>
    <YOffset>16</YOffset>
    <MoveRate>180</MoveRate>
    <MaxVelocityModifier>50</MaxVelocityModifier>
    <NoEffectWidth>5</NoEffectWidth>
    <ImageIndex>0</ImageIndex>
  </Paddle>

The Ball is also derived from BlockInst and is represented in code by the Ball class. The data representation of the ball is defined by Ball.

  <Ball>
    <YOffset>180</YOffset>
    <Velocity>160</Velocity>
    <ImageIndex>1</ImageIndex>
  </Ball>

The level can define one or more Row DataRows in the level DataSet. Each Row represents a row of blocks in the level and which are defined in code by BlockInst instances.

  • X is the starting x location, within the game area (not the screen), of the row.
  • Y is the starting y location, within the game area (not the screen), of the row.
  • XSpace is the space, in pixels, to put between each block in the row.
  • XWidth is the space, in pixels, represented by an empty space in the row.
  • Blocks represents the list of block indices. Each index corresponds to an element in the block definition list defined by BlockDefinition entries in the level data. A -1 represents an empty space (see XWidth above).
  <Row>
    <X>0</X>
    <Y>17</Y>
    <XSpace>1</XSpace>
    <XWidth>32</XWidth>
    <Blocks>-1,0,0,0,0,-1</Blocks>
  </Row>

A BlockDefinition entry defines a type of block that is used in the level. The level uses this data to create instances of blocks in the Row entries for the level.

  <BlockDefinition>
    <Points>10</Points>
    <HitsToDestroy>1</HitsToDestroy>
    <ImageIndex>2</ImageIndex>
  </BlockDefinition>

Finally, the level defines a list of images that are used by various elements within the level. These images are used to create a list which has elements that correspond to any ImageIndex entries used by the level's other data elements.

  <Image><FileName>paddle.bmp</FileName></Image>
  <Image><FileName>ball.bmp</FileName></Image>
  <Image><FileName>block1.bmp</FileName></Image>
  <Image><FileName>level1.bmp</FileName></Image>

Level

The Level class defines a level of the game and is associated with a leveln.xml file. The Level class is responsible for loading all of the resources required to play a level, including the paddle and ball, and maintains lists of all interactive components that exist within the level.

The following members are defined by the Level class:

  • m_images: List of bitmap images used by the level. This defines everything from the gameplay elements, such as blocks, the ball and the paddle, to the background image of the level.
  • m_gameArea: The game area is the rectangle in which the ball is in play. If the ball collides with an edge of the game area, it bounces inward.
  • m_blocks: List of blocks that exist in the level. Blocks are the objects that the player is attempting to destroy.
  • Done: Property that specifies when the player has completed the level by destroying all of the blocks.
  • m_numBlocks: Number of blocks in the level.
  • MyPaddle: Property that accesses the paddle (player). This is always element 0 of the block list.
  • MyBall: Property that accesses the ball. This is always element 1 of the block list.
  • m_back: Defines the background image of the level. This can be null if the level does not have a background.
  • m_borderWidth: Defines the width of the border that is to be drawn around the game area. If this value is 0 or less then no border is drawn. Typically, a border would only be drawn if there is no background image.
  • m_borderColor: Color of the border to be drawn if the width is greater than 0.
  • m_textColor: Color in which to draw the text on the given level.
  • Mode: Enum that defines the various game modes. These modes are used to determine when informational text should be displayed and when the game is in a state that is waiting for user input.
  • m_mode: Mode in which the level is currently running.
  • Level: The constructor for the Level class is responsible for loading all resources used by the level, based on the XML data file.
  • ParseRGBToPixel: When loading the level, colors are defined within the XML data files as text "R,G,B" and this method converts that to a Color instance.
  • AddBlockRow: Given a list of block definitions and DataRow, this method adds the appropriate blocks, as defined by DataRow, to the level's block list.
  • Reset: The Reset method sets the level back to a state where it is waiting for user input before starting the ball moving again. This method is utilized when the player misses a ball and there are still lives left to play.
  • Update: The Update method applies physics to the ball and checks for collisions before drawing of the level takes place.
  • CheckCollisions: This method checks collisions between the ball and all other gameplay elements, including the game area extents.
  • Draw: The Draw method draws all graphical elements to the screen.
  • Dispose: Frees any resources allocated by the level.
private ArrayList m_images = new ArrayList();
internal Rectangle GameArea { get { return m_gameArea; } }
private Rectangle m_gameArea = new Rectangle(0,0,0,0);
public ArrayList Blocks { get { return m_blocks; } }
private ArrayList m_blocks = new ArrayList();
public bool Done { get { return m_numBlocks <= 0; } }
private int m_numBlocks = 0;
internal Paddle MyPaddle { get { return (Paddle)m_blocks[0]; } }
internal Ball MyBall { get { return (Ball)m_blocks[1]; } }
private GXBitmap m_back;
private int m_borderWidth;
private Color m_borderColor;
public Color TextColor { get { return m_textColor; } }
private Color m_textColor;
public enum Mode
{
    kWaitingToStart,
    kWaitingToReset,
    kPlaying,
    kGameOverWaiting,
    kGameOver
}
public Mode CurrentMode { get { return m_mode; } }
private Mode m_mode = Mode.kWaitingToStart;

The Level class constructor loads the level from a DataSet that defines it. First, the game area is loaded which includes the actual rectangular region, border information, and the background image to be used in the level.

The level then loads the images that are used in the level. These are loaded as a separate list within the level so that they can be instanced (used by multiple objects) so as to save memory. For example, numerous blocks may share the same bitmap.

Block definitions are defined in the data and used to instance blocks within the level. Similar to images, block definitions are loaded as a separate list so as to allow block instances to be created from the same definition. Once the level is loaded, the block definitions are discarded since they are no longer required, unlike the images. Each block definition defines all of the parameters required to create a block, such as the number of points the player gets for destroying it and the number of hits required to destroy it.

The player is essentially a block with some additional parameters and is defined as an instance of the Paddle class which is derived from the BlockInst class. Likewise, the ball is also derived from the BlockInst class and is defined as an instance of the Ball class.

The block instances are also loaded from the level data. Blocks are defined as instances of the BlockInst class and are constructed from BlockDef instances. Each row is comprised of a string of indices, where each index is associated with an element in the block definition list.

public Level(DataSet ds, GXGraphics gx)
{
    // Game Area
    DataTable dt = ds.Tables["GameArea"];
    DataRow dr = dt.Rows[0];
    m_gameArea.X = int.Parse((string)dr["X"]);
    m_gameArea.Y = int.Parse((string)dr["Y"]);
    m_gameArea.Width = int.Parse((string)dr["Width"]);
    m_gameArea.Height = int.Parse((string)dr["Height"]);
    m_borderWidth = int.Parse((string)dr["BorderWidth"]);
    if (m_borderWidth > 0)
    {
        m_borderColor = ParseRGBToPixel((string)dr["BorderColor"], gx);
    }
    int imageId = int.Parse((string)dr["ImageIndex"]);

    m_textColor = ParseRGBToPixel((string)dr["TextColor"], gx);

    // Images
    dt = ds.Tables["Image"];
    foreach (DataRow drImage in dt.Rows)
    {
        GXBitmap bmp = new
            GXBitmap(GameMain.GetFullPath((string)drImage["FileName"]),
            gx);
        m_images.Add(bmp);
    }

    if (imageId >= 0)
        m_back = (GXBitmap)m_images[imageId];
    else
        m_back = null;

    // Block definitions
    ArrayList blockDefs = new ArrayList();
    dt = ds.Tables["BlockDefinition"];
    foreach (DataRow drBlock in dt.Rows)
    {
        BlockInst.BlockDef block = new BlockInst.BlockDef(drBlock);
        blockDefs.Add(block);
    }

    // Player
    dt = ds.Tables["Paddle"];
    dr = dt.Rows[0];
    Paddle paddle = new Paddle(dr, m_gameArea, m_images);
    m_blocks.Add(paddle);

    // Ball
    dt = ds.Tables["Ball"];
    dr = dt.Rows[0];
    Ball ball = new Ball(dr, m_gameArea, m_images);
    m_blocks.Add(ball);

    // Rows
    dt = ds.Tables["Row"];
    foreach (DataRow drRow in dt.Rows)
    {
        AddBlockRow(drRow, blockDefs);
    }

    // Set the block count for the level
    m_numBlocks = m_blocks.Count - 2;

    // Try to clean up memory now so it does not happen
    // during gameplay
    blockDefs = null;
    GC.Collect();
}

ParseRGBToPixel converts a string of the format "R,G,B" to a Color.

private Color ParseRGBToPixel(string rgb, GXGraphics gx)
{
    string[] components = rgb.Split(',');

    Debug.Assert(components.Length == 3,
        "Level.ParseRGBToPixel: Invalid RGB border color");

    int r = int.Parse(components[0]);
    int g = int.Parse(components[1]);
    int b = int.Parse(components[2]);

    return Color.FromArgb(r,g,b);
}

AddBlockRow is a private method that reads the data specified by the provided DataRow and creates a list of blocks that are added to the level's block instance list. Each row of blocks is a string of indices, where each index corresponds to an element in the block definition list.

private void AddBlockRow(DataRow dr, ArrayList blockDefs)
{
    int xOffset = int.Parse((string)dr["X"]);
    int yOffset = int.Parse((string)dr["Y"]);
    int xSpace = int.Parse((string)dr["XSpace"]);
    int xWidth = int.Parse((string)dr["XWidth"]);

    string[] txtIndex = ((string)dr["Blocks"]).Split(',');

    int curX = xOffset + m_gameArea.X;

    foreach (string s in txtIndex)
    {
        int blockIndex = int.Parse(s);
        if (blockIndex >= 0 && blockIndex < blockDefs.Count)
        {
            BlockInst.BlockDef bDef =
                (BlockInst.BlockDef)blockDefs[blockIndex];

            BlockInst bi = new BlockInst(bDef, curX, yOffset +
                m_gameArea.Y, m_images);
            if (curX + bi.Width > m_gameArea.Right)
                break;

            curX += bi.Width + xSpace;

            m_blocks.Add(bi);
        }
        else
        {
            curX += xWidth + xSpace;
        }
    }
}

The Reset method sets the ball and paddle back to their initial starting positions and waits for user input to start again.

public void Reset()
{
    MyBall.Reset();

    MyPaddle.Reset();

    m_mode = Mode.kWaitingToStart;
}

The Update method checks user input and updates the paddle accordingly. It is also responsible for maintaining the game state by updating the ball, blocks, and checking collisions.

public void Update(GXInput gi)
{
    if (m_mode == Mode.kWaitingToReset)
    {
        if (gi.AnyKeyJustPressed())
            Reset();

        return;
    }

    if (m_mode == Mode.kGameOverWaiting)
    {
        if (gi.AnyKeyJustPressed())
            m_mode = Mode.kGameOver;

        return;
    }

    if (m_mode == Mode.kGameOver)
        return;

    if (m_mode == Mode.kWaitingToStart)
    {
        if (gi.KeyJustPressed((int)Keys.Left))
            MyBall.StartMoving(Keys.Left);
        else if (gi.KeyJustPressed((int)Keys.Right))
            MyBall.StartMoving(Keys.Right);
        else
            return;

        m_mode = Mode.kPlaying;

        return;
    }

    foreach (BlockInst bi in m_blocks)
    {
        bi.Update(gi, m_gameArea);
    }

    CheckCollisions();
}

The CheckCollisions method checks if the ball has collided with any blocks in the level and calls the appropriate Bounce method that causes the ball to bounce off of the block. The method then confines the ball to the game area by checking collisions with the game area edges. Finally, the ball is checked for collision with the player's paddle and determines if the ball has succeeded in progressing below the paddle.

private void CheckCollisions()
{
    Ball ball = MyBall;
    Paddle paddle = MyPaddle;

    // Check collisions with blocks
    for (int i = 2; i < m_blocks.Count; i++)
    {
        BlockInst bi = (BlockInst)m_blocks[i];

        if (!bi.Active)
            continue;

        if (ball.Right >= bi.X && ball.X <= bi.Right &&
            ball.Bottom >= bi.Y && ball.Y <= bi.Bottom)
        {
            bi.Hits = bi.Hits - 1;
            if (bi.Hits <= 0)
            {
                bi.Active = false;
                m_numBlocks--;

                paddle.Points = paddle.Points + bi.Points;
            }

            // Bounce the ball off of the block
            ball.Bounce(bi);

            break;
        }
    }

    // Check that the ball is confined within the game area
    ball.Confine(m_gameArea);

    // Check if the ball collided with the paddle
    if (ball.CollidePaddle(paddle))
        return;

    // If the ball is below the paddle then lose a life and reset
    if (ball.Bottom > paddle.Y)
    {
        m_mode = Mode.kWaitingToReset;

        paddle.Lives = paddle.Lives - 1;
        if (paddle.Lives <= 0)
            m_mode = Mode.kGameOverWaiting;
    }
}

The draw method draws the background (whether this is the border and/or an image) and cycles through every game object in the world, drawing each in turn.

public void Draw(GXGraphics gx)
{
    if (m_back != null)
    {
        int x = (gx.ScreenWidth - m_back.Width) >> 1;
        int y = (gx.ScreenHeight - m_back.Height) >> 1;

        Rectangle src = new Rectangle(0, 0, m_back.Width,
            m_back.Height);
        gx.DrawBitmap(x, y, src, m_back);
    }
    else
    {
        Rectangle fill = new
            Rectangle(0,0,gx.ScreenWidth,gx.ScreenHeight);
        gx.DrawFilledRect(fill, Color.Black);
    }

    if (m_borderWidth > 0)
    {
        Rectangle fill = new Rectangle(0,0,0,0);

        fill.X = m_gameArea.X - m_borderWidth;
        fill.Y = m_gameArea.Y - m_borderWidth;
        fill.Width = m_borderWidth;
        fill.Height = m_gameArea.Height + (m_borderWidth << 1);
        gx.DrawFilledRect(fill, m_borderColor);

        fill.X += m_borderWidth;
        fill.Width = m_gameArea.Width + m_borderWidth;
        fill.Height = m_borderWidth;
        gx.DrawFilledRect(fill, m_borderColor);

        fill.X = m_gameArea.Right;
        fill.Y += m_borderWidth;
        fill.Width = m_borderWidth;
        fill.Height = m_gameArea.Height + m_borderWidth;
        gx.DrawFilledRect(fill, m_borderColor);

        fill.X = m_gameArea.X;
        fill.Y = m_gameArea.Bottom;
        fill.Width = m_gameArea.Width;
        fill.Height = m_borderWidth;
        gx.DrawFilledRect(fill, m_borderColor);
    }

    // Draw the blocks
    for (int i = 2; i < m_blocks.Count; i++)
    {
        ((BlockInst)m_blocks[i]).Draw(gx);
    }

    // Draw the paddle and ball
    for (int i = 0; i < 2; i++)
    {
        ((BlockInst)m_blocks[i]).Draw(gx);
    }
}

Dispose frees any resources allocated for the images used in the level.

public void Dispose()
{
    foreach (GXBitmap bmp in m_images)
    {
        bmp.Dispose();
    }
}

Block Definitions and Instances

Block definitions and instances work together in that a definition is used to define an instance. For example, a level may define three different blocks and then create five instances of each for a total of fifteen blocks. Block definitions are used only to create the block instances and live only as long as the constructor for the level. Once the definitions have been used to create the block instances in the level, they are discarded.

The BlockInst class defines the following members:

  • BlockDef: This class defines a block and is used to create an instance of BlockInst.
  • Right: Defines the right edge of a block.
  • Bottom: Defines the bottom edge of a block.
  • Width: Defines the width of a block.
  • Height: Defines the height of a block.
  • m_active: Specifies of a block is currently active. If not then no collisions are checked against it and it is not drawn.
  • m_hitsLeft: Specifies the number of hits remaining until the block is destroyed.
  • m_points: Specifies the amount of points the player gets for destroying the block. In the case of the player (Paddle class) which is derived from BlockInst, this represents the number of points the player has.
  • m_x: The x pixel location of the block.
  • m_y: The y pixel location of the block.
  • m_image: The image that defines the block. All blocks must be represented by a bitmap.
  • BlockInst: The constructor of a block instance creates the instance based on a block definition. An empty constructor is also provided for derived classes.
  • Update: This virtual method updates the block.
  • Draw: This virtual method draws the block.
  • Reset: This virtual method resets the block awaiting a restart of the level.

The BlockDef class is relatively simple. It defines the point value of a block, the number of hits required to destroy it, and the index of the image that defines it. The constructor for the BlockDef class requires a DataRow that defines the BlockDef instance.

internal class BlockDef
{
    public int m_points;

    public int m_hitsToDestroy;

    public int m_imageId;

    public BlockDef(DataRow dr)
    {
        m_hitsToDestroy = int.Parse((string)dr["HitsToDestroy"]);
        m_imageId = int.Parse((string)dr["ImageIndex"]);
        m_points = int.Parse((string)dr["Points"]);
    }
}

The following properties and members are defined within the BlockInst class.

public float Right { get { return m_x + m_image.Width; } }
public float Bottom { get { return m_y + m_image.Height; } }
public int Width { get { return m_image.Width; } }
public int Height { get { return m_image.Height; } }
public bool Active
{
    get { return m_active; }
    set { m_active = value; }
}
protected bool m_active = true;
public int Hits
{
    get { return m_hitsLeft; } 
    set { m_hitsLeft = value; }
}
protected int m_hitsLeft;
public int Points
{
    get { return m_points; }
    set { m_points = value; }
}
protected int m_points;
public float X { get { return m_x; } }
protected float m_x;
public float Y { get { return m_y; } }
protected float m_y;
protected GXBitmap m_image;

The BlockInst class constructor initializes a block based on the specified BlockDef. The parameter list also includes the world coordinates at which the block is located and the level's image list.

public BlockInst(BlockDef def, int x, int y, ArrayList imageList)
{
    m_points = def.m_points;
    m_hitsLeft = def.m_hitsToDestroy;
    m_x = x;
    m_y = y;
    m_image = (GXBitmap)imageList[def.m_imageId];

    Reset();
}

The BlockInst empty constructor is provided for derived classes.

protected BlockInst()
{
    m_points = 0;
}

The Update method needs not do anything for blocks, but rather is provided for derived classes. If the blocks contained animations then this method could be used to update the animations.

virtual public void Update(GXInput gi, Rectangle gameArea)
{
}

The Draw method provided by the BlockInst class simple draws the image at the block's location.

virtual public void Draw(GXGraphics gx)
{
    if (!m_active)
        return;

    Rectangle src = new Rectangle(0, 0, m_image.Width,
        m_image.Height);
    gx.DrawBitmap((int)m_x, (int)m_y, src, m_image);
}

Much as the Update method, the Reset method is empty and provided for derived classes.

virtual public void Reset()
{
}

Paddle

The Paddle class defines the object controlled by the player. For convenience of storage and code re-use, this class is derived from BlockInst.

The paddle affects the velocity when the ball hits it, depending upon the location of the paddle at the time of the collision. The paddles defines a "no effect" area at its center. When the ball hits the paddle within this area there is no affect on the ball's velocity, however, when the ball hits the paddle outside of this are, the x velocity of the ball is increased by a calculated amount. The amount the ball is affected is a linear interpolation from the edge of the "no effect" area to the edge of paddle and has a maximum affect out the outer edge. Figure 2 demonstrates this.

Figure 2. Paddle Effect on Ball Velocity.

In addition to the members defined in the base class BlockInst, the Paddle class defines the following members:

  • m_startX: The initial x location of the paddle. This is used to reset the paddle when the level is reset.
  • m_startY: The initial y location of the paddle. This is used to reset the paddle when the level is reset.
  • m_noEffectWidth: This member defines the width of the "no effect" area of the paddle.
  • m_maxVelocityModifier;
  • m_moveRate;
  • m_livesRemaining;
  • Paddle(DataRow dr, Rectangle gameArea, ArrayList imageList)
  • GetModifier(float x)
  • Update(GXInput gi, Rectangle gameArea)
  • Reset()
private float m_startX;
private float m_startY;
private int m_noEffectWidth;
private float m_maxVelocityModifier;
private float m_moveRate;
public int Lives
{
    get { return m_livesRemaining; }
    set { m_livesRemaining = value; }
}
private int m_livesRemaining;

The Paddle constructor creates the paddle based on the given DataRow.

public Paddle(DataRow dr, Rectangle gameArea, ArrayList imageList)
{
    m_image = (GXBitmap)imageList[int.Parse((string)dr["ImageIndex"])];
    m_startX = gameArea.X + ((gameArea.Width - m_image.Width) >> 1);
    m_startY = gameArea.Bottom - int.Parse((string)dr["YOffset"]);
    m_moveRate = float.Parse((string)dr["MoveRate"]);
    m_noEffectWidth = int.Parse((string)dr["NoEffectWidth"]);
    m_maxVelocityModifier =
        float.Parse((string)dr["MaxVelocityModifier"]);

    m_livesRemaining = GameMain.kNumLivesAtStart;

    Reset();
}

GetModifier is the method responsible for determining for determining the effect that the paddle will have on the ball's velocity if they collide. The amount returned by this method should be added to the ball in the x direction in which the ball is currently moving.

public float GetModifier(float x)
{
    float minX = m_x + ((m_image.Width - m_noEffectWidth) >> 1);
    float maxX = m_x + ((m_image.Width + m_noEffectWidth) >> 1);

    if (x >= minX && x <= maxX)
        return 0.0F;
        if (x < minX)
    {
        return ((minX - x) / (minX - m_x)) * m_maxVelocityModifier;
    }

    return ((x - maxX) / (m_x + m_image.Width - maxX)) *
        m_maxVelocityModifier;
}

The paddle Update method checks for input and updates the location of the paddle accordingly. The amount of movement is determined by the movement rate of the paddle.

override public void Update(GXInput gi, Rectangle gameArea)
{
    if (gi.KeyPressed((int)Keys.Left))
    {
        m_x -= m_moveRate * GameMain.kSecondsPerFrame;

        if (m_x < gameArea.X)
            m_x = gameArea.X;
    }
    else if (gi.KeyPressed((int)Keys.Right))
    {
        m_x += m_moveRate * GameMain.kSecondsPerFrame;

        if (Right > gameArea.Right)
            m_x = gameArea.Right - m_image.Width;
    }
}

The Reset method moves the paddle back to its starting location.

override public void Reset()
{
    m_x = m_startX;
    m_y = m_startY;
}

Ball

The Ball class is derived from BlockInst and implements some extra members in order to tack the ball's position in the game and its interaction with other gameplay elements.

In addition to the members implemented by BlockInst, Ball implements the following members:

  • m_prevX: Records the x location of the ball in the previous frame.
  • m_prevY: Records the y location of the ball in the previous frame.
  • m_startX: Stores the ball's initial x position. This is used when the level is reset.
  • m_startY: Stores the ball's initial y position. This is used then the level is reset.
  • m_vel: Velocity at which to move the ball. This is defined in the level data.
  • m_velX: Velocity of the ball in the x direction.
  • m_velY: Velocity of the ball in the y direction.
  • Ball: The constructor loads and initializes and instance of the game ball based on the data in the level XML file.
  • StartMoving: This method starts the ball moving based on its velocity and the initial direction.
  • Reset: Resets the ball to its initial position.
  • Update: Updates the ball. This method is responsible for updating the location of the ball based on its velocity.
  • Draw: Draws the ball.
  • Bounce: Bounces the ball off of a specific block.
  • Confine: Confines the ball to the game area and bounces it off of the walls if necessary.
  • CollidePaddle: Checks for collision with the player's paddle.
private float m_prevX;
private float m_prevY;
private float m_startX;
private float m_startY;
private float m_vel;
private float m_velX;
private float m_velY;

public Ball(DataRow dr, Rectangle gameArea, ArrayList imageList)
{
    m_image = (GXBitmap)imageList[int.Parse((string)dr["ImageIndex"])];
    m_startX = gameArea.X + ((gameArea.Width - m_image.Width) >> 1);
    m_startY = gameArea.Bottom - int.Parse((string)dr["YOffset"]);
    m_vel = float.Parse((string)dr["Velocity"]);

    m_image.SetSourceKey(0,0);

    Reset();
}

The StartMoving method takes the direction specified by the user key press and determines a starting direction to within a 45 degree arc centered straight down.

public void StartMoving(Keys dir)
{
    double theta = 0.0F;
    if (dir == Keys.Left)
        theta = 7.0F * Math.PI / 4.0F;

    theta += GameMain.Random() * Math.PI / 4.0F;

    m_velY = m_vel * (float)Math.Cos(theta);
    m_velX = m_vel * (float)Math.Sin(theta);
}

override public void Reset()
{
    m_x = m_startX;
    m_y = m_startY;
}

override public void Update(GXInput gi, Rectangle gameArea)
{
    m_prevX = m_x;
    m_prevY = m_y;

    m_x += m_velX * GameMain.kSecondsPerFrame;
    m_y += m_velY * GameMain.kSecondsPerFrame;
}

override public void Draw(GXGraphics gx)
{
    if (!m_active)
        return;

    gx.SetDrawFlags(GXGraphics.DrawFlags.GDBLTFAST_KEYSRC);

    base.Draw(gx);

    gx.ClearDrawFlags(GXGraphics.DrawFlags.GDBLTFAST_KEYSRC);
}

The Bounce method takes the current and previous locations of the ball and utilizes them to determine a "zone" in which the ball collided with the block. These four zones represent colliding with the block from the top, left, right, and bottom consecutively.

public void Bounce(BlockInst bi)
{
    if (m_prevX + (Width >> 1) >= bi.X && m_prevX + (Width >> 1) <= bi.Right)
    {
        if (m_prevY < bi.Y + (bi.Height >> 1))
        {
            // Zone 1 so bounce up
            m_y = bi.Y - m_image.Height - (Bottom - bi.Y);
        }
        else
        {
            // Zone 4 so bounce down
            m_y = bi.Bottom + (bi.Bottom - m_y);
        }

        m_velY = -m_velY;
    }
    else
    {
        if (m_prevX < bi.X + (bi.Width >> 1))
        {
            // Zone 2 so bounce left
            m_x = bi.X - m_image.Width - (Right - bi.X);
        }
        else
        {
            // Zone 3 so bounce right
            m_x = bi.Right + (bi.Right - m_x);
        }

        m_velX = -m_velX;
    }
}

The Confine method confines the ball to the game area. This is done by determining if the ball collides with the edges of the game area and bounces it back in if it does.

public void Confine(Rectangle gameArea)
{
    if (m_x < gameArea.X)
    {
        m_x = gameArea.X + gameArea.X - m_x;
        m_velX = -m_velX;
    }
    else if (Right >= gameArea.Right)
    {
        m_x = gameArea.Right - m_image.Width – 
            (Right - gameArea.Right);
        m_velX = -m_velX;
    }

    if (m_y < gameArea.Y)
    {
        m_y = gameArea.Y + gameArea.Y - m_y;
        m_velY = -m_velY;
    }
}

This method checks if the ball has collided with the paddle. To get an accurate detection of the point at which they collide, the time of the collision is interpolated and the point of collision is detected at that time. This is necessary because a collision will likely occur between frames.

public bool CollidePaddle(Paddle paddle)
{
    if (Bottom > paddle.Y)
    {
        float deltaY = Bottom - paddle.Y;

        // Determine how far back in time the y collision took place
        float deltaT = deltaY / m_velY;

        // Calculate the x difference at the time y collided
        float deltaX = deltaT * m_velX;

        // Determine if a collision occurred
        if (Right - deltaX < paddle.X || m_x - deltaX > paddle.Right)
            return false;

        // Get the velocity modifier from the paddle
        float mod = paddle.GetModifier(m_x - deltaX +
            (m_image.Width >> 1));
        if (m_velX < 0.0F)
            m_velX -= mod;
        else
            m_velX += mod;

        // Adjust the Y coordinate to bounce off of the paddle
        m_y = paddle.Y - m_image.Height - (Bottom - paddle.Y);

        // Never let the x velocity get hight than 75% of the total
        // velocity
        if (Math.Abs(m_velX) > .75F * Math.Abs(m_vel))
        {
            if (m_velX < 0.0F)
                m_velX = -.75F * m_vel;
            else
                m_velX = .75F * m_vel;
        }

        // Re-adjust the y velocity so that the total velocity is still
        // the same.
        m_velY = -(float)Math.Sqrt(m_vel * m_vel - m_velX * m_velX);

        return true;
    }

    return false;
}

Test Application

The test application comprises two classes, GameMain and GameForm. The GameForm class is a simple shell Form which instantiates GameMain and runs the game application. The GameMain class has the responsibility of implementing the game loop.

The GameMain Class

The GameMain class defines the game loop and is responsible for handling level loads and displaying informational text to the user. The Form that owns the instance of GameMain need only instantiate it, call Run to execute the game, and Dispose it when it returns from Run.

The GameMain class defines the following members:

  • kNumLivesAtStart: Number of lives when the game starts.
  • kSecondsPerFrame: Defines the locked framerate in seconds / frame.
  • kInitialSplashTime: Length of time, in seconds, for which to display the splash screen.
  • m_done: Specifies when the game is done and should exit the game loop.
  • m_gx: Instance of GXGraphics used by the game.
  • m_gi: Instance of GXInput used by the game.
  • m_update: Update delegate used to update the game each frame. This delegate gets set depending upon which state the game is in.
  • m_rnd: Instance of Random class used throughout the game.
  • m_level: Instance of Level class defining the current level.
  • m_curLevel: Specifies the level that is currently loaded.
  • m_displayedLevel: Specifies the level that is displayed to the player. This may be greater than m_curLevel if the player exceeds the number of available levels in the game.
  • m_font: Instance of GXFont used to draw UI text in the game.
  • m_splash: Splash bitmap displayed at the start of the game.
  • m_remainingSplashTime: Length of time, in seconds, remaining until the splash screen can be freed and the level can be displayed.
  • GameState: Enum that defines the high-level state of the game. This is used to determine when to display text, etc.
  • m_curState: State the game is currently in.
  • GameMain: The constructor initializes the GX systems used by the game and loads the game font.
  • Random: Returns a random double from 0-1. There is also an overload that returns an integer within a min and max range.
  • GetFullPath: Returns the path to the directory in which the application executable is running.
  • LoadLevel: Loads the next level.
  • Run: Runs the game. This is the entry point to the GameMain class.
  • UpdateSplash: Update the splash screen.
  • UpdateLevel: Update the current level.
  • Dispose: Free any resources allocated by the GameMain class.
public const int kNumLivesAtStart = 3;
public const float kSecondsPerFrame = 1.0F / 50.0F;
private const float kInitialSplashTime = 3.0F;
private bool m_done = false;
private GXGraphics m_gx = null;
private GXInput m_gi = null;
private UpdateDelegate m_update = null;
private delegate void UpdateDelegate();
private static Random m_rnd = null;
Level m_level = null;
private int m_curLevel;
private int m_displayedLevel;
private GXFont m_font;
private GXBitmap m_splash;
private float m_remainingSplashTime = kInitialSplashTime;
private enum GameState
{
    kNotLoaded,
    kLoaded,
    kLoading,
    kUpdateLevelRequest,
    kUpdatingLevel
}
private GameState m_curState = GameState.kNotLoaded;

public GameMain(Control owner)
{
    // Create a GXGraphics instance
    m_gx = new GXGraphics(owner);
    Debug.Assert(m_gx != null,
        "GameMain.GameMain: Failed to initialize GXGraphics object");

    // Create a GXInput instance
    m_gi = new GXInput();
    Debug.Assert(m_gi != null,
        "GameMain.GameMain: Failed to initialize GXInput object");

    // Register the hardware buttons
    m_gi.RegisterAllHardwareKeys();

    // Initialize the random number generator
    m_rnd = new Random();

#if USE_GAPIDRAW
    m_font = new GXFont(GetFullPath("font.png"));
#elif USE_GDI
    m_font = new GXFont("Arial");
#else
    m_font = new GXFont(GetFullPath("font.fnt"));
#endif
}

public static double Random()
{
    return m_rnd.NextDouble();
}

public static int Random(int min, int max)
{
    return m_rnd.Next(min, max);
}

public static string GetFullPath(string fileName)
{
    Debug.Assert(fileName != null && fileName.Length > 0,
        "GameMain.GetFullPath: Invalid string");

    Assembly asm = Assembly.GetExecutingAssembly();
    string fullName = Path.GetDirectoryName(asm.GetName().CodeBase);
    return Path.Combine(fullName, fileName);
}

The LoadLevel method attempts to load the level specified by m_curLevel by loading the appropriate level XML file. If the file does not exist then the method loads the previous level again.

private void LoadLevel()
{
    m_curState = GameState.kLoading;

    DataSet ds = new DataSet();
    string fullPath = GetFullPath(string.Format("level{0}.xml",
        m_curLevel));
    if (!File.Exists(fullPath))
    {
        m_curLevel--;
        fullPath = GetFullPath(string.Format("level{0}.xml",
            m_curLevel));
    }

    ds.ReadXml(fullPath);
    m_level = new Level(ds, m_gx);

    m_curState = GameState.kLoaded;
}

The Run method is the entry point to the GameMain class. This method loads the splash screen and starts the game loop.

public void Run()
{
    // Start with level 1
    m_curLevel = 1;
    m_displayedLevel = 1;

    // Load the splash screen
    m_splash = new GXBitmap(GetFullPath("splash.bmp"), m_gx);

    // Set the current update method as the level updater
    m_update = new UpdateDelegate(UpdateSplash);

    // Create a stopwatch instance for timing the frames
    StopWatch sw = new StopWatch();
    Debug.Assert(sw != null,
        "GameMain.Run: Failed to initialize StopWatch");

    // Loop until the game is done
    while (!m_done)
    {
        switch (m_curState)
        {
            case GameState.kUpdateLevelRequest:
                m_update = new UpdateDelegate(UpdateLevel);
                m_curState = GameState.kUpdatingLevel;
                break;
            default:
                break;
        }
        // Store the tick at which this frame started
        Int64 startTick = sw.CurrentTick();

        // Update input
        m_gi.Update();

        if (m_gi.KeyJustPressed((int)m_gi.HardwareKeys[3]))
            m_done = true;

        // Update the game
        m_update();

        // Check for pending events from the OS
        Application.DoEvents();

        // Lock the framerate...
        Int64 delta_ms = sw.DeltaTime_ms(sw.CurrentTick(), startTick);
        Int64 target_ms = (Int64)(1000.0F * kSecondsPerFrame);

        // Check if the frame time was fast enough
        if (delta_ms <= target_ms)
        {
            // Loop until the frame time is met
            while (sw.DeltaTime_ms(sw.CurrentTick(), startTick) <
                target_ms)
            {
                Thread.Sleep(0);
                Application.DoEvents();

                if (m_gi.KeyJustPressed((int)m_gi.HardwareKeys[3]))
                    m_done = true;
            }
        }
    }
}

UpdateSplash is assigned to the UpdateDelegate m_update at the start of the Run method. UpdateSplash displays the splash screen and on the first frame starts an asynchronous load of the first game level. Once the load is complete and the splash screen has been displayed for the minimal amount of time, the game is started by updating the game state.

private void UpdateSplash()
{
    if (m_curState == GameState.kNotLoaded)
    {
        m_curState = GameState.kLoading;
        Thread loadThread = new Thread(new ThreadStart(LoadLevel));
        loadThread.Start();
    }
    else if (m_curState == GameState.kLoaded &&
             m_remainingSplashTime <= 0.0F)
    {
        m_curState = GameState.kUpdateLevelRequest;
    }

    m_remainingSplashTime -= kSecondsPerFrame;

    int x = (m_gx.ScreenWidth - m_splash.Width) >> 1;
    int y = (m_gx.ScreenHeight - m_splash.Height) >> 1;

    m_gx.DrawBitmap(x, y, new Rectangle(0,0,m_splash.Width,
        m_splash.Height), m_splash);

    m_gx.Flip();
}

The UpdateLevel method is assigned to the UpdateDelegate m_update when the level is loaded and gameplay begins. If this method determines the game is over, the first level is re-loaded and the game state is updated. This method is also responsible for displaying various text depending on the state of the game.

private void UpdateLevel()
{
    if (m_level.CurrentMode == Level.Mode.kGameOver)
    {
        m_level.Dispose();
        m_level = null;

        // Try to clean up memory now so it does not happen
        // during gameplay
        GC.Collect();

        m_curLevel = 1;
        m_displayedLevel = 1;
        LoadLevel();

        return;
    }

    if (m_level.Done)
    {
        int points = m_level.MyPaddle.Points;
        int lives = m_level.MyPaddle.Lives;
        m_level.Dispose();
        m_level = null;

        // Try to clean up memory now so it does not happen
        // during gameplay
        GC.Collect();

        m_curLevel++;
        m_displayedLevel++;
        LoadLevel();

        m_level.MyPaddle.Points = points;
        m_level.MyPaddle.Lives = lives;

        return;
    }

    // Update the level
    m_level.Update(m_gi);

    // Draw the level
    m_level.Draw(m_gx);

    // Show the score
    m_gx.DrawText(m_gx.ScreenWidth >> 1, m_gx.ScreenHeight - 18,
        string.Format("{0}", m_level.MyPaddle.Points),
        m_level.TextColor, m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

    // Show the number of lives
    m_gx.DrawText(m_gx.ScreenWidth - 50, m_gx.ScreenHeight - 18,
        string.Format("{0}", m_level.MyPaddle.Lives),
        m_level.TextColor, m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

    // Show the level
    m_gx.DrawText(50, m_gx.ScreenHeight - 18,
        string.Format("{0}", m_displayedLevel), m_level.TextColor,
        m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

    if (m_level.CurrentMode == Level.Mode.kWaitingToReset)
    {
        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) - 10,
            "You Missed!", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) + 10,
            "Press any button", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) + 30,
            "to continue", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);
    }
    else if (m_level.CurrentMode == Level.Mode.kWaitingToStart)
    {
        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) - 10,
            "Your Move", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) + 10,
            "Press Left or Right", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) + 30,
            "to start", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);
    }
    else if (m_level.CurrentMode == Level.Mode.kGameOverWaiting)
    {
        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) - 10,
            "Game Over", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) + 10,
            "Press any button", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);

        m_gx.DrawText(m_gx.ScreenWidth >> 1,
            (m_gx.ScreenHeight >> 1) + 30,
            "to continue", m_level.TextColor,
            m_font, GXFont.DrawFlags.GDDRAWTEXT_CENTER);
    }

    // Flip the back buffer
    m_gx.Flip();
}

public void Dispose()
{
    if (m_gx != null)
        m_gx.Dispose();
}

As advertised, the code that instantiates and runs the GameMain class from the form is quite simple and takes place in the form's Load method. Notice that a try/catch/finally branch is used to ensure the proper clean-up and error reporting for the GameMain instance. The Form is subsequently closed when the game is finished.

GameMain gm = null;

try
{
    // Create and run an instance of the game
    gm = new GameMain(this);
    gm.Run();
}
catch (Exception ex)
{
    MessageBox.Show(string.Format("Fatal exception {0}", ex.Message));
}
finally
{
    // Clean up game resources
    if (gm != null)
        gm.Dispose();

    this.Close();
}

Conclusion

Hopefully this article has provided you with a sound foundation for creating entertainment and real-time based applications within the .NET Compact Framework. I also hope you enjoy playing with the sample as much as I do.

Page view tracker