Object oriented Game Development -P6 docx

30 341 0
Object oriented Game Development -P6 docx

Đ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

– for example, line-of-sight calculations might refer to the portals the graphics system uses to prune visible data. Now what that means in practice is that I cannot use one part of the game engine – 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 ugly horned head again. This time it’s busy gobbling precious RAM and making life more 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 a resource that it needs, even if that resource is in a part of the engine you do not use. 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 please most 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 your company is continually churning out the same sort of game over the course of several years, but the moment you diversify it’s back to the drawing board for a new game engine. 5.2.2 The alternative We really, really, really want to take a big hammer to the idea of an engine and break down the monolith into a series of components. The ideal is to turn the relationships 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 our hammer, and broken it down into two packages: one for rendering, one for AI. There is no logical or physical connection between these packages, and why should there be? Why would renderers need to know anything about AI? And what interest would the AI have in a renderer? (Suggested answers: absolutely no reason whatsoever, absolutely none.) Object-oriented game development136 NPC AI EnvironmentRenderer Maths Your engine NPC AI EnvironmentRenderer My game Figure 5.1 Duplication of code and functionality within an application when using a game engine. 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 136 Now, we can’t break the relationship between the NPC AI and the environ- ment the AI must traverse. However, we can move the complexity to where it 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 environments 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 of independent 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! The component model for game development 137 AIRenderer My Environment Environment My NPC AI NPC AI My Renderer Renderer My game Environment Figure 5.2 Game architecture using a component philosophy. 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 137 Before we get too carried away, let’s apply the brakes ever so gently. The pre- ceding paragraph describes the goal of component-based development. In reality, 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 these subclasses become reusable components in themselves. Can you see now why the component model is appealing? Notice that the component architecture model remains open – it is not geared to write a particular type of game; it remains flexible because behaviours can 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 about dependency 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 open system, it would be impossible to document every component that you could write, but we’ll deal with the major ones in some detail here: ● application-level components ● containers ● maths ● text and language processing ● graphics ● collision ● resource management ● Newtonian physics ● networking. 5.3 Some guiding principles Before we look in detail at the components, we shall discuss some basic princi- ples – philosophies, if you will – that we will use in their architecture. These principles 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 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. To enforce this to some extent, we can use either a namespace for each component or some kind of package prefix on identifier names. 1 Keep the namespace (or prefix) short: four or five characters will do (we use upper-case characters for namespaces). Object-oriented game development138 1 There may even be an argument for using both prefixes and name spaces. The reason for presenting a choice is that there are still compilers out there that don’t like name spaces. 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 138 The first practical manifestation of this is to stick to C++’s built-in data types 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: namespace COMP { typedef unsigned char uint8; } or 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 princi- ple universally, we would end up with components that were so self-contained 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 C 0 , below which a component can happily implement its own version, and above which we’re happy to import the defini- tion from elsewhere. Using C 0 we can define three sets: ● S – is the set of classes whose complexity is much less than C 0 . ● S + is the set of classes whose complexity is much greater than C 0 . ● S 0 is the set of classes whose complexity is about C 0 . 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 flexi- bility in your policy. For example, suppose a component requires a very basic The component model for game development 139 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 139 container (implementing addition, removal and iteration only, say). Even if our policy says containers are imported, simple (‘diet’, if you will) containers can be defined 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 component you 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 we bind 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 not have the required artwork or audio data to hand, yet we still wish to present some 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’s some abridged sample code to illustrate this: // File: fx_Explosion.hpp #include <stdio.h> namespace FX { class Explosion { public: void Render() { printf( "BANG! (%f,%f,%f)\n", … ); } private: float m_Pos[3]; }; } Object-oriented game development140 S – S 0 S + Integral types … Containers, vectors, Everything else matrices … Table 5.1 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 140 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 polymorphic method for rendering, nothing more need be specified regarding its visualisation: class Object { public: virtual void Render( /*?*/ ) {} }; (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: The component model for game development 141 ObjectVisual Object Visual3D Visual VisualText Rend Component Visual Figure 5.3 Keeping object data and their visual representation separate. 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 141 class ObjectVisual : public Object { public: void Render( /*?*/ ) const { m_pVisual->Render( /*?*/ ); } private: REND::Visual * m_pVisual; }; The purpose of this principle will become apparent when we consider the his- torical precedents. In a previous era, with target platforms that were not so powerful, the luxury of having separate functions to update an object and then to render it was often avoided. Code for drawing the object and refreshing its internal state was mixed freely. And, of course, this made changing how the object was viewed next to impossible in the midst of development. By keeping the visual representation separate from the internal state of the object, we make 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 { public: void Render(/*?*/) const { m_pVisual->Render(/*?*/); } virtual void Update( Time aDeltaT ); private: REND::Visual * m_pVisual; }; Note that the Render() method is kept const as it should not modify the internal state – when we need to do that, we call Update(). This allows us to maintain frame-rate independence – the update makes no assumptions about how long it takes to draw a scene, and we can call it as many times as we wish before calling Render() and still get a scene that is visually consistent with the internal state. Object-oriented game development142 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 142 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 a number 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 no need 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. Now here’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). The component model for game development 143 Static Data Object NObject 2Object 1 Figure 5.4 Many objects referring to a single static dataset. Dynamic ObjectInstance ObjectData Static Object DataData Object Figure 5.5 Separating static and dynamic data using instancing. 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 143 The equivalent code is shown below: // File: Object.hpp class ObjectInstance; class Object { public: ObjectInstance * CreateInstance(); private: StaticData m_StaticData; }; // File: ObjectInstance.hpp class Object; class ObjectInstance { public: ObjectInstance( Object & anObject ); private: DynamicData m_DynamicData; Object & m_Object; }; Notice that the Object contains a factory method for creating instances. This method can be polymorphic in hierarchies, and the object classes can easily keep track of all the instances of that object in the game. That’s all pretty abstract – the Object could be anything (including a Game Object, 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’s authoring package, converting that to some format that can be loaded by our game. This format will consist of the following data: a hierarchy description and a series of associated visuals (presumably, but not necessarily, meshes of some kind), as shown in Figure 5.6. Notice that the abstract Visual class can represent a single or multiple Visuals using the VisualPlex class (which is itself a Visual). This is another common and useful pattern. Now we can think about static and dynamic data. Suppose we have an Animation class in another component. This will work by manipulating the transformation data inside the hierarchy of an object, which means that our hierarchy data will be dynamic, not static. Also, consider that our model might be skinned. Skinning works by manipulating vertex data Object-oriented game development144 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 144 within a mesh – which is the Visual side 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 ModelInstance shown in Figure 5.7. We use factory methods to create instances from the static classes, as illus- trated in the following code fragments (as ever, edited for brevity): // File MODEL_Model.h namespace MODEL { class Hierarchy; class Visual; class ModelInstance; class Model { public: ModelInstance * CreateInstance(); The component model for game development 145 HierarchyDesc Model Visual VisualPlex Visual Hierarchy definition *Visual Figure 5.6 Object diagram for the Model class. HierarchyInstance ModelInstance VisualInstance VisualPlexInstance Visual Hierarchy *Instances Figure 5.7 Object diagram for the ModelInstance class. 8985 OOGD_C05.QXD 1/12/03 2:38 pm Page 145 [...]... private: STATE::StateManager * m_pStateMgr; GUI::ViewManager * m_pViewMgr; Renderer * m_pRenderer; } } 151 8985 OOGD_C05.QXD 152 1/12/03 2:38 pm Page 152 Object- oriented game development We shall have more to say about applications when we look at cross-platform development in a later chapter 5.4.3 Container components Where do containers fit in the grand scheme of things? They are extremely common data structures,... interface-compatible): #include // An array CONT::array aSomeInts(10); // A function object struct MyFunctor { public: // Some compilers will only inline code // if the data declarations are made first int m_iCount; 155 8985 OOGD_C05.QXD 156 1/12/03 2:38 pm Page 156 Object- oriented game development public: inline MyFunctor() : m_iCount(0) {} inline void operator()( int & n ) { n = m_iCount++;... 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 pronouncements 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... specifics of each component Instead, we’ll discuss something much more powerful: the architectural issues lying behind the component design 149 8985 OOGD_C05.QXD 150 1/12/03 2:38 pm Page 150 Object- oriented game development 5.4.1 Naming conventions Within the component architecture, we shall make extensive use of name spaces to (i) group logically and physically related entities and (ii) prevent identifier...8985 OOGD_C05.QXD 146 1/12/03 2:38 pm Page 146 Object- oriented game development private: Visual * m_pVisual; Hierarchy * m_pHierarchy; }; } // File MODEL_Model.cpp #include "MODEL_Model.hpp" #include "MODEL_ModelInstance.hpp" #include "MODEL_Visual.hpp" #include... T> class array_const_iterator { // Much the same as above but const-correct }; // template class array { 153 8985 OOGD_C05.QXD 154 1/12/03 2:38 pm Page 154 Object- oriented game development public: /* * Constants/typedefs/enums */ enum { GROW_FACTOR = 2 }; typedef array_iterator iterator; typedef array_const_iterator const_iterator; typedef T data_type; /* * Lifecycle... class HierarchyInstance; class Hierarchy { public: HierarchyInstance * CreateInstance() const { return new HierarchyInstance(*this); } }; } 147 8985 OOGD_C05.QXD 1/12/03 148 2:38 pm Page 148 Object- oriented game development 5.3.4 Avoid illogical dependencies Boy, am I fed up of seeing code that looks something like this: #include "FileSystem.h" class Whatever { public: // Stuff… void Load( FileSystem... in mind, consider the hash table container A common way to implement this is as follows: template class hash_table { 157 8985 OOGD_C05.QXD 158 1/12/03 2:38 pm Page 158 Object- oriented game development public: // Interface private: enum { NUM_BUCKETS = 31 }; std::list m_aBuckets[ NUM_BUCKETS ]; }; This open hashing system is technically superior in performance to a closed... MATHS_Vector2.hpp namespace MATHS { template class Vector2 { private: T m_v[2]; public: // Deliberately empty default ctor inline Vector2() { } 159 8985 OOGD_C05.QXD 160 1/12/03 2:38 pm Page 160 Object- oriented game development inline Vector2( T v0, T v1 ) { m_v[0] = v0; m_v[1] = v1; } inline Vector2( T v[] ) { m_v[0] = v[0]; m_v[1] = v[1]; } // // Access // T & x() { return m_v[0]; } T & y() { return m_v[1];... (see Figure 5.9) Maths LINALG Vector3 Matrix33 Figure 5.9 Avoiding the binding of matrices and vectors by creating a linear algebra subcomponent 8985 OOGD_C05.QXD 162 1/12/03 2:38 pm Page 162 Object- oriented game development While we’re thinking about this, what else would go into the linear algebra component? Anything that could turn a vector into a matrix or a matrix into a vector For example, getting . model for game development 143 Static Data Object NObject 2Object 1 Figure 5.4 Many objects referring to a single static dataset. Dynamic ObjectInstance ObjectData Static Object DataData Object Figure. below: // File: Object. hpp class ObjectInstance; class Object { public: ObjectInstance * CreateInstance(); private: StaticData m_StaticData; }; // File: ObjectInstance.hpp class Object; class ObjectInstance { public: ObjectInstance(. Object; class ObjectInstance { public: ObjectInstance( Object & anObject ); private: DynamicData m_DynamicData; Object & m _Object; }; Notice that the Object contains a factory method for

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

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan