1. Pong
One of the first games. Tennis for Two and Space War were earlier, but they didn't have a score.
This is the first game and where possible, simplifications will be made. The goal is to develop game
construction principles that can be scaled. Rather than jump to what is the best solution, the code starts
off simple and will be improved on.
Focus on controls, core mechanic, playfield, design, and programming.
Objectives
● developing a plan: features, iterations, refactoring, scaling
● setting up a 3D camera
● drawing the court, paddles, ball in 3D
● setting up a 2D camera and drawing the heads up display in 2D
● handling player input: keyboard and joystick
● collisions with the playfield, reflections
● collisions with paddles, point versus rectangle
● dynamically created objects, pointers, clean coding
● game controller class follows the singleton pattern
● recording and playing sounds
Introduction
These are the notes that go along with the presentation.
There are lots of game development concepts to learn from even the simplest game. And pong is a
simple game. This development doesn't attempt to re-create a fun version of pong.
Development Plan: 1000 Features
The agile practice of creating a list of features you want in your game seems to work best. Write out a
list and give each feature an unique rank from 1000 to 0 -- 1000 being the most important to your
game. Generally devote more time and energy to the higher ranking features.
The feature list should include only things that can be seen, heard, felt, or otherwise tested by playing
the game and not include work under the hood.
basic court, ball, paddles are drawn
•
ball collisions with the walls
•
input from keyboard
•
input from joysticks
•
ball collisions with the player paddles
•
2. score displayed
•
game waits for start button, plays, ends
•
ball speeds up, English on the ball
•
This list is meant to get the big things out on the table and also give you ideas for a development plan.
Development Plan: Iteration
Take a step back and think about the development process and your goals.
Game development lends itself well to an iterative approach. Build something small and work to
improve it in steps. Consistent forward progress depends on choosing the size and direction of your
iterative steps.
Games are built by combining a number of individual technologies. The set of technologies you must
pull together depend on the game, the engine, and the development environment.
Iteration steps should be small and able to be completed in one sitting. In the beginning of a project,
find steps that result in visual changes. Start by getting the main game objects drawn.
a hello world window
•
overhead camera and drawing the court
•
draw the ball
•
draw the paddles
•
Next, do one step with all the different tech your game needs. The goal is to write code that can be
improved on. The first part of a project focuses on a kind of breadth-first development. Keep your
steps small -- aim for something you will complete in one sitting.
player input and connect it to the paddles
•
collisions of the ball with the court
•
collisions of the ball with the player paddles
•
playing of a sound
•
2D heads up display of score
•
It's important not to wait too long before tackling this next stage. Setup your game to run in an attract
mode loop, play the game, and exit without any memory leaks.
write a game controller that manages state transitions including the following
•
display a title screen
•
wait for start button to start the game
•
start and play the game until someone wins
•
display the game over screen
•
testing for clean, balanced coding
•
loop until the player exits
•
exit without any memory leaks
•
After the first pass of your game elements and technology is completed, you will likely have some
messy code. Now is a good time to look at the overall structure of your game and decide if you can
improve it. Also, look at each system, improving the names usually leads to better code.
3. improve the structure
•
improve the names, define and follow a coherent approach
•
Now, focus on the game itself. Iterate on the elements that stick out the most. Eventually, your focus
will change and instead of fixing things that are problems, you'll be improving things and looking for
ways to make things more interesting.
find the worst thing, iterate on it until it's not the worst thing
•
find things that you can make better
•
Realize that you will never run out of things to work on, but you will run out of time.
Developing a Plan: Refactoring
Plan on refactoring your code from the start. Rather than work to write perfect code, write good code
that can be refactored. This means that you should write code that with distinct names. Set yourself up
to be able to search and rename sections of code. Also, write code that groups a feature together.
Refactor the sections when they get too big or the logic starts getting tangled or they clearly belong in
their own function.
It seems that most code is refactored two or three times during a project. However, don't be over
anxious to refactor code, you should wait to refactor until the old code becomes unbearable.
The Game Loop
The beginning of the game controller class. Independent update and draw. Keep features orthogonal.
Class pointers and references
Pointers are an important part of game programming. Get used to using them.
Common Identifier Space
Start all of your identifiers at one. Reserve zero for defaults and uninitialized tests. Define one set of
identifiers. Use these for game ids, return codes, other uses.
enum {
// int, unique ids for any use in the game
// rather than have ids all over the place
GAMEID_TMP=1, // reserved
GAMEID_GAME, // game instance
// game modes
GAMEID_MODE_ATTRACT,
GAMEID_MODE_GAME,
// return codes
GAMEID_BALL_REFLECT,
GAMEID_BALL_OFFLEFT,
GAMEID_BALL_OFFRIGHT,
};
4. Function construction
Initialize. Body. Finalize and exit. One return.
Initialization section must initialize all the objects. Design your objects so zeroing them initializes
them.
Return negative values for errors.
int function()
{
int r;
/////////////
// initialize
///////
// body
r=0; // normal exit code, success
BAIL:
/////////////////////
// finalize, clean up
return r;
} // function()
Free what you allocate
Clean up after yourself. Write your final code so that it is path independent. If non-zero, free and zero.
Public Classes
Projects go through a few phases. In the beginning, setup your code for flexibility. The beginning of a
project is marked by changes in direction and structure. One way to facilitate this is with classes with
all the elements marked public. Once you understand the problem space and your solution, you will
refactor your code to move fields into the private or protected. Until then, it will only slow you down.
Protecting fields becomes important when you scale your project up and add programmers. Be sure to
set aside time for this step.
Feature based accessors, not straight-to-vars. Peter Bennett.
Allocating and Freeing Objects
In C, memory allocated and freed with the malloc() and free() functions and objects must be setup by
the programmer. In C++, the new operator calls malloc() to allocate memory and calls the constructor
function to setup the object. Likewise, the delete operator calls the destructor function to cleanup the
object and then calls free() to release the memory.
Memory is one of your primary resources. Game objects are defined by classes and take up memory
when they are created.
5. QE Base Classes
Derive from qe base classes so all the allocations run through the engine. Objects derived from qe base
classes zero their memory when allocated.
Derive from qeUpdateBase for objects that are managed by the game engine. Normally, you will
override the update() and draw() functions with your own code.
Derive from qe for simple, non-managed objects.
Bracketing Resources
Bullet-proof your resource allocation and freeing. Build a structure that guarantees that you free all the
resources you allocate. This is accomplished with a 3-part structure: initialization, use, and
finalization.
The initialization is code that is guaranteed to run early in the game, and finalization code is guaranteed
to run before you exit. Initialization code should simply zero all resource fields. The finalization code,
which is also guaranteed to run, checks resource fields, frees any non-zero resource, and then zeros that
field. There must be only one owner of each resource and one initialization and finalization section of
code for each resource field.
The code that runs in-between the initialization and finalization must follow the simple rules to test the
resource field before each use and set the field if the resource is successfully created. This structure
allows you to bracket your resource usage and provides the necessary structure to write code that
guarantees balanced resource usage.
initialization (guaranteed): zeros
•
finalization (guaranteed): tests, frees & zeros
•
usage: tests, allocates & sets, uses
•
Game Controller Class
This class manages the game.
Load all the initial content including images and sounds.
Modes should be avoided because they promote structural discontinuity and ultimately complicate your
program greatly. However, modes do make prototyping easier and novice programmers should use
modes until a modeless structure resolves itself.
Start off in attract mode. When the start button is pressed, drop into game mode.
Attract mode waits for the start button and then drops into game mode. Display the last scores.
Game mode runs the game until the game is over. Then drops back into attract mode.
Matrices
A matrix is a set of numbers that define any combination of movements, rotations, and scaling of
objects in a 3D world. The matrices used here are 12 element matrices. The first 3 entries are the X,
Y, and Z positions. The next 9 entries define a simple rotation and scaling matrix. Taken together, the
12 elements are all you need for most game operations and qe provides a set of functions to operate on
the raw 12 element array of floats.
6. Simple 3D Camera
For now, it's best to use the code and experiment with it as you go.
Cameras are a software metaphor implemented with matrices. The camera matrix transforms objects
from world space into camera space. In other words, it treats the camera as the center of the world, and
moves and rotates everything in the world in front of the camera. In OpenGL, this matrix is called the
MODELVIEW matrix.
There are two matrices in OpenGL. You can think of the MODELVIEW matrix as the way you
position the camera, and the PROJECTION matrix as the camera's lens.
OpenGL's PROJECTION matrix transforms objects from camera space into clip coordinates. The
perspective division (divide by z) then transforms clip coordinates into normalized device coordinates.
These coordinates are then mapped through the viewport into a window.
BRK(), Code to Continue
This macro expands to into an interrupt call that breaks the program execution and 'wakes up' the
debugger at that point. If the debugger is not running, the macro has no effect. Write your code to
detect errors and continue running. Players don't want to see a dialog box asking them if they want to
send an error report to Microsoft. Even in extreme cases, do your best to continue running.
That said, you must detect and handle all errors. When you run into an error, report it, and write code
to do the best it can to continue running -- this may mean an object is drawn without a texture or may
even be missing.
Drawing The Court
The court is managed by the world class. A solid rectangle is drawn for the court.
// JFL 14 Aug 07
// JFL 15 Mar 09
int World::draw(void)
{
glColor3f(0,0,1); // set color to blue
glPolygonMode(GL_FRONT,GL_FILL); // draw filled polygons
glBegin(GL_QUADS); // draw quads counter-clockwise from camera's view
glVertex3f(this->xyzMin[0],this->xyzMin[1],this->xyzMin[2]);
glVertex3f(this->xyzMin[0],this->xyzMin[1],this->xyzMax[2]);
glVertex3f(this->xyzMax[0],this->xyzMin[1],this->xyzMax[2]);
glVertex3f(this->xyzMax[0],this->xyzMin[1],this->xyzMin[2]);
glEnd();
return 0;
} // World::draw()
Reset Flags Once
Flags can be used to signal a request from many possible places. Handle the flag in one place. Test the
flag, clear it, call the handler. Clear the flag in only one section of code.
7. 000ZY Coordinates
This choice is largely arbitrary, but should be something you're comfortable with. One unit = one foot
is the standard I use. This depends on the scale of your game, but should be determined before you
start coding.
Velocities
Variable frame rate systems must multiply the velocity by the amount of time elapsed since the last
loop. Keep track of the time since the last update.
float t;
// find time since last update
t=this->timeOfLastUpdate;
this->timeOfLastUpdate=qeTimeFrame();
t=this->timeOfLastUpdate-t; // delta
// xyz += vel*t
this->xyz[0]+=this->vel[0]*t;
this->xyz[1]+=this->vel[1]*t;
this->xyz[2]+=this->vel[2]*t;
This update method -- moving by adding the velocity times the elapsed time -- is called Euler
integration. The timeOfLastUpdate variable must be reset when the object is reset.
3D World, Camera
The world is quot;in 3Dquot;. However, the camera is fixed above it and looking down. This gives the
appearance of a 2D game.
//////////////////////
// Camera class fields
float fovyHalfRad; // field of view angle in y direction in radians
float nearClip; // near clipping plane
float farClip; // far clipping plane
float winWidth; // in pixels
float winHeight; // in pixels
float winWDivH; // window aspect ratio
float nearHeight; // height of window at near plane
float mat12[12]; // camera matrix
///////////////////////
// Camera setup -- once
this->nearClip = 1;
this->farClip = 500;
this->fovyHalfRad = 0.5*((63*PI)/180.0); // 0.5*(degrees->radians)
this->nearHeight = this->nearClip * MathTanf(this->fovyHalfRad);
// camera matrix transforms from world space into camera space
SET3(pos,0,CAMERA_Y,0); // position of camera
SET3(at,0,0,0); // where camera is looking at
SET3(up,0,0,-1); // the camera's up direction
qeCamLookAtM12f(this->mat12,pos,at,up); // compute camera mat
8. /////////////////////////////////////////////////////
// Camera matrices -- before you draw with the camera
if(qeGetWindowSize(&this->winWidth,&this->winHeight)<0)
bret(-2);
this->winWDivH=this->winWidth/this->winHeight;
// set the PROJECTION matrix (the camera lens)
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
float yy = this->nearHeight,xx=this->nearHeight*this->winWDivH;
glFrustum(-xx,xx,-yy,yy,this->nearClip,this->farClip);
// set the MODELVIEW matrix (position and orientation of the camera)
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
qeGLM12f(this->mat12); // set matrix
2D Gameplay
The keyboard and joystick are used for the player's up and down.
Keyboard as Buttons Input
By default, the keyboard acts like a set of buttons to the game engine. QE uses button counts which are
incremented when the buttons are pressed and released. When the button is down, the count is odd,
and when the button is up, the count is even. The bottom bit is set for odd numbers and can easily be
tested with a bitwise test.
// up-down-left-right arrow mapping
if(1&qeInpButton(QEINPBUTTON_UP))
/* up button is pressed */;
else if(1&qeInpButton(QEINPBUTTON_DOWN))
/* down button is pressed */;
Joystick Input
Joystick positions are returned as floating point values. Some care should be taken to drop out the
values when the joystick is near the center.
// get player 0 joystick value -1..0..1
stick=qeInpJoyAxisf(0,QEJOYAXIS_LEFT_Y);
#define STICK_DEADZONE 0.2
// enforce stick deadzone & re-normalize
if(stick>STICK_DEADZONE)
stick=(stick-STICK_DEADZONE)/(1.0-STICK_DEADZONE);
else if(stick<-STICK_DEADZONE)
stick=(stick+STICK_DEADZONE)/(1.0-STICK_DEADZONE);
else // in deadzone, zero
stick=0;
Sounds
Record sounds into .wav files. Most sound effects should be recorded in mono at a low resolution.
Trigger the sounds in the game.
9. // setup sound quot;bumpquot; on channel 1
if((r=qeSndNew(quot;bumpquot;,M_SNDNEW_CH_1,0,quot;art/sounds/pongbump.wavquot;))<0)
BRK();
// trigger the sound
qeSndPlay(quot;bumpquot;);
Use Restart Functions
Use restart rather than start functions. Names matter. Start functions usually hide assumptions
regarding the state of the game.
Drawing 2D: Camera and Image
Use the 32 bit image of the digits. The image has been created so there are 8 digits on the first row and
2 on the second row. 0 1 2 3 4 5 6 7 and 8 9.
// register image
if(qeImgNew(quot;icons1quot;,0,quot;art/images/icons1.tgaquot;)<0)
BRK(); // make sure path is correct
qeTexBind(quot;icons1quot;);
2D images are drawn the same way other polygons are drawn. However, the camera and draw modes
have to be setup correctly.
qefnSysCamFlat(); // ortho: (0,0) top left (1,1) bottom right
// setup texture mode to blend
glPolygonMode(GL_FRONT,GL_FILL);
glEnable(GL_TEXTURE_2D);
glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_ADD);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
Drawing 2D: UVs and Vertices
As always, vertex coordinates are specified in the space defined by the camera matrices and define the
structure of the object. UV coordinates define how the image maps from texture space to the polygon.
UVs are specified in normalized image space.
Ways to get the images to stretch or be drawn close to one to one from the image.
To find UVs, start with the image coordinates and divide by the image size.
// find image coordinates for a positive integer n
// icons in our image are 32x32 and there are 8 across horizontally
// these coordinates are in pixel coordinates of the image
// top left of the image 0,0
u0=(n&7)*GAME_ICON_WIDTH;
v0=(n>>3)*GAME_ICON_HEIGHT;
// map image coordinates into uv space
u0/=imgWidth;
v0/=imgHeight;
10. Name Your Objects
Debugging is easier when object have string names. When debugging, checking the names of objects
is a good way to verify system and game integrity. Also, you can sometimes trap on on the name.