- Super fast component iteration
- Multithreading support out of the box
- Fully runtime: you can describe Components and Systems without using templates
- Has C-API and can be used with other programing languages like ex: mustache-lua
- Has integration with profiler
The entity-component-system (also known as ECS) is an architectural pattern used mostly in game development. For further details:
- Entity Systems Wiki
- Evolve Your Hierarchy
- ECS on Wikipedia
- About ECS on github
- Telegram channel about ECS
#include <mustache/ecs/ecs.hpp>
int main() {
struct Position {
float x, y, z;
};
struct Velocity {
float x, y, z;
};
mustache::World world;
for (uint32_t i = 0; i < 1000000; ++i) {
(void) world.entities().create<Position, Velocity>();
}
const auto run_mode = mustache::JobRunMode::kCurrentThread; // or kParallel
world.entities().forEach([](Position& pos, const Velocity& dir) {
constexpr float dt = 1.0f / 60.0f;
pos.x += dt * dir.x;
pos.y += dt * dir.y;
pos.z += dt * dir.z;
}, run_mode);
return 0;
}
To be able to use mustache
, users must provide a full-featured compiler that
supports at least C++17.
The requirements below are mandatory to compile the tests:
CMake
version 3.7 or later.
An mustache::Entity
is a class wrapping an opaque uint64_t
value allocated by the mustache::EntityManager
.
Each mustache::Entity
has id
(identifier of Entity), version
(to check if Entity is still alive) and worldId
.
Creating an entity is as simple as:
#include <mustache/ecs/ecs.hpp>
mustache::World world;
const auto entity = world.entities().create();
And destroying an entity is done with:
world.entities().destroy(entity); // to destroy while the next world.update()
world.entities().destroyNow(entity); // to destroy right now
The general idea of ECS is to have as little logic in components as possible.
All logic should be contained in Systems.
But mustache has very weak requirements for struct / class to be component.
You must provide the next public methods (trivial or not):
- default constructor
- operator=
- destructor
As an example, position and direction information might be represented as:
struct Position {
float x, y, z;
};
struct Velocity {
float x, y, z;
};
To associate a Component with a previously created Entity call world.entities().assign<C>(entity);
with the component type, and any component constructor arguments:
// Assign a Position with x=1.0f, y=2.0f and z = 3.0f to "entity"
world.entities().assign<Position>(entity, 1.0f, 2.0f, 3.0f);
There are two ways to create Entity with given set of the Components:
world.entities().create<C0, C1, C2>(entity);
world.entities().begin()
.assign<C0>(/*args to create component C0*/)
.assign<C1>(/*args to create component C1*/)
.assign<C2>(/*args to create component C2*/)
.end();
You may wish to iterate over only changed components (see examples), for this reason mustache has built-in component version control system.
Each type you query non-const component from entity, mustache updates component version.
Mustache groups components into blocks and track WorldVersion(int value, updates every world.updte()
) for such blocks.
You can change this block size by call EntityManager::addChunkSizeFunction
, ex.
world.entities().addChunkSizeFunction<Component0>(1, 32); // store version for every 1..32 instance of Component0
world.entities().addChunkSizeFunction<Component1>(1, 1); // store version for every instance of Component1
To query component data from Entity you can call
EntityManager::getComponent<C>(Entity);
to update component version and get mutable componentEntityManager::getComponent<const C>(Entity);
to get cont component
In case of invalid entity or missed component nullptr will be returned.
To query all entities with a set of components assigned you can use the next method.
world.entities().forEach([](Entity entity, Component0& required0, const Component1& required1, const Component2* not_required) {
// Iterates over each entity with Component0 and Component1
// updates version of Component0
// not_required is ptr to const Component2(if entity has such component) or nullptr.
});
The other way is to use PerEntityJob, such job might be implemented with something like the following:
struct MySuperJob : public PerEntityJob<MySuperJob> {
void operator()(const Component0&, Component1&) {
// ...
}
ComponentIdMask checkMask() const noexcept override {
// we want to update Component1 only if Component0 has changed.
return ComponentFactory::makeMask<Component1>();
}
bool extraArchetypeFilterCheck(const Archetype& archetype) const noexcept override {
const auto component_id = ComponentFactory::instance().registerComponent<Component2>();
return !archetype.hasComponent(component_id); // skip entities with Component2
}
};
MySuperJob job; // create instance of job
// call operator() for each entity with Component0, changed Component2 and without Component2
job.run(world);
Mustache has builtin multithreading support, to run job in parallel mode, just use code like following:
// run job parallel
job.run(world, JobRunMode::kParallel);
// run forEach parallel
world.entities().forEach(function, JobRunMode::kParallel);
In the case where a component has dependencies on other components, a helper class exists that will automatically create these dependencies.
eg. The following will also add Component0 and Component1 components when a MainComponent component is added to an entity.
world.entities().addDependency<MainComponent, Component0, Component1>();
Systems implement behavior using one or more components. Implementations are subclasses of System and must implement the update() method, as shown below.
A basic movement system might be implemented with something like the following:
struct System2 : public System<System2> {
void onConfigure(mustache::World&, mustache::SystemConfig& config) override {
config.update_after = {"System0", "System1"};
config.update_before = {"System3"};
config.priority = 0; // default priority
}
void onUpdate(mustache::World&) override {
// run some jobs or do any other logic
}
};
Events are objects emitted by systems, typically when some condition is met. Listeners subscribe to an event type and will receive a callback for each event object emitted. An mustache::EventManager
coordinates subscription and delivery of events between subscribers and emitters. Typically subscribers will be other systems, but need not be.
Events are not part of the original ECS pattern, but they are an efficient alternative to component flags for sending infrequent data.
As an example, we might want to implement a very basic collision system using our Position
data from above.
First, we define the event type, which for our example is simply the two entities that collided:
struct Collision {
Entity left;
Entity right;
};
Next we implement our collision system, which emits Collision
objects via an mustache::EventManager
instance whenever two entities collide.
class CollisionSystem : public System<CollisionSystem> {
protected:
void onUpdate(mustache::World& world) override {
world.entities().forEach([&](Entity first, const AABB& first_aabb){
world.entities().forEach([&](Entity second, const AABB& second_aabb){
if (collide(first_aabb, second_aabb)) {
world.events().post(Collision{first, second});
}
});
});
}
};
Objects interested in receiving collision information can subscribe to Collision
events by first subclassing the class Receiver<_EventType>
:
struct DebugSystem : public System<DebugSystem>, public Receiver<Collision> {
void onConfigure(mustache::World&, mustache::SystemConfig& config) override {
world.events().subscribe<Collision>(this);
// you can use unsubscribe(); to stop getting events
}
void onEvent(const Collision &collision) {
Logger{}.debug("entities collided: %d and %d", collision.first.id().toInt(), collision.second.id().toInt());
}
};
You can also create subscriber by providing callback:
// sub is a std::unique_ptr<Receiver<Collision> >, you can use sub.reset() to unsubscribe
auto sub = event_manager.subscribe<Collision>([](const Collision& event){
// some logic
});
You can use mustache in you projects by doing the following steps
- Clone or copy sources into your project eg.
cd third_party && git submodule add https://github.com/kirillochnev/mustache.git
- Add mustache subdirectory with cmake
add_subdirectory(third_party/mustache)
- Link mustache to your project
target_link_libraries(${PROJECT_NAME} mustache)
The proposed entity-component system is incredibly fast to iterate entities and components, this is a fact. Some compilers make a lot of optimizations because of how mustache works, some others aren't that good. In general, if we consider real world cases, mustache is somewhere between a bit and much faster than many of the other solutions around, although I couldn't check them all for obvious reasons.
Let's make a little benchmark.
For this benchmark we will compare some ECS frameworks and classic OOP
- mustache
- mustache binding to LuaJit
- EnTT (using owning groups)
- EntityX
- OpenEcs
- OOP
- OOP with object pool
We create entities with 2 components
struct Position {
glm::vec<3, int32_t> value {0, 0, 0};
};
struct Velocity {
int32_t value { 1 };
};
And call the next function for them
inline void updatePositionFunction(Position& position, const Velocity& velocity) {
constexpr glm::vec<3, int32_t > forward {1, 1, 1};
position.value += forward * velocity.value;
}
Create time: (lower is faster)
Update time: (lower is faster)
Mustache has integration with EasyProfiler.
To enable profiling build mustache with cmake option MUSTACHE_BUILD_WITH_EASY_PROFILER=ON
.
You can choose profile depth by set MUSTACHE_PROFILER_LVL
to 0, 1, 2 or 3.
This profiling result of mustache unit-tests: