Celesteroids Source Code


Project Overview

The Celesteroids source code comprises two distinct component areas--the "Front-End" GUI using wxWidgets, and the "Core" which provides the "game model" and is wxWidgets independent.


Requirements

You will need to produce a build of the wxWidget library before you can link Celesteroids against it. Refer to the installation notes provided with wxWidgets for further information. I initially built Celesteroids with wxWidgets version 2.6.2, and later updated to 2.6.3. I have not tested it with earlier versions.


Directory Structure


Provided IDE Projects

Project files are available in the "projects" directory for the following Windows compilers/IDEs:

Shown, in italics, are the pre-defined wx-library build targets I have tested with these projects. My personal preference has been for Code::Blocks, as you can see by the number of target options I've been playing with.

Visual C++ 2005 Notes
IMPORTANT: I have a preference for building stand-alone executables which do not require additional DLLs to run. I, therefore, built my wxWidget libraries using the "Multi-threaded (/MT)" runtime library option, and my Celesteroids project is likewise. You will need to modify the VC Celesteroids project to suit, otherwise, you can be sure of seeing some linker errors.

If you built your wxWidget libraries with the default project settings, most likely you will need to change the Celesteroids project to:

C/C++ Code Generation - Runtime Library: Multi-threaded DLL (/MD)
Linker Input - Ignore Specific Library: libc.lib; libcmt.lib; libcd.lib; libcmtd.lib; msvcrtd.lib

The VC2005 project assumes that the wxWidget directories have been specified in your VC options (rather than the project build settings). Add the following to suit your wxWidgets installation:

All other paths are relative. Refer to the installation notes provided with wxWidgets for further information.

Code Blocks Notes
The wxWidgets directory is specified in the build options as a "custom variable". Change it to suit.

Dev-C Notes
The wxWidgets directory is specified simply as a fully qualified path where appropriate. Change it to suit. Other paths are relative.


How it all Works

Game Core

This area of source code is responsible for maintaining the game model. The concept is one of a poll driven "game engine" which maintains a list of "game objects". At a regular interval (approx. 100 ms) the engine is polled by a call to its tick() method so that it may update the game state (i.e. move objects according to their respect velocities & detect collisions etc.).

Game objects are derived from a common abstract base class (Obj) which provides properties for position*, velocity*, basic implementation and a series of pure virtual methods. A concrete class is derived from Obj for each kind of visual object you see in the game, including rocks of various sizes, the ship, an alien and small transient particles such as thrust, fire and debris.

You should note that the Core is not responsible for rendering on to the screen. Instead, the Obj base class offers a points() accessor which provides a series of polygon point values* to the Front-End, thus allowing rendering to take place outside of the Core. Polygon point values are initialized in the constructor of each object. Note that an exception is the Label class, which provides a text object with no polygon.

The behavior of each concrete object is defined by how it implements the virtual methods of Obj. For example, Obj declares mass() virtually which returns an arbitrary "mass" value. Each object type may return a different value and is used by Engine when collisions occur to calculate rebounds using a conservation of momentum rule.

*The std::complex<double> type is used to represent x-y positions, velocity vectors and polygon point values.


 

Core Class Hierarchy

The Engine

  • Celesteroid::Engine

Game Object Hierarchy

  • Celesteroid::Obj (abstract)
    • Celesteroid::RockBase (abstract)
      • Celesteroid::BigRock
      • Celesteroid::MedRock
      • Celesteroid::SmallRock
    • Celesteroid::Ship
    • Celesteroid::Alien
    • Celesteroid::Fire
    • Celesteroid::Debris
      • Celesteroid::Spark
    • Celesteroid::Thrust
    • Celesteroid::Label

Note that the namespace "Celesteroid" in used in Core files.


When the Engine::tick() method is called, it further calls Obj::tick() on every object in the game. For all objects, tick() will increment the position property (pos) according to the object's current velocity vector (dir). For things which rotate, such as rocks, it applies a rotation transformation to its polygon values. Concrete classes usually implement additional type specific behavior in tick(). For example, Alien::tick() contains a collision avoidance algorithm which calculates a suitable velocity away from its nearest neighbor. While Ship::tick() implements ship movements and fire in response to player input commands.

Collision Detection, Rebounds & Explosion
Collision detection and rebound calculations are performed on each tick() within the Engine class with the aid of the collision() and rebound() methods. The collision() method determines whether any two objects are in collision, and if so rebound() calculates their new respective velocity vectors according to a conservation of momentum rule. A call by Engine to an object's fatal() method determines whether a collision was fatal, and if so the object's explode() method is called.

The collision detection technique used by collision() is simple and makes use of an "average radius value", as determined for each object. This is obtained by calculating the absolute value of the sum of polygon point values and dividing by the number of points. In other words, it represents an average distance from an object's center to its outer boundary. When the distance between two objects is less than the sum of their radii, they are deemed to be in collision.

Special Note. For a collision to occur, collision() requires that objects are actually moving toward each other, otherwise it would be possible for object rebounds to occasionally oscillate--causing them to become locked together.

When Objects Die
When an object's explode() method is called, the object is not immediately destroyed. Rather, it sets an internal "dead" flag and may optionally add other fragmentation objects to it's owner's game object list. Dead objects from the previous tick are later destroyed in the next call to Engine::tick().

Game Start, Stop & Tick
As previously described, the Engine's tick() method is called at a regular interval. There is a some scope for varying the interval, but typically it should be around 100ms. Calling it too infrequently will result in sluggish game play, while calling too often will require lightening reactions.

A new game is started when startGame() is called and active() will then return true. Objects, including a ship, will be added to the game list automatically and further calls to tick() will cause the game state to be update each time. If Engine is not in an active state, tick() does nothing. When the game ends, the gameOver() accessor will return true but the game will remain in the active state indefinitely (without a ship) until stop() is called.

Additionally, the startDemo() method is used to play a "demo". In this, the Engine is active but the ship is controlled randomly by the Engine (ship input controls are ignored).

Ship Control Inputs
The following Engine methods provide for ship control: rotate(), thrust(), fire() and reload(). These are fed from outside the Core by key input events (the Core does not handle key input events itself).

The rotate() method accepts an int, where a negative value corresponds to rotate left and positive to rotate right. The ship will continue to rotate until a value of zero is specified (rotate(0) is called on a key up event).

The fire() method is used to cause the ship to fire in response to a fire key down event, but a "fire lock" ensures the ship will not fire again until reload() is called (key up). This prevents automatic rapid fire due to repeating key down events.

Likewise, thrust(true) is called on key down and thrust(false) on key up.

Game Space Regions
The Engine's game space is toroidal, meaning that when an object reaches the game's edge, it will reappear on the other side.

The game's visible play area may be changed at any time, even during play, by calling the Engine's setPlayDims() method. This defines the visible play area, but not the actual area, as certain objects (such as rocks) may inhabit regions outside the visible area. This is done so that new rocks may be created off-screen and drift naturally into the play area, rather than appearing suddenly from no-where. You should take the game space dimensions to represent a "clipping rectangle" around the game, but not assume objects can't have positions beyond it. I refer to areas outside the visible space as "Kuypier Regions" in my code.

Increasing the game play dimensions will increase the space in which objects have to move around in, but will not result in a scale change to their polygons values as the Engine does not perform any scaling itself (this must be done in the Front-End).

Sound Flags
The Core does not implement game sounds; rather it sets a series of sounds flags to signify that a sound should occur, accessed as follows: rockExplodeSnd(), fireSnd(), thrustSnd(), alienSnd() and diedSnd(). These are called after each game tick in the Front-End to determine whether the corresponding sound should play.


Front-End Stuff

The source files in the "Front-End" directory use the wxWidget framework to provide the user interface, render the game, respond to key input events and play sound media.

The MainFrame class embodies the application's main window and menus. The bulk of Front-End functionality is contained within the GamePanel object, however, which is derived from wxPanel. This class instantiates a game Engine object and owns the timer used to poll the Engine's tick() method. It also renders the game state within it's own client area, generates the introductory screens and forwards key input events to the game Engine. In fact, it pretty much does everything except respond to menu events. MainFrame creates an instance of GamePanel which it assigns to its client area.

On each timer event, the game Engine's tick() method is called and the game is re-rendered on the client area. Rendering actually takes place within the drawPlayDC method, which loops through the objects in the Engine and draws their respective polygons. When the window is resized, the Engines setPlayDims mutator is called so as to set the game's aspect ratio to that of GamePanel's client area, while keeping the Engine's internal play area constant. A scaling factor is calculated and used in mapping Engine's internal game space to the client area of the window.

Flicker Free Rendering
My initial approach was to use the wxBufferedDC class to draw the game on to the client area of GamePanel. While this provided flicker free screen updating, it resulted in unacceptably high CPU usage when the main window was maximized to full screen.

The technique I finally ended up with uses the non-buffered wxClientDC class instead, but I had to optimize the drawing code in drawPlayDC so that each polygon is over-drawn and re-drawn sequentially. To do this, I had to modify the Core Obj class so that it kept a record of its previous position and polygon values (see it's "old" position property and points() accessor.) In addition, I ensure that score labels at the top of the screen are over-drawn only when they change. This approach results in minimal flicker and low CPU usage.

Note. The wxBufferedDC rendering code is still present, but is #defined out. Build with project with the GAME_BUFFERED_DC to use wxBufferedDC instead of wxClientDC.

Sounds (wxMediaCtrl)
GamePanel loads game sound files from the applications directory. These are played using the class wxMx--derived from wxMediaCtrl. This implements a wxEVT_MEDIA_LOADED event so that it may begin playing the sound media automatically once loading is successful. GamePanel owns an array of media controls--one for each sound event type, thus allowing multiple sounds to be played at the same time (wxSound won't allow this).

In Windows, the wxMEDIABACKEND_MCI back-end is used in preference of the default wxMEDIABACKEND_DIRECTSHOW. I found that using wxMEDIABACKEND_DIRECTSHOW caused the main window flash to horribly for each wxMediaCntrl assigned to GamePanel as a child object.

Config INI File
In version 1.2, the feature to store game settings to a configuration file in the user's data directory (i.e. " C:\Documents and Settings\username\Application Data\") was added. Personally, I steer clear of the Windows Registry if I can help it.


Link Libraries & Build Options

wxWidget Static Libraries

Individual Libraries
Celesteroids requires the following wxWidget libraries. Use unicode or dynamic runtime variants (DLLs) as preferred.

Monolithic
Or for a monolithic build of wxWidgets:


Required Windows Link Libraries

In Windows, Celesteroids requires:


Conditional Defines used in Celesteroids

WxWidget Generic

Always define:

WxWidget Unicode Linkage

For Unicode, define:

WxWidget Dynamic Runtime Library

For runtime wxWidget linkage, define:

Celesteroid Specific Conditionals

Optionally, define:


Technical Support

None. But suggestions and comments appreciated. See BigAngryDog.com for my email address.

Andy Thomas

< Back

 

Copyright : © Andy Thomas 2006