Skip to content

Component Entity System Coding

Alex Miyamoto edited this page Jun 1, 2022 · 23 revisions

Components

Components are data only. We define them in protocol buffer files so they can be used in C++, in reflection in the editor or even in LUA scripts. Components should be nouns, they are something that systems act on.

message ModelComponent
{
	option (nanopb_msgopt).has_onactivate = true;
	option (nanopb_msgopt).has_ondeactivate = true;
	option (nanopb_msgopt).has_onloaded = true;

	required string name = 1 [(nanopb).max_size = 64];
	required bool bDynamic = 2 [default = true];
	required bool bPerBoneCulling = 3 [default = true];
	required uint32 uPreloadCount = 4 [default = 1];
	required bool bShadowCast = 5 [default = true];
}

Note required and optional are currently not behaving as they should, if you set a member to optional and don't declare it it will be marked as not existing rather than taking on the default value. If you wish to have a default value make a required variable with a default value.

The required fields (each numbered and with optional default values) are the data in the component which designers will be able to set in yml, for example:

ModelComponent:
  name: test/RazorTest.vmdf
  bDynamic: true

Now lets take a look at whats going on with the options. The three options indicate that there will be templated functions defined in C++ for this class (these allow us to create runtime data; in the case of a model that is the pointer to the model). For example the option has_onactivate means that a template specialization of OnActivate for that component type will be declared in C++.

In order to have this C++ runtime specific data you should specify in the proto file where the runtime data struct will be defined, e.g.

option (nanopb_fileopt).include = "Engine/Scene/Model/ModelComponents.h";

The runtime data in ModelComponents.h is a template specialization of the struct RuntimeData for the model component

template<>
struct RuntimeData<usg::Components::ModelComponent>
{
	Model* pModel;
};

The actual templated functions can live in a cpp file rather than a header however. The templated functions for ModelComponent live in Engine/Scene/Model/ModelComponents.cpp.

template<>
void OnActivate<ModelComponent>(Component<ModelComponent>& p)
{
	p.GetRuntimeData().pModel = NULL;
}

OnActivate is usually no longer necessary, it is called when a component is created either from C++ or when an entity is spawned from file, and we no longer recommend spawning anything from C++.

template<>
void OnLoaded<ModelComponent>(Component<ModelComponent>& p, ComponentLoadHandles& handles,
	bool bWasPreviouslyCalled)
{
	bool bIsModelNull = p.GetRuntimeData().pModel == NULL;

	if (bWasPreviouslyCalled && !bIsModelNull)
	{
		return;
	}

	InitModel(p, handles);
}

OnLoaded is called when all the components associated with an entity has been loaded. It may be called again if the entity is merged using the merge function (bWasPreviouslyCalled will then be true). In this case the runtime data will be assigned using the model manager (which is in the ComponentLoadHandles struct).

template<>
void OnDeactivate<ModelComponent>(Component<ModelComponent>& p, ComponentLoadHandles& handles)
{
	ClearModel(p, handles);
}

OnDeactivate is used to clean up run time data (in this case the pointer to our model data is returned to the model manager which is inside the ComponentLoadHandles.

There are other options available for component declarations:

lua_generate says that LUA needs to know about this struct or enum and to generate the corresponding LUA files.

option (nanopb_msgopt).lua_generate = true;

from_header instructs nanopb not to generate the C++ struct representing this class as it already exists with the same name in the specified file.

option (nanopb_msgopt).from_header = "Engine/Maths/Vector3f.h";

doc_en and doc_jp give documentation (to be used by the Level Editor

option (usagi_msg).doc_jp = "ヘルスを減らす";

Individual variables can be documented in the same way as default values are set

required float fPrev = 1 [(usagi).doc_jp = "変更前の値"];

Entities

Entities theoretically have no data, they are simply a concept to tie together components. In practice for optimization purposes they do contain pointers to all of the components associated with a given entity but you should never be treating them as anything more than a handle. Generally they will be spawned from an entity definition file:

usg::Entity sector = pEntityLoader->SpawnEntityFromTemplate("MyEntity.vent", AttachEntity, spawnParams);

As you can see they will usually be spawned attached to another entity (often the world entity).

Events

Events, like components, are defined in protocol buffer files. They are the method of sending signals between systems on different entities (or systems on the same entity).

package usg.Events;

message ScaleModel
{
	required float fScale = 1;
}

Events are fired using the event manager:

ScaleModel scaleModel = { 0.5f };
inputs.eventManager->handle->RegisterEventWithEntity(*inputs.target, scaleModel, ON_ENTITY);

The above example will only fire the event on that entity but you can pass a bitfield of:
ON_ENTITY
ON_PARENTS
ON_CHILDREN

Meaning you can fire events anywhere in the hierarchy.

Some events, such as a loud noise or pausing the game you might want to fire at every entity at once

inputs.eventManager->handle->RegisterEvent(event);

If you want the events to fire over the network too you should use

RegisterNetworkEventWithEntity

and

RegisterNetworkEvent

Defining a component entity hierarchy

The contents of our entities are defined in yaml or in the level editor (note the level editor is not presently in a usable state).

TransformComponent:
MatrixComponent:
PlayerShipController:
ShipControlInput:
RigidBody:
  bDynamic: true
  bEnableCCD: true
  fDensity: 0.1
  bDisableGravity: true
  fLinearDamping: 2.0
  fAngularDamping: 4.0
SphereCollider:
  vCenter: {x: 0.0, y: 0.0, z: 0.0}
  fRadius: 5.0
  material:
    fDynamicFriction: 0.0
    fStaticFriction: 0.0
    fBounciness: 1.0 # Assuming it has a shield so it's bouncing off
    eRestitutionCombineMode: <%= Usg::CombineMode::MAX %>
    eFrictionCombineMode:  <%= Usg::CombineMode::MIN %>
ModelComponent:
  name: test/RazorTest.vmdf
Children:   # Add new children
  - Identifier:
      name: "cam"  # Name our camera 
    HMDCameraComponent:
    CameraComponent:
      fFOV: 40.0
      fNearPlaneDist: 0.1
      fFarPlaneDist: 1000.0
    TransformComponent:
      position: {x: 0, y: 0.0, z: -15.0}
    MatrixComponent:
    AudioListenerComponent:
    InitializerEvents:
      - SetFieldOfView:
          fFOV: 40.0

The above is an example of defining a player entity an implicit root entity and an explicit child entity (the camera). Note that there are other implicit entities here - every bone in the model RazorTest is automatically constructed with an Identifier (matching the bone name), TransformComponent, MatrixComponent and BoneComponent.

Initializer events are events to be fired off when the entity is spawned.

    InitializerEvents:
      - SetFieldOfView:
          fFOV: 40.0

These allow us to send one time events on creation rather than padding components with initialization data.
For example you may want to override a color for a model on a particular material - this is a feature already supported by events in runtime, so rather than doing something like adding an array of colors you want to set you just add an event for each color that you wish to override when the model is spawned.

If you wish to override one of these automatically generated entities (or any you have inherited) use the following syntax

Overrides:
  - EntityWithID: gunBase  # We are now overriding the components in a previously defined entity called gunBase
    SoundComponent:
    GunComponent:
      fRateOfFire: 2.0

The above would add Sound and Gun components to the entity with an identifier "gunBase" if it didn't already have them or override any specified values if it did.

But what if you have a customizable player, and customizable weapons, or worse yet the player needs a script attached for each mission - you can't have a yaml variant of every single combination!

Fear not, merge is here to help you out. Files designed to be merged into an existing entity look very similar to an entity definition, except for the keyword ~Merge:

- ~Merge:
  ScriptComponent:
    filename: "Scripts/PlayerCTFMode.lua"

You can apply these overrides to an entity using:

pEntityLoader->ApplyTemplateToEntity("Entities/CTFOverrides.vent", entity);

It's still not very flexible though, if you're in capture the flag mode you might want a component to attach the flag on collision, and what about one to wave it about as you run... but that would be in your hand, we don't want an overrides file per entity in the players hierarchy.

- ~Merge:
  ScriptComponent:
    filename: "Scripts/PlayerCTFMode.lua"
- ~Merge:
    entityWithID: hand_bone
  TurnableComponent:
  FlagAttachComponent:

Note the hand_bone will already have a TransformComponent as it was spawned from the hierarchy, there is no need to specify it again here (although equally there is no harm in doing so).

Systems

To start with lets look at the most basic and fundamental system, the system to generate a world matrix from a local transform. The system takes in a TransformComponent (local space) and produces a MatrixComponent (world space).

class ConstructWorldMatrix : public System
{
public:
	struct Inputs
	{
		Required<TransformComponent>		tran;
		Optional<MatrixComponent, FromParents>  parentMtx; 
	};

	struct Outputs
	{
		Required<MatrixComponent>      worldMtx;
	};

	DECLARE_SYSTEM(SYSTEM_TRANSFORM)

	static  void Run(const Inputs& inputs, Outputs& outputs, float fDelta)
	{
		Matrix4x4& mOut = outputs.worldMtx.Modify().matrix;
		mOut = inputs.tran->rotation;
		mOut.Translate(inputs.tran->position.x, inputs.tran->position.y, inputs.tran->position.z);

		if(inputs.parentMtx.Exists() && inputs.tran->bInheritFromParent)
		{
			mOut = mOut * inputs.parentMtx.Force()->matrix;
		}

		
	}
};

As you can see, all systems inherit from the class System. They should be placed in cpp files, nothing about them needs to be exposed in headers (the boiler plate will automatically take care of hooking them up). Standard classes should be one class per file, but feel free to add multiple systems per file, with no header required we found grouping systems made them easier to keep track of (and given that most systems only perform a couple of lines of code giving each its own cpp file can needlessly increase compile times).
They must also define a set of inputs and outputs:
Required components must be present in order for a system to run.
Optional components are not needed to run and will only be valid if they do. .Exists() can be used check this at run time.

Constant pointers to Required input components are accessed with the -> operator.
Non-constant references to output components are accessed with function .Modify()
Optional components must be confirmed to exist using the .Exists() function, and if present the function Force() can be used to access a reference to their component (which can then be used in the same way as Required components.

// Read required input
const Quaternionf& qRot = inputs.tran->rotation;
// Write required output
outputs.tran.Modify().rotation = qRot;
// Read optional input if present
if (inputs.tran.Exists())
{
    const Quaternionf& qRot = inputs.tran.Force()->rotation;
}

Inputs can be:

  • FromSelf - In this entity
  • FromSelfOrParents - Either in this entity or a parent
  • FromParents - In a parent entity
  • FromParentWith - A parent entity with a specified component
  • FromUnderCompInc - Either in this entity or a parent entity upto and including one with the specified component

FromParentWith and FromUnderCompInc are templated types, so if you wanted the EntityID of your parent with ModelComponent (which is usually going to be the root entity of a given actor in the world), you would specify:

Required< EntityID, FromParentWith<ModelComponent> > baseEntity;

This would be useful if you wanted to fire events from a child entity to a specific parent.

FromUnderCompInc* is a new type used to separate one hierarchy attached to another. The specified component acts as a stopper, saying to look at parents until you find one with the specified component and no further (in this case so that you don't try using an animated parents animation component on a static model component).

Required<ModelAnimComponent, FromUnderCompInc<ModelComponent> >	anim;

For thread safety outputs can only be FromSelf

You could even separate ConstructWorldMatrix out into two systems, one for entities with parents who also have matrices, and one for those that don't, this can be done by making the parent matrix component Required in the former, and adding the following to the latter:

EXCLUSION(IfHas<MatrixComponent, FromParents>)

Note that you can excluded multiple components per system, just put a comma between them.

class ConstructWorldMatrix : public System
{
public:
	struct Inputs
	{
		Required<TransformComponent>		tran;
	};

	struct Outputs
	{
		Required<MatrixComponent>      worldMtx;
	};

	DECLARE_SYSTEM(SYSTEM_TRANSFORM)
	// Prevent the system from being run on entities who have parents with MatrixComponents
	EXCLUSION(IfHas<MatrixComponent, FromParents>)


	static  void Run(const Inputs& inputs, Outputs& outputs, float fDelta)
	{
		Matrix4x4& mOut = outputs.worldMtx.Modify().matrix;
		mOut = inputs.tran->rotation;
		mOut.Translate(inputs.tran->position.x, inputs.tran->position.y, inputs.tran->position.z);	
	}
};

class ConstructParentedWorldMatrix : public System
{
public:
	struct Inputs
	{
		Required<TransformComponent>		tran;
		Required<MatrixComponent, FromParents>  parentMtx; 
	};

	struct Outputs
	{
		Required<MatrixComponent>      worldMtx;
	};

	DECLARE_SYSTEM(SYSTEM_TRANSFORM)

	static  void Run(const Inputs& inputs, Outputs& outputs, float fDelta)
	{
		Matrix4x4& mOut = outputs.worldMtx.Modify().matrix;
		mOut = inputs.tran->rotation;
		mOut.Translate(inputs.tran->position.x, inputs.tran->position.y, inputs.tran->position.z);
		mOut = mOut * inputs.parentMtx.Force()->matrix;		
	}
};

The following line is required to set up a system

DECLARE_SYSTEM(SYSTEM_TRANSFORM)

SYSTEM_TRANSFORM is an entry in the SystemCategory enum used to specify when a system should be run. You do not need to use this enum, you can (and should) extend it with a per project enum which is used to control execution order. As the world matrix should not be constructed until all movement has taken place SYSTEM_TRANSFORM is a relatively high value in the enum.

Update Signals

Run is not the only update signal:

// Called after all systems Run functions have been called
static void LateUpdate(const Inputs& inputs, Outputs& outputs, float fDelta)
{
    ...
}

// Called last at a safe point to update GPU data (e.g. constant buffers, particle vertex buffers)
static  void GPUUpdate(const Inputs& inputs, Outputs& outputs, GPUHandles* pGPUData)
{
   ...
}

UpdateEntityIO

You shouldn't need to alter this function, but you should be aware of it. The savings in a component entity system come in part from the amount of branching which is removed at run time. UpdateEntityIO is called not only whenever an entity is created or altered, but also whenever a parent of an entity is altered. Presently this means iterating over all potential systems that could run and determining if the entity and parents have the required components for the system.

This is an obvious candidate for optimization, a change of a single component will have no impact on the majority of systems. That said adding and removing components from high up in the hierarchy should not be done regularly to alter behavior - try and design in such a way that most components will either not change their components, or will only change a handful of times over their lifetime.

System Events

It's all very well and good to say that a system can only act on a single entity, but what happens when changes in one entity dictate changes in one above it in the hierarchy, or we just made a noise that gives us away to AI in the game.

We described making events before, but how do we "listen" for those. Easy, simply declaring an OnEvent function is enough

static void OnEvent(const Inputs& inputs, Outputs& outputs, const LoudNoise &evt)
{

}

Collision Events

If you define a system as being collidable and give it collision inputs it will be able to receive collision events. ColliderInputs are defined like standard inputs, except the components you receive are from the entity which collided with this one rather than the entity associated with the system.

You mark a system as collidable using the COLLIDABLE macro after DECALRE_SYSTEM, passing in the collision flags the system cares about.

struct ColliderInputs
{
	Required<usg::EntityID> entity;
	Required<usg::CollisionMasks> collisionMasks;
	Optional<usg::NetworkOwner, FromSelfOrParents> ownerUID;
};

DECLARE_SYSTEM(usg::SYSTEM_DEFAULT_PRIORITY) COLLIDABLE(usg::COLLM_ANY)

The OnCollision signal will provide the same Inputs and Outputs for the local entity as the other signals, but also passes in the ColliderInputs for the entity which collided with this one.

static void OnCollision(const Inputs& inputs, Outputs& outputs, const ColliderInputs& colliderInputs, const usg::Collision& collision)
{
    //...
}

If rather than collisions you are looking for overlaps anything entering your entities shape which is marked as a trigger you can use the OnTrigger event instead

struct TriggererInputs
{
	Required<usg::EntityID> entity;
	Required<usg::CollisionMasks> collisionMasks;
	Optional<usg::NetworkOwner, FromSelfOrParents> ownerUID;
};

DECLARE_SYSTEM(usg::SYSTEM_DEFAULT_PRIORITY) TRIGGERABLE

static void OnTrigger(const Inputs& inputs, Outputs& outputs, const TriggererInputs& triggererInputs, usg::TriggerEventType eventType)
{

}

You can also mark systems as RAYCASTERS

DECLARE_SYSTEM(usg::SYSTEM_DEFAULT_PRIORITY) RAYCASTER

static void OnRaycastHit(const Inputs& in, Outputs& out, const RaycastResult& result);

Here is an example of how to generate a raycast request from a system.

usg::AsyncRaycastRequest req;
req.vFrom = vLocalPos;
req.uMaxHits = 1;
req.uFilter = ~usg::COLLM_VEHICLE;
req.uRaycastId = N++;
req.vTo = vLocalPos - 1000 * vUp;
RaycastAsync(inputs, req);