Chronicle Objects
September 28, 2021
Chronicle Game Data Hierarchy
This simple chart shows how game data is currently set up in Chronicle. CrWorld is the top level container and is essentially a vector of all the CrObjects currently in that world. Then, each CrObject in the world has a vector of all its active components. Multiple worlds can be active at a time and are managed by a WorldManager. Presently, there’s not a lot of ways for objects in different worlds to interact with each other, outside physics since they are all in the same physics simulation world.
An important thing to note, as I’ll explain more below, is I do allow CrComponent to hold a reference to its CrObject and CrObject holds a reference to its CrWorld. This does create some loose circular references which can cause problems, but the lifetime of any thing is always managed by its owning container. If a component wants to add or remove a component, it has to go through its CrObject reference, and if a CrObject wants to add or remove another CrObject to its world, it has to go through its CrWorld reference. As such, the ease of use far outweighs the potential for abuse by other engineers. Plus, catching abuse of things like this are what best practices and code reviews are made for :-).
Object Overview
In my last post, I showcased videos with quick descriptions of what's currently possible in Chronicle. In this post, I’ll go into more detail about the Chronicle Object system. The core to any engine is how it handles game objects, and I’ve always found it’s important to keep objects as light as possible such that any object in a game only has the data it needs for its purpose. It’s also very important that it be easy for everyone, from engineers to artists and designers, to add / remove / edit what an object does.
With that in mind, I chose an object component system for Chronicle. I’ve worked with variations of this sort of system a few times over my career and always found them easy to understand and expand. In an object component system, the object is mostly a thin wrapper containing an unique ID (something I haven’t actually handled in Chronicle yet), a vector of components, and some methods for querying for components on the object. Components are what actually do all the work and store all the data. With components, you get composition over inheritance, or “has-a” instead of “is-a.”
As an example: If you had a inheritance-based engine, you would probably have a base class Vehicle that the classes Car and Truck derive from; in that case, Car and Truck would return true for IsAVehicle. In a composition-based engine, your Car and Truck classes would have a Vehicle member variable and they become a HasAVehicle. In Chronicle’s Object Component system, it takes that a step further such that everything object “is-a” CrObject, or ChronicleObject, and you add components to the object to give it the functionality you want. In the vehicle example, you would make a new object, add 4 wheel components, 2 axle components, an engine, and a body, then set up any data on these components to make the “vehicle” object function how you want. Although I wouldn’t actually make a vehicle this way, it’s a good example for showing the idea behind the object component method.
With that overview of the object component system, let’s go into more detail on Chronicle's implementation. First up is the CrObject, with Cr the abbreviation for Chronicle that I tend to use in low-level systems of the engine.
NOTE: I’ll mostly omit any references to my reflection system, which leverages Ponder, as that’s for tooling, and saving / loading data that I won’t go into now.
CrObject
Here’s a somewhat simplified class definition for CrObject. As you can see, for something that is used to represent every game object, there’s not a lot to it.
Members
mName: Mostly there for debug purposes; holds the object's “name” as set in the tools.
mComponents: The core of the object; just a vector of all the components on it.
mComponentsByUpdate: This vector of vectors holds the same Component pointers in Components, but has the components in buckets based on what UpdateStage the component cares about. In each of these buckets, the components are also sorted by their UpdatePriority. I’ll talk about those below.
mWorld: The World this object is in.
mDispatcher: The dispatcher for handling messages sent to this object by its components or the global dispatcher. I’ll go over the message system some other time.
mDependentObjects: Vector of objects that depend on this object.
mParentObject: Object that this object depends on. This and mDependentObjects are used to maintain update hierarchies. I’ll talk more about this later as well.
Methods
Update
Parameters
timeStep: The time delta from the last frame
updateStage: The update stage for this update.
UpdateStage is an enum covering the different “stages” in a single frame.
As you can see, currently it’s all built around Physics and Animation. These are the most interesting systems right now and, in my experience, tend to be the things other gameplay systems are feeding data into or reading out of.
Currently with this approach, each CrObject’s Update gets called 6 times. This isn’t ideal since it’s going to need to access the vector of vectors each update, but if there are no components in that stage on the object, it doesn’t do any additional work.
Logic
The update is pretty simple: Using mComponentsByUpdate, it just loops over any components in the current stage and calls their Update method passing along the timeStep.
OnAddToWorld/RemoveFromWorld
Not much to say here, just goes through all it’s components and calls the corresponding handler so that components can do any work / clean-up on being added / removed from a world.
Add/RemoveComponent
Mostly used by the tooling for editing objects in real time, but I’ve left it exposed in the runtime side as it can be helpful to add / remove components to change it’s function on the fly based on game state changes.
NOTE: Care has to be taken when allowing this sort of thing with multithreaded updates.
FindComponent/QueryInterface
These methods are used to search an object for a specific Component or Interface, which I’ll explore in the CrComponent section.
Loops through all the components on the object and returns the first component that matches or has a matching Interface in the Query version.
NOTE: I also provide methods that return a vector to handle getting all matching Components / Interfaces as there is no restriction that only 1 component of a type can be on an object.
The rest of the shown methods are for message handling and maintaining object update hierarchies, which I’ll cover in another post as this one is already getting pretty long!
CrComponent
Here’s a somewhat simplified class definition for CrComponent.
Members
mObject: Reference to the CrObject component is on.
mName: Name for debug purposes.
mPriority: The priority for this component's update. This is used by the owning CrObject to sort components so that components update in a predictable order that can also be controlled by a Components author.
mUpdateStage: The stage of the update this component will update it in. Again, used by the owning CrObject to place components into update buckets.
Methods
CrComponent has a fair amount of virtual methods as this is the main place in Chronicle that uses inheritance. Though it’s usually kept to 1 level of inheritance, Interfaces are used if a component covers a wide range of functions.
DoQueryInterface: Derived components override this and return this if it implements the requested interface.
On*: These methods are all called by owning CrObject as a component's state changes so that any work clean-up can happen in a component.
Update: The meat of any component; any work a component does will be done here as well as message handlers, but we’ll save that for another day.
Clone: This is provided to handle instancing / copying a CrObject and handle creating a copy of this component. For example, an ObjectSpwaner will have a CrObject “template” that it holds onto and then creates instances with any data changes such as it’s starting position, velocity, etc.
Get*: Self explaining getter methods.
SetUpdateStage: This is exposed because, in some cases, a component may actually want to update in a different stage. This method is only valid before a component’s OnAddToObject and will assert otherwise.
The best current example of this being useful is the AnimationComponent. By default, it updates after physics and assumes output from physics is used to update the animation system. However, it also has a checkbox to flag root motion on, which means it now works the other way, with physics taking root motion output from animation to set desired velocity.
Interfaces
I’ve mentioned component interfaces a few times already, so let’s go into more detail here. Chronicle makes heavy use of Interfaces for components as it’s an easy way to make things extensible without ever needing to touch a low level engine component. A good example is the PosRotComponent, which holds the Transform for an object. Instead of any engine level component that wants to read / write an object's position, when grabbing an object's PosRotComponent, it grabs an object's IPosRotComponent interface. So: If some reason a game wants to handle transforms in some new way (maybe you’re making a 4D game so you need your Transform component to store extra data), you can implement a new component that uses the IPosRotComponent interface and any engine systems that depend on that can get still a transform for, say, updating the renerable for a mesh.
When making a brand new component, you’ll probably end up adding a new interface as well to go along with it. Heavily using interfaces also lets you make use of multiple inheritance without the headaches that can come from multiple inheritance with non-abstract classes. This way, you can combine functionality into bigger components at the game level by implementing multiple interfaces and still get the benefit of your new component working with other components looking for those interfaces.
In this image, although I put the interfaces in the same header, each is actually in their own header. This also lets you expose just the component interface headers in public headers for other libraries to use, instead of the component implementation headers.
The End
Thanks for reading to the end of this rather long post! Hopefully, this gives you a better idea of Chronicles Object Component system and how this sort of approach is very extensible and fairly easy once you understand how components and their interfaces interact.
For my next post:
Prefabs - I’ve started working on prefabs or a collection of objects that can be created and added to worlds(with per-instance overrides).