- 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
- Built-in component lifecycle hooks
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 a component.
You must provide the following 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:
world.entities().assign<Position>(entity, 1.0f, 2.0f, 3.0f);
There are two ways to create an Entity with a given set of Components:
world.entities().create<C0, C1, C2>();
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. Mustache has a built-in version control system.
Each time you request a non-const component from an entity, the version is updated.
You can configure granularity by:
world.entities().addChunkSizeFunction<Component0>(1, 32); // version per chunk of 32
world.entities().addChunkSizeFunction<Component1>(1, 1); // version per instance
world.entities().getComponent<C>(entity); // mutable (and version will be updated)
world.entities().getComponent<const C>(entity); // const access
Returns nullptr
if the component is missing or entity is invalid.
world.entities().forEach([](Entity entity, Component0& required0, const Component1& required1, const Component2* optional2) {
// iterate over all entities with required0 and required1
// optional2 may be nullptr if Component2 is missing
});
Alternative: use PerEntityJob
:
struct MySuperJob : public PerEntityJob<MySuperJob> {
void operator()(const Component0&, Component1&) {
// iterate over all entities with required0 and required1
}
};
MySuperJob job;
job.run(world);
The function passed to EntityManager::forEach
or the operator()
of a PerEntityJob
can accept the following arguments (in any order):
-
Components
- Passed as reference (
T&
orconst T&
) — required. - Passed as pointer (
T*
orconst T*
) — optional,nullptr
if entity doesn't have the component. - If
const
, version tracking is not updated. - If non-const, version is updated.
- Passed as reference (
-
Entity
— the current entity. -
World&
— world reference. -
JobInvocationIndex
— provides per-invocation info:
struct JobInvocationIndex {
ParallelTaskId task_index;
ParallelTaskItemIndexInTask entity_index_in_task;
ParallelTaskGlobalItemIndex entity_index;
ThreadId thread_id;
};
Jobs that inherit from BaseJob
(or PerEntityJob
) can override advanced hooks:
Function | Description |
---|---|
checkMask |
Mask of components to track changes |
updateMask |
Mask of components to mark as updated |
name , nameCStr |
Job name string |
extraArchetypeFilterCheck |
Additional filter for archetypes |
extraChunkFilterCheck |
Additional filter per-chunk |
applyFilter |
Full override of entity filter logic |
taskCount |
Split job into N tasks |
onTaskBegin / onTaskEnd |
Hook per task |
onJobBegin / onJobEnd |
Hook before and after whole job |
See the source of BaseJob
for more details.
Mustache has built-in multithreading support:
job.run(world, JobRunMode::kParallel);
world.entities().forEach(func, JobRunMode::kParallel);
Mustache allows defining optional functions for components to control their lifecycle:
Function | When it's called |
---|---|
static void afterAssign(...) |
After assigning the component to an entity |
static void beforeRemove(...) |
Before removing the component from an entity |
static void clone(...) |
When cloning a component during entity clone |
static void afterClone(...) |
After cloning is done, used for finalization (e.g. remap references) |
These functions are automatically detected and invoked by Mustache.
Examples:
- See
example/component_add_remove_events.cpp
forafterAssign
andbeforeRemove
usage. - See
example/clone_entity.cpp
for cloning andafterClone
use.
Automatically assign dependencies when assigning a component:
world.entities().addDependency<MainComponent, Component0, Component1>();
struct System2 : public System<System2> {
void onConfigure(World&, SystemConfig& config) override {
config.update_after = {"System0", "System1"};
config.update_before = {"System3"};
}
void onUpdate(World&) override {
// Your logic
}
};
Define:
struct Collision { Entity left, right; };
Emit:
world.events().post(Collision{first, second});
Subscribe:
struct DebugSystem : public System<DebugSystem>, Receiver<Collision> {
void onConfigure(World&, SystemConfig&) override {
world.events().subscribe<Collision>(this);
}
void onEvent(const Collision &event) override {}
};
Lambda-style:
auto sub = event_manager.subscribe<Collision>([](const Collision& event){ });
add_subdirectory(third_party/mustache)
target_link_libraries(${PROJECT_NAME} mustache)
Enable with:
cmake -DMUSTACHE_BUILD_WITH_EASY_PROFILER=ON
Then use EasyProfiler viewer: