Often, for interesting game behavior, several objects need to interact. For instance, an ActBox
might activate another object, causing it to fall, which hits another ActBox
, which causes a precisely positioned missile launcher to start launching missiles at the player. This collection of, say, five objects forms a logical unit, but is realized as 5 separate objects with implicit connections either through object references in OAD
data, or through object references in scripts.
The problem is if you want to copy this behavior, you have to copy all 5 objects and rework the object references in OAD
data and scripts.
There are a couple of approaches to solving this problem.
- smart copy - copy all selected objects, automatically fix-up inter-object references
- prototype compound object - a "deferred" case of smart copy. generate a library of external files, each containing one functioning collection of objects which act as a single unit. at level editing time you have ONE object which is a reference to the external file. at level conversion time (iff2lvl?) contents of the external file are actually instantiated into the level and the inter-object references within the collection are resolved. This can be better than smart copy because if you want to change the behavior globally, you just change it once in the external file, and it gets newly instantiated at level conversion time, so all instances get updated with the new behavior. With regular (instant) smart copy, each instance is already fully instantiated at level editing time, so a global change of behavior is not possible.
- I am starting to think that it is very nearly impossible for a purely RAD (point-and-click) 3D game creation system (like WorldFoundry
or Blender) to completely realize reusable multi-object behavior. This is an instance of the "Composite Design Pattern", which requires that composite objects (composed of several child objects) have exactly the same interface as child objects, thus making composites and primitives interchangeable. I am becoming more convinced that this can only be done with the expressive power of an object-oriented language. Look at the Nebula Device - it supports this fully, but you need to write C++ to construct the composite objects. Look at GUI RAD development environments like Delphi - you can create composite components, but you also need to write Object Pascal code to create new components. No general-purpose component-based toolkit (GUI toolkits are the best examples to look at) allows component-based design, following the Composite Design Pattern, without requiring the components to be coded in an object-oriented language. This is because of the strictness of the rule that composites must have the same interface as primitives. This means that EVERYTHING you do with a primitive, you MUST also be able to do with a composite, strictly (in the sense of identical function signatures). This can only be expressed correctly in an object-oriented programming language; the Composite Design Pattern is indeed an object-oriented solution. Either that, or there needs to be some way of expressing this OO concept purely visually by pointing and clicking. I have seen no system which supports composites in a purely visual way. Key to the idea of the composite design pattern is the idea of an INTERFACE (since the composite interface and the primitive interface must be identical); how do you express a functional INTERFACE visually?
Example of what can't work without programming in WF - how do I declare a group of 5 objects as being a new OAS type? I can't. This requires C programming. This means that I cannot declare a group of 5 objects as being a logical single unit with a set of attributes and values, meaning that the composite is a loose collection, not a coherent entity like the base game OAS classes (Missile, StatPlat
, etc). Indeed, WorldFoundry
makes this distinction: new OAS types must be coded in C by the engine programmer; they cannot be visually pieced together in the 3D editor. The Composite Design Pattern cannot be applied in a purely visual setting.
-- As I said in an earlier email, I think the way to build composite objects is to have some sort of grouping ability in the 3D editor, where objects are stored in a tree instead of a flat array. So composite components are able to refer to each other as siblings. For static (non-generated) objects this coull all be resolved at conversion time. For now ignore generated objects. Once we had that it would not be very hard to make a new oad type which was "composite object", which the parent of a group of objects could be set to (the engine wouldn't need to know about this object at all). This would make it easy to duplicate a set of behaviors, but would still require editing components to change any behavior.
So the next step would be to modify attribedit to know about composite objects, and to aggregate all of the sub-objects attributes together into one huge list (and be able to write them back out to individual objects).
Finally, I could add a way to edit what fields a composite object shows (and rename them). So for the actbox example above, the composite object would only have a few fields which could be edited, like what object activates the whole sequence, and maybe missle launch rate. The rest would be hidden (most likely all of the components would be marked as hidden in the 3D editor so that the parent object is all that they see).
Now for generated objects (generating a composite). The engine would need to be changed to know about object groups and support refering to objects in heirarchies (since compile time resolving of object references would no longer be sufficient). This doesn't sound too hard, I would just replace the flat array with a tree and write an iterator for all of the existing code which traversed it like a flat array. The only code which would need work would be how object references are looked up.
If we did the engine updates from the start we could avoid a bunch of work on the level converter resolving composite component references.
I don't think this is a composite design pattern, it doesn't really fit any of the patterns in the book (kind of like facade?), it is just an agregation or grouping of objects to make them easier to edit. Composite would be if the engine didn't know there were sub-objects in the tree at all, and the parent propogated render requests, etc. down the tree. That would be much more work, and I am not sure we would gain anything over what I suggest here.
I guess it depends on frame of reference, from the designer's point of view it IS composite, but not from the engine's point of view.
-- Using the above approach: given a composite group of 5 objects, and another different composite group of 15 objects, can I group these 2 composites into another composite, which from attribedit would be traversed recursively (at the first recursion level there are 2 composite objects in the group; the first sub-object again has 5 primitive sub-objects, and the second sub-object has again 15 primitive sub-objects)? If so, then from the engine's point of view, I would say that this is indeed the composite design pattern; the engine doesn't know if an "object" being grouped is a simple or a composite object, and goes through the same C++ abstract interface to tell a grouped object "add yourself to this group" or "show me your published group attributes". The attribute editor doesn't know if an object being queried for attributes is a primitive or composite; it merely says "give me your attributes to edit", and indeed propagates this attribute request further down the tree. So, at a strict C++-function-signature level with regard to grouping and attribute editing, the grouping and attribute editing interface would be a composite. This is all assuming that multi-level grouping is possible. If this is not possible, then this is not a composite design pattern, but this would limit groups to consist of primitive objects, preventing full reuse (i.e. re-grouping) of composites.
-- I would certainly do it as a tree, so recursive grouping would be possible. The reason I was thinking it isn't really Composite is the the engine won't call Render on the parent object, but instead recurse through the tree and call render (and other functions like update physics) on each child object. So the groups on't encapsulate or hide the underlying objects, groups are only used for inter-object references and creation of generarted objects. I think the hardest part of implementing this will be updating the level converter.
The grouping/attributes part of the code knows nothing about what will be done with the groups later (rendered? Printed in an HTML report? Written to disk?). The grouping interface (i.e. the abstract C++ class/interface "Groupable", which both "PrimitiveObject" and "GroupObject" inherit from) as well as the attribute interface is identical for both PrimitiveObject
instances, and all Groupable::xxx() methods (pure virtual in Groupable, overridden in PrimitiveObject
) are callable on both PrimitiveObject
. The engine will do recursive calls on the attribute-getting/attribute-setting functionality - the group forwards get/set attributes to the proper sub-group or sub-object. This forwarding is done in an opaque way. As an attribute editor, I don't know which objects the group is made of, or to which primitive objects my attribute values are actually getting sent (I also should not know nor care about the primitive objects; as an attribute editor, I am interested in the attributes that the highest-level group exposes, because that is the level of abstraction at which I am viewing the composite). Thus the group encapsulates and hides the attributes of the child groups and child objects. Rendering, however, is done at a non-composite level. So the grouping interface (which can/should be a separate pure abstract interface class called "Groupable") is composite; the rendering interface (which exists only in objects of class PrimitiveObject
and not in objects of class GroupObject
) is not (I would say that the act of rendering a group is an Iterator running over a Composite). For rendering purposes, the rendering class would need a special method (ideally only accessible to the renderer) to query the child objects of a group. For attribute editing purposes, this method is not allowed to be used (because attribute editing has no business knowing about the child objects or composition of a group). The opaque encapsulated details of how the group forwards external attribute requests to its internal child groups or child objects is the detail which is hidden from the outside world, and whose specification requires a more powerful system than currently available in WF.
Typically these "opaque forwarding details" are done in forwarding code. Here is a C++ example of how you would do grouping and object forwarding in C++. Note that the "grouping" aspect is inherent in C++; if you declare a member variable in a class, it is part of that class. Any class can be a member of any other class (any group can be a member of any other group). This is component-based reuse in C++. Thus no Groupable class is shown below, because it is inherent in C++ semantics.
- class MissileLauncher : public GameObject
- private // by declaring member variables we are putting these members in ourself, i.e. in our "group", and making ourselves a composite
- Missile missile // one member of self is a missle, which is again a composite (composed of sub-parts)
- Billboard smallExplosion // one member of self is a billboard, which is again a composite (composed of sub-parts)
- ParticleSystem hugeExplosion // one member of self is a particle system, which is again a composite (composed of sub-parts)
- int explosionSize // one member of self is an integer, which is a primitive (not composite) object
- string MissileLauncher::GetAttribute(string attribName)
- return this->missile->GetAttribute("size");
- return this->explosionSize
- if(explosionSize < 100)
- return this->smallExplosion->GetAttribute("size");
- return this->hugeExplosion->GetAttribute("particleRadius");
(Note: this example has many flaws - you wouldn't use a GetAttribute
function, you would normally declare accessor functions, but for illustrative purposes all attributes have been grouped together into the GetAttribute
function to centrally illustrate how different attribute mappings are done)
Notice how the exposed attriutes of MissileLauncher
(missileSize, explosionSize, explosionColor) are ''mapped'' to recursive GetAttribute
calls on the sub-objects, and that the actual sub-objects and the attributes are not known to the caller. The mapping is opaque to the caller - the caller only can access the exposed attributes, not directly access the sub-parts or their attributes. Notice also how the mapping can be simple (attribute explosionSize), can be forwarded to another object (attribute missileSize), or it can even be computed (e.g. the explosionColor attribute). Finally, notice how the missile launcher itself can be reused as a composite in the MegaMissileLauncher
class (or group). This is the general-case attribute mapping problem.
The question is how to support this kind of attribute mapping without requring programming. How do you
- specify the sub-parts of a composite, which themselves in turn can be primtive or composite
- specify which attributes of the composite are exposed
- specify the ''computation'' of an attribute
- it may be simple (a stored integer or string)
- it may be mapped to another exposed attribute of a sub-object
- it may be computed
Again, I believe the Nebula Device supports this concept fully. Game objects are C++ objects, which all descend from a common ancestor. The commmon ancestor provides for hierarchy (objects can have child objects) and cloning. If each game object correctly implements the cloning semantics, then an object is a subtree in the object hierarchy, and any subtree can clone itself. A single node or a hierarchy of 1000 nodes are both trees, and are subject to the exact same syntax and semantics - allowing the Composite Design Pattern. Incidentally "attribute editing" in Nebula is done via serialization to a script, which is an identical interface for both primitive and compound objects (i.e. for object trees of any depth). The "exposed attributes" in Nebula are actually exposed TCL functions.
include old email discussion on this subject here