Part B - Direct3D

Sprites in Motion

Introduce time into the framework's model layer
Create two moving objects using one graphic and one texture
Intorduce a reference frame to hold the current position of a drawable object


Sample | Framework | Window | Base | Coordinator
Design | Object | Graphic | Texture | Exercises



The fourth step in building a digital game, following the rendering of a static image, is the simulation of motion.  Motion gives a digital game some dynamic sense.  To determine changes in the positions of objects in a scene, we introduce time.  The time between the drawing of successive frames determines the amount of displacement of each object in the scene. 

The Design object specifies the initial position of each object in the scene and defines the formulas for calculating the objects' displacements between successive frames.  The Coordinator object holds pointers to all of the design elements in the game, including Objects, Graphics and Textures, and manages their suspension, restoration, release and destruction so that the game can restart at its left off state whenever the user reconfigures the application or recovers focus. 

This chapter describes the upgrades to the framework associated with introducing motion.  We track positions of objects in the scene using coordinates in the two-dimensional space of the screen. 


Sprites Sample

The Sprites sample displays two identical images each moving across the screen at constant speed: one moving horizontally, the other moving vertically.  When either object reaches a screen boundary, it reverses the direction of its motion. 

sprites in motion sample

The vertically moving sprite is perfectly opaque, while the horizontally moving one is translucent.


Framework

The upgrades to the framework affect the Window, Design, Coordinator, Object, Graphic, and Texture components.  No new components are introduced. 

The framework includes a new class that holds the variables shared by classes of the Model Layer.  This new class is called Base and serves as the base class for the Model Layer classes.

Model Layer Connectivity

Topics

The topics covered with this sample are:

  • screen coordinates
  • timing
  • colour keying
  • translucency

Screen Coordinates

The coordinate system for describing positions on the screen has its origin located at the top left pixel of the screen with the x-axis positive to the right and the y-axis positive downwards. 

two-dimensional coordinate system

Timing

The interval between successive frames can vary throughout a game and this variation needs to be accounted for to create the sense of steady motion for objects that are travelling at constant speed.  The framework records the time at which a frame is drawn, subtracts the time at which it drew the previous frame and calculates the displacement of each object using the elapsed time and the object's speed. 

The operating system returns the current time rounded to the nearest microsecond.  Because of the integer nature of the reported time, the elapsed time between successive frames should not be too short.  If the framework draws frames too close to one another in time, inaccuracies arise due to a lack of significant digits in the interval measure.  To avoid fluctuations due to such inaccuracies, we keep the interval between successive frames large enough to yield elapsed times with several significant digits.  That is, we impose an upper bound on the frame rate.  If the framework attempts to draw at a higher rate, we skip the drawing step until the frame rate drops below this upper bound. 

The elapsed time between successive frames should also not be too long.  The framework simulates continuous motion through rapid presentation of discrete frames.  To avoid disrupting the sensation of continuous motion, the frame rate should be no less than 16 frames per second.  This rate is called the flicker fusion threshold

Color Keying

Colour keying is a technique that replaces a colour in an image with no colour at all; that is, perfect transparency.  Images are stored on file in the form of two-dimensional rectangles using the additive colour model.  We can represent those parts of these images that do not contribute to the object by a specific colour.  We call this colour the colour key.  When converting the image into a texture on video memory, the copying function converts each colour point that matches the colour key into a perfectly transparent point.  The file image has the colour key, while its copy in video memory copy has perfect transparency in place of the key.

The image on the file used for this sample uses black as its colour key.

sprite with a black colour key

Translucency

Superimposing a translucent image upon a background image requires some blending of the colours in both image.  The alpha value of the foreground image typically controls this alpha blending.  The transparency of the foreground image determines the degree to which the background image appears.  We measure translucency from 0 for perfect opacity to a maximum value for perfect transparency. 

Model Settings

The model settings for this sample are. 

 // Model.h

 #define TEXTURE_DIRECTORY L"..\\..\\resources\\textures"
 #define TEX_ALPHA '\xAF' // default transparency [\x00,\xff]
 #define COLOR_KEY 0xFF000000  // default color key = black
 #define UNITS_PER_SEC   1000  // units of system time in one second
 #define FPS_MAX          100  //  > 16 (flicker fusion threshold)
 #define SPEED (100.0f / UNITS_PER_SEC)

Window

The Window component reports the system time.  The iAPIWindow interface exposes the time() method to the Coordinator class:

 // iAPIWindow.h

 class iAPIWindow {
     // ...
     virtual int time() const = 0;
     friend class Coordinator;
 };
 iWindow* CreateAPIWindow(void*, int);

The APIWindow class defines the time() method:

 // APIWindow.h

 class APIWindow : public iAPIWindow, public APIBase {
     // ...
     int time() const;
 };

The time() method retrieves the system time using the Windows API:

 int APIWindow::time() const { return timeGetTime(); }

mmsystem.h contains the prototype for the timeGetTime() function.  The winmm.lib library file contains the function's implementation.


Base

The Base class holds variables shared by classes of the Model Layer.  This class is the Model Layer's counterpart to the Translation Layer's APIBase class.  The Coordinator object exposes this set of common variables to the other classes of the Model Layer. 

For this particular sample the Base class supports only serves the Coordinator class.  In later samples, its support extends to the other classes of the Model Layer. 

The Base class includes six class variables:

  • a pointer to the Coordinator object
  • the current time and the time of the last update
  • the application's activity flag
  • the client area's dimensions
 // Base.h

 class Base {
   protected:
     static iCoordinator* coordinator; // points to the Coordinator object
     static unsigned      now;         // current time in system units
     static unsigned      lastUpdate;  // time of the last update
     static bool          active;      // application is active?
     static int           width;       // width of model area
     static int           height;      // height of model area
 };

Each class variable is initially zero-valued. 

 // Base.cpp

 iCoordinator* Base::coordinator = nullptr;
 unsigned      Base::now         = 0;
 unsigned      Base::lastUpdate  = 0;
 bool          Base::active      = false;
 int           Base::width       = 0;
 int           Base::height      = 0;

Coordinator

The Coordinator component controls timing, updating, and rendering intervals.  The Coordinator object draws each frame as soon as possible within the pre-defined upper bound and updates the drawable objects just before it draws the frame. 

The Coordinator object derives from the Base class.  The address of the Coordinator object and the application's activity state are now part of the Base class. 

coordinator hierarchy

The iCoordinator interface exposes the update() method to the framework:

 class iCoordinator {
     // ...
     virtual void update() = 0;
 };
 iCoordinator* CoordinatorAddress();

The Coordinator class defines arrays of pointers to the design elements along with counters for those sets of elements:

 // Coordinator.h

 class Coordinator : public iCoordinator, public Base {
     // ...
     bool getConfiguration();
     void render();
     void initialize() { }
     void update()     { }
   protected:
     iGraphic*            graphic[2];  // points to the graphics
     iTexture*            texture[2];  // points to the textures
     iObject*             object[3];   // points to the objects
     int                  nObjects;    // number of objects
     int                  nGraphics;   // number of graphics
     int                  nTextures;   // number of textures
     virtual ~Coordinator();
     // ...
 };

Implementation

Construct

The constructor initializes the timer variables in the base class:

 Coordinator::Coordinator(void* hinst, int show) {
     // ...
     // timers
     now        = 0;
     lastUpdate = 0;
 }

Get Configuration

The getConfiguration() method resets the timer variables to the current system time:

 void Coordinator::getConfiguration() {
     // ...
     now = window->time();
     lastUpdate = now;
     return rc;
 }

Run

The run() method renders successive frame and updates the time of the last rendering.  This method draws a frame only if sufficient time has elapsed to admit accurate updating calculations:

 int Coordinator::run() {
     // ...
     while (keepgoing) {
         // ...
         else {
             now = window->time();
             // render only if sufficient time has elapsed since last frame
             if (now - lastUpdate >= UNITS_PER_SEC / FPS_MAX) {
                 display->beginDrawFrame();
                 render();
                 display->endDrawFrame();
                 lastUpdate = now;
             }
         }
     }
     return rc;
 }

Render

The render() method updates the game design and renders each one of the drawable objects in the scene:

 void Coordinator::render() {
     update();
     for (int i = 0; i < nObjects; i++)
         if (object[i]) object[i]->render();
 }

Suspend, Restore, and Release

The suspend() method suspends the connections of the Graphic and Texture objects to the display device:

 void Coordinator::suspend() {
     for (int i = 0; i < nTextures; i++)
         if (texture[i]) texture[i]->suspend();
     for (int i = 0; i < nGraphics; i++)
         if (graphic[i]) graphic[i]->suspend();
     active = false;
 }

The restore() method resets the timer variables to the current system time:

 void Coordinator::restore() {
     display->restore();
     now = window->time();
     lastUpdate = now;
     active     = true;
 }

The release() method releases the connections of the Graphic and Texture objects to the display device:

 void Coordinator::release() {
     for (int i = 0; i < nTextures; i++)
         if (texture[i]) texture[i]->release();
     for (int i = 0; i < nGraphics; i++)
         if (graphic[i]) graphic[i]->release();
     // ...
 }

Destroy

The destructor deletes all of the design elements:

 void Coordinator::~Coordinator() {
     for (int i = 0; i < nObjects; i++)
         if (object[i]) object[i]->Delete();
     for (int i = 0; i < nTextures; i++)
         if (texture[i]) texture[i]->Delete();
     for (int i = 0; i < nGraphics; i++)
         if (graphic[i]) graphic[i]->Delete();
     // ...
 }

Design

The Design component describes the game logic in two separate parts.  The initialize() method on the Design object creates the drawable objects that exist at the start of the game and the update() method calculates changes in the positions of the moving objects throughout the game. 

The Design class overrides the Coordinator class' default definition of the update() method:

 // Design.h

 class Design : public Coordinator {
     // ...
   public:
     Design(void*, int);
     void initialize();
     void update();
 };

Implementation

Initialize

The initialize() method creates the Texture, Graphic, and Object instances and translates the movable objects to their initial positions: 

 void Design::initialize() {
     nTextures = 2;
     nGraphics = 2;
     nObjects  = 3;

     texture[0] = CreateTexture(L"stonehenge.bmp");
     graphic[0] = CreateGraphic();
     object[0]  = CreateObject(graphic[0], 0xFF);
     object[0]->attach(texture[0]);

     graphic[1] = CreateGraphic(120, 120);
     texture[1] = CreateTexture(L"sprite.bmp");

     object[1]  = CreateObject(graphic[1], 0xAF);
     object[1]->attach(texture[1]);
     object[1]->translate(0, height / 2 - 40);

     object[2]  = CreateObject(graphic[1], 0x4F);
     object[2]->attach(texture[1]);
     object[2]->translate(width / 2 - 40, 0);
 }

Update

The update() method updates the position of each movable object.  It uses the time since the previous update and a constant speed (SPEED) to calculate their displacements.  This method also ensures that the objects remain within the client area:

 void Design::update() {
     static bool left = false, down = true;
     int delta = (int)((now - lastUpdate) * SPEED);
     int dx = left ? - delta : delta;
     int dy = down ? delta : - delta;

     // keep sprites within limits and reverse directions at limits 
     int x, y;
     object[1]->position(x, y);
     if (x + dx <= 0) {
         dx = 2 * x + dx;
         left = false;
     }
     else if (x + dx + object[1]->width() >= width) {
         dx = 2 * (width - x - object[1]->width()) - dx;
         left = true;
     }
     object[1]->translate(dx, 0);

     object[2]->position(x, y);
     if (y + dy <= 0) {
         dy = 2 * y + dy;
         down = true;
     }
     else if (y + dy + object[1]->height() >= height) {
         dy = 2 * (height - y - object[1]->height()) - dy;
         down = false;
     }
     object[2]->translate(0, dy);
 }

Object

The Object component manages the drawable design elements.  It consists of two classes: an Object class and a Frame class.  The Frame class holds the positioning data, while the Object class holds addresses of design elements used in the drawing process.  These elements include the Graphic object that holds the graphic representation and optionally, the Texture object. 

object component

Frame Class

The Frame class for this sample and the next contains two instance variables that describe the object's position in screen coordinates:

  • x - the number of pixels from the top left corner in the right direction
  • y - the number of pixels from the top left corner in the down direction
 // Frame.h

 class Frame {
     int x;
     int y;
   public:
     Frame() : x(0), y(0) { }
     void translate(int dx, int dy) { x += dx; y += dy; }
     void position(int& xx, int& yy) const { xx = x; yy = y; } 
 };

The constructor initializes the position of the Frame as the top left corner of the screen.  The translate() method translates the position by the displacements received.  The position() method returns the current position in terms of x, y coordinates.

Object Class

The iObject interface inherits from the Frame class and exposes two methods that return the object's dimensions for use by the update() metohd of the Design class:

 // iObject.h

 class iObject : public Frame {
     virtual int  width() const     = 0;
     virtual int  height() const    = 0;
     virtual void attach(iTexture*) = 0;
     virtual void render()          = 0;
     virtual void Delete() const    = 0;
     friend class Coordinator;
     friend class Design;
 };
 iObject* CreateObject(iGraphic* v, unsigned char = 0xFF);

The CreateObject() function accepts the object's transparency as an optional second argument, which defaults to perfect opacity.

The Object class includes an instance variable that holds the object's transparency:

 class Object : public iObject, public Base  {
     iGraphic*     graphic; // points to the graphic representation
     iTexture*     texture; // points to the attached texture
     unsigned char alpha;   // transparency '\x00' to '\xFF'
     virtual   ~Object();
   public:
     Object(iGraphic*, unsigned char);
     int width() const;
     int height() const;
     // ...
 };

Construct

The constructor initializes the object's transparency to the value received or to TEX_ALPHA if the value received is zero:

 Object::Object(iGraphic* v, unsigned char a) : graphic(v),
  texture(nullptr), alpha(a ? a : TEX_ALPHA) { }

Width and Height

The width() and height() methods return the dimensions of the object's bounding rectangle:

int Object::width() const { return graphic->width(); }
int Object::height() const { return graphic->height(); }

Render

The render() method uses the object's position and the dimensions of its bounding rectangle in rendering its graphic representation at that position using the object's transparency:

void Object::render() {
    if (graphic && texture) {
        int x, y;
        position(x, y);
        graphic->beginDraw();
        texture->attach(graphic->width(), graphic->height());
        graphic->render(x, y, alpha);
        texture->detach();
        graphic->endDraw();
    }
}

Graphic

The Graphic component manages the graphic representations of all drawable objects.  Its Graphic class includes variables that hold the dimensions of a representation's bounding rectangle. 

graphic component

The iGraphic interface exposes two new methods that return the dimensions of the graphic representation and upgrades an existing method to accept positional and translucency data:

 // iGraphic.h

 class iGraphic {
   public:
     virtual int  width() const                               = 0;
     virtual int  height() const                              = 0;
     virtual void beginDraw()                                 = 0;
     virtual void render(int = 0, int = 0, unsigned char = 0) = 0;
     virtual void endDraw()                                   = 0;
     virtual void suspend()                                   = 0;
     virtual void restore()                                   = 0;
     virtual void release()                                   = 0;
     virtual void Delete() const                              = 0;
     friend class Coordinator;
     friend class Design;
     friend class Object;
 };
 iGraphic* CreateGraphic(int = 0, int = 0);

The Graphic class reports the bounding rectangle's dimensions:

 class Graphic : public iGraphic {
    int          width_;     // width of the enclosing rectangle
    int          height_;    // height of the enclosing rectangle
    // ...
  public:
    Graphic(int, int);
    void* clone() const { return new Graphic(*this); }
    int  width() const  { return width_; }
    int  height() const { return height_; }
    void render(int, int, unsigned char);
    // ...
};

The constructor saves the dimensions received:

 Graphic::Graphic(int w, int h) : width_(w), height_(h) {
     apiGraphic = CreateAPIGraphic();
 }

The render() method draws the texture at the received screen coordinates with the received translucency:

 void APIGraphic::render(int x, int y, unsigned char a) {
     if (!ready) setup();
     if (d3dd && sprite) {
         D3DXVECTOR3 topLeft((float)x, (float)y, 1.f);
         sprite->Draw(texture, nullptr, nullptr, &topLeft,
          D3DCOLOR_RGBA(255, 255, 255, a ? a : '\xFF'));
     }
 }

If the translucency value is zero, this method adopts perfect opacity.


Texture

The Texture component manages all of the textures that are mapped to drawable objects.  Its APITexture class holds the colour key to be used in creating the texture from an image on file. 

texture component

Texture Class

The attach() method on the iTexture interface accepts two optional arguments that define the width and height in pixels of its projection onto the screen.  The CreateTexture() function accepts an optional second argument that defines the colour key, which defaults to black:

 // iTexture.h

 class iTexture {
     // ...
     void attach(int = 0, int = 0) = 0;
     // ...
 }
 iTexture* CreateTexture(const wchar_t*, unsigned = 0u);

The constructor creates the APITexture object using the received colour key unless it is zero.  If the received value is zero, the constructor sets the colour key to the default value (COLOR_KEY):

 Texture::Texture(const wchar_t* file, unsigned key) {
     // ...
     apiTexture = CreateAPITexture(fileWithPath, key ? key : COLOR_KEY);
     // ...
 }

APITexture Class

The iAPITexture interface exposes its attach() method with two arguments to the Texture class:

 // iAPITexture.h

 class iAPITexture {
     // ...
     virtual void attach(int, int) = 0;
     friend class Texture;
 };
 iAPITexture* CreateAPITexture(const wchar_t*, unsigned);

The APITexture class includes an instance variable that holds the colour key:

 class APITexture : public iAPITexture, public APIBase {
     wchar_t*           file; // points to file with texture image
     unsigned           key;  // color key
     IDirect3DTexture9* tex;  // interface to the texture COM object
     void setup(int, int);
     virtual ~APITexture();
   public:
     APITexture(const wchar_t*, unsigned);
     void attach(int, int);
     // ...
 };

Construct

The constructor initializes the texture's colour key to the value received:

 APITexture::APITexture(const wchar_t* file, unsigned k) : key(k) {
     // ...
 }

Setup

The setup() method retrieves an interface to the Direct3DTexture COM object and copies the image on file to video memory using the colour key.  This method receives the texture's screen dimensions.  If these dimensions are zero, this method adopts the window dimensions as the texture's dimensions:

 void APITexture::setup(int w, int h) {
     if (file && FAILED(D3DXCreateTextureFromFileEx(d3dd, file,
      w ? w : width, h ? h : height, D3DX_DEFAULT, 0, D3DFMT_A8R8G8B8,
      D3DPOOL_MANAGED, D3DX_DEFAULT, D3DX_DEFAULT, key, nullptr, nullptr,
      &tex))) {
         error(L"APITexture::11 Failed to create texture object from file");
         tex = nullptr;
     }
 }

Attach

The attach() method receives the screen dimensions for the texture and sets it up if necessary:

 void APITexture::attach(int w, int h) {
     if (!tex) setup(w, h);
     if (tex)
         texture = tex;
 }

Exercises

  • Introduce a third sprite to the Design object along with logic to control its motion.  Follow the same pattern as for the original two sprites.
  • Introduce logic to the Design::update() function to identify a collision between the moving objects and to reverse the directions of their travel on impact.



Previous Reading  Previous: Background Image Next: Sprites with User Control   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo