NG Design Document/Viewer Architecture/Framework
From Rex community wiki
Introduction
The new viewer is meant to be de facto standard for virtual world platforms. It needs to be robust, flexible and easily extensible. Therefore a modular design is needed that gathers to the aforementioned requirements.
The design should allow for new features and functionality to be added to existing world objects, or modifying the existing behavior without the need to modify other parts of the system. Some parts may be immutable in practice due to not having access or rights to modify the source code of the system.
New requirements may often come from 3rd parties, who might want to implement the features themselves. In these cases it can't be assumed that the persons implementing the new features have deep knowledge of the insides of the whole system. Spending inordinate amount of time studying the whole of the existing system should not be a requirement for implementing new features.
Picture 1. Extensible Framework.
In addition to having world objects extensible, it should be possible to replace or add new functionality to the engine itself. The framework should be plug-in based, where all major features that are not distinct to a single world object are implemented as plug-ins to the system. It should be possible for the user to disable and enable new plug-ins at runtime.
Use Cases for Extending the Framework
- Bob is not satisfied with the quality and efficiency of the physics simulation on the viewer. He goes online and buys and downloads a new physics plug-in for the viewer. After installing the new plug-in, he starts up the viewer and opens a dialog to manage the viewer's plug-ins. He deselects the default physics plug-in and selects the newly installed plug-in. Bob can now enjoy improved physics simulation in the viewer.
- Company A is impressed how one can easily share a computer desktop inside the viewer using VNC. However they would prefer using their own implementation of VNC over the default implementation offered with the viewer. The viewer, including the default VNC plug-in, is open source, so a programmer from company A can use the default plug-in as base for implementing the new VNC plug-in. The new plug-in can be offered as a separate download from the client and can be distributed freely by company A.
In short, the design should allow for easy implementation of new features and it should not require intimate knowledge of the whole system. As much as possible, common tasks required when implementing new features should be automated to reduce possibility of errors and potential frustration in the implementation phase; having to copy existing code when implementing new features should be avoided at all costs.
Making it easy to implement new features should not imply that the rest of the system can be arbitrarily complex - quite the opposite. Even if implementing new features is easy on the surface, it can still lead to hard to debug mistakes if the overarching design and implementation is complex and hard to understand. The framework that is common to all parts of the system should be decomposed naturally into smaller parts and should have consistent and simple semantics. It should be possible for a normal person to comprehend it and reason about its functioning one component at a time.
When implementing new functionality, the new parts should be able to communicate with already existing parts of the system with robust and well defined means without introducing coupling between the parts except where absolutely necessary. Furthermore any new disparate parts should be able to negotiate its own interactions with other new parts, without the system assuming too much about the content of such communication. Where possible, such communication should be facilitated by the system without the need to device external mechanisms for message passing.
Picture 2. Core Interaction.
The designed system also needs a modicum of efficiency. In game engine design efficiency is often one of the most important aspects of the design. Efficiency is important if truly immersive, expansive and visually rich worlds are meant to be created. However, in a virtual world viewer efficiency can't be the prime concern, especially if it is meant to be a platform upon which diverse applications can be build on. A careful balancing must happen between the efficiency, and between robustness and ease of use.
Multi-core processors are the future. The design should account for parallel execution of disparate and discrete tasks. Taking advantage of all the processors in the system, whether CPU or GPU, can give a much greater experience for the user. (1)
A virtual world viewer is essentially also a game engine. Some of the emphasis on various systems may be different but most systems are the same and mostly same principles apply as in a game engine design. One of the problems with parallelism in game engines is that a lot of the data used by the engine needs to be shared across multiple threads and some of the data may be quite large, calculated in megabytes. There is also much interaction with different systems in the engine. Furthermore game engine contains one critical part that should not be delayed under any circumstances: the renderer. This presents unique challenges for creating a parallel game engine design.
Another consideration with multithreading the viewer is how large the benefits will be against the negative effects. More precisely, the question is between implicit and explicit support for parallelism. With explicit threading support model each subsystem is responsible for it's own threading and for synchronizing it's own internal data with that of the global shared data, usually in a single pass after the main thread has finished with rendering and other tasks. This is the simpler of the two models.
With implicit support for parallelism each subsystem has a copy of the shared data and the framework implicitly handles synchronizing the data without any of the subsystems needing to interfere. Implementing such a model for parallelism can introduce some complexity into the system and reduce the efficiency of the renderer, but should not be dismissed because of this.
The most important goal of parallel game engine design is to keep the system responsive and have the renderer running as fast as possible while heavy tasks are calculated in the background. For maximum efficiency, it is important to carefully construct the rendering loop and maintain control of the execution path.
Below is presented a framework with modular, robust, flexible and easily extensible design and which can handle parallel tasks. Before that however, we'll take a look at some existing solutions.
Existing Solutions
Object Oriented World Object design
The traditional design of a world object structure is object oriented design. This creates a deep hierarchy where specializations of generic objects reside deeper in the hierarchy (see picture 3). An example of this kind of hierarchy is UnrealEngine2, see UnrealScript Language Reference (2) for more detailed information.
Picture 3. Example of deep object hierarchy
In this model there may be heavy dependencies between different levels of the hierarchy. Also very deep hierarchies may ultimately form as more functionality is added to the engine, which can make the model heavy, complex and unwieldy. Class interfaces may become bloated. See (3), (4, Chapter 34). In essence, we face all the problems that may be present when using inheritance heavily.
Extending world objects that use such deep hierarchies can also be problematic. New object types can be created by inheriting from appropriate parent object types, but any refactoring of classes higher up in the hierarchy may necessitate refactoring of child classes also, and can cause hard to debug problems. Modifying existing behavior can be difficult unless very specifically accounted for, and often requires a complete change in what class an object uses.
Object oriented design is however very familiar to many people and it is often considered to be easy to use.
Component based World Object design
A model used in many massively multiplayer online (MMO) games is a component based (5), an aggregation rather than inheritance. This model is also used in Tony Hawk series of games (6). See picture 4.
Picture 4. Component based design. Image from [6]
In component based model, a world object is nothing but an aggregate of its components. In practice, the object may be a collection of components, or it may be pure aggregation where the concrete concept of an object does not necessarily even exist.
For examples of components, see picture 5.
Picture 5. Examples of Components
- Protocol specific components are components that contain data that is shared between the server / protocol and the client.
- Legacy components are components which are important in the early stages of the viewer development, but which we would like to replace with our own implementation at some point, or in case of terrain, have the possibility to remove it completely from the world (for example a space world that has no terrain).
- Client implementation specific components are components that contain data specific to the client implementation, for example when using Ogre as the 3D renderer, Mesh component might contain Ogre::SceneNode and Ogre::Entity. They should also contain the component specific asset data, such as Ogre::Mesh.
- Avatar components contain data that is specific to avatars.
In the above example data that is not implementation specific is separated from the data that is implementation specific. This increased decoupling between data and behavior should make it easier to replace the behavior where needed, for example by replacing the rending engine, or physics engine with a new one, and it should make it easier to modify the protocol, at least as long as interfaces remain stable.
The benefits of component based systems are that the components themselves can be lightweight. Reduced complexity leads to fewer bugs and an overall system that is easier to comprehend, as individual components can be examined separately. (6) Desired object behavior can be achieved by combining the needed components and nothing else. Similar behavior can easily be added to markedly different kinds of objects. Different types of behaviors are decoupled from each other. Creating data-driven component architecture is also relatively easy; there is a clear connection between a component and its data, which is not always the case with deep hierarchies. Programmer tools can also benefit from componentization, for example an object inspector should be relatively straightforward to implement, thanks to the clear decoupling.
On the other hand, a component based system may introduce higher overhead and require additional checks for presence of specific components. It may also in some cases make debugging harder (6). Care should be taken when deciding between these two models.
Plug-in Design
For creating a pluggable framework, there are many existing solutions. One of the most common ways, especially in C/C++, is to use shared, dynamically loaded libraries. The libraries can be distributed separately and as long as interfaces stay stable, they can be used with different versions of the framework without the need to update the library itself. It should be noted though that different platforms have their own way of handling shared libraries. A cross platform support for a pluggable framework may take some additional work.
Extensions to Data and Behavior
Component-Based Design
Component based system for world object structure will be used; entity will act as a container for its components. This should allow for maximum flexibility and decoupling with minimal overhead costs.
The framework should be designed in such a way that new components to world objects can be introduced, and old components can be replaced easily. All components should have a common interface to keep components as generic and as decoupled from each other and the system as possible. The extra overhead from using a component based model over object oriented model should be taken into account, but should not be viewed as a reason to not use component based model. It should be noted though that the component system can not be leveraged fully until the protocol and server model can be rewritten to match. It will be necessary to have extra functionality that maps the current packets in the protocol into the right components.
Component access should be instrumented and if any bottlenecks should arise, appropriate shortcuts should be added but only to where absolutely necessary. Shortcuts should not be added for convenience of access.
To make sure the viewer is multi-core ready in the future, even if multithreading support is not implemented right from the start, shared data across components should be well defined and contained within getters and setters inside the component. In this way it will be easier to implement appropriate synchronization locks (multiple reader / single writer) or software / hardware memory transaction support.
In any case, when designing the framework for parallel execution, care must be taken with timing and proper execution paths. For example it may be necessary to always first update entity's animation component before updating its physics component to make sure all entities maintain a consistent state. It may be necessary to have the components in a tree hierarchy, and update the components depth-first.
Plug-ins
For plug-ins, shared, dynamically loaded libraries should be used. For cross platform support, PoCo C++ Libraries's (7) SharedLibrary functionality will be used. Each plug-in will contain functions for loading and initializing the plug-in. It should be noted that loading the plug-in into memory does not necessarily mean that it will be initialized and activated immediately.
Each plug-in will have a definition file that contains the plug-in's entry point. The framework will scan a plug-in directory for these definition files and adds each found plug-in to available plug-ins -list.
Below is an example of a class that defines a plug-in:
class PluginA
{
void onload(Framework)
{
One-time static initialization. Called when plug-in is loaded into memory.
}
void onUnload(Framework)
{
One-time static deinitialization. Called when plug-in is unloaded from memory.
}
void onInitialize(Framework)
{
Initialize plug-in for use. Called every time plug-in is enabled.
}
void onUninitialize(Framework)
{
Deinitialize plug-in. Called every time plug-in is disabled.
}
}
Code Listing 1.
Interactions Between Plug-Ins
The framework will include stable interfaces for core plug-ins. This is to lessen coupling between various plug-ins. It will be possible to get direct access to the underlying implementation in many of the core plug-ins, for example the renderer. This allows for advanced features but since it couples plug-ins tightly and lessens the modular nature of the framework, the feature should be used sparingly and only where absolutely necessary. Alternative would be to provide comprehensive interface abstractions where needed, but the cost of maintaining them and even the difficulty of creating a common interface over many disparate implementations make this not practical.
Each plug-in will be responsible for implementing a service interface and then registering the service to the framework. Other plug-ins can then query the registered service from the framework and interact with the module without direct dependency or heavy coupling. The diagram below shows how plug-ins and services interact.
Picture 6. Services
3rd party plug-ins will have to rely on direct access when interacting with each other where events are not sufficient. When 3rd party plug-in reaches high enough maturity or is deemed to be within the scope of realXtend-NG, a stable interface can be provided for it through the framework. In this way good quality 3rd party modules can be better integrated with the overall design of the viewer.
Components and Plug-Ins
Components will be declared in their respective plug-ins, for example the renderer plug-in may declare 3D mesh component, or the sound plug-in may declare a sound entity plug-in. Each plug-in is responsible for registering it's components to the framework. The registering is done via a component factory. Each component type has their own factory and the framework has an industry of registered component factories.
The framework will include helper macros to make component registration implicit. With the help of these macros, component registration happens automatically when a plug-in is initialized / deinitialized. The macros will reduce the need to write boilerplate code when implementing new plug-ins and components. The design contains factory and registrar classes for each component, but these are implicit classes contained in macros and hidden from the developer.
Example of a component class. Much of the boilerplate code is hidden by the DECLARE_EC macro.
class Component
{
DECLARE_EC(Component) // Macro that contains the component's factory and registrar.
private:
constructor()
copy_constructor() // For cloning the component
public:
destructor()
assignment_operator()
}
Code Listing 2.
In addition, a component class may include some data and getters / setters for the data.
Below is a diagram of the relationships of components, entities, world logic and the core.
Picture 7. Foundation - WorldLogic - Scene - Entity - Component Relationships
The Foundation contains a pointer for the currently visible scene. There may be several scenes which are all managed by WorldLogic. Each scene is composed of several entities and it is in fact possible for an entity to belong to several scenes. Components are owned by entities, but a weak pointer will exist to each component in the framework for easier access.
Each entity contains all its components and again a component can belong to more than one entity. This allows for data and not just behavior to be linked between entities. For example several lights in the world can be linked in such a way that when one light's intensity changes, all the linked light's intensity changes. In picture 8 there is an example of linking light intensity by using a common component on an entity.
Picture 8. Linking Light Components.
There will be several ways provided to access components, for example through the framework or via an owning entity. Access may be provided through standard C++ iterators for component collections, or by direct access for individual components by component name. Some very common data may be directly provided in the entity for ease of access and efficiency. Care should be taken that no more data is added to the entity class, and data access performance tests should be executed on an ongoing basis.
WorldLogic will include most of the functionality in the viewer. It means in practice that the framework and almost all modules are there to offer services for the WorldLogic. Since our architecture is based on an existing protocol which we do not plan to modify until after the new viewer reaches some level of maturity, many parts of the WorldLogic will be legacy even before it is written. To alleviate this issue, the whole WorldLogic should be heavily modularized. That way when legacy features are replaced with new functionality, it is simply a matter of disabling the old legacy module and enabling the new module, in a best case scenario.
Plug-in Based Use Cases
- Creating a module that extends world objects.
- A new shared library project is created for the module.
- An entry class is created, as in code listing 1.
- Additional classed needed by the module are created. These classes are managed by the module entry class; when the module gets initialized, the additional classes are instantiated. When module is deinitialized, the class objects are destroyed.
- A component class is defined, as in code listing 2. The component will extend world objects.
- The component will contain all data of the module that is specific to the entity.
- The module itself will contain all data that is global to the system.
- Finally a module definition file is created that specifies the name of the entry class. The name of the definition file matches the name of the module. The file is placed to a special directory that contains definition files for all available modules.
- Interacting with other modules and the framework.
- Framework will offer services, such as logging, configuration management and instrumentation. These can be accessed directly through the framework.
- The module will subscribe to interesting events from the framework. Since each module may define their own set of events they may raise, one must go through the modules for interesting events.
- The module creates new user interface elements by inquiring the framework for a UI module interface and using it to create UI widgets.
- The module hooks to user interface element events. When an event occurs, such as user accepts input on a chat text box, the module gets notified and can act accordingly.
- To draw something on the screen, such as a speech bubble, the module registers for the render event and when the event is invoked, the module draws to the scene through the renderer module interface provided by the event.
Component Based Use Cases
- New entity is created that is both a mesh and a light
-
- WorldLogic uses Scene to create new entity, Scene returns the newly created entity.
- WorldLogic creates Light Component using the foundation and adds it to the newly created entity.
- WorldLogic creates Mesh Component using the foundation and adds it to the newly created entity.
- Server sends new update regarding light intensity of an entity in the world
-
- WorldLogic receives the packet. The packet contains a unique identifier for the entity common to the server and all clients.
- The identifier is transformed to scene/client specific identifier.
- Knowing the component type (light component) and entity id, WorldLogic requests the Foundation for the component, using its type and the entity's id.
- WorldLogic updates the data of the component to match the contents of the received packet.
- User wants to remove all lights from the world
-
- WorldLogic receives the request to remove all lights from ui module.
- WorldLogic requests iterator for all components of type light from the foundation.
- WorldLogic iterates through all the components and removes them from their owner entity.
- User wants to make all entities in the scene into lights
-
- WorldLogic receives the request from ui module.
- WorldLogic requests an Entity Iterator from Scene.
- WorldLogic iterates through all the entities, adding a light component to them if one doesn't already exist.
- User wants to link two lights together, so when one's intensity changes, so does the other's (Sharing components between entities)
-
- WorldLogic receives the request to link two lights together.
- WorldLogic gets the entities that contain the light components from Scene.
- Light Component is destroyed from one of the entities, and the first entity's Light Component is added to the other's.
- Avatar Generator wants to display the avatar in its own Scene (A Scene wants to use another scene's components)
-
- There are two Scenes: the world Scene and an Avatar Generator scene. The AG Scene wants to display the user's avatar.
- WorldLogic gets the avatar entity from the world Scene.
- WorldLogic creates new entity to the AG Scene.
- WorldLogic adds avatar specific components from the entity in the WorldScene to the entity in the AG Scene.
- Removing a component from entity
-
- WorldLogic gets the Entity from the Scene.
- The Component is accessed through the Entity by type name.
- The Component is removed from the Entity via remove component call.
- Entity signals the framework that the Component was removed.
- Framework goes through the owners of the component. If it is not owned by anyone, it gets deleted.
- Rationale for using the Foundation as component factory
-
Components should be independent from entities and scenes: single component instance should be usable on multiple entities and scenes. Components should be independent from modules: different modules should be able to define same component types (which module's component gets created depends on which modules are enabled).
The Application
The Design Motivation
It is to be noted that the "core" design work has major consequences towards shaping how the rest of the development will proceed. If the design is something really large (to try to accommodate as many requirements as possible) or filled with features that need lots of self-education to successfully reason about, the end result can be that individual developers will end up with a partial (or worse, differing) understanding of the system. Our programmer teams lack the deep hierarchy that big companies have and as such we can't afford to have a system that is understood only by one or two "senior" engineers, with others working only on subsystems of smaller scale. We might have several people writing, and perhaps even designing, core classes independently and above all, we want to avoid bugs being introduced because programmers understood a complex design in different ways.
Therefore we need a core design that is suitably simple so that most of the programmers can, if necessary, compare how the implementation matches the overall design. I believe that this desire for simplicity does not mean we would be making amendments on supported features, like parallelism or extensibility. Rather, several advanced features can be implemented out of the "core runtime", while keeping the overall complexity low.
At the other end of the scale, we need to see to it that the core design does not end up being too nonexistent, or shallow. In that case, any long run development effort welcome from the community can deteriorate to ad-hoc extensions that lack structure. Badly organized open source projects tend to suffer from the phenomenon where over time enthusiastic people have kept pushing in new code and features while any design was long gone and forgotten. The proposal that is described below tries to defend against that by providing an interface for custom extensions, but it is not a guarantee of course.
The Design
The core runtime design is presented in the diagram below. Note that while presented below as a compact set of classes, there might not be a one-to-one correspondence of the objects in the diagram and the classes in the actual implementation.
Picture 9. Core Runtime.
The root application object is the CoreRuntime class. It instantiates all the initial modules, as defined in a configuration file, and then transitions to a frame update loop. The CoreRuntime also manages an application-wide event system for different modules to communicate. These events should be used in the case where the event producer doesn't have the knowledge of the possible event receiver. (e.g. InputDevice firing a keyboard event to RexLogic / UserInterface etc.) The modules themselves are not limited to communicating with each other through these events only, but can query the CoreRuntime for a reference to other Modules, and can directly call their methods. This makes messaging easier in cases where there is a strict dependency between two modules (e.g. RexLogic module directly depends on the Scene module in order to manage the world contents) and when the events are too unwieldy for the task.
Hierarchical Organization
To avoid having a big "cloud" of events being fired between an arbitrary set of publishers and subscribers, the modules are instantiated to a tree hierarchy. The events are passed along this hierarchy, with the CoreRuntime as the root node. Parent modules can decide what to do with each event, and for example whether to pass the event down to one or more child modules, or perhaps to produce a totally another event for the children. The FrameUpdate processes are performed in this tree order as well. The diagram below shows how the hierarchy could be organized.
Picture 10. Module Hierarchy.
The rationale for a design like this is twofold. First, instead of traditionally defining the owner-child relationships between classes at compile-time, this tree defines them at runtime. The aim is not to go about at changing these roles wildly during application lifetime, but to allow external 3rd party modules to hook to some part of the message chain for overriding or extending certain functionality. This allows a natural sense of state in the hierarchy, e.g. RexLogic can easily choose whether to pass out keyboard messages to the UserAgent (Avatar) module or the UserInterface module (say, when a textbox is active for typing).
The second reason is that it is still desirable to maintain these ownership hierarchies between modules, instead of having a flat list of modules as peers. With a flat list of modules, each potentially being a publisher or a subscriber of events, debugging the events as they go around in a "cloud of messages" can be difficult; the dependencies are implicit. Opposingly in a hierarchy, most (hopefully all) events flow through the same hierarchy, so it is not necessary to create code for setting up explicit event chains for each type of event.
The drawback of this is of course that the hierarchy is not different for different types of events (i.e. for event X, module A should be the parent of B, and for event Y, vice versa). This does not sound like a burden though, since most of the cases that violate this smell of bad design. Any few exceptions can be specifically coded to use explicit events that go outside the tree.
Citations
- Designing the Framework of a Parallel Game Engine
- (http://software.intel.com/en-us/articles/designing-the-framework-of-a-parallel-game-engine/)
- UnrealScript Language Reference
- (http://udn.epicgames.com/Three/UnrealScriptReference.html)
- Game Object Structure: Inheritance vs. Aggregation
- (http://www.gamearchitect.net/Articles/GameObjects1.html)
- Herb Sutter and Andrei Alexandrescu, C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Addison Wesley Professional, 2004.
- Entity Systems are the future of MMOG development
- (http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/)
- Evolve Your Hierarchy
- (http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/)
- PoCo C++ Libraries
- (http://pocoproject.org/)
|