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

Object oriented Game Development -P11 ppt

30 311 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 Standard University
Chuyên ngành Game Development
Thể loại Bài tập lớn
Năm xuất bản 2003
Thành phố City Name
Định dạng
Số trang 30
Dung lượng 208,51 KB

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

Nội dung

Noticealso that simply allocating from the pool returns an object that may have beenused previously and so contains ‘garbage’: no constructor will have been called.This motivates the use

Trang 1

template<class T>

T * mem_Pool<T>::Allocate(){

m_FreeItems.push( pItem );

}template<class T>

void mem_Pool<T>::FreeAll(){

Notice that the pool class allocates an array of objects dynamically This meansthat any class that requires pooling must provide a default constructor Noticealso that simply allocating from the pool returns an object that may have beenused previously and so contains ‘garbage’: no constructor will have been called.This motivates the use of the placement form of operator new: this calls a con-structor on an existing memory block:

class Duck{

public:

enum Gender { DUCK, DRAKE };

enum Breed { MALLARD, POCHARD, RUDDY, TUFTED };

Duck( Breed eBreed, Gender eGender );

// etc

Trang 2

{

mem_Pool<Duck> s_DuckPool( 100 );

}

Duck * pDuck = s_DuckPool.Allocate();

// Call placement new on our raw data

new (pDuck) Duck( MALLARD, DRAKE );

Some ad hoc tests on my PIII 500 laptop indicate the pool class to be about ten

times faster than malloc() The class is extremely simple and robust, and does

not demand the sort of person-power resources that a global allocator does

Furthermore, plumbing it in to your code is as simple as adding operator new

and delete for each class to be pool allocated:3

// Thing.hpp

class Thing

{

public:

void * operator new( size_t n );

void operator delete( void * p );

};

// Thing.cpp

namespace

{

const int MAX_THINGS = 100;

mem_Pool<Thing> s_Pool( MAX_THINGS );

Trang 3

A really dull application for pools

In the course of development, you’ll write some really fun code and you’ll writesome really bland stuff Annoyingly, a lot of how your game performs may well

be dictated by how well the dull stuff runs

There isn’t much duller stuff than linked lists Yet, if you need a data ture where you can add and remove items in constant time, you’d behard-pushed to find a better container class The temptation to use STL’sstd::list class is overwhelming, but a cursory investigation of any of thecommon implementations will yield the slightly disturbing realisation that afair number of calls to operators newand deletetake place during addandremove operations Now, for some purposes that may not matter – if you doonly one or two per game loop, big deal But if you’re adding and removing lots

struc-of things from lists on a regular basis, then newand deleteare going to hurt

So why are newand deletebeing called? If you require objects to be in eral lists at once, then the link fields need to be independent of the object in anode class And it’s the allocation and freeing of these nodes that can cause theinvocation of dynamic memory routines

sev-The thought occurs: why not write an allocator for nodes that uses ourpools, and plug it into the STL list class? After all, they provide that nice littlesecond template argument:

template<class T,class A = allocator<T> >

class list { /*…*/ };

Brilliant! Except for the fact that it really doesn’t work very well It depends toocritically on which particular version of STL you are using, so although yoursolution may possibly be made to work for one version of one vendor’s library,

it is quite unportable across versions and platforms

For this reason, it’s usually best to write your own high-performance tainer classes for those situations (and there will be many in a typical game)where STL will just not cut the mustard But we digress

con-Asymmetric heap allocationImagine an infinite amount of RAM to play with You could allocate and allo-cate and allocate, and not once would you need to free Which is great, becauseit’s really the freeing that catalyses the fragmentation process

Now, it’ll be quite a while before PCs and consoles have an infinite quantity

of free store However, the only constraint you need be concerned with is not

an infinite amount but just all that your game requires In fact, we can be a bitmore specific than that, because if the game is level-based, and we know ahead

of time exactly how much we need for a level, then we can pre-allocate it at thestart of the level, allocate when required, and the only free we will ever need isthe release of everything at the end of the level

Trang 4

This technique generalises to any object or family of objects whose exactmaximum storage requirements are known ahead of time The allocator – called

an asymmetric heap because it supports only allocation of blocks, not freeing bar

the ability to free everything – is an even simpler class to implement than the

mem_AsymmetricHeap( int iHeapBytes );

// Constructs a heap using a pre-allocated block// of memory

mem_AsymmetricHeap( char * pMem, int iHeapBytes );

~mem_AsymmetricHeap();

// The required allocate and free methods

void * Allocate( int iSizeBytes );

void FreeAll();

// Various stats

int GetBytesAllocated() const;

int GetBytesFree() const;

Trang 5

mem_AsymmetricHeap::~mem_AsymmetricHeap(){

if ( m_bDeleteOnDestruct ){

delete [] m_pData;

}}

void * mem_AsymmetricHeap::Allocate( int iSize ){

void * pMem = 0;

// Ensure we have enough space left

if ( m_pNextAllocation + iSize < m_pEndOfData ){

pMem = m_pNextAllocation;

m_pNextAllocation += iSize;

}return( pMem );

}

void mem_AsymmetricHeap::FreeAll(){

// You can’t get much faster than this!

m_pNextAllocation = m_pData;

Trang 6

The remaining methods, along with the required bullet-proofing, and other

bells and whistles such as alignment, are left as exercises for the reader

Control your assets

Having said all I have about fragmentation, you may be forgiven for thinking

that I’m a bit blasé about it You’d be wrong In some applications, there comes a

time when fragmentation doesn’t just slow things down, it actually brings down

the whole house of cards Witness this error message from a recent project:

Memory allocation failure

Requested size: 62480

Free memory: 223106

Fragmentation has left memory in such a state that there is more than enough

RAM free; it’s just formed of itty bitty blocks of memory that are no use on

their own

Time to panic? Not quite yet The first step is to get rid of anything inmemory that is no longer needed until there is enough RAM The scary bit

comes when you realise that the error message appeared after we’d done that

Time to panic now? No, we’ll take a deep breath and hold on a momentlonger The next step to take here is for some sort of block merging to be per-

formed To be able to perform this efficiently, internally your memory allocator

ideally needs to support relocatable blocks Now, to be able to do this, the access

to the things that can be relocated needs to be controlled carefully, because

simply caching a pointer could lead you to accessing hyperspace if the object is

moved Using some kind of handle will solve this problem – we will discuss

han-dles shortly

This motivates us to control our game resources carefully in databases

Within these databases, we can shuffle and shunt around the memory-hogging

resources at will without adversely affecting the remainder of the game

We’ll look at the asset management issue in some detail in a later chapter

Strings

If there is one class above all that is going to fragment your memory, then it is

probably going to be the string In fact, you don’t even need to have a class to

do it; just use a combination of char *’s, functions such as strdup() and

free(), and you are almost just as likely to suffer

What is it about strings that hammers allocators? Well, for starters, they areusually small and they are all different sizes If you’re malloc()’ing and free()’

ing lots of one- and two-byte strings, then you are going to tax the heap

man-ager beyond breaking point eventually

But there is a further – related – problem Consider the programmer whowrites a string class because they hate having to use ugly functions such as

strcmp(), strdup()and strcpy(), and having to chase down all those string

Trang 7

pointer leaks So they write a string class, and this allows such semantic ness as:

neat-String a = "hello";

String b = a;

Now there are (at least) two schools of thought as to what that looking =should do One says that the request is to copy a string, so let’s do justthat Schematically:

innocent-strcpy( b.m_pCharPtr, a.m_pCharPtr );

However, the second school says that this is wasteful If bnever changes, whynot just sneakily cache the pointer?

b.m_pCharPtr = a.m_pCharPtr;

But what happens if bchanges? Well, we then need to copy ainto band update

b This scheme – called copy-on-write (COW) – is implemented by many stringclasses up and down the land It opens up the can of worms (which is whatCOW really stands for) inside Pandora’s box, because the string class suddenlybecomes a complex, reference-counting and rather top-heavy affair, rather thanthe high-performance little beast we hoped for

So if we’re to avoid COW, we are then forced into using strings that forciblyallocate on creation/copy/assignment, and free on destruction, including pass-ing out of scope In other words, strings can generate a substantial number ofhits to the memory management system(s), slowing performance and leading tofragmentation Given that C++ has a licence to create temporary objects when-ever it feels like it, the tendency of programmers who know how strings behave

is to forgo using a string class and stick with the combination of static arraysand the C standard library interface Unfortunately, this does reject some of thepower that can be derived from a string class For example, consider a hash tablethat maps names to objects:

template<class Key,class Type>

class hash_table{

// Your implementation here

};

namespace{

hash_table<char *,GameObject *> s_ObjectMap;

Trang 8

The problem with this is that if the hash table uses operator==to compare

keys, then you will compare pointers to strings, not the strings themselves One

possibility is to create a string proxy class that turns a pointer into a comparable

object – a ‘lite’ or ‘diet’ string, if you will:

class string_proxy

{

public:

string_proxy(): m_pString(0){

}

string_proxy( const char * pString ): m_pString( pString )

{}

bool operator==( const string_proxy & that ) const{

return(!strcmp( m_pString, that.m_pString ));

Trang 9

strcpy( cBuffer, pText );

s_strText = string_proxy( cBuffer );

}

void bar(){

int x;

// Some stuff involving x

}

void main(){

}}

This will – at least some of the time – print ‘no!’ and other times may print ‘yes!’

or perhaps just crash Why? Because cBufferis allocated on the stack, and thecall to bar()can overwrite the region where the text was, and the string proxy

is pointing to Boy, did I have fun with those bugs!

We are left with the distinct impression that a string class is still the least bad

of several evils How might we go about creating one that doesn’t fragment, isn’tslower than a snail on tranquillisers and doesn’t leave us with dangling pointers?One way – a poor way – is to create strings with fixed-size buffers of themaximum expected string length:

class string{

public:

// A string interface here

private:

enum { MAX_CAPACITY = 256; }char m_cBuffer[ MAX_CAPACITY ];

Trang 10

The trouble with this is that it is very wasteful: if most strings are shortish – say

16–32 characters long – then we are carrying around a lot of dead storage space

It might not sound much, but you could well be scrabbling around for that

space at the end of the project

Our preferred solution is to use a series of pool allocators for strings Startingwith a minimum string buffer length (say 16), we allocate a pool for 16-character-

length strings, another for 32-character strings, and so on up to a maximum string

length By allowing the user to control how many strings of each length can be

allocated, we can bound the amount of memory we allocate to strings, and we

can customise the sizes according to the game’s string usage pattern

Strings then allocate their buffers from the pools As they grow, they cate from the pool of the smallest suitable size that can contain the new length

allo-Since the strings use pool allocators, no fragmentation of string memory occurs,

and the allocation and free processes are very fast And there are no pointer

issues to fret over Job done

The moral of the tale is this: prefer local object allocation strategies to global ones

In the end, our game should be a collection of loosely coupled components This

degree of independence means that if we solve the memory allocation issues

within the components, then we get the global level pretty much for free Hurrah!

But – and there’s always a but – some people insist that this isn’t the whole

story And they have a point They say that there are reasons why you may take

the step of writing a global allocator early on It’s just that those reasons have

nothing to do with performance or fragmentation In fact, given that your

allo-cation strategies will depend critically on your pattern of usage, you will need

some sort of mechanism to instrument the existing allocator so that you can

find out what those patterns are Calls to newand deleteshould be logged,

allowing you to trace leaks and multiple deletions and also to build up a picture

of how your game uses heap memory

These are fine motivations However, they should be evaluated in the text of the priorities of the project, and they may do more harm than good

con-Consider the following code skeleton:

// Memory.hpp

#if defined( DEBUG )

#define MEM_LogNew( Class )\

new( FILE , LINE ) Class

// Provide a new ‘new’ that logs the line and file the

// memory was allocated in

Trang 11

Every time you need to create a new object and keep a record of its creation(and presumably deletion), you need to include memory.hpp That creates one

of those dependency things that is going to hamper reusability If a colleaguefeels like using one of your components but has already written a differentmemory management system, then they’re going to have to modify the code.Chances are, they won’t take the code, or they’ll sulk if they do

In general, anything that causes universal (or near-universal) inclusion ofheader files can seriously weaken the reuse potential of your systems, andchanges to those universal files will cause rebuilds left, right and centre Ofcourse, we have heard this argument before

There’s no reason why we can’t apply this same global technique at thepackage local level, though If each package keeps track of its own memory,then we get the global tracking without the binding Interestingly, though, atthe local level memory management can often be so simple that the problemcan simply dissolve away

7.2.2 Referencing

So we’re writing (say) an air combat game, though the genre is largely tant for this discussion We create a type of object called a missile, of whichthere are two varieties: one that just flies in a path dictated by an axial thrustand gravity, and another that homes in on a specific target that the player orNPC has selected It’s the latter object we’re interested in Here’s a sketch of thecode sections we’re interested in:

unimpor-class Missile : public GameObject{

HomingMissile( /*stuff*/, GameObject * pTarget );

/*virtual*/ void Update( float dt );

// bada bing

private:

GameObject * m_pTarget;

};

Trang 12

When the homing missile is created, we provide a target object that it should

head towards When we update the missile, it does some sums to work out

tra-jectories and then adjusts itself accordingly:

void HomingMissile::Update( float dt )

But we’re not concerned about the maths and physics at this moment The

problem we need to address is this: suppose another missile blows up our target,

which is deleted (and potentially new objects such as explosions and debris put

in their place)? But hold on, our homing missile still has a pointer to a now

non-existent object, so next time it is updated we can be sure that something

undesirable will happen

How do we solve this? It’s part of the general problem of how we referenceobjects safely One common method is to prevent the application from holding

pointers to allocated game objects at all Instead, all object transactions use

object handles, which cannot be deleted We then have some kind of object

management interface that accepts handles and returns the relevant data:

class ObjectManager

{

public:

ObjectHandle CreateObject( const char * pType );

void FreeObject( ObjectHandle hObject );

MATHS::lin_Vector3 GetPosition( ObjectHandle hObj );

private:

ObjectFactory * m_pFactory;

};

So what is an ObjectHandleexactly? In short, it’s anything you want it to be It’s

called an opaque type because as far as the outside world – the client code – is

con-cerned, it cannot see what the type does Only the ObjectManagerknows what to

do with handles – how to create them, validate them and dereference them

As an oversimplified example, consider using a 32-bit integer as the handletype:

Trang 13

Within the object manager, we have a table of pointers to objects that the ger indexes into:

inte-// ObjectManager.cppnamespace

{const int MAX_OBJECTS = 200;

GameObject * s_Objects[ MAX_OBJECTS ];

int s_iNextFreeObject = 0;

}

Now whilst that’s not the whole story, suffice to say that many games use asystem such as this, so there is a lot to recommend it Unfortunately, it’s not asystem that I favour The main objection is that handles collapse type informa-tion We went to such pains to design an elegant object hierarchy, and nowwe’ve reduced everything to either a lowest-common-denominator interface or

a hugely bloated monolithic one, because you need to be able to do anythingwith a handle that you could do with a GameObjector any of its subclasses.

Handles do have their uses, however, and we’ll discuss those later To solve ourimmediate problem, though, we can invoke the flexible reference-counting mecha-nism we discussed in the previous section Recall that we used a base class toimplement reference counting, which had a virtual function OnUnreferenced()and that was called when the reference count hits zero This mechanism can be pig-gybacked to do exactly what we require:

Trang 14

{

int MAX_SCENERY = 111;

mem_Pool<Scenery> s_SceneryPool( MAX_SCENERY );

// Creator called by ObjectFactory

GameObject * createScenery(){

return( s_SceneryPool.Allocate() );

}}

The only thing we have to watch out for is always to call Release()on pointers

to GameObjects rather than delete Protecting the destructor could enforce this

somewhat, but protected status is too easy to remove via inheritance to be

watertight, and making the destructor private would cause compiler errors since

we could never destroy the GameObjectclass from a subclass

There’s another little gotcha waiting to bite us when we allocate gameobjects from a pool Previously, we encountered problems because we held on to

a pointer to an object that was deleted The pointer becomes invalid;

consider-ably more likely than not, it will address data inside a new object or point at

fragile memory manager data Dereferencing it would be a disaster However,

when we allocate objects from a pool, it is now extremely likely – indeed, given

long enough, 100% certain – that our cached pointer will point to the start of a

new object that is actually in use Ouch! This is a subtle and annoying bug

To get around this one, we need to create a smart pointer class Also, ever we create an object, we need to give it a unique identifier The simplest way

when-is to use a 32-bit integer:

Trang 15

int s_iIdGenerator = 0;

}

GameObject::GameObject(): // …

, m_iUniqueId( s_iIdGenerator++ ){

}

Our smart pointer class obtains the unique identifier when it is created It canthen tell if the referred object is valid by comparing its cached identifier withthe actual ID of the object (which is safe because being in a pool, the data arenever deleted as such during game execution):

// ObjectPointer.hppclass ObjectPointer{

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

TỪ KHÓA LIÊN QUAN