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.
Defining a new component
To define a new component type, a few steps must be followed.
- Declare the new component structure, deriving from Cogs::ComponentModel::Component.
- Provide specialization for the getNameImpl function
- Register the new component in the type database
- Register fields and methods used by the component
#include "Foundation/ComponentModel/Component.h"
#include "Foundation/Reflection/TypeDatabase.h"
{
public:
int data = 42;
{
};
}
};
template<>
inline Cogs::StringView getNameImpl<MyComponent>() {
return "MyComponent"; }
#include "MyComponent.h"
void main()
{
MyComponent::registerType();
}
Base class for Component instances.
Definition: Component.h:143
static COGSFOUNDATION_API void registerType()
Register the Component type in the global type database.
Definition: Component.cpp:17
Field definition describing a single data member of a data structure.
Definition: Field.h:68
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
Contains reflection support.
Definition: Component.h:11
Represents an unique name.
Definition: Name.h:70
Managing components in a pool
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.
#include "Foundation/ComponentModel/ComponentPool.h"
#include "MyComponent.h"
{
};
#include "MyComponentPool.h"
void main()
{
MyComponentPool pool;
auto handle = pool.allocateComponent();
auto myComponent = handle.resolveComponent<MyComponent>();
printf("Data: %d\n", myComponent->data);
for (auto & component : pool) {
printf("Data: %d\n", component.data);
}
}
Typed component pool.
Definition: ComponentPool.h:124
Attaching a component to an Entity
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.
#include "MyComponentPool.h"
#include "Foundation/ComponentModel/Entity.h
void main()
{
MyComponentPool pool;
...
auto entity = new Cogs::ComponentModel::Entity();
entity->addComponent(pool.allocateComponent());
auto myComponent = entity->getComponent<MyComponent>();
if (myComponent) {
printf("Data: %d\n", myComponent->data);
}
...
}
Change tracking
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;
bool hasFieldChanged(
FieldId id)
const;
void resetFieldChanged(
FieldId id);
void resetChanged();
void resetCarryChanged();
...
}
uint16_t FieldId
Type used to index fields.
Definition: Name.h:54
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"
{
float data = 0.0f;
};
void main()
{
MyComponent m;
m.data = 100.0f;
m.setChanged();
...
if (m.hasChanged()) {
if (m.hasFieldChanged(&MyComponent::data)) {
m.resetFieldChanged(&MyComponent::data);
}
if (m.hasFieldChanged(&MyComponent::otherField)) {
}
if (needAnotherUpdate) {
m.setFlag(ComponentFlags::CarryChanged);
Engine->triggerRedraw(...);
}
}
}
void setFieldChanged(const Reflection::FieldId fieldId)
Sets the component to the ComponentFlags::Changed state without carry.
Definition: Component.h:218
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"
{
float data = 0.0f;
float otherData = 0.0f;
static void registerType()
{
...
}
};
void main()
{
MyComponent::registerType();
MyComponent m;
m.data = 100.0f;
m.setFieldChanged(&MyComponent::data);
...
if (m.hasFieldChanged(&MyComponent::data)) {
}
if (m.hasFieldChanged(&MyComponent::otherData)) {
}
if (m.hasChanged()) {
}
}
Component change handling in rendering frame
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.
A component using another component can
- 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.