GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing gameprogrammingpatterns.com — Type Object · Behavioral Patterns · Game Programming Patterns Type ObjectGame Programming PatternsBehavioral PatternsIntentAllow the flexible creation of new “classes” by creating a single class, each instance of which represents a different type of object. MotivationImagine we’re working on a fantasy role-playing game. Our task is to write the code for the hordes of vicious monsters that seek to slay our brave hero. Monsters have a bunch of different attributes: health, attacks, graphics, sounds, etc., but for example purposes we’ll just worry about the first two. Each monster in the game has a value for its current health. It starts out full, and each time the monster is wounded, it diminishes. Monsters also have an attack string. When the monster attacks our hero, that text will be shown to the user somehow. (We don’t care how here.) The designers tell us that monsters come in a variety of different breeds, like “dragon” or “troll”. Each breed describes a kind of monster that exists in the game, and there can be multiple monsters of the same breed running around in the dungeon at the same time. The breed determines a monster’s starting health — dragons start off with more than trolls, making them harder to kill. It also determines the attack string — all monsters of the same breed attack the same way. The typical OOP answerWith that game design in mind, we fire up our text editor and start coding. According
to the design, a dragon is a kind of monster, a troll is
another kind, and so on with the other breeds. Thinking object-oriented, that
leads us to a The public The constructor is protected and takes the starting health for the monster. We’ll have derived classes for each breed that provide their own public constructors that call this one, passing in the starting health that is appropriate for that breed. Now let’s see a couple of breed subclasses:
Each class derived from Then, strangely, things start to bog down. Our designers ultimately want to have hundreds of breeds, and we find ourselves spending all of our time writing these little seven-line subclasses and recompiling. It gets worse — the designers want to start tuning the breeds we’ve already coded. Our formerly productive workday degenerates to:
We spend the day frustrated because we’ve turned into data monkeys. Our designers are frustrated because it takes them forever to get a simple number tuned. What we need is the ability to change breed stats without having to recompile the whole game every time. Even better, we’d like designers to be able to create and tune breeds without any programmer intervention at all. A class for a classAt a very high level, the problem we’re trying to solve is pretty simple. We have a bunch of different monsters in the game, and we want to share certain attributes between them. A horde of monsters are beating on the hero, and we want some of them to use the same text for their attack. We define that by saying that all of those monsters are the same “breed”, and that the breed determines the attack string. We decided to implement this concept using inheritance since it lines up with
our intuition of classes. A dragon is a monster, and each dragon in the game is
an instance of this dragon “class”. Defining each breed as a subclass of an
abstract base
Each instance of a monster in the game will be of one of the derived monster types. The more breeds we have, the bigger the class hierarchy. That’s the problem of course: adding new breeds means adding new code, and each breed has to be compiled in as its own type. This works, but it isn’t the only option. We could also architect our code so
that each monster has a breed. Instead of subclassing
That’s it. Two classes. Notice that there’s no inheritance at all. With this
system, each monster in the game is simply an instance of class To associate monsters with breeds, we give each What’s especially powerful about this pattern is that now we can define new types of things without complicating the codebase at all. We’ve essentially lifted a portion of the type system out of the hard-coded class hierarchy into data we can define at runtime. We can create hundreds of different breeds by instantiating more instances
of The PatternDefine a type object class and a typed object class. Each type object instance represents a different logical type. Each typed object stores a reference to the type object that describes its type. Instance-specific data is stored in the typed object instance, and data or behavior that should be shared across all instances of the same conceptual type is stored in the type object. Objects referencing the same type object will function as if they were the same type. This lets us share data and behavior across a set of similar objects, much like subclassing lets us do, but without having a fixed set of hard-coded subclasses. When to Use ItThis pattern is useful anytime you need to define a variety of different “kinds” of things, but baking the kinds into your language’s type system is too rigid. In particular, it’s useful when either of these is true:
Keep in MindThis pattern is about moving the definition of a “type” from the imperative but rigid language of code into the more flexible but less behavioral world of objects in memory. The flexibility is good, but you lose some things by hoisting your types into data. The type objects have to be tracked manuallyOne advantage of using something like C++’s type system is that the compiler handles all of the bookkeeping for the classes automatically. The data that defines each class is automatically compiled into the static memory segment of the executable and just works. With the Type Object pattern, we are now responsible for managing not only our monsters in memory, but also their types — we have to make sure all of the breed objects are instantiated and kept in memory as long as our monsters need them. Whenever we create a new monster, it’s up to us to ensure that it’s correctly initialized with a reference to a valid breed. We’ve freed ourselves from some of the limitations of the compiler, but the cost is that we have to re-implement some of what it used to be doing for us. It’s harder to define behavior for each typeWith subclassing, you can override a method and do whatever you want to — calculate values procedurally, call other code, etc. The sky is the limit. We could define a monster subclass whose attack string changed based on the phase of the moon if we wanted to. (Handy for werewolves, I suppose.) When we use the Type Object pattern instead, we replace an overridden method with a member variable. Instead of having monster subclasses that override a method to calculate an attack string using different code, we have a breed object that stores an attack string in a different variable. This makes it very easy to use type objects to define type-specific data, but hard to define type-specific behavior. If, for example, different breeds of monster needed to use different AI algorithms, using this pattern becomes more challenging. There are a couple of ways we can get around this limitation. A simple solution is to have a fixed set of pre-defined behaviors and then use data in the type object to simply select one of them. For example, let’s say our monster AI will always be either “stand still”, “chase hero”, or “whimper and cower in fear” (hey, they can’t all be mighty dragons). We can define functions to implement each of those behaviors. Then, we can associate an AI algorithm with a breed by having it store a pointer to the appropriate function. Another more powerful solution is to actually support defining behavior completely in data. The Interpreter and Bytecode patterns both let us build objects that represent behavior. If we read in a data file and use that to create a data structure for one of these patterns, we’ve moved the behavior’s definition completely out of code and into content. Sample CodeFor our first pass at an implementation, let’s start simple and build the basic
system described in the motivation section. We’ll start with the Very simple. It’s basically just a container for two data fields: the starting health and the attack string. Let’s see how monsters use it: When we construct a monster, we give it a reference to a breed object. This
defines the monster’s breed instead of the subclasses we were previously using.
In the constructor, This very simple chunk of code is the core idea of the pattern. Everything from here on out is bonus. Making type objects more like types: constructorsWith what we have now, we construct a monster directly and are responsible for passing in its breed. This is a bit backwards from how regular objects are instantiated in most OOP languages — we don’t usually allocate a blank chunk of memory and then give it its class. Instead, we call a constructor function on the class itself, and it’s responsible for giving us a new instance. We can apply this same pattern to our type objects: And the class that uses them: The key difference is the After our changes, it’s like this: So, why do this? There are two steps to creating an object: allocation and
initialization. However, that all happens after allocation. We’ve already got a chunk of memory to put our monster into before its constructor is called. In games, we often want to control that aspect of object creation too: we’ll typically use things like custom allocators or the Object Pool pattern to control where in memory our objects end up. Defining a “constructor” function in Sharing data through inheritanceWhat we have so far is a perfectly serviceable type object system, but it’s pretty basic. Our game will eventually have hundreds of different breeds, each with dozens of attributes. If a designer wants to tune all of the thirty different breeds of troll to make them a little stronger, she’s got a lot of tedious data entry ahead of her. What would help is the ability to share attributes across multiple breeds in the same way that breeds let us share attributes across multiple monsters. Just like we did with our original OOP solution, we can solve this using inheritance. Only, this time, instead of using our language’s inheritance mechanism, we’ll implement it ourselves within our type objects. To keep things simple, we’ll only support single inheritance. In the same way that a class can have a parent base class, we’ll allow a breed to have a parent breed: When we construct a breed, we give it a parent that it inherits from. We can
pass in To make this useful, a child breed needs to control which attributes are
inherited from its parent and which attributes it overrides and specifies itself. For our
example system, we’ll say that a breed overrides the monster’s health by having
a non-zero value and overrides the attack by having a non- There are two ways we can implement this. One is to handle the delegation dynamically every time the attribute is requested, like this: This has the advantage of doing the right thing if a breed is modified at runtime to no longer override, or no longer inherit some attribute. On the other hand, it takes a bit more memory (it has to retain a pointer to its parent), and it’s slower. It has to walk the inheritance chain each time you look up an attribute. If we can rely on a breed’s attributes not changing, a faster solution is to apply the inheritance at construction time. This is called “copy-down” delegation because we copy inherited attributes down into the derived type when it’s created. It looks like this: Note that we no longer need a field for the parent breed. Once the constructor is done, we can forget the parent since we’ve already copied all of its attributes in. To access a breed’s attribute, now we just return the field: Nice and fast! Let’s say our game engine is set up to create the breeds by loading a JSON file that defines them. It could look like: We’d have a chunk of code that reads each breed entry and instantiates a new
breed instance with its data. As you can see from the Since both of them have zero for their health, they’ll inherit it from the base
Design DecisionsThe Type Object pattern lets us build a type system as if we were designing our own programming language. The design space is wide open, and we can do all sorts of interesting stuff. In practice, a few things curtail our fancy. Time and maintainability will discourage us from anything particularly complicated. More importantly, whatever type object system we design, our users (often non-programmers) will need to be able to easily understand it. The simpler we can make it, the more usable it will be. So what we’ll cover here is the well-trodden design space, and we’ll leave the far reaches for the academics and explorers. Is the type object encapsulated or exposed?In our sample implementation, We can easily change this and allow
Doing this changes the design of
How are typed objects created?With this pattern, each “object” is now a pair of objects: the main object and the type object it uses. So how do we create and bind the two together?
Can the type change?So far, we’ve presumed that once an object is created and bound to its type object that that binding will never change. The type an object is created with is the type it dies with. This isn’t strictly necessary. We could allow an object to change its type over time. Let’s look back at our example. When a monster dies, the designers tell us sometimes they want its corpse to become a reanimated zombie. We could implement this by spawning a new monster with a zombie breed when a monster dies, but another option is to simply get the existing monster and change its breed to a zombie one.
What kind of inheritance is supported?
See Also
|
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |