MiniOrk

MiniOrk is a derivative of the open-source Orkid mediaENGINE which I designed several years ago. Santa Cruz Games sponsored some development of Miniork, although I still put ALOT of my own free time into it, as did a few others. It is called “Mini” Ork, because we stripped it down so it would run on a NintendoDS, and we removed any GPL covered code from the game side (some GPL still remains in the tool side, but we don’t ship that). It’s first appearance was on the wonderful game (sarcasm;>) Godzilla Unleashed (NDS), followed by TombRaider Underworld (NDS), Igor The Game(PC/WII), and finally Nicktoons SpongeBob GravJet Racing(PC/XBox360). Despite the name “MiniOrk”, it is currently larger than the original, so it is not really mini anymore.  The current feature set includes:

1. Introspection/Reflection. All Ser-Deserializable and editable objects are introspected via a property registration system. This includes containers of objects. We support stl-like but custom (console-friendly) dynamically sized vectors, fixed capacity vectors(arrays), associative sorted-vectors of pairs (LUT), fixed capacity LUT’s, and light-weight pooled-strings (via a stringtable). We used LUT’S instead of maps because LUT’S are typically only modified at edit or deserialization time. We trade insert performance for better memory and read performance, when deserializing a LUT, you can reserve its size. Typically for associative containers, only ints, floats or strings are used for keys, and usually instantiated-objects are the values. There is a generic introspective property editor for instantiating and editing objects. Sometimes we wish we had custom, more efficient forms for editing specific types of objects (say a timeline editor for editing sequences, etc..), but the generic property editor works well for 90+% of game objects

2. Game/Tool Hybrid. The game and tool are related. The tool is nothing more than the game engine with some editing features bolted on. They are kept separate however, there is very minimal tool code leakage into the engine itself, It might occur in one or two places throughout the entire code base. The game runs in the tool. As long as you do not change object topology, and follow some other restrictions, you can edit objects live while the simulation is running. This allows fast prototyping of physics systems, particle systems, character-compositions, etc….. For editing game-objects, there is only ONE tool. The same property editor and 3d placement editor is used for levels, entity-placement, character(entity)-composition, particle systems, physics-tuning, etc… This was more due to the lack of resources more than anything else. It is much simpler to engineer/code/debug one tool than it is to engineer/code/debug several. And when engineers come and go along with the maintenance of the tools they coded, having one tool to maintain was CRITICAL. The one argument that kept creeping up that also makes sense to me, and which I mentioned above is that sometimes it would have been nice to have custom forms for certain object types (The example I used before was a timeline editor for editing a sequence). Although we do have a pimped out property sheet with gradient editors, curve editors, lut editors, etc…, cramming most editor functionality into a generic property sheet can be cumbersome some times. At least we had 3d manipulators ;>

3. Scene/Entity/Component system. I first encountered Entity/Component based game systems at Jaleco on Goblin Commander with the Truth engine architected by Ken Klopp. I am on my 3rd or 4th revision to the scene-entity-component model. A scene-entity component system allows you to construct a game or simulation by compositing behavior together. I always hear a lot of heated discussions about composition/aggregation vs inheritance. We use both composition AND inheritance, utilizing the strengths of both. We do not subclass scenes (correction: we did subclass scenes at one time, but have since learned the error of our ways). We do subclass components and component interfaces. All major objects are split into const, but editable and (de)serialized “Data” parts and runtime-mutable “Instance” counterparts. Each runtime-mutable part const-ref’s its const data counterpart. This makes it very simple to restart a scene in a pristine state, just by recreating a new set of instances, and const-reffing their unchanged const-data providers. The running scene can NOT modify any const data object, only the editor or deserializer can. Examples of these part/counterpart items are SceneData, SceneInst, EntData, Entity(Inst), SceneComponentData, SceneComponentInst, EntityComponentData, EntityComponentInst, etc…

[rant] I have heard it argued recently (at an interview) that you should not reuse tech for an engine for a new game, you rewrite it from scratch. I say WTF!. Maybe you can rewrite some or engineer new components per game, but not the whole engine!. I suppose if you have money to throw away then rewriting from scratch is acceptable. Perhaps that attitude is a factor in the escalation in game budgets into the 10’s of millions of dollars. I am used to budgets between 300k-1M, so I try to do things a little cheaper…. [/rant]

Anyway, this system is game agnostic, you can plug in a physics based racer, or an animation driven platformer, or pretty much any type of game into it. This is done by varying the components which are present in the scene, you can easily have different game types per level in the same executable. The physics based racer would have for example, one BulletWorldSceneComponent and one or more entities with BulletObjectComponent’s. Swap out components and you can run a completely different type of game on the same system. As an example, In one scene I might have an Igor type level, in another a SpongeBob GravJet Racing type level, and in another a procedural terrain experiment, I am not claiming those are good matches in a single game, just using the examples that I have personally worked on. I am sure the reader can come up with a set of minigame types that do work together. We can support these distinct types of environments using a single executable. The game update loop at the high level is structured to update all like objects in succession, but it does not have to know specifically what those types(families) are. It needs to know which families exist, and in what order to update the families. This has the benefit of being very simple, has increased instruction-cache locality (due to like objects with like v-tables being updated close to each other), and has a higher potential for task and data parallelism by promoting work compartmentalization, payload multi-buffering and pipelining.  It looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void SceneInst::Update()
{
    float fdeltatime  = ComputeDeltaTime();
    ///////////////////////////////////////////////////////
    // update component families (pseudocode)
    // sorted by family order
    // usually something like
    //      (INPUT,CONTROL,PHYSICS,ANIMATION,VISIBILITY,AUDIO)
    // or  (INPUT,CONTROL,PHYSICS,VISIBILITY,ANIMATION,AUDIO)
    ///////////////////////////////////////////////////////
    for FAMILY in SORTEDFAMILIES
    {
        UpdateEntityComponents( FAMILY,fdeltatime );
        // note components may spawn/despawn other entities,
        //  but spawn/despawn requests are queued, and processed later
    }
    for FAMILY in SORTEDFAMILIES
    {
        UpdateSceneComponents( FAMILY,fdeltatime );
        // note components may spawn/despawn other entities,
        //  but spawn/despawn requests are queued, and processed later
    }
    ///////////////////////////////////////////////////////
    // spawn, despawn entities and components
    ///////////////////////////////////////////////////////
    ProcessSpawnDespawnQueues();
}

The layout of a scene looks something like this:
Note the separation of editable, const-referenceable data objects and mutable runtime objects.
The mutable runtime objects const-ref their data object counterparts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
////////////////////////////////////////////////////////////////
// A Drawable is an Entity's Renderable Emitter
// Example Drawables are:
//  ModelDrawable
//  ParticleSystemDrawable
//  SkyboxDrawable
//  CallbackDrawable (generic drawable)
//  There are Renderable counterparts for each Drawable Type
////////////////////////////////////////////////////////////////
class Drawable : public ork::Object
{
    virtual void EmitRenderables( Renderer& rr ) = 0;
};
////////////////////////////////////////////////////////////////
// Editable Entity Component Data Abstract Base Class
// Examples include: ModelComponentData,
//   BulletPhysicsObjectComponentData, ParticleControllerComponentData, etc..
////////////////////////////////////////////////////////////////
class EntityComponentData : public ork::Object
{
};
////////////////////////////////////////////////////////////////
// Runtime Instantiated(Mutable) Entity Component Instance Abstract Base Class
// Examples include: ModelComponentInst,
//   BulletPhysicsObjectComponentInst, ParticleControllerComponentInst, etc..
////////////////////////////////////////////////////////////////
class EntityComponentInst : public ork::Object
{
public:
    EntityComponentInst( const EntityComponentData& edata );
    void Update( SceneInst* psi );
private:
    virtual void DoUpdate( SceneInst* psi ) = 0;
    const EntityComponentData& mData;
};
////////////////////////////////////////////////////////////////
// Editable Scene Component Data Abstract Base Class
// Scene Components are like Entity Components,
//  with the added restriction that there is only one per type per scene.
// Examples include: BulletWorldComponentData, AudioManagerComponentData, etc..
////////////////////////////////////////////////////////////////
class SceneComponentData : public ork::Object
{
};
////////////////////////////////////////////////////////////////
// Runtime Instantiated(Mutable) Scene Component Instance Abstract Base Class
// Examples include: BulletWorldComponentInst, AudioManagerComponentInst, etc..
////////////////////////////////////////////////////////////////
class SceneComponentInst : public ork::Object
{
public:
    SceneComponentInst( const SceneComponentData& scd );
    void Update( SceneInst* psi );
private:
    virtual void DoUpdate( SceneInst* psi ) = 0;
    const SceneComponentData& mData;
};
////////////////////////////////////////////////////////////////
// Scene Object Base
////////////////////////////////////////////////////////////////
class SceneObject : public ork::Object
{
};
////////////////////////////////////////////////////////////////
// Archetype is a data-driven Entity blueprint.
// It is a data-driven "builder" design pattern
////////////////////////////////////////////////////////////////
class Archetype : public SceneObject
{
public:
    void ComposeEntity( Entity* pent );                // compose an entity, create its component instances
    void DeComposeEntity( Entity* pent );             // decompose an entity, destroy its component instances
    void LinkEntity( SceneInst* psi, Entity* pent );  //  link this entity's components with other entities/components in the scene
    void UnLinkEntity( Entity* pent );    // unlink ......
private:
    orklut < PoolString,EntityComponentData* > mComponentDatas;

};
////////////////////////////////////////////////////////////////
// an EntData is a spawn record for an entity, with
// a spawn location and any spawn modifiers for
// the entity
////////////////////////////////////////////////////////////////
class EntData : public SceneObject
{
    Archetype* mpArchetype;    // what kind of entity are we spawning ?
    SpawnInfo  mSpawnInfo;     // spawn location, spawn modifiers, etc...
};
////////////////////////////////////////////////////////////////
// Runtime Entity (Instantiated, mutable EntData)
////////////////////////////////////////////////////////////////
class Entity
{
public:
    Entity( const EntData& ed );
private:
    const EntData& mEntData;
    orklut < PoolString, EntityComponentInst* > mComponents;
    orkvector< Drawable* > mDrawables;
};
////////////////////////////////////////////////////////////////
// Collection of data driven scene objects
// source of a SceneInstance
////////////////////////////////////////////////////////////////
class SceneData
{
    orklut < PoolString, SceneObject* >                              mSceneObjects;
    orklut < ComponentFamilyType, SceneComponentData* > mSceneComponentDatas;
};
////////////////////////////////////////////////////////////////
// collection of Runtime-Mutable Entity Components
////////////////////////////////////////////////////////////////
class EntityComponentInstGroup
{
    orklist< EntityComponentInst* > mEntityComponents;
};
////////////////////////////////////////////////////////////////
// collection of Runtime-Mutable Scene Components
////////////////////////////////////////////////////////////////
class SceneComponentInstGroup
{
    orklist< SceneComponentInst* > mSceneComponents;
};
////////////////////////////////////////////////////////////////
// Runtime-Mutable Scene Instance
////////////////////////////////////////////////////////////////
class SceneInst
{
public:
    void Update();
    void Render(  Renderer& rr );
    SceneInst( const SceneData& sd );
private:
    void ProcessSpawnDespawnQueues();
    const SceneData& mSceneData;
    orklut < ComponentFamilyType, EntityComponentInstGroup* > mActiveEntityComponents;
    orklut < ComponentFamilyType, SceneComponentInstGroup* > mActiveSceneComponents;
    orklist < Drawable* > mActiveDrawables;
    void ActivateEntity( Entity* pent );
    void DeactivateEntity( Entity* pent );
};

4. Renderer. The update loop’s job is to read input, simulate and generate data for the renderer (and audio-renderer). This generated data used by the renderer is multi-buffered and synchronized. This allows the renderer and and update to occur in different threads. The update thread enumerates and computes visible “drawables” while generating renderable data placing the data into the synchronized drawable buffer. The rendering thread locks a previously generated drawable buffer, and splits drawables in it up into “renderables” which then go into a “render-queue”. A drawable may have multiple materials or clusters, but a renderable will only have one material or cluster. Examples of clusters would be a set of bones and associated primitives for a skinned character model, or a spatial subdivision and associated primitives (usually a cubical diced spatial subdivision) for a rigid environment model. Miniork prefers large and few subdivisions in order to keep the number of batches low. The render-queue is sorted by state, with the most expensive state changes as the highest influence. Examples of the factors affecting the sorting bin are: MaterialType, Depth, Transparency State (Opaque==FrontToBack, Translucent==BackToFront), etc.. After the render-queue is sorted, it is submitted to the graphics device. The renderer supports most features of a what a modern game engine does, although to a smaller degree, dictated by the budget of the games and the hardware we would typically work on. Some example features include hardware-skinning, light-maps, normal-maps, per-pixel lighting, full-screen post-processing effects, etc… The content pipeline is based on ColladaMaya and CgFx for WYSIWYG support. Content was also modified in ZBrush, for generating detail via Normal Maps. Models appear in Maya as they do in game, with the exception of in-context lighting. The lighting equation was the same, but in the Maya versions of shaders, there is just a headlight, which has so far seemed perfectly sufficient for authoring shader based content. Miniork-tool takes in dae files, performs extensive conditioning/processing on them and spits out platform ready binary assets on multiple platforms (Wii/PC/XB360,etc..). Static and Dynamic Lights are placed in the miniork-tool, which also enables light-map baking.

5. Dataflow systems. More to come, for now, See this

6. Multithreading.
I am currently researching increasing parallelism for game engines on multicore systems.
See Thoughts on new multithreaded scene update design


No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

You must be logged in to post a comment.