Components can be created deriving from the provided Cogs::ComponentModel::Component base class. A Cogs::ComponentModel::ComponentPool can be used to control manage component instances.
To define a new component type, a few steps must be followed.
// MyComponent.h
#include "Foundation/ComponentModel/Component.h"
#include "Foundation/Reflection/TypeDatabase.h"
using namespace Cogs::Reflection;
class MyComponent : public Cogs::ComponentModel::Component
{
public:
int data = 42; // Single data member.
static void registerType()
{
Field fields[] = {
Field(Name("data"), &MyComponent::data),
};
TypeDatabase::createType().setBase().setFields(fields);
}
};
// Specialize for the reflection system.
template<> inline Cogs::StringView getNameImpl() { return "MyComponent"; }
//Main.cpp
#include "MyComponent.h"
void main()
{
MyComponent::registerType();
// The component is now ready to use.
}
Components are made to be managed in pools. A component pool will handle a single subtype of components and set up continuous storage for a set of components in memory. New component instances will be allocated from this storage, making component creation (of trivially constructible components) a constant time operation with no memory allocations.
Note that the ComponentPool implementation does raw copying of the component data number of components exceeds pool bucket size, not C++ move operator. Only data structures that allow memcpy type operation can be used. Ex: no pointer to internal component data. Note Microsoft std::vector in debug builds fails here.
// MyComponentPool.h
#include "Foundation/ComponentModel/ComponentPool.h"
#include "MyComponent.h"
class MyComponentPool : public Cogs::ComponentModel::ComponentPool
{
};
//Main.cpp
#include "MyComponentPool.h"
void main()
{
// Create a pool
MyComponentPool pool;
// Allocate a new component
auto handle = pool.allocateComponent();
// Resolve the handle to an actual pointer to the held component
auto myComponent = handle.resolveComponent();
printf("Data: %d\n", myComponent->data);
// Loop over the contents of the pool.
for (auto & component : pool) {
printf("Data: %d\n", component.data);
}
}
Components are not useful by themselves, but need to be attached to a single Cogs::ComponentModel::Entity instance. Using entities, we can compose different components together to control the functionality we want.
//Main.cpp
#include "MyComponentPool.h"
#include "Foundation/ComponentModel/Entity.h
void main()
{
MyComponentPool pool;
...
auto entity = new Cogs::ComponentModel::Entity();
// Add a MyComponent instance to the entity.
entity->addComponent(pool.allocateComponent());
// We can now lookup the MyComponent instance in the entity.
auto myComponent = entity->getComponent();
if (myComponent) {
printf("Data: %d\n", myComponent->data);
}
// We must track the lifetime of the entity manually and release the component
// from the pool when no longer in use.
...
}
There is no automatic internal tracking of state changes in components. When deriving from the Cogs::ComponentModel::Component class and defining data fields, these are regular C++ member fields and as such do not support any automatic change notification.
It is therefore up to the application developer to track state changes to Component instances manually, and the Component class includes several facilities to make this task easier.
Each Component contains a set of internal flags, which can be manipulated using a set of utility methods available on the Component instance. The most relevant methods for change tracking are the following:
class Component
{
...
void setChanged();
bool hasChanged() const;
void setFieldChanged(FieldId id);
bool hasFieldChanged(FieldId id) const;
void resetFieldChanged(FieldId id);
void resetChanged();
void resetCarryChanged();
...
}
When changing a field on a derived class, the typical method for signaling a component-wide change would be as follows:
#include "Foundation/ComponentModel/Component.h"
class MyComponent : Cogs::ComponentModel::Component
{
float data = 0.0f;
};
void main()
{
MyComponent m;
// Change some data in a component field.
m.data = 100.0f;
// Mark only given field changed
m.setFieldChanged(&MyComponent::data);
// Notify that we changed the state (all fields marked changed).
m.setChanged();
...
// Component update from component itself or owning System.
// This will evaluate to true due to the call to setChanged()
if (m.hasChanged()) {
// Update
if (m.hasFieldChanged(&MyComponent::data)) {
// Update changed data.
// Optional reset of field changed flag.
m.resetFieldChanged(&MyComponent::data);
}
if (m.hasFieldChanged(&MyComponent::otherField)) {
// Update changed otherField.
}
// Possible triggering another rendering update of Component.
if (needAnotherUpdate) {
// Avoid Clearing changed flags in after update finished.
m.setFlag(ComponentFlags::CarryChanged);
Engine->triggerRedraw(...);
}
}
}
For more fine-grained control, change tracking methods with field information are supported. The internal information on which field has changed is stored using the FieldId identifier assigned to each field in a Type. Note that only the first 24 fields are tracked individually, the remaining fields share changed flag.
For convenience, templated overloads are provided, which automatically resolve the field id of a pointer to member variable.
#include "Foundation/ComponentModel/Component.h"
class MyComponent : Cogs::ComponentModel::Component
{
float data = 0.0f;
float otherData = 0.0f;
static void registerType()
{
// Type registration, field registration
...
}
};
void main()
{
MyComponent::registerType();
MyComponent m;
// Change some data in a component field.
m.data = 100.0f;
// Notify that we changed the state.
m.setFieldChanged(&MyComponent::data);
...
// This will evaluate to true, since the data field changed.
if (m.hasFieldChanged(&MyComponent::data)) {
// Update
}
// This will not evaluate to true, since only the data field changed.
if (m.hasFieldChanged(&MyComponent::otherData)) {
// Update
}
// The general call will still evaluate to true due to the call to setFieldChanged(),
// ignoring which specific field has changed.
if (m.hasChanged()) {
// Update
}
}
More details are specified in Cogs.Core Component documentation.
The carryChanged flag for components are first reset using resetCarryChanged().
The components are updated. Each component can test overall hasChanged() and/or hasFieldChanged(..) to detect required updates.
resetChanged() for components are called.
Check for changes in that component, using either hasChanged() or hasFieldChanged(..), but not reset any flags.
Update fields in the component. If the referred component has got its resetCarryChanged() as in step 1. above the CarryChanged will be set again ensuring that the component (and fields) will be marked changed in the next rendering frame.
Note that this may lead to a extra update of the referred component. Expensive updates can be avoided if component checks and resets its fieldChanged flags.