Skip to content

kirillochnev/mustache

Repository files navigation

mustache - A fast, modern C++ Entity Component System

Build Status

Why mustache

Introduction

The entity-component-system (also known as ECS) is an architectural pattern used mostly in game development. For further details:

Code Example

#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;
}

Requirements

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.

Overview

Entities

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

Components (entity data)

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

Creating components

As an example, position and direction information might be represented as:

struct Position {
    float x, y, z;
};
struct Velocity {
    float x, y, z;
};

Assigning components to entities

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();

Component version control

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

Component access

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.

Querying entities and their components

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);

Supported function arguments

The function passed to EntityManager::forEach or the operator() of a PerEntityJob can accept the following arguments (in any order):

  1. Components

    • Passed as reference (T& or const T&) — required.
    • Passed as pointer (T* or const T*) — optional, nullptr if entity doesn't have the component.
    • If const, version tracking is not updated.
    • If non-const, version is updated.
  2. Entity — the current entity.

  3. World& — world reference.

  4. JobInvocationIndex — provides per-invocation info:

struct JobInvocationIndex {
    ParallelTaskId task_index;
    ParallelTaskItemIndexInTask entity_index_in_task;
    ParallelTaskGlobalItemIndex entity_index;
    ThreadId thread_id;
};

Job customization

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.

Multithreading

Mustache has built-in multithreading support:

job.run(world, JobRunMode::kParallel);
world.entities().forEach(func, JobRunMode::kParallel);

Component hooks

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:

Component dependencies

Automatically assign dependencies when assigning a component:

world.entities().addDependency<MainComponent, Component0, Component1>();

Systems

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
    }
};

Events

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){ });

Integration

add_subdirectory(third_party/mustache)
target_link_libraries(${PROJECT_NAME} mustache)

Performance

Create time: Create time

Update time: Update time

Profiling

Enable with:

cmake -DMUSTACHE_BUILD_WITH_EASY_PROFILER=ON

Then use EasyProfiler viewer:

Profiling result

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •