Part A - Foundations

Framework and API Design

Describe the architecture of the instructional framework
Identify the issues in the design of API components

Framework Design | Selection Sample | API Component Design | Summary | Exercises



The instructional framework provides the bridge between your game design and the supporting APIs.  The framework consists of components and layers - a model layer and a translation layer.  The model layer consists of the classes used in game design and is platform independent.  The translation layer consists of classes that convert the model layer instructions into communications with the supporting APIs.  This layer holds all platform specific code.  Some of the framework's components are local to one of the two layers, while other components, like the lights, graphics, and textures span both layers.  This architecture supports a complete uncoupling of the game design from the APIs that interface with the underlying hardware. 

framework layers

This chapter describes some key architectural details of the framework and recasts the dialog sample described in the preceding chapter to suit the framework's design rules.  This chapter also provides an extremely brief overview of the structure of API components.  A comprehensive explanation of API design is available separately on the course web site.  Understanding API design in detail is not necessary for understanding either the framework or game programming concepts, but does deepen appreciation of the Component Object Model technology upon which DirectX itself is built and explains why the framework has been designed in the way it has. 


Framework Design

The framework consists of components organized in a layered design.  Its components are independent of one another.  Their independence lets the framework programmer focus on developing one component at a time without affecting the design of the other components.  The framework's component structure is distinct from its layering.  A component may belong to one layer, to the other, or span both. 

Each component contains one or more classes.  Each class belongs to either the Model Layer or the Translation Layer but not both.  This localizes the code that the framework programmer needs to change in switching APIs and protects game designs from changes due to API switches. 

The logic of a game design consists of design elements and operations on them.  A game programmer uses only these elements to build their game.  Each element is an instance of the model class of one of the framework's components.  This structure lets the game programmer focus on game design without concern for implementation details, housekeeping details, or supporting APIs. 

framework layers in detail

Structure

Two Layers

The framework's layered structure completely uncouples game design from the supporting APIs by distinguishing API dependent code from the platform-independent code.  The game design only accesses classes in the Model Layer.  The classes in the Translation Layer form wrappers on the API calls and are independent of both the Model Layer and the game design. 

Components

Those components that support design elements, which the game designer uses in creating a game, span the Model and Translation Layers, providing a direct connection from the game logic to the hardware.  These vertical components are wholly independent of one another.  Each has two distinct classes: a model class named after the design element and a translation class with the same name, but prefixed with the letters API.  For example, a "graphic" design element is an instance of the Graphic class, which uses the APIGraphic class to access the supporting APIs. 

Aside from the vertical components that support the design elements for any game, the framework includes components unrelated to game design and not accessible directly by the game designer.  These include the Coordinator and APIUserInput components.  The Coordinator component integrates all of the design elements across the Model Layer, manages flow of control through them, accesses other components within the Translation Layer, which are also inaccessible to the game designer, and provides the default states for any game design.  The APIUserInput component of the Translation Layer manages user input and communicates with the user through its underlying API. 

Flow of Control

The Coordinator component manages the event-driven iteration.  This component responds to system messages and follows game logic in updating game states.  Its run() method, which contains this iteration, processes events such as loss or restoration of focus, change of existing configuration, and updates to the game state over time.  Execution control flows from this iteration in the Model Layer through the Translation Layer to the supporting APIs.

The Entry component is the exception with respect to the direction of flow of control.  This is the only component that transfers control from the Translation Layer to the Model Layer.  All other components transfer control from the Model Layer to the Translation Layer and subsequently to the supporting APIs. 

Design Conventions

Several design conventions used are worthwhile noting here.  They appears as pattern throughout the code reported in this text. 

Interfaces

The components communicate with one another through interfaces.  These interfaces expose the methods that are available to other components and enable upgrading without alterations to calling components.  Those components that create an object defined in another component do so using a global Create() function, which returns that object's address.  The framework track its object through their addresses.  Components destroy these objects through Delete() methods on their addresses.  This convention, borrowed from API design, keeps the components highly uncoupled and ensures complete destruction.

Singleton Classes

Because the Windows API pre-defines the signature of each window procedure, a window procedure cannot retrieve a callback address to a client application through its own parameter list.  It needs some technique of accessing public methods on objects defined outside itself. 

This signature constraint has induced many game programmers to define global application objects.  Digital games and texts on game programming with abundant global variables are not uncommon.  But, applications with global definitions are cumbersome and notoriously difficult to maintain, upgrade, and re-use: they impose absolute constraints on object identification across an entire development life-cycle. 

The framework uses singleton classes as its technique.  We define the objects that we need to access from within window procedures as instances of singleton classes.  A singleton is a class that is instantiated only once.  We store the object's address in a class variable and expose it through a global function that calls a class method.  The address provides access to methods on the object.  Note that this address is that of the object and not of an interface to the object. 

Header File Design

Implementation files for digital games typically access methods on a variety of different objects.  Identifying all of those methods often requires inclusion of many header files.  Header files that include other header files create dependencies.  To minimize these dependencies, we derive each class definition from its own interface and access its methods on that interface.  The interface only includes the information required for access.  To avoid including header files within other header files, we use forward declarations as liberally as possible.

Copy Prevention

Some classes within the framework, like the singleton classes, are by definition not copyable.  To prevent copying and assignment, we explicitly identify their copy constructors and assignment operators as private members.  With this identification, there is no need to define these functions.  The complier will reject all attempts to copy or assign one instance to another.


Selection Sample

Let us refactor the dialog sample described in the preceding chapter to suit the framework's structure.  In this Selection sample, we localize all API-related code within the Translation Layer and introduce a coordinator within the Model Layer.  This coordinator controls the flow of execution throughout the framework as shown below. 

layout of selection sample

Design

The Design class derives from the Coordinator class of the Model Layer and manages the game design for the framework.  In this particular sample, the Design class is empty. 

 // Design.h

 class Design : public Coordinator {
     Design(const Design& s);            // prevents copying
     Design& operator=(const Design& s); // prevents assignment
   public:
     Design(void*, int);
 };

The assignment operator and copy constructor are private to prevent any copying or assignment.  The Design constructor passes data that it receives to the Coordinator class constructor. 

 Design::Design(void* h, int s) : Coordinator(h, s) { }

Model Layer

In this example, there are no design items and the Model Layer consists solely of the Coordinator component.

Coordinator

The Coordinator component controls the flow of execution.  Its interface exposes its run() method to the framework. 

 // iCoordinator.h

 class iCoordinator {
   public:
     virtual int run() = 0;
 };

We use lower case i prefix to identify an interface.  This naming convention holds for all interfaces within the framework.

The Coordinator class derives from this interface and holds a pointer to the APIUserInput object. 

 // Coordinator.h

 class Coordinator : public iCoordinator {
    iAPIUserInput* userInput; // points to the user input object
    Coordinator(const Coordinator& s);            // prevents copying
    Coordinator& operator=(const Coordinator& s); // prevents assignment
  protected:
    virtual ~Coordinator();
  public:
    Coordinator(void*, int);
    int run();
};

The constructor creates an instance of the APIUserInput class.  The run() method keeps calling the getConfiguration() method until it returns false.  The Delete() method deletes the APIUserInput object. 

 // Coordinator.cpp

 Coordinator::Coordinator(void* hinst, int show) {
     userInput = CreateAPIUserInput(hinst);
 }

 int Coordinator::run() {
     while (userInput->getConfiguration());
     return 0;
 }

 Coordinator::~Coordinator() { userInput->Delete(); }

We follow this convention for creating and destroying objects throughout the framework.

Translation Layer

The Translation Layer contains the code for the Entry and APIUserInput components.

The Translation.h header file defines the macros for the dialog box:

 // Translation.h

 #define WND_CAPTION L"fwk4gps"
 #define IDD_DLG 101
 #define IDC_DIS 102
 #define IDC_RES 103
 #define IDC_GO  104
 #define COPYRIGHT_LINE_1 "fwk4gps is copyright (c) 2012, Chris Szalwinski."
 #define COPYRIGHT_LINE_2 "PostgreSQL Open Source License (TPL). \
 ../Licenses.txt"

Entry

The Entry component manages entry and exit from the framework.  Its WinMain() function creates the Design object and calls the run() method on its base class. 

 // Entry.cpp

 int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hprev, LPSTR cp, int show) {
     Design game(hinst, show);
     return game.run();
 }

APIUserInput

The APIUserInput component manages communications with the user.  Its interface file exposes four methods to the framework and two global functions. 

 // iAPIUserInput.h

 class iAPIUserInput {
   public:
     virtual bool getConfiguration()     = 0;
     virtual void populate(void*)        = 0;
     virtual bool saveUserChoices(void*) = 0;
     virtual void Delete() const         = 0;
 };
 iAPIUserInput* CreateAPIUserInput(void*);
 iAPIUserInput* APIUserInputAddress();

The APIUserInput class holds the addresses of its own instance, the application itself, and the dialog window.  This class also holds the display and mode identifiers for the congiguration selected by the user. 

 // APIUserInput.h

 class APIUserInput : public iAPIUserInput {
     static iAPIUserInput* address;     // points to this singleton
     void*                 application; // points to the application
     void*                 hwnd;        // points to the dialog window
     int                   displayId;   // display identifier
     int                   modeId;      // mode identifier
     APIUserInput(const APIUserInput&);            // prevent copies
     APIUserInput& operator=(const APIUserInput&); // prevent assignments
     virtual ~APIUserInput();
   public:
     static iAPIUserInput* Address() { return address; }
     APIUserInput(void*);
     bool getConfiguration();
     void populate(void*);
     bool saveUserChoices(void*);
     void Delete() const             { delete this; }
 };

The class pointer stores the address of the current object and the class method returns that address.  Storing the address of the current object in a class pointer only makes sense if only one instance of the class exists. 

The CreateAPIUserInput() function create a APIUserInput object if none exists.  The APIUserInput constructor initializes the instance variables.  The getConfiguration() method creates the dialog box and transfers control to it.  The populate() method populates the combo boxes before the dialog box is displayed.  The saveUserChoices() method saves the combo box selections in instance variables before the dialog box returns control to its caller.  The APIUserInputAddress() function returns the APIUserInput object's address. 

 // APIUserInput.cpp

 #define WIN32_LEAN_AND_MEAN
 #include <windows.h>
 #include "APIUserInput.h" // for the APIUserInput class definition
 #include "Translation.h"  // for IDC_???

 BOOL CALLBACK dlgProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
 iAPIUserInput* APIUserInput::address = nullptr;
 iAPIUserInput* APIUserInputAddress() { return APIUserInput::Address(); }

 iAPIUserInput* CreateAPIUserInput(void* hinst) {
     return APIUserInputAddress() ? APIUserInputAddress() :
      new APIUserInput(hinst);
 }

 APIUserInput::APIUserInput(void* hinst) : application(hinst) {
     address   = this;
     hwnd      = nullptr;
     displayId = -1;
     modeId    = -1;
 }

 bool APIUserInput::getConfiguration() {
     bool rc;
     rc = DialogBox((HINSTANCE)application, MAKEINTRESOURCE(IDD_DLG),
      nullptr, dlgProc) == TRUE;
     hwnd = nullptr;
     return rc;
 }

 void APIUserInput::populate(void* hwndw) {
     int dev, res;
     HWND hwnd = (HWND)hwndw;

     SendDlgItemMessage(hwnd, IDC_DIS, CB_RESETCONTENT, 0, 0L);
     dev = SendDlgItemMessage(hwnd, IDC_DIS, CB_ADDSTRING, 0,
      (LPARAM)L"Display A");
     SendDlgItemMessage(hwnd, IDC_DIS, CB_SETITEMDATA, dev, 100);
     dev = SendDlgItemMessage(hwnd, IDC_DIS, CB_ADDSTRING, 0,
      (LPARAM)L"Display B");
     SendDlgItemMessage(hwnd, IDC_DIS, CB_SETITEMDATA, dev, 200);
     SendDlgItemMessage(hwnd, IDC_DIS, CB_SETCURSEL, 0, 0L);

     SendDlgItemMessage(hwnd, IDC_RES, CB_RESETCONTENT, 0, 0L);
     res = SendDlgItemMessage(hwnd, IDC_RES, CB_ADDSTRING, 0,
      (LPARAM)L"Resolution A");
     SendDlgItemMessage(hwnd, IDC_RES, CB_SETITEMDATA, res, 10);
     res = SendDlgItemMessage(hwnd, IDC_RES, CB_ADDSTRING, 0,
      (LPARAM)L"Resolution B");
     SendDlgItemMessage(hwnd, IDC_RES, CB_SETITEMDATA, res, 20);
     SendDlgItemMessage(hwnd, IDC_RES, CB_SETCURSEL, 0, 0L);

     if (displayId >= 0 && modeId >= 0) {
         wchar_t str[31];
         wsprintf(str, L"Display %d Mode %d", displayId, modeId);
         MessageBox(hwnd, str, L"You Selected", MB_OK);
     }
 }

 bool APIUserInput::saveUserChoices(void* hwndw) {
     HWND hwnd = (HWND)hwndw;

     int dev   = SendDlgItemMessage(hwnd, IDC_DIS, CB_GETCURSEL, 0, 0L);
     displayId = SendDlgItemMessage(hwnd, IDC_DIS, CB_GETITEMDATA, dev, 0L);
     int res   = SendDlgItemMessage(hwnd, IDC_RES, CB_GETCURSEL, 0, 0L);
     modeId    = SendDlgItemMessage(hwnd, IDC_RES, CB_GETITEMDATA, res, 0L);
     return true;
 }

 APIUserInput::~APIUserInput() { }

Note that the populate() and saveUserChoices() methods transfer data between the APIUserInput object and the window procedure for the dialog box.

The window procedure extracts the address of the APIUserInput object through the APIUserInputAddress() global function and calls the object's methods on that address:

 BOOL CALLBACK dlgProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
     BOOL           rc        = FALSE;
     static bool    firsttime = true;
     iAPIUserInput* apiDialog = APIUserInputAddress();

     switch (msg) {
       case WM_INITDIALOG:
         SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE)
          | WS_EX_LAYERED);
         SetLayeredWindowAttributes(hwnd, 0, (255 * 95) / 100, LWA_ALPHA);
         apiDialog->populate(hwnd);
         rc = true;
         break;
       case WM_COMMAND:          // user accessed a dialog box control
         switch (LOWORD(wp)) {   // which control?
           case IDC_DIS:
             EnableWindow(GetDlgItem(hwnd, IDC_RES), TRUE);
             EnableWindow(GetDlgItem(hwnd, IDC_GO), TRUE);
             break;
           case IDC_GO:  // user pressed the Go button
             // save the context information
             if (apiDialog->saveUserChoices(hwnd)) {
                 EndDialog(hwnd, TRUE);
                 firsttime = true;
                 rc = TRUE;
             }
             break;
           case IDCANCEL:  // user pressed Cancel button, or Esc, etc.
             EndDialog(hwnd, FALSE);
             firsttime = true;
             rc = TRUE;
             break;
         }
         break;
     }
     return rc;
 }

API Component Design

APIs develop independently of frameworks and client applications.  An API may be upgraded many times before a framework that uses it is upgraded.  Conversely, a framework may be upgraded many times before the APIs that support it are upgraded.  Moreover, APIs may be compiled on compilers that are completely different from those used to compile frameworks and client applications or on different versions of the same compiler.  Accommodating these scenarios requires cross-compiler compatibility and backward compatibility. 

To enable cross-compiler compatibility, API components are designed for binary encapsulation.  Binary encapsulation differs from the simpler syntactic encapsulation that the C++ language offers.  To enable independent upgrade paths, API components are also designed for backward compatibility.  Finally, for simultaneous use by several applications, API components are developed to a uniform standard.

To demonstrate the difference between typical C++ coding and coding that meets these three requirements, we take a simple example and rewrite it in an upgraded form. 

Original Code

Let us start with three source files:

  • an application file - application.cpp
  • a header file - Component.h
  • an implementation file - Component.cpp

We will add an interface file - iComponent.h in the recasting.

The program listed below:

  • creates an instance of a String class on dynamic memory
  • displays the C-style null-terminated string stored within that instance on standard output
  • displays the string's length on standard output
  • deletes the instance from dynamic memory
 // application.cpp

 #include <iostream>
 using namespace std;
 #include "Component.h"

 int main() {
     char s[] = "Hello";
     String* str = new String(s);
     if (str) {
         cout << "String is " << str->getstr() << endl;
         cout << "Length is " << str->length() << endl;
         delete str;
     }
 }

The class definition for the String class is:

 // Component.h

 class String {
     char* str;
 public:
     String(const char* s);
     ~String();
     const char* getstr() const;
     int length() const;
 };

The implementation of the String class defines a constructor, two methods, and a destructor

 // Component.cpp

 #include <cstring>
 using namespace std;
 #include "Component.h"

 String::String(const char* s) :
  str(new char[strlen(s) + 1]) { strcpy(str, s); }
 String::~String() { delete [] str; }
 const char* String::getstr() const { return str; }
 int String::length() const { return strlen(str); }

This application's executable includes the binary code for the String class.  If we upgrade the String class, but do not change the application, we must still recompile the application along with the new implementation to create the new executable.

Upgraded Code

Upgrades to the code are highlighted below.  A CreateString() function now allocates dynamic memory for the String object.  The second argument to this function specifies the interface to use in creating that object.  The third argument returns the address on which to call the object's methods.  The DynamicCast() function switches interfaces.  The first argument specifies the requested interface.  The second argument returns the address.  The Delete() method releases the interface and deallocates memory for the String object if there are no outstanding interfaces.  The find() method was unavailable on the iString interface, but is available on the later iStringEx interface.

 // application.cpp

 #include <iostream>
 using namespace std;
 #include "iComponent.h"

 int main() {
     char s[] = "Hello", c = 'o';
     int rc;
     iString* str;

     rc = CreateString(s, "iString", (void**)&str);
     if (rc) {
         cout << "String is " << str->getstr() << endl;
         cout << "Length is " << str->length() << endl;
         iStringEx* strEx;
         rc = str->DynamicCast("iStringEx", (void**)&strEx);
         if (rc) {
             cout << "Index  is " << strEx->find(c) << endl;
             rc = strEx->Delete();
             if (!rc)
                 cerr << "Error during deletion" << endl;
         }
         str->Delete();
     }
 }

The iComponent.h header file lists all of the interfaces and the prototypes for the CreateString() function:

 // iComponent.h

 class iBase {
 public:
     virtual int DynamicCast(const char* s, void** p) = 0;
     virtual int Delete()                             = 0;
 };
 class iString : public iBase {
 public:
     virtual const char* getstr() const = 0;
     virtual int length() const = 0;
 };
 class iStringEx : public iString {
 public:
     virtual int find(char c) const = 0;
 };
 extern "C" __declspec(dllexport)
 int CreateString(const char* s, const char* iid, void** p);

The class definition derives from both interfaces:

 // Component.h

 #include "iComponent.h"

 class String : public iString, public iStringEx {
     int len;
     char* str;
 public:
     String(const char* s);
     ~String();
     int DynamicCast(const char* s, void** p);
     int Delete();
     const char* getstr() const;
     int length() const;
     int find(char c) const;
 };

The upgraded implementation file reflects the signature changes:

 // Component.cpp

 #include <cstring>
 using namespace std;
 #include "Component.h"

 String::String(const char* s) :
  len(strlen(s)), str(new char[len + 1]) { strcpy(str, s); }
 String::~String() { delete [] str; }
 const char* String::getstr() const { return str; }
 int String::length() const { return len; }
 int String::find(char c) const {
     int n = -1;
     for (int i = 0; i < len; i++)
         if (str[i] == c) {
             n = i;
             i = len;
         }
     return n;
 }
 int CreateString(const char* s, const char* iid, void** p) {
     iString* i = new String(s);
     i->DynamicCast(iid, p);
     return *p != NULL;
 }
 int String::Delete() {
     delete this;
     return 1; // always succeeds
 }
 int String::DynamicCast(const char *s, void** p) {
     if (strcmp(s, "iStringEx") == 0)
         *p = static_cast<iStringEx*>(this);
     else if (strcmp(s, "iString") == 0)
         *p = static_cast<iString*>(this);
     else if (strcmp(s, "iBase") == 0)
         // return iString* to avoid ambiguity
         *p = static_cast<iString*>(this);
     else
         *p = 0;
     return *p != NULL;
 }

Summary

Framework Design

The framework exhibits that following design features. 

  • interfaces are uncoupled from class definitions, enabling changes to the definitions and implementations without requiring recompilation of components that refer to functions on those interfaces
  • calling methods through interfaces avoids symbolic name dependencies between framework components
  • Create() functions uncoupled from class definitions avoids class definitions in components that create the objects
  • Delete() methods on the interfaces ensure complete destruction of objects in class hierarchies

The framework uses singletons for classes that communicate with window procedures.  Global functions return the addresses of these instances. 

API Component Design

The design of an API component requires binary encapsulation, backward compatibility, and standardization.  The conversion of syntactically encapsulated code into binary encapsulated code involves introducing:

  • an interface to uncouple the class definition from client applications, enabling changes to the definition or implementation without re-compilation of the application
  • interfaces to avoid symbolic name dependencies between the component and client applications, enabling cross-compiler compatibility
  • a Create() function to avoid the class definition in the component that creates an instance of that class
  • a Delete() method to match the Create() function, ensuring complete destruction of an object of an inheritance hierarchy

Provision of backward compatibility involves introducing:

  • an interface hierarchy to expose new methods through derived interfaces
  • a DynamicCast() method to enable access to a newer interfaces from within client applications originally developed for earlier releases

Component standardization involves introducing:

  • function signatures that report errors through the function return value
  • a parameter that passes back an interface address

These three feature are central to the COM technology that underlies DirectX described in the next chapter.

Comparing Framework and API Components

The framework components share much of their design with that of API components.  There are two obvious exceptions.

Since we compile the framework components using the same compiler, there is no need for run-time linkage.  The interface files for the framework components allow name mangling and do not specify C linkage or dynamic linking for the Create() functions. 

Since the creation of framework objects is much simpler than the retrieval of API interfaces and their creation does not generate error codes, we have omitted passing the address of a created object through the Create() function's parameter list and instead passed it directly through its return values. 


Exercises

  • Download the Selection Sample from the course repository to your local computer
  • Build the solution and run the sample code




Previous Reading  Previous: Windows Programming Next: COM and Direct X   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo