dougherty distilled

Bryan Dougherty's thoughts on technology and software development.
in

November 2005 - Posts

Mobile Pac-Man: Part 3 - A Smartphone Game Framework

Introduction

With this latest effort, I decided to take a step backward in order to go forward.  That's because I took a couple POCs and turned them into a framework for building Smartphone games.  This post will describe the SmartphoneGame Framework I've put together, discuss some of the issues run into in the development, and describe how a basic version of the Pac-Man application runs on it.

Framework Goals

Any set of application building blocks has to have an end goal.  Otherwise, you find yourself having philosophical arguments between what makes sense and what your CS textbooks would say.  My overall goal here can be summarized as "Support the easy creation of future 2-D games for a Smartphone".  More specific goals for this version of the framework are listed below. 

  1. A game board that controls efficient painting and game flow
  2. A basis for defining game characters supported by the game board
  3. Tools supporting common interactions between characters

Yes, I did say these are goals for "this version."  That's because I'm trying to get all the basics in place in a short period of time.   There's no doubt that there will be room for improvement (i.e. support for more accurate collision detection), but that can come later.

Litmus test:  Someone should be able to pick up this framework and be able to create a game like Galaga or Frogger by only writing new code related to interactions between the characters in the new game.

Architectural Tour

We'll walk through the new structure of the game as it exists at this point.  No big buildings to see here.  Just a new dll added to our Pac-Man solution aptly named SmartphoneGame. 


Files on Smartphone

Its foundation consists of two main classes, Sprite and Gameboard.  Some derived classes and supporting classes also exist.  We'll walk through two main classes and then describe how they are used in the Pac-Man application.

Sprite

The Sprite class provides the basis for all characters in a game.  In Pac-Man, this includes Pac-Man himself, ghosts, dots, and screen walls.  The class is abstract, forcing all derived classes to implement two methods:

public abstract void Update()

public abstract void Draw(Graphics gameSurface)

Not surprisingly, they expect the derived class to update their state and draw themselves to the game board, respectively.  Methods are also provided for getting the instance of the GameBoard the Sprite has been added to.  The Sprite class also provides a static method for detecting collisions between one Sprite and a list of other Sprites.  In addition, a Sprite instance also tracks several properties:

  • ID - must be unique per GameBoard
  • Bounds - a rectangle that defines the perimeter of the object.  A future version could use a Region or other construct to more accurately track the area taken up by the Sprite.  This property is used for collision detection and drawing.
  • Invalid - a boolean that tells the GameBoard whether a Sprite needs to be redrawn
  • Visible - a boolean that determines whether a Sprite should be drawn or not
  • CollisionDetection - a boolean that determines whether the Sprite should be used when detecting collisions.  This may only be used in rare occasions, but I threw it in anyway.  It could be used to when a Sprite in a list should be ignored from collision detection (i.e. a Dot already eaten by Pac-Man) but you don't want to actually remove it from the list. 

 

GameBoard

The GameBoard controls the flow of the game.  It simply derives from System.Windows.Forms.Form, and like the Sprite class, it is not creatable.  It's worth noting that one trick to this new framework dll was making sure I referenced the System dlls in my C:\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\Smartphone\ directory.  Creating a Smartphone Application project sets this by default for you, but I had to do this manually because I just created a Class Library project.  I still have an error with the designer, somehow related to the referencing, but I haven't spent time researching a fix for since I'm working under the assumption that it will mainly be worked with with hand-written code.  

Each game application should create it's own game board that derives from GameBoard.  In our Pac-Man app, this is our frmGame.  Much of the code from previous posts has now been moved into the framework GameBoard, though.  This inheritance structure allows you to handle any Form events (like overriding KeyPress) you need for your game. 

The constructor of the dervied GameBoard should create all the Sprites need to start the game.  Currently, the overlapping of drawn Sprites is handled by assuming the topmost Sprites were added to the GameBoard last.  This could be handled better in the future.  In thinking about this, I did realize that the SortedList class is not supported by the Compact Framework.  For more on support for classes you can look here (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_evtuv/html/etconnetcompactframeworkclasses.asp).

After the GameBoard loads, it uses a Timer as discussed in the previous posts to call the following method:

protected virtual void AdvanceGame(object sender, EventArgs e)
{

    foreach(Sprite sprite in _SpritesByZIndex)
    {
        sprite.PreviousBounds = sprite.Bounds;
        sprite.Update();
    }

    // make sure we're listening to user input
    Application.DoEvents();
 
    this.Invalidate();
}

In the last line, Invalidate will force the OnPaint method to fire.  Again, we're controlling the as seen below.

protected override void OnPaint(PaintEventArgs e)
{
    Graphics g = Graphics.FromImage(_BackBuffer);

    // blank out any invalid Sprites previous position in the back buffer
    foreach(Sprite sprite in _SpritesByZIndex)
    {
          if(sprite.Invalid)
          {
            g.FillRectangle(new SolidBrush(Color.Black), sprite.PreviousBounds);
           }
    }

    // now draw them to the back buffer
    foreach(Sprite sprite in _SpritesByZIndex)
    {
        if(sprite.Invalid && sprite.Visible)
        {
            sprite.Draw(g);
        }
    }

    if(_FirstPaintComplete)
    {
        // only draw the parts that have changed
        foreach(Sprite sprite in _SpritesByZIndex)
        {
            if(sprite.Invalid && sprite.Visible)
            {
                e.Graphics.DrawImage(_BackBuffer, sprite.Bounds, sprite.Bounds, GraphicsUnit.Pixel);
                e.Graphics.DrawImage(_BackBuffer, sprite.PreviousBounds, sprite.PreviousBounds, GraphicsUnit.Pixel);

                // reset Invalid flag
                sprite.Invalid = false;
            }
        }
    }
    else
    {
        // draw the entire back buffer the first time
        e.Graphics.DrawImage(_BackBuffer, 0, 0);
        _FirstPaintComplete = true;

        foreach(Sprite sprite in _SpritesByZIndex)
        {
            // reset Invalid flag
            sprite.Invalid = false;
        }
    }

}

What's new now is that we're double buffering.  We're drawing everything to an offscreen image first, and then updating the screen with the resulting image.  Originally, I was just drawing the entire back buffer to the screen.  That worked fine for a Pac-Man and two ghosts.  Adding dots and walls slowed things down quite a bit.  Now I'm just painting the screen with the areas of the back buffer that have changed.

So now the game flow is basically taken care of.  All that's left to do is implement the details of the specific game being created.  In Pac-Man that means creating the following Sprite classes: PacMan, Ghost, Dot, Wall.  I'll only highlight a couple examples.  In a simple example, a Dot Sprite simple draws a rectangle to the screen in its Draw method, and does nothing in its Update method.

public override void Draw(Graphics gameBoard)
{
    if(!_IsEaten)
    {
        gameBoard.FillRectangle(new SolidBrush(Color.LightBlue), _Bounds);
    }
}

Both the PacMan and Ghost Sprites derive from an intermediate class that supports Drawing a series of images to show animation like Pac-Man's mouth chomping and the Ghost's bodies flowing.

In a more complicated example, the PacMan Sprite's Update method checks for and reacts to collisions at sets it new coordinates.  Pac-Man eats Dots, dies when running into a Ghost, and has to backup when its new coordinates would put it on a wall.

public override void Update()
{
    switch(_MovementDirection)
    {
        case MovementDirections.Right:
        {
            _Bounds.X += _MovementDelta;
            break;
        }   
        case MovementDirections.Left:
        {
            _Bounds.X -= _MovementDelta;
            break;
        }
        case MovementDirections.Down:
        {
            _Bounds.Y += _MovementDelta; 
            break;
        }
        case MovementDirections.Up:
        {
            _Bounds.Y -= _MovementDelta;
            break;
        }   
    }

    Sprite[] collisions = DetectCollisions(this, GetGameBoard().GetSprites());
    if(collisions != null)
    {
        foreach(Sprite collision in collisions)
        {
            if(collision is Ghost)
            {
                Alive = false;

                return;
            }
            else if(collision is Dot)
            {
                Dot dot = ((Dot)collision);
                if(!dot.IsEaten)
                {
                    dot.IsEaten = true; 
                    dot.Invalid = true;
               }

            }
            else if(collision is Wall)
            {
                switch(_MovementDirection)
                {
                    case MovementDirections.Right:
                    {
                        _Bounds.X -= _MovementDelta;
                        break;
                    }
                    case MovementDirections.Left:
                    {
                        _Bounds.X += _MovementDelta;
                        break;
                    }
                    case MovementDirections.Down:
                    {
                        _Bounds.Y -= _MovementDelta; 
                        break;
                    }
                    case MovementDirections.Up:
                    {
                        _Bounds.Y += _MovementDelta;
                        break;
                    }      
                }
            }
        }
    }

    _Invalid = true;

}



App running in emulator

With very little work a simple version of the game is written on the SmartphoneGame framework.  Not too much left.  Next I'll work on making the correct wall configuration, adding some AI to the Ghosts (currently then just move back and forth), add a score board, fixing some bugs, and other details.  Okay, so clearly there's some work left, but it's getting there.  In my next post, I'll include a version to download and try out.