Entity Components - m1keall1son/ofxMediaSystem GitHub Wiki
This design pattern is currently and historically used in the gaming industry. Th basic principle is separation of behavior and state favoring a compositional approach to object design. In ofxMediaSystem the best practice is to separate state and behavior in ways that create re-usable code and make development easier and more consistent across projects. It is possible to make very complex component-system interactions, for instance building a very sophisticated 3D object renderer. However, if you just need to draw some images and simple UI to the screen, combining state and logic into components that leverage oF's renderer (as seen in the overview example) may be a simpler approach.
The principle generic object in the ofxMediaSystem is the Entity
class. Fundamentally, it is an integer identifier, std::bitset
describing its current state and a reference to its parent scene (this reference is kept purely for interface convenience and could be removed in future versions).
An Entity is an identification handle that pulls together distributed sets of data. By default an Entity
contains an ofNode
for 3D positioning and an EntityGraph
component for easy scene graphing. For convenience these components can be accessed via a passthrough interface on the entity itself.
//assume a scene is created
EntityHandle handle = scene.createEntity();
EntityRef entity = handle.lock();
//set transform properties
entity->setPosition(x, y, z);
entity->setScale(x, y, z);
//etc.
//or access the ofNode directly
auto node = entity->getComponent<ofNode>();
node->setPosition(x, y, z);
node->setScale(x, y, z);
//etc.
...
EntityHandle otherEntityHandle = scene.createEntity();
entity->addChild(otherEntityHandle);
entity->setPosition(10,10,10);
auto other = otherEntityHandle.lock();
glm::mat4 transformMatrix = other->getGlobalTransformMatrix();
assert(other->getPosition() == glm::vec3(0));
assert(entity->getPosition() == glm::vec3(transformMatrix[3]));
//or access the EntityGraph component directly
auto graph = entity->getComponent<EntityGraph>();
graph->removeChild(otherEntityHandle);
Components in ofxMediaSystem are any structs, classes, or primitive types added to an Entity
at runtime. Creating a component is as simple as adding the type to the entity via the convenience methods on the Entity
class:
//assume an entity is created
EntityRef targetEntity = ...;
//a struct containing some data
struct TargetPractice {
explicit TargetPractice( ofRectangle area ):target(area){}
ofRectangle target;
int hitCount{0};
};
//a handle (or weak_ptr) to the component instance is returned from a call to createComponent
std::weak_ptr<TargetPractice> targetDataHandle = targetEntity->createComponent<TargetPractice>(ofRectangle(0,0,10, 10));
//an alternative syntax could be
std::weak_ptr<ofRectangle> rectHandle = targetEntity->createComponent<ofRectangle>(0,0,10,10);
std::weak_ptr<int> hitCountHandle = targetEntity->createComponent<int>(0);
//assume an entity is created
EntityRef contestantEntity = ...;
//a struct containing some data
struct Contestant {
bool hasShot{false};
int score{0};
float handicap{0.f};
};
//a handle (or weak_ptr) to the component instance is returned from a call to createComponent
std::weak_ptr<Contestant> contestantDataHandle = contestantEntity->createComponent<Contestant>();
auto contestantData = contestantDataHandle.lock();
contestantData->handicap = 2.2;
NOTE: Entities can only have ONE version of a type attached as a component. Adding a second, destroys the first and replaces it with the second.
std::weak_ptr<ofRectangle> rectHandle = targetEntity->addComponent<ofRectangle>(0,0,10,10);
std::weak_ptr<ofRectangle> secondRectHandle = targetEntity->addComponent<ofRectangle>(0,0,20,20);
//first handle is no longer valid
assert(rectHandle.expired());
std::shared_ptr<ofRectangle> rect = targetEntity->getComponent<ofRectangle>();
//only the second rect survives.
assert(rect.get() == secondRectHandle.lock()->get());
Accessing an entity can be done by asking for the component by type.
//retrieving a component can be done two ways:
//1. retrieve by handle, useful for checking if the component still exists or you want to share or store it
std::weak_ptr<Contestant> contestantDataHandle = contestantEntity->getComponentHandle<Contestant>();
//2. retrieve directly, perform the .lock() step automatically for convenience. can result in a nullptr if
//component no longer exists.
std::shared_ptr<Contestant> contestantData = contestantEntity->getComponent<Contestant>();
//can also check to see if an entity has a component
bool has_it_or_not = contestantEntity->hasComponent<Contestant>();
NOTE: it is an ofxMediaSystem best practice to not store std:shared_ptr<...>
s to entities or components directly in a scene or app, but they should be stored by handle (std::weak_ptr<...>). This is because calling
destroy()` methods on an entity will remove them from the scenes's knowledge but not free the resources until the ref count reaches 0.
Components are stored at the scene scope in a ComponentManager
, which is a convenience wrapper around a map of keyed by entity id, the value attached to each key is a list of generic component pointers (std::shared_ptr) associated with that entity.
NOTE: a major optimization of this mechanism will be incorporating a more efficient small object allocator as all components are currently being allocated on the heap.
There is a specialized iterator that can be retrieved from ComponentManager::getComponents<typename>
for systems that want to iterate over all current components of a specific type.
ComponentManager componentManager;
...
auto myComponents = componentManager.getComponents<MyComponent>();
auto iter = myComponents.iter();
while(auto it = iter.next()){
//do something with (*it)
}