The Renderer and SoundManager types withinthe PLATFORM package are Strawman classes again, see Chapter 4 that define no behaviour, only pass type information, e.g.. // File: PLATFORM_Ren
Trang 1What we want to do is move the abstract concept of renderers and sound managersinto the PLATFORM namespace without bogging down said namespace with anyspecifics about hardware or implementation In other words, we’d like the sort ofstructure illustrated in component terms here and in classes in Figure 6.7:
which decouples the packages The Renderer and SoundManager types withinthe PLATFORM package are Strawman classes (again, see Chapter 4) that define
no behaviour, only pass type information, e.g
// File: PLATFORM_Renderer.hpp
#ifndef PLATFORM_RENDERER_INCLUDED
#define PLATFORM_RENDERER_INCLUDED
namespace PLATFORM{
class Renderer{
SoundManagerPlatform
Renderer
PLATFORMFigure 6.7
Object diagram for
platform-independent
renderer and sound manager
PLATFORM
Trang 2} // end of PLATFORM namespace
#endif
The Renderer within the REND package defines the generic renderer behaviour
Though in principle we could have placed that generic behaviour within the
PLATFORM component, that would probably result in contamination of the
PLATFORM name space with generic renderer specifics, which it really has no
business in knowing This way keeps it sparkling clean
Cast your mind back to Figure 6.3, the outline of a cross-platform renderer
Notice that there is no provision for rendering to off-screen buffers (for
exam-ple, to draw shadows or a rear-view mirror in a vehicle-based game) This is
because we may not be able to assume that all targets have sufficient video RAM
(VRAM) to allow this It’s often better – and easier – in the long run to not
define a piece of non-generic functionality in a generic system than to provide
it and disable it in a platform-specific system It is always possible to create a
new component associated with the renderer that provides the functionality in
the middle-level functionality band
Now, we have to make the abstract into the concrete For each platformtype we need to support, we define a namespace that implements the Platform
class and defines the specific services (see Figure 6.8)
In the PLATFORM1 package, we define and implement our Platform classsomething like this:
SoundManagerPlatform1Platform1
RendererPlatform1
SoundManager
SOUND
RendererREND
SoundManagerPlatform
RendererPLATFORM
PLATFORM1
Figure 6.8Cross-platforminfrastructure
Trang 3class Platform1 : public PLATFORM::Platform{
#include "PLATFORM1_Platform.hpp"
#include "PLATFORM1_Renderer.hpp"
using namespace PLATFORM1;
Platform1::Platform1(){
}Platform1::~Platform1(){
}/*virtual*/
PLATFORM::Renderer * Platform1::CreateRenderer(){
return( new PLATFORM1::Renderer );
}
Trang 4Notice that the PLATFORM1 namespace looks a bit ‘flat’ We’ve lost the
compo-nent structure we’d introduced to keep independent functionality neatly
partitioned To remedy this, we can subclass the service namespaces just as we
did for PLATFORM (see Figure 6.9)
The application component
So, by using the PLATFORM package we can define a factory for creating
plat-form-specific classes and components The intention is to use these in an
application, so the next element in our model is to define an abstract
applica-tion class Platform-specific subclasses of the applicaapplica-tion class will create an
appropriate Platform subclass and use that to create the required service
compo-nents for the game, thus:
SoundManagerPlatform1Platform1
RendererPlatform1
SoundManager
SOUND
RendererREND
SoundManagerPlatform
RendererPLATFORM
PLATFORM1
Figure 6.9Cross-platforminfrastructure withpartitioned namespaces
Platform1ApplicationPlatform1
PlatformApplication
PLATFORM
PLATFORM1
Trang 5Which translates to C++ like this:
// File: PLATFORM_Application.hpp
#ifndef PLATFORM_APPLICATION_INCLUDED
#define PLATFORM_APPLICATION_INCLUDED
namespace PLATFORM{
class Platform;
class Application{
public:
Application( Platform * pPlatform ): m_pPlatform( pPlatform )
{}
virtual ~Application(){
delete m_pPlatform;
}
// Main loop code for the application
virtual void Run() = 0;
Platform * GetPlatform(){
return( m_pPlatform );
}private:
Trang 7namespace RENDPLATFORM1{
class Renderer;
}
class GamePlatform1 : public PLATFORM1::ApplicationPlatform1{
{// Note: DON’T try to set up the services here – // virtual functions don’t work in a constructor.}
GamePlatform1::~GamePlatform1(){
delete m_pRenderer;
}
REND::Renderer * GamePlatform1::GetRenderer(){
if ( m_pRenderer == 0 )
Trang 8// These casts are safe because we know what // platform we’ve created the object for.
PLATFORM1::Platform1 * pPlatform = (PLATFORM1::Platform1 *)GetPlatform();
m_pRenderer = (RENDPLATFORM1::Renderer*) pPlatform->CreateRenderer();
bool bGameOver = false;
/* Main loop for the game… */
while( !bGameOver ){
/* … */
}
/* Termination… */
}
Hmmm, now that we can see it in the flesh, that Run method seems to be a bit
of a code-sharing obstacle Suppose that we had identical main loops on our n
target platforms – which we can achieve using (say) the State Manager system
described in Chapter 4 – then we’d write the same loop code n times Let’s avoid
that by writing a game loop class with which we can initialise our application:
Trang 9virtual void Initialise() = 0;
virtual void Run() = 0;
virtual void Terminate() = 0;
private:
};
} // end of namespace PLATFORM
#endifThe amended application looks like this:
namespace PLATFORM{
class GameLoop;
class Application{
Application::
Application( Platform * pPlatform, GameLoop * pGameLoop ): m_pPlatform( pPlatform )
, m_pGameLoop( pGameLoop ){
}void Application::Run(){
m_pGameLoop->Initialise();
m_pGameLoop->Run();
m_pGameLoop->Terminate();
}
Trang 10The only thing left to do now is to choose what sort of application we want to
create Though we could do this using a preprocessor macro, let’s try to avoid
using one of those altogether, as the use of it as either a command-line option
or a global include file will invite physical dependencies where none is needed
Instead, let’s use a separate file defining main()for each build platform:
pGame->Run();
return( pGame->GetExitCode() );
}
6.2 Summary
● Cross-platform development is not easy There are many ways to screw up, and if
you do it can be costly Object orientation provides natural ways to organise thedevelopment of code on multiple target platforms in parallel By separating and
Trang 11subclassing the variant behaviours, we can – with care – create a generic set ofcomponents to be used in all the skus without compromising performance orstructure, in some cases irrespective of the differences between the varioushardware architectures In the cases where we cannot do this, object-orientedanalysis still gives us metrics that we can use to organise our code and data inways that are beneficial to the development process.
● The differences in toolsets between platforms can make life pretty awkward forthe developer Use tools such as PC-Lint to analyse code more thoroughly thancompilers to find the trouble spots
● Use intermediate file formats to simplify, segregate and clarify the import andexport of data
● As well as the capability and methodology of hardware, the distinction betweenmajor and minor platforms can have an impact on the game architectures.Identify these and plan the architecture accordingly
● Components work well in multiplatform systems By introducing platform nents, we can abstract away the differences in underlying hardware and still usegeneric components in the majority of the game
Trang 12compo-In this chapter, we’ll examine the design and implementation of the central
participants in a game’s architecture, the game object or GOB Getting thecorrect logical and physical structure here is particularly critical, since thegame’s functioning will depend intimately on the operation and cooperation of
these class instances, and we will find that the project’s compile and link times
also depend critically on how your object classes are written
We’ll look at three strategies for implementing game objects and analysethe pros and cons of writing them that way We’ll then go on to discuss man-
agement of the objects, in particular memory-allocation strategies and some
other common implementation issues that you’ll meet along the way
7.1 Open your GOB
The term ‘game object’ is both accurate and misleading After all, every class in
your game will have an instance that is an object And while we’re at it, doesn’t
every application – never mind game – contain objects? Flippancy aside, there’s
an important issue here: every non-trivial application has an object hierarchy
What sort of hierarchies might we see?
7.1.1 Collapsed hierarchy
The collapsed hierarchy is shown below There is no inheritance in this
whatso-ever It’s a throwback to those bad old C programming days, and you’re pretty
unlikely to see it in a medium to large modern C++ project
Trang 13Notice that there are only ‘has a’ relationships between classes This makes reuse
at least awkward and more likely than not near impossible Nevertheless, thereare benefits to be gained from this structural organisation First, remember thatinheritance is a strong binding between classes: physically, you have to includethe header file of the base class in the derived class’s header file With no inheri-tance, there are fewer compile-time dependencies: things are going to compileand link just about as quickly as they can
Second, one of the classes in the hierarchy will be the ‘ApplicationObject’,the class around which almost all behaviour pivots Since these are all identical
in size and basic functionality, the creation and deletion of these objects can be
made arbitrarily efficient by pool allocation At run time, allocate a block of
these, slap them into a free list and allocate as required When the pool dries up,allocate another bunch, add those to the free list, and so on This kind of alloca-tion strategy is extremely fast compared with the usual new/malloccombination and also helps to reduce fragmentation within the memory man-ager (which can lead to increasingly long allocation times when the applicationhas been running for some time)
So that’s the good news Now, why might we choose not to adopt a lapsed hierarchy? Well, for one thing, we may have made allocation efficientand reduced fragmentation, but this is at the cost of storage overhead Each ofour objects needs to support the functionality of any entity in the system Thatmeans its data fields and member functions are comprised of the union of allthe subclasses it needs to support functionality for
col-That means a lot of wasted space: many of the subclass objects will requireonly a small subset of the data and functionality in the object If there are lots
of objects in the system, then that’s potentially a lot of RAM that you know youwill be grovelling for as you near master Do you really want to do that? Evenmore importantly, you have created a software engineering bottleneck here: theobject grows in complexity, functionality depends on state, and state is some-times far from obvious One piece of legacy code I inherited on a project(admittedly written – badly – in C) a while back had this sort of thing going on:struct OBJECT
{//…
context With hilarious consequences
Trang 14In a team environment where there is only one object class, there will be abig demand from your colleagues to edit and modify that class Over the course
of time, they’ll add more and more var fields, or find novel and bug-ridden ways
to use the existing ones Welcome to development hell
Having a monolithic, do-everything class is exactly what object orientationtries to avoid Yes, a well-written C++ project will have quite a few more files
kicking around than a C project of equivalent scope might have had, but the
ease of maintenance gained by the divide-and-conquer methodology is not to
be sniffed at
So, in summary, the collapsed hierarchy has little to recommend it to thetwenty-first-century developer
7.1.2 Shallow hierarchy
The temptation for inexperienced developers (or, for that matter, some so-called
experienced ones) is to use this wonderful thing called inheritance to scatter
some basic properties (such as the ability to have a name, or to be serialised)
The defining pattern for the shallow hierarchy is, for most, if not all, of theobjects in your system to inherit from one single base class, as here:
If your object hierarchy looks a bit like this, then you’re in good company:
Microsoft’s MFC is one system that looks quite similar But don’t let that put
you off
The act of factoring common functionality into a base class is able and certainly correct in principle Which is really damning with faint
commend-praise, because although the shallow hierarchy has certain traits that look like
object orientation, it exhibits the hallmarks of a flawed design or a serious lack
of design
Why, for example, do a file and a container share a common base class? Thefact that they can be serialised does not, in itself, justify the cost of inheriting
from the purportedly common class And what does it mean to serialise a
window? In other words, are there classes in the system that have functionality
that it doesn’t even make sense for them to have?
Let’s assume the base object confers useful functionality or passes typeinformation meaningful to some higher-level system There is no pressing need
to slap all those bits of functionality into the space of one class if they can
hap-pily sit in separate classes and be inherited where needed
The shallow hierarchy is definitely an improvement on the collapsed chy; at least we have a bunch of smaller classes that are more easily maintained
Trang 15and reasonably defined Its flaw is that base class, which has become the place
to dump functionality that looks even slightly useful to its client classes Those of you who have worked on projects with this sort of structure willalso be familiar with the terrifying announcement towards the end of develop-ment that someone has changed (‘fixed’) a problem with the base class, andyou just know that invoking a build is going to take the best part of the morn-ing away
7.1.3 Vertical hierarchy
This structure is usually the result of realising that the collapsed hierarchy isunusable and that the shallow hierarchy gives little gain and a moderate deal ofpain The general idea is to start from an abstract description of the object andincrementally add properties in a series of subclasses Figure 7.1 shows anabstract vertical hierarchy We start with a near-dataless base class and add prop-erties 1, 2, 3 and 4 in a series of subclasses
As a concrete example, I’ll use a design I was playing with for a space game
a while ago First, here’s the base class, which as you can see is not atomic initself; it depends on a reference-counting property class:
432
11
2
3
4
Figure 7.1Abstract vertical
hierarchy
Trang 16virtual void Draw( Renderer * pRenderer ) = 0;
virtual void Update( float fDeltaT ) = 0;
private:
};
Reference counting is an integral part of almost all non-trivial game systems It
is tempting to make this a common base class for all application classes, but
that really ought to be resisted without further thought Here’s a first take on a
reference counting class:
class IsReferenceCounted
{
public:
IsReferenceCounted(): m_iCount(0)
{}
virtual ~IsReferenceCounted(){
}
void AddRef(){
++m_iCount;
}
void Release(){
m_iCount;
if ( m_iCount == 0 ){
delete this;
}}
private:
int m_iCount;
};
This is fine, so long as:
● the object was allocated via new;
● you want to delete only unreferenced objects