Urho3D's scene model can be described as a component-based scene graph. The Scene consists of a hierarchy of scene nodes, starting from the root node, which also represents the whole scene. Each Node has a 3D transform (position, rotation and scale), a name and an ID, and a freeform VariantMap for user variables, but no other functionality.
Rendering 3D objects, sound playback, physics and scripted logic updates are all enabled by creating different Components into the nodes by calling CreateComponent(). As with events, in C++ components are identified by type name hashes, and template forms of the component creation and retrieval functions exist for convenience. For example:
In script, strings are used to identify component types instead, so the same code would look like:
Because components are created using object factories, a factory must be registered for each component type.
Components created into the Scene itself have a special role: to implement scene-wide functionality. They should be created before all other components, and include the following:
- Octree: implements spatial partitioning and accelerated visibility queries. Without this 3D objects can not be rendered.
- PhysicsWorld: implements physics simulation. Physics components such as RigidBody or CollisionShape can not function properly without this.
- DebugRenderer: implements debug geometry rendering.
Unlike nodes, components do not have names; components inside the same node are only identified by their type, and index in the node's component list, which is filled in creation order. See the various overloads of GetComponent() or GetComponents() for details.
When created, both nodes and components get scene-global integer IDs. They can be queried from the Scene by using the functions GetNodeByID() and GetComponentByID(). This is much faster than for example doing recursive name-based scene node queries.
There is no inbuilt concept of an entity or a game object; rather it is up to the programmer to decide the node hierarchy, and in which nodes to place any scripted logic. Typically, free-moving objects in the 3D world would be created as children of the root node. Nodes can be created either with or without a name, see CreateChild(). Uniqueness of node names is not enforced.
Whenever there is some hierarchical composition, it is recommended (and in fact necessary, because components do not have their own 3D transforms) to create a child node. For example if a character was holding an object in his hand, the object should have its own node, which would be parented to the character's hand bone (also a Node.) The exception is the physics CollisionShape, which can be offsetted and rotated individually in relation to the node. See Physics for more details. Note that Scene's own transform is purposefully ignored as an optimization when calculating world derived transforms of child nodes, so changing it has no effect and it should be left as it is (position at origin, no rotation, no scaling.)
Scene nodes can be freely reparented. In contrast components are always created to the node they belong to, and can not be moved between nodes. Both child nodes and components are stored using SharedPtr containers; this means that detaching a child node from its parent or removing a component will also destroy it, if no other references to it exist. Both Node & Component provide the Remove() function to accomplish this without having to go through the parent. Note that no operations on the node or component in question are safe after calling that function.
It is also legal to create a Node that does not belong to a scene. This is useful for example with a camera moving in a scene that may be loaded or saved, because then the camera will not be saved along with the actual scene, and will not be destroyed when the scene is loaded. However, note that creating geometry, physics or script components to an unattached node, and then moving it into a scene later will cause those components to not work correctly.
Nodes and components can be excluded from the scene update by disabling them, see SetEnabled(). Disabling for example a drawable component also makes it invisible, a sound source component becomes inaudible etc. If a node is disabled, all of its components are treated as disabled regardless of their own enable/disable state.
To implement your game logic you typically either create script objects (when using scripting) or new components (when using C++). Script objects exist in a C++ placeholder component, but can be basically thought of as components themselves. For a simple example to get you started, check the 05_AnimatingScene sample, which creates a Rotator object to scene nodes to perform rotation on each frame update.
Unless you have extremely serious reasons for doing so, you should not subclass the Node class in C++ for implementing your own logic. Doing so will theoretically work, but has the following drawbacks:
- Loading and saving will not work properly without changes. It assumes that the root node is a Scene, and all the child nodes are of the Node class. It will not know how to instantiate your custom subclass.
- The Editor does not know how to edit your subclass.
Scenes can be loaded and saved in either binary or XML format; see the functions Load(), LoadXML(), Save() and SaveXML(). See Serialization for the technical details on how this works. When a scene is loaded, all existing content in it (child nodes and components) is removed first.
Nodes and components that are marked temporary will not be saved. See SetTemporary().
To be able to track the progress of loading a (large) scene without having the program stall for the duration of the loading, a scene can also be loaded asynchronously. This means that on each frame the scene loads resources and child nodes until a certain amount of milliseconds has been exceeded. See LoadAsync() and LoadAsyncXML(). Use the functions IsAsyncLoading() and GetAsyncProgress() to track the loading progress; the latter returns a float value between 0 and 1, where 1 is fully loaded. The scene will not update or render before it is fully loaded.
Just loading or saving whole scenes is not flexible enough for eg. games where new objects need to be dynamically created. On the other hand, creating complex objects and setting their properties in code will also be tedious. For this reason, it is also possible to save a scene node (and its child nodes, components and attributes) to either binary or XML to be able to instantiate it later into a scene. Such a saved object is often referred to as a prefab. There are three ways to do this:
- In code by calling Save() or SaveXML() on the Node in question.
- In the editor, by selecting the node in the hierarchy window and choosing "Save node as" from the "File" menu.
- Using the "node" command in AssetImporter, which will save the scene node hierarchy and any models contained in the input asset (eg. a Collada file)
To instantiate the saved node into a scene, call Instantiate() or InstantiateXML() depending on the format. The node will be created as a child of the Scene but can be freely reparented after that. Position and rotation for placing the node need to be specified. The NinjaSnowWar example uses XML format for its object prefabs; these exist in the bin/Data/Objects directory.
For more information on the component-based scene model, see for example http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/. Note that the Urho3D scene model is not a pure Entity-Component-System design, which would have the components just as bare data containers, and only systems acting on them. Instead the Urho3D components contain logic of their own, and actively communicate with the systems (such as rendering, physics or script engine) they depend on.