Cross-Platform Game Programming with gameplay3d/gameplay3d Design Concepts

From Wikibooks, open books for an open world
Jump to navigation Jump to search

This chapter provides an overview of some of the fundamental design principles underlying the gameplay3d framework.

Managing shared objects[edit]

Unlike certain other programming languages (e.g. Java), C++ does not have any automatic garbage collection. Accordingly, it is necessary to keep track of dynamically created objects and delete them when necessary. Difficulties can arise when more than one part of the code refers to the same object, i.e. when there are 'shared objects'.

For example, consider the following pseudocode:

// Create a boss enemy and aim the camera towards it
GameObject* pBossEnemy = new BossEnemy;
cutSceneCamera.setTarget(pBossEnemy);

//... some code ...

// Some time later, the boss is destroyed
delete pBossEnemy;

//... some more code ...

// Ooops, cutSceneCamera now references a dangling pointer!
cutSceneCamera.updateOrientation();

Gameplay3d's approach to dealing with this problem is to use reference counting. The basic aim is to ensure that (i) an object is not destroyed while any part of the code is still using it; and (ii) once the object is no longer being used, it is deleted. This aim is achieved by storing a reference count inside the object. Every time the object is shared, the reference count is incremented, and every time a part of the code stops using the object, the reference count is decremented. When the count reaches 0, the object is no longer being referred to by anyone and is therefore destroyed.

The Ref base class[edit]

Reference counting is implemented in gameplay3d by using classes which inherit from the Ref class. The relevant parts of the source code (which can be found in its complete form, together with comments, in Ref.h and Ref.cpp) are as follows:

class Ref
{
public:
    void addRef(); // Increments the reference count
    void release(); // Decrements the reference count
    unsigned int getRefCount() const;

protected:
    Ref();
    Ref(const Ref& copy);
    virtual ~Ref();

private:
    unsigned int _refCount;
}

void Ref::addRef() {
    ++_refCount;
}

void Ref::release() {
    if ((--_refCount) <= 0) {
        delete this;
    }
}

unsigned int Ref::getRefCount() const {
    return _refCount;
}

Creating objects in practice[edit]

In most cases, you will not need to call addRef() directly. Instead, you will create objects by calling static functions whose names begin with create* (e.g. Mesh::createMesh(), Font::create(), Scene::create() etc.). These functions return an instance of a class inheriting from Ref, with the reference count for that object set to one. Accordingly, it is necessary to call release() on the object when it is no longer being used. Usually, the best way to do this is to make use of gameplay3d's SAFE_RELEASE() macro, which both calls release() and sets the pointer to NULL.

You should also be aware that certain gameplay3d functions which take an object as an argument increase that object's reference count. Intuitively, this is because the object being passed in as an argument is now being shared with another part of the code. Examples of this are:

  • the Model::create() method, which takes a Mesh* as a parameter. (This returns a pointer to a Model with a reference count of one and also increases the reference count of the Mesh by one.)
  • the Texture::Sampler::create() method, which takes a Texture* as a parameter; and
  • the setMaterial() member function in the Model class, which takes a Material* as a parameter.

An very simple example (taken from CreateSceneSample.cpp in the sample-browser project) showing how gameplay3d's reference counting system works in practice is as follows:

void CreateSceneSample::initialize()
{
    // Create the font for drawing the framerate.
    _font = Font::create("res/ui/arial.gpb");

    // Create a new empty scene.
    _scene = Scene::create();

    // ... omitted code setting up camera and lights ...

    // Create the cube mesh and model.
    Mesh* cubeMesh = createCubeMesh();
    Model* cubeModel = Model::create(cubeMesh);
    
    // Release the mesh because the model now holds a reference to it.
    SAFE_RELEASE(cubeMesh);

    // ... omitted code setting up the material for the cube model ...

    // Add a node to the scene, then attach the cube model to it
    _cubeNode = _scene->addNode("cube");
    _cubeNode->setModel(cubeModel);
    _cubeNode->rotateY(MATH_PIOVER4);

   // Release the model because the node now holds a reference to it.
    SAFE_RELEASE(cubeModel);
}

void CreateSceneSample::finalize()
{
    SAFE_RELEASE(_font);
    SAFE_RELEASE(_scene);
}

Using the DebugMem build configuration to avoid memory leaks[edit]

Finally, it is worth noting that gameplay3d's DebugMem build configuration provides a useful way of checking whether you have successfully remembered to deallocate all Ref objects. On exiting your program, any failures to deallocate Ref objects will be reported in the Debug Output window, as well as any other memory leaks arising from your code.

Data-driven design[edit]

A basic principle underlying data-driven design is that code is a poor location for behavior that needs to be changed frequently. One way of dealing with this problem is to move game behavior out of code and into data files. This should result in a more efficient process for tweaking content and gameplay.

Gameplay3d supports data-driven design by allowing certain game configuration data to be loaded from text-based data files. These files include:

  • game.config file(s) for high-level settings such as screen resolution, Lua script settings, default UI theme etc.;
  • .scene files for overall scene layout (these will generally contain cross-references to other data files);
  • .physics files for configuration of physics objects;
  • .material files for configuration of materials;
  • .theme files for UI themes;
  • .form files for UI forms (which use a UI theme);
  • .animation files for animation details (e.g. configuration of named animation "clips" which can be used in-game); and
  • .lua forms for Lua scripts.

More detail regarding the structure of these data files, and how to write your own, will be given in the chapters regarding the subject matter to which they relate.

Platform abstraction[edit]

The basic idea behind platform abstraction is, as far as possible, to separate platform-independent code from platform-specific code in order to maximize code re-use.

Gameplay3d is designed so that it uses platform-specific code only where necessary. Obvious areas in which platform-specific code is required include:

  • window creation;
  • OpenGL initialization;
  • the message pump; and
  • input handling.

However, because gameplay3d either handles these tasks automatically, or allows the user to handle these tasks through its platform-abstracted API, it is generally possible to avoid using platform-specific code in your own projects.

To the extent that you wish to review (or amend) the platform-specific code, most of this is tucked away neatly in the following source code files:

  • gameplay-main-android.cpp
  • gameplay-main-blackberry.cpp
  • gameplay-main-ios.mm
  • gameplay-main-linux.cpp
  • gameplay-main-macosx.mm
  • gameplay-main-windows.cpp
  • PlatformAndroid.cpp
  • PlatformBlackberry.cpp
  • PlatformiOS.mm
  • PlatformLinux.cpp
  • PlatformMacOSX.mm
  • PlatformWindows.cpp

Creating and importing assets[edit]

Overview[edit]

While gameplay3d does not currently include any GUI-based content creation tools (other than the particle editor in the "sample-particles" project), it supports various existing industry-standard file formats, which means that you can create assets in existing content creation packages and import these into your gameplay3d project.

Certain assets can be imported directly into gameplay3d, whereas other formats are required to be converted into the documented gameplay bundle format (.gpb) using the gameplay-encoder executable. The reason for requiring this extra conversion step is that, although these formats are popular and have the widest support in tooling options, they are not considered efficient runtime formats. Converting them into binary format ensures that the assets will load as quickly as possible and at the highest quality within the platform hardware limitations.

The supported external file formats which do not require conversion into .gbp format are as follows:

  • .ogg for audio;
  • .wav for audio (although it is recommended that the compressed .ogg format be used when releasing your game title);
  • .png for image files;
  • .dds and .pvr for compressed textures; and
  • .lua for Lua source code.

The supported external file formats which do require conversion are as follows::

  • .fbx (Autodesk) for creating 3D scenes and models; and
  • .ttf (TrueType Font) for fonts.

Converting assets into .gbp format[edit]

Assets can be converted into .gbp format using the gameplay-encoder tool. The gameplay-encoder executable tool comes pre-built for Windows 7, MacOS X and Linux. They can be found in the <gameplay-root>/bin folders. The general usage is:

Usage: gameplay-encoder [options] <file(s)>

You can display a list of the supported options by running gameplay-encoder with no arguments.

Building gameplay-encoder[edit]

Even though the gameplay-encoder tool comes pre-built, you may want to customize it and build it again yourself. To build the gameplay-encoder project, open the gameplay-encoder project in Visual Studio or XCode and build the executable.

The content pipeline[edit]

The content pipeline for fonts and scenes works as follows:

  • Identify all required TrueType fonts and FBX scene files.
  • Run the gameplay-encoder executable passing in the font or scene file path and optional parameters to produce a gameplay binary version for the file (.gpb).
  • Bundle your game and include the gameplay binary file(s) as a binary game asset.
  • Load any binary game assets using the gameplay::Bundle class.

Using binary bundles[edit]

Use the gameplay::Bundle class from your C++ game source code to load your encoded binary files as bundles. The class offers methods to load both fonts and scenes. Scenes are loaded as a hierarchical structure of nodes, with various entities attached to them. These entities include things like mesh geometry or groups of meshes, and cameras and lights. The gameplay::Bundle class also has methods to filter only the parts of a scene that you want to load.

Scripting with Lua[edit]