Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 30 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
30
Dung lượng
226,58 KB
Nội dung
up with a satisfactory structure for supporting arbitrary primitive types – virtual func- tions are too inefficient because primitives are quite low-level and there are therefore lots of them in a typical scene, which rather knocks inheritance of a primitive abstract type on the head. This leaves us with the following associations: Renderer, Transform, Texture, Screen, Camera, Mesh, Model, Frame, Position, Rotation, Viewport, Triangle, Quad, Palette, Colour, Light, Ambient, Directional, Spotlight. Their relationships are shown in Figure 4.2. 4.3 Patterns Patterns were made popular in the 1990s by the ‘Gang of Four’ (GoF) book (Gamma et al., 1994). They represent the logical relationships between classes that just keep appearing in your software, irrespective of whether you are writ- ing spreadsheets, missile-guidance systems or resource managers. Patterns are, at the very least, interesting, and more often than not they are extremely useful. Often, you’re sitting there trying to work out how to get func- Object-oriented game development76 Light Light spot *Quads Model Quads *Triangles Mesh Triangles *Textures Texture Clut Clut *Meshes Frame Position Rotation Renderer Screen Screen Viewport Viewport Camera Transform Position Rotation Transform Camera *Children Frame Frame Frame Figure 4.2 Class diagram for a basic renderer. 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 76 tionality from one class to be utilised by another without violating encapsula- tion, or how to construct mechanisms to perform complex functions, and a flick through the GoF book is all you need to gain the required insight. But this is only half the battle – we still need to implement the pattern, and writing useful pattern code still takes a bit of thought and effort. This section looks at a few of the useful patterns and shows how they can be implemented with a reasonable degree of generality and efficiency. 4.3.1 The interface Also known as ‘protocol’, ‘compilation firewall’, ‘pimpl’ The first pattern we’ll look at is both simple and important. Remember earlier we were concerned about dependencies in common components? When a common component’s header file changed, all the dependent source modules had to be recompiled. Any action, adding a method, changing an implementa- tion detail or indeed the implementation, will trigger this rebuild. It’s an irritating and schedule-consuming waste of time. Can we do anything about it? The short answer is ‘Yes, we can’. However, the solutions (they are similar in philosophy) are not applicable to all classes. Let’s look at the solution methods before discussing their merits. Interface In Java, there is a specification of an interface – a set of functions that a class that supports the interface must supply. We can do a similar thing in C++ by defining a dataless class that consists of pure virtual functions. // Foo.hpp class Foo { public: Foo(); virtual ~Foo(); virtual void Bar() = 0; virtual void Pub() = 0; }; // Foo.cpp #include "Foo.hpp" Foo::Foo() { } Foo::~Foo() { Object-oriented design for games 77 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 77 // Always put an empty virtual destructor in the // cpp file. // Being virtual, it won’t be inlined anyway, and // some compilers have problems with virtual // destructors defined within headers } Since there is no implementation to change, the only condition that might trig- ger a cross-package rebuild of many modules would be the addition or removal of a method. For mature interfaces, this is considerably less likely than for an ‘in-progress’ module. Now the trick. Define an instantiable class that uses the Foo interface. Call it FooImpl to indicate that it’s an implementation of an interface: // FooImpl.hpp #include "Foo.hpp" class FooImpl : public Foo { public: FooImpl(); /*virtual*/ void Bar(); /*virtual*/ void Pub(); private: int m_iData; }; // FooImpl.cpp #include "FooImpl.hpp" FooImpl::FooImpl() : Foo() , m_iData(0) { } void FooImpl::Bar() { m_iData = 1; } void FooImpl::Pub() { m_iData /= 2; } Object-oriented game development78 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 78 Let’s assume for the moment that we have some way of creating a FooImpl that is internal to the component. What we get back is a pointer to a Foo, which has an identical virtual interface. We are free to modify FooImpl as we please, because external modules only ever include Foo.hpp. The only snag is that since we never see FooImpl.hpp, we have no way of creating a FooImpl on the client side. The solution is to have a manager class do the allocation for us: // FooManager.hpp class Foo; struct FooManager { static Foo * CreateFoo(); }; // FooManager.cpp #include "FooManager.hpp" #include "FooImpl.hpp" /*static*/ Foo * FooManager::CreateFoo() { return new FooImpl(); } Pimpl The name of the pattern is a contraction of ‘pointer to implementation’. Instead of defining member data in the header file, we gather the data into a structure that we place in the source file and forward declare a pointer to this structure as the only member of our class: // Foo.hpp struct FooImpl; class Foo { public: Foo(); ~Foo(); void Bar(); void Pub(); private: FooImpl * m_this; }; Object-oriented design for games 79 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 79 // Foo.cpp struct FooImpl { int m_iData; }; Foo::Foo() : m_this( new FooImpl ) { } Foo::~Foo() { delete m_this; } void Foo::Bar() { m_this->m_iData = 1; } void Foo::Pub() { m_this->m_iData /= 2; } The interface method is cleaner but requires the extra manager or utility compo- nent to create the instances. The pimpl method requires no virtual functions; however, there is an extra dynamic allocation for every instance new’d (and a corresponding dynamic deallocation on destruction), and the m_this-> nota- tion is slightly unpleasant. In the end, both solutions are essentially the same: in the interface method, the m_this is logically replaced by the implicit pointer to the class’s virtual function table. Both use indirection to decouple their interfaces from their implementation. So a similar analysis will work for both cases. The first thing to notice is that neither system has any data in its header, so we cannot in-line any functions. This is doubly so for the interface method because, except in very rare circumstances, virtual functions cannot be in- lined. 2 Also, since both systems require indirection, there is a performance penalty for creating a class in this way. Object-oriented game development80 2A function that is declared in-line virtual will be in-lined if, and only if, it is invoked explicitly via the class::method() notation and there are no uses of references or pointers to base classes calling that method. 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 80 What this means is that the interface is unsuitable for ‘light’ classes that have high-performance requirements. For example, a vector class would not be a good candidate for interface abstraction. However, a renderer or manager class whose methods support non-trivial functionality (i.e. the overhead incurred by the indirection is small compared with the amount of time spent in the func- tion) may well be suitable. If many other components will be dependent on ours, then there is some motivation to protect those clients from the evolution- ary tos and fros of interface design and, more importantly, implementation details of which class behaviour should be independent. 4.3.2 Singleton A singleton is a class that logically has only a single instance in any application. It is a programmatic error to create more than one of these, and the implemen- tation should prevent a user doing so. There are several ways to achieve this, and in this section we’ll evaluate their merits and the value of the pattern. Introduction First, what sort of classes are singletons? Well, what do we have only one of? How about a display? That is a better candidate, though some systems may sup- port multiple monitors. Since it is conceptually possible to have more than one display, chances are that it is not a clear-cut choice for a singleton. A 3D model is also not a good candidate – most games will have more than one! However, if we keep all the game’s 3D models in one place (perhaps along with other data), then we have a system that there is logically only one of – after all, if there are several repositories, where do the models go? And in the end, we would need to keep a repository of repositories, so the repository is a fundamental singleton. In general, this reflects a pattern: large database objects that oversee a set of controlled objects are natural singletons. Many other possibilities exist, though. Implementation Back in the bad old days of hack-and-hope C programming, we might have declared a singleton as a global object, thus: #include "ModelDatabase.h" struct ModelDatabase g_ModelDatabase; Yikes, a global variable! Anyone can read or write it from anywhere. Perhaps, a little more safely, we can write; #include "ModelDatabase.h" static struct ModelDatabase s_ModelDatabase; Object-oriented design for games 81 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 81 and we can control access to the model database via a suitable API: void modeldb_Initialise(); void modeldb_Terminate(); void modeldb_LoadModel( const char * pModelName ); void modeldb_FreeModel( struct Model * pModel ); So far, so good. There is one instance of a model database in the module. However, nothing prevents the user creating one of their own. If it controls access to some set of system-wide resources, that might create all sorts of unde- fined havoc. Also notice the Initialise and Terminate calls. At some suitable points in the game, we need to call these. When we graduate to C++, we get the opportunity to call these automatically via constructors and destructors: #include "ModelDatabase.hpp" namespace { // Data declared in here is private to this module. ModelDatabase s_ModelDatabase; } ModelDatabase::ModelDatabase() { // Do what we did in modeldb_Initialise(). Initialise(); } ModelDatabase::~ModelDatabase() { // Do what we did in modeldb_Terminate(). Terminate(); } We’re still stuck with the fact that we can create any number of model data- bases. And now that there is a physical object rather than a procedural interface to interact with, the user needs to get hold of the single instance. We solve these (linked) problems with the following structure: // ModelDatabase.hpp class ModelDatabase { Object-oriented game development82 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 82 public: // There’s only one of a static object – just what // we need! static ModelDatabase & Instance(); // … private: // Private ctor and dtor! ModelDatabase(); ~ModelDatabase(); static ModelDatabase c_Instance; }; // ModelDatabase.cpp #include "ModelDatabase.hpp" ModelDatabase ModelDatabase::c_Instance; /*static*/ ModelDatabase & ModelDatabase::Instance() { return c_Instance; } We can now access the instance in this fashion: #include "ModelDatabase.hpp" int main( int argc, char ** argv ) { ModelDatabase & aDb = ModelDatabase::Instance(); //… return 0; } The private lifecycle (constructor and destructor) of the class prevents users from creating instances. The only instance that can be created is the c_Instance static member – and since it is a member, its constructor and destructor can be called in member functions. Voila! A variant of this approach is to declare the static instance of the singleton inside the Instance() function: /*static*/ ModelDatabase & ModelDatabase::Instance() { static ModelDatabase anInstance; Object-oriented design for games 83 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 83 return( &anInstance ); } The bonus here is that the instance is not constructed until the first time Instance() is called. C++ has added an invisible code block to construct the object the first time the thread of execution enters the code block, and it uses the standard library call atexit() to schedule calling the destructor. One prob- lem with this variant is that (at least currently) some compilers have trouble with the private constructors and destructors, to the extent that you may have to make them public, thus weakening the intent of the pattern. This is a common way of implementing singletons. It will work for a large number of applications, but the pattern of implementation has a flaw or two. First, the single instance is constructed at file scope. Regrettably, C++ has no rules about the order of construction – if another object at file scope in another module requires ours and the model database has not yet been constructed, well, whatever happens is not good. Similarly, our class can fail to initialise cor- rectly if it depends on another system not yet constructed. Second, by constructing a static instance, we allow for only a single behaviour of the single- ton. What would happen if we wanted – or needed – to allow the user to subclass the singleton? For example, supposing we wanted to change the behav- iour of the database depending on a variable read at run time from an initialisation file. Then it makes sense to have a pointer to an instance instead of an object itself: // ModelDatbase.hpp class ModelDatabase { public: // as before private: static ModelDatabase * c_pInstance; }; class ModelDatabaseFast : public ModelDatabase { //… }; // ModelDatabase.cpp #include "ModelDatabase.hpp" #include "IniFile.hpp" ModelDatabase * ModelDatabase::c_pInstance = 0; Object-oriented game development84 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 84 /*static*/ ModelDatabase & ModelDatabase::Instance() { if ( c_pInstance == 0 ) { if (IniFile::GetString( "modeldb" )== "fast" ) { c_pInstance = new ModelDatabaseFast; } else { c_pInstance = new ModelDatabase; } } return( *c_pInstance ); } That solves that problem. However, we have a new one (no pun intended!) – we have dynamically allocated the object on the heap and will have to delete it at some point around program termination. I say ‘at some point’ because in a non- trivial application many classes will delete resources at shutdown and we need to avoid deleting objects twice or not at all, or accessing an already deleted resource in a destructor. We could add a static Terminate() method, but it could be a thorny issue as to when we call it. We’d really like it to be called at global scope because most systems will have purged their resources by then. Can we arrange that? Well, yes we can. We can use the STL’s template class auto_ptr, which I summarise here: namespace std { template<class T> class auto_ptr { public: explicit auto_ptr( T * pItem ); ~auto_ptr() { delete m_pItem; } T * operator->() { return m_pItem; } private: T * m_pItem; }; } Object-oriented design for games 85 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 85 [...]... is, alas, not a singleton 4.3.3 Object factory The object factory allows us to address a shortcoming of C++ We’d like to be able to write class Object { /*…*/ }; class Rocket : public Object { /*…*/ }; class Npc : public Object { /*…*/ }; 89 8985 OOGD_C04.QXD 90 1/12/03 2:32 pm Page 90 Object- oriented game development Type aType = Rocket; // Allocates a rocket Object * pObject = new aType; but (alas)... tCreator aCreator); Object * Create( const char * pType ); static ObjectFactory & Instance(); private: ObjectFactory(); ~ObjectFactory(); tCreatorMap m_Creators; }; // ObjectFactory.cpp #include "ObjectFactory.hpp" // Ugly macro for brevity #define VT(t,c) tCreatorMap::value_type((t),(c)) using std::string; 93 8985 OOGD_C04.QXD 94 1/12/03 2:32 pm Page 94 Object- oriented game development bool ObjectFactory::... the dependency on the Object. hpp header file Now we can add a manufactured class, and only the implementation file need change We can weaken the dependencies further by moving the factory to a separate component: // ObjectFactory.hpp class Object; struct ObjectFactory { static Object * CreateObject( const char* pType ); }; // ObjectFactory.cpp #include "ObjectFactory.hpp" #include "Object. hpp" #include... type that represents the different flavours of object you can create: // Object. hpp class Object { public: enum Type { NPC, ROCKET, DUCK, // New types above here NUM_TYPES }; // This is the ‘factory’ static Object * Create( Type eType ); }; // Object. cpp #include "Object. hpp" #include "Rocket.hpp" #include "Duck.hpp" #include /*static*/ Object * Object: :Create( Type eType ) { switch( eType... inefficient next to integer comparisons, the act of creating an object is relatively expensive compared with either, and if your game is manufacturing many classes every frame, you may need to have a really hard look at your architecture // Object. hpp class Object { public: static Object * Create( const char * pType ); }; // Object. cpp #include "Object. hpp" #include "Rocket.hpp" #include "Npc.hpp" #include... /*static*/ 91 8985 OOGD_C04.QXD 92 1/12/03 2:32 pm Page 92 Object- oriented game development Object * Object: :Create( const char * pType ) { if ( !strcmpi( pType, "rocket" ) ) return new Rocket; else if ( !strcmpi( pType, "npc" ) ) return new Npc; else if ( !strcmpi( pType, "duck" ) ) return new Duck; else { assert( !"CreateObject() : unknown type" ); } return 0; } Note the use of case-insensitive... will behave like an object factory Figure 4.3 shows A typical manager pattern 95 8985 OOGD_C04.QXD 1/12/03 96 2:32 pm Page 96 Object- oriented game development Figure 4.3 Class diagram for an abstract manager Manager Factory *Managed objects Base class Derived class 2 Factory Derived class 2 4.3.5 Visitor/iterator Looking at the manager pattern above, we can see that the controlled objects will be stored... #include "Npc.hpp" #include "Duck.hpp" /*static*/ Object * ObjectFactory::CreateObject( const char *pType ) { // As before } We are still left with a compilation and linkage bottleneck due to these includes, and we are still a bit worried about all those string compares What can we do 8985 OOGD_C04.QXD 1/12/03 2:32 pm Page 93 Object- oriented design for games about these? Quite a bit, as it happens We... 91 Object- oriented design for games case DUCK : return new Duck; default: // Handle the error assert( ! "Object: :Create(): unknown type" ); break; } } Common this may be, but there are two big problems with this method, and both are related to the enumeration Type First, if we are loading and saving the integer values of the type, then the application that writes them must be synchronised with the game. .. m_Creators.insert(VT(str,aCreator)).second; } Object * ObjectFactory::Create( const char * pType ) { tCreatorMap::iterator i = m_Creators.find( string( pType ) ); if ( i != m_Creators.end() ) { tCreator aCreator = (*i).second; return aCreator(); } return 0; } // Rocket.cpp #include "Rocket.hpp" #include "ObjectFactory.hpp" // Another macro to save printing space #define OF ObjectFactory::Instance() namespace { Object * createRocket() . component: // ObjectFactory.hpp class Object; struct ObjectFactory { static Object * CreateObject( const char* pType ); }; // ObjectFactory.cpp #include "ObjectFactory.hpp" #include " ;Object. hpp" #include. aCreator); Object * Create( const char * pType ); static ObjectFactory & Instance(); private: ObjectFactory(); ~ObjectFactory(); tCreatorMap m_Creators; }; // ObjectFactory.cpp #include "ObjectFactory.hpp" //. Object factory The object factory allows us to address a shortcoming of C++. We’d like to be able to write class Object { /*…*/ }; class Rocket : public Object { /*…*/ }; class Npc : public Object