1. Trang chủ
  2. » Công Nghệ Thông Tin

Object oriented Game Development -P6 docx

30 342 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Object-Oriented Game Development
Trường học Unknown
Chuyên ngành Object Oriented Game Development
Thể loại Academic Paper
Năm xuất bản 2003
Thành phố Unknown
Định dạng
Số trang 30
Dung lượng 240,81 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Renderer MathsYour engine Renderer My game Figure 5.1Duplication of code and functionality within an application when using a game engine... Nothing is stopping us from adding a whole bu

Trang 1

– for example, line-of-sight calculations might refer to the portals the graphicssystem uses to prune visible data.

Now what that means in practice is that I cannot use one part of the gameengine – the NPC AI – without taking another – the environmental system.Now, what’s the betting that the environment system comes with strings too?Perhaps the rendering API? Too bad that our game already has a renderer This is our dependency demon from previous chapters rearing its uglyhorned head again This time it’s busy gobbling precious RAM and making lifemore complex than it needs to be This is illustrated beautifully in Figure 5.1.This isn’t just a matter of losing memory, though If the engine gains own-ership of system hardware, then your objects can be locked out And the reverse

is true, of course: the game engine may fail because you have allocated aresource that it needs, even if that resource is in a part of the engine you do notuse If you have the luxury of being able to modify engine code, then you may

be able to fix this problem However, this is frequently not the case

For these reasons, it is really very difficult to write a game engine to pleasemost of the people most of the time The best engines are limited in scope: first-person shooter engines, extreme sports engines, and so on Which is fine if yourcompany is continually churning out the same sort of game over the course ofseveral years, but the moment you diversify it’s back to the drawing board for anew game engine

5.2.2 The alternative

We really, really, really want to take a big hammer to the idea of an engine andbreak down the monolith into a series of components The ideal is to turn therelationships in Figure 5.1 into those shown in Figure 5.2

The first big change is (as promised) that we’ve hit the engine with ourhammer, and broken it down into two packages: one for rendering, one for AI.There is no logical or physical connection between these packages, and whyshould there be? Why would renderers need to know anything about AI? Andwhat interest would the AI have in a renderer? (Suggested answers: absolutely

no reason whatsoever, absolutely none.)

Renderer

MathsYour engine

Renderer

My game

Figure 5.1Duplication of code and

functionality within an

application when using a

game engine

Trang 2

Now, we can’t break the relationship between the NPC AI and the ment the AI must traverse However, we can move the complexity to where it

environ-belongs – in the game code OK, I can hear you grumbling at that, so let’s back

up a little The idea is to keep everything that is specific to your game

environ-ment in the game and to move everything that is generic about environenviron-ments

into the AI package’s environment component Nothing is stopping us from

adding a whole bunch of toolkit routines and classes to the AI package that do a

lot of common mathematical or logical operations; and we can declare virtual

functions in the abstract environment base class that we implement in our

con-crete environment

Let’s generalise this to define a component architecture This is a set ofindependent packages that:

● implements toolkit classes and functions for general common operations;

● declares concrete and abstract classes in such a way as to define a template

for how the game classes should be organised

So far, so grossly simplified However, it illustrates the point that we have taken

a strongly coupled engine and converted it into a series of loosely coupled or

even independent building blocks

Consider, then, the possibilities of working with a component architecture

Writing a game becomes a matter of choosing a set of components and then

gluing them together Where the components define abstract, partial or just

plain incorrect functionality, we can override the default behaviours using the

help of our friends polymorphism and inheritance Instead of facing the

cre-ation of an entire game engine from scratch, we – in effect – create a bespoke

one from off-the-shelf building blocks that can be made into just what we need

and little or no more No need to put up with redundant systems and data any

more No more monolithic engines Product development time is reduced to

writing the glue code, subclassing the required extra behaviour and writing the

game Welcome to a brave new world!

AIRenderer

a component philosophy

Trang 3

Before we get too carried away, let’s apply the brakes ever so gently The ceding paragraph describes the goal of component-based development Inreality, there is still a lot of hard work, especially in writing those subclasses.However, bear in mind that if written with sufficient care and attention thesesubclasses become reusable components in themselves Can you see now whythe component model is appealing?

pre-Notice that the component architecture model remains open – it is notgeared to write a particular type of game; it remains flexible because behaviourscan always be overridden when required; and if a component runs past its sell-

by date, it’s only a small system to rewrite and there’s no worrying aboutdependency creep because there are no – or, at worst, few – dependencies.All right, enough of the hard sell Let’s look at how we might go about cre-ating a component architecture Because this is – as stressed above – an opensystem, it would be impossible to document every component that you couldwrite, but we’ll deal with the major ones in some detail here:

5.3 Some guiding principles

Before we look in detail at the components, we shall discuss some basic ples – philosophies, if you will – that we will use in their architecture Theseprinciples apply to game software in general, so even if you don’t buy the com-ponent architecture model wholesale, these are still useful rules to apply.5.3.1 Keep things local

princi-Rule number 1 is to keep things as local as you can This applies to classes, macros

and other data types; indeed, anything that can be exported in a header file Toenforce this to some extent, we can use either a namespacefor each component

or some kind of package prefix on identifier names.1 Keep the namespace (orprefix) short: four or five characters will do (we use upper-case characters fornamespaces)

1 There may even be an argument for using both prefixes and name spaces The reason for presenting

Trang 4

The first practical manifestation of this is to stick to C++’s built-in datatypes wherever possible:

int, char, short, long, float, double

and any unsigned variants of these (should you need them) This is in

prefer-ence to creating type definitions such as:

typedef unsigned char uint8;

You may recall from previous chapters that definitions such as these are

fre-quently overused with little justification for their existence If you need these

sort of constructs, make sure they are included in the name space and keep

them out of public interfaces:

typedef unsigned char comp_Uint8;

Remember that macros do not bind to name spaces and so should be replaced

by other constructs (such as in-line functions) where possible, prefixed with the

component identifier or perhaps removed altogether

There is a balance to be struck here If we were to apply this locality ple universally, we would end up with components that were so self-contained

princi-that they would include vast amounts of redundancy when linked into an

application Clearly, this would defeat the object of the exercise So, for the

moment, let’s say that there’s a notional level of complexity of a class or other

construct, which we’ll denote by C0, below which a component can happily

implement its own version, and above which we’re happy to import the

defini-tion from elsewhere Using C0we can define three sets:

● S–is the set of classes whose complexity is much less than C0

● S+is the set of classes whose complexity is much greater than C0

● S0is the set of classes whose complexity is about C0

Now although these are very vague classifications, they do give us a feel for

what goes into our components and what needs to be brought in (see Table 5.1)

The idea behind the vagueness is to give you, the programmer, some bility in your policy For example, suppose a component requires a very basic

Trang 5

flexi-container (implementing addition, removal and iteration only, say) Even if ourpolicy says containers are imported, simple (‘diet’, if you will) containers can bedefined within our component (and if we’ve written our containers using tem-plates, then the size of redundant code is exactly zero bytes).

To summarise: by keeping appropriate C++ constructs local to a componentyou will improve the chances that this component can exist as a functional entity

in its own right without the requirement to pull in definitions from elsewhere.5.3.2 Keep data and their visual representations logically and physically apart

This is an old principle in computer science, and it’s still a good one When webind any data intimately with the way we visualise those data, we tend to make

it awkward if we decide to change the way we view the data in the future.Consider an explosion class In the early phases of development, we may nothave the required artwork or audio data to hand, yet we still wish to presentsome feedback that an explosion has occurred One way of doing this might be

to simply print the word ‘BANG!’ in the output console with a position Here’ssome abridged sample code to illustrate this:

// File: fx_Explosion.hpp

#include <stdio.h>

namespace FX{

class Explosion{

public:

void Render(){

Trang 6

We have introduced a dependency between an explosion and the way we view

it When the real artwork finally arrives, we’re going to have to change that

ren-dering function even though the explosion has not physically changed It is

always a good idea to keep the simulation and your visualisation of it separate

That way, you can change the representation with the object itself being utterly

oblivious This general principle is shown in Figure 5.3

The base class Object is a data holder only Apart from a polymorphicmethod for rendering, nothing more need be specified regarding its visualisation:

(We haven’t specified how we’re going to render yet, and it’s superfluous to this

discussion for the moment, hence the commented question mark.)

Each object that requires to be viewed is subclassed as an ObjectVisual,which contains a pointer to a visual This abstract class confers some kind of

renderability on the object, though the object instance neither is aware of nor

cares about the details:

ObjectVisualObject

Visual3D

Visual

VisualTextRend

Component

Visual

Figure 5.3Keeping object data and their visualrepresentation separate

Trang 7

class ObjectVisual : public Object{

REND::Visual * m_pVisual;

};

The purpose of this principle will become apparent when we consider the torical precedents In a previous era, with target platforms that were not sopowerful, the luxury of having separate functions to update an object and then

his-to render it was often avoided Code for drawing the object and refreshing itsinternal state was mixed freely And, of course, this made changing how theobject was viewed next to impossible in the midst of development By keepingthe visual representation separate from the internal state of the object, wemake life easier for ourselves in the long run And we like it when it’s easier,don’t we?

Let’s just sum this principle up in a little snippet of code:

class Object{

Trang 8

5.3.3 Keep static and dynamic data separate

Like separating state and visual representation, this principle makes code

main-tenance simpler However, it can also greatly improve code performance and

can reduce bloating with redundant data

The principle is very simple at heart If you have an object that controls anumber of sub-objects, and you want to update the parent, then that involves

updating all of the child objects as well If there aren’t too many children, then

the cost may be negligible, but if there are lots of objects or the objects have lots

of children, then this could become costly

However, if you know that some of the children are static, then there’s noneed to update them, so we can reduce the overhead by placing the data that do

not change in a separate list

So that, in a nutshell, is how we can make our code more efficient Nowhere’s the way we can save data bloating By recognising the existence of static,

unchanging data and separating them from the dynamic data, we can

confi-dently share those static data between any number of objects without fear of

one object corrupting everyone else’s data (see Figure 5.4)

We’ll see this sort of pattern time and time again, so we should formalise it

a little We make a distinction between an object and an object instance The

former is like a template or blueprint and contains the static data associated

with the class The latter contains the dynamic data (see Figure 5.5)

Static Data

Object N

Object 2Object 1

Figure 5.4Many objects referring to

a single static dataset

Separating static anddynamic data usinginstancing

Trang 9

The equivalent code is shown below:

// File: Object.hppclass ObjectInstance;

class Object{

class ObjectInstance{

That’s all pretty abstract – the Objectcould be anything (including a GameObject, a class that gets a whole chapter to itself later on) So let’s take a rela-tively simple example – a 3D model

We’ll assume we have some way of exporting a model from the artist’sauthoring package, converting that to some format that can be loaded by ourgame This format will consist of the following data: a hierarchy description and

a series of associated visuals (presumably, but not necessarily, meshes of somekind), as shown in Figure 5.6

Notice that the abstract Visualclass can represent a single or multiple

Visuals using the VisualPlexclass (which is itself a Visual) This is anothercommon and useful pattern Now we can think about static and dynamic data.Suppose we have an Animationclass in another component This will work bymanipulating the transformation data inside the hierarchy of an object, whichmeans that our hierarchy data will be dynamic, not static Also, consider that

our model might be skinned Skinning works by manipulating vertex data

Trang 10

within a mesh – which is the Visualside of things In other words, the Visual

can be dynamic too So we consider separating out the static and dynamic data

in these classes into instances, and in doing so we create a new class – the

ModelInstanceshown in Figure 5.7

We use factory methods to create instances from the static classes, as trated in the following code fragments (as ever, edited for brevity):

Trang 11

Hierarchy * pHier = m_pHierarchy->CreateInstance();

return new ModelInstance( pVI, pHier );

}

// File:MODEL_Visual.hppnamespace MODEL

{class VisualInstance;

class Visual{

class VisualPlexInstance;

class VisualPlex : public Visual

Trang 13

5.3.4 Avoid illogical dependenciesBoy, am I fed up of seeing code that looks something like this:

#include "FileSystem.h"

class Whatever{

public:

// Stuff…

void Load( FileSystem & aFileSys );

void Save( FileSystem & aFileSys );

// More stuff

};

Remember: the idea is that the class should contain just enough data and ways

to manipulate those data as necessary, and no more Now, if the class is related

to a file system, then there may be a case for these methods being there, butsince the vast majority of classes in a game aren’t, then it’s safe to assume thatthey are unjustified in being class members

So how do we implement loading (and maybe saving)? We delegate it toanother object (Figure 5.8)

Rather than create a dependency between component (or package or class) Aand element B, we create a new element, AB, which absorbs that dependency Notonly does this keep A and B clean and simple to maintain; it also protects userswho need the services of A or B from needing to include and link with the other.With specific reference to serialisation, for each class that requires the abil-ity to be loaded (or saved), we usually write a loader class to decouple the objectfrom the specifics of the input or output classes

AB

Figure 5.8Avoiding the binding of

components A and B by

creating a third

component, AB

Trang 14

5.3.5 Better dead than thread?

Modern computer and console operating systems almost ubiquitously provide

some sort of facility for running concurrent threads of execution within the

game This is a powerful paradigm, but – as always – with power comes danger

Often, less experienced programmers are lured into making such

pronounce-ments as ‘Wouldn’t it be really cool if, like, each game object ran its AI on a

separate thread? Then the objects would just run autonomously, and I could use

the threading API to control how the AI was scheduled so I don’t overrun

frames and …’

Well, my opinion is: ‘No, it would not be cool’ If you catch yourself sayingsomething like that in the previous paragraph, step back and take a deep breath

I offer you two very good reasons why you should avoid the use of threads in

your game wherever possible:

● The technicalities of thread synchronisation will cause you major

headaches, obfuscate code, make debugging difficult and significantlyimpact development times in the wrong direction

● If you are considering multiplatform development, then the threading

facil-ities will vary widely from machine to machine, making it difficult toguarantee identical behaviour on the various versions of the game

In most circumstances, it is both possible and desirable to avoid using threads,

and it is easier to not use them than to use them In other situations, you may

find that a thread is required, for example in a network manager that needs to

respond to asynchronous events on a socket However, in these situations the

‘threadedness’ of the component should be utterly hidden from the user All the

unpleasantness with mutexes (or other flavours of synchronisation object)

should be encapsulated logically and preferably physically within the

compo-nent A pollable interface should be provided so that any of the asynchronous

stuff that may have occurred since the last update can be queried; effectively, a

threaded component becomes a message queue that can be interrogated at will

when the game dictates, rather than when the kernel decides that it feels like it

In this way, you localise the problems of thread synchronisation and keep

con-trol of your code

5.4 Meet the components

It’s now time to look at what goes into making components The discussions

won’t be about how to implement the specifics of each component Instead,

we’ll discuss something much more powerful: the architectural issues lying

behind the component design

Trang 15

5.4.1 Naming conventionsWithin the component architecture, we shall make extensive use of namespaces to (i) group logically and physically related entities and (ii) prevent iden-tifier name collisions Now – incredibly – some compilers out there still can’thandle namespaces, in which case you will have to fall back on the prefixing ofclass names with component tags Physically, the components will reside in asubdirectory called ‘components’ (rocket science, huh?), and a component lives

in a single flat subdirectory of that Files within that directory are named PONENT_FileName.Extension

COM-5.4.2 The applicationThe application component is extremely simple We can assume confidentlyand almost universally that an application will have the following three discretephases of execution:

class Application{

public:

virtual bool Initialise() = 0;

virtual void Terminate() = 0;

virtual void MainLoop() = 0;

};

}

The user’s concrete subclass of APP::Application can then be used to hangglobal data from (with, of course, the requisite access control) It is also a good –

if not the prototypical – candidate to be a singleton

Now let’s go a step further with the application class: we can add internalstate management and user interface components to it (like MFC does, only ele-gantly) We have already met the state manager and the GUI components inearlier chapters, and they can be built into the application class wholesalebecause they are entirely generic:

Ngày đăng: 01/07/2014, 15:20

TỪ KHÓA LIÊN QUAN