vecs
Fast, flexible ecs in C++ with ergonomic API
Loading...
Searching...
No Matches
Vecs Documentation

Concepts

Vecs is an ECS (Entity Component System) framework built for C++. ECS encourages data-driven design, composition, and modularity by decoupling data from behavior.

Entity: An identifier associated with a collection of components

Component: Any piece of data

System: A function that operates on entities with specific components

Vecs extends traditional ECS by offering a flexible systems mechanism with automatic dependency injection, allowing system functions to request parameters by specifying them in the function’s parameter list, in any order.

Features

System Parameters

There are several built-in system parameters included to facilitate common tasks:

Time

Provides access to delta time in milliseconds between each ECS world tick:

world.add_system([](const Time& time) {
printf("The delta time between each frame: %f\n", time.delta);
});

Local<T>

A container for local data specific to a system:

world.add_system([](Local<int>& i) {
++(*i);
printf("The local value is: %i\n", *i);
});

Resource<T>

A container for data shared between systems:

int resource_data = 123;
world.add_resource(&resource_data);
world.add_system([](Resource<int>& i) {
++(*i); // Mutate resource
});
world.add_system([](const Resource<int>& i) {
printf("The int resource value is: %i\n", *i); // Read-only resource access
});

Query<...Components>

Provides a query mechanism for component data in the ECS world:

world.spawn().insert(Position {}, Velocity {});
world.add_system([](const Time& time, Query<Position, const Velocity>& query) {
for (auto [p, v] : query) {
p.x += v.x * time.delta;
p.y += v.y * time.delta;
}
});
// Optional<T> allows querying for optional components
world.add_system([](
const Time& time,
Query<Position, Optional<const Velocity>>& query
) {
for (auto [pos, maybe_vel] : query) {
if (maybe_vel) pos.x += maybe_vel->x * time.delta;
}
});
// Query<Components...>::get(Entity) allows obtaining a single `Record` matching the query
world.add_system([](Query<const Position>& query) {
Entity e = 123;
auto record = query.get(e);
if (record) {
auto [position] = *record;
printf("Entity `%zu` has position component: (%f, %f)\n", e, position.x, position.y);
}
});
// Query<Components...>::get_single() allows obtaining a single `Record` if a single entity matches the query
world.add_system([](Query<Entity, const Position, const Velocity>& query) {
if (auto record = query.get_single()) {
auto [entity, position, velocity] = *record;
printf(
"A single entity `%zu` has position and velocity component: (%f, %f), (%f, %f)\n", e,
velocity.x, velocity.y,
position.x, position.y
);
}
});

Observer<...Components>

Provides an observer mechanism for tracking entity component changes within the current tick per system:

world.spawn().insert(3);
world.spawn().insert(2);
world.spawn().insert(1);
world.add_system([](const Observer<int>& observer) {
for (const auto& entity : observer.added()) {
// Entities with `int` added for the first time
}
for (const auto& entity : observer.inserted()) {
// Entities with `int` inserted, including re-inserts
}
for (const auto& entity : observer.removed()) {
// Entities with `int` removed
}
});

Commands

Enables safe, deferred interactions with entities from within systems:

world.spawn().insert(123);
world.add_system([](Commands& commands, Query<Entity, int>& query) {
for (auto [e, i] : query) {
if (i == 123) {
commands.entity(e).despawn();
}
}
});

Custom

Easily define custom system parameters for use within your systems:

struct MySystemParam {
int i {123};
};
template <>
struct into_system_param<MySystemParam> {
static MySystemParam get(World&) {
return MySystemParam {}; // Constructed at system creation time
}
};
world.add_system([](const MySystemParam& param) {
printf("MySystemParam i value is: %i\n", param.i);
});

Component Storage

Vecs provides archetypal (default) and optional sparseset storage for components.

Archetype

Archetype storage is optimized for iteration, preferring less frequent changes to the archetype type (for example, inserting new components or removing existing ones kick-off an internal process that changes the archetype type).

struct MyComponent {}; // Default is archetypeal

SparseSet

SparseSet storage is optimized for frequent component insertion/removal, with slower iteration.

struct MyComponent {};
template<>
struct into_component_storage<MyComponent> {
using storage_type = StorageType::SparseSet;
};

Note: Only consider SparseSet if archetypal storage does not meet expectations.

Component Bundles

Vecs provides a way to define component bundles, allowing easy grouping of related components with default values.

// Without a bundle
world.spawn().insert(Transform {}, ProjectionMatrix::new_3d(/*...*/), Camera {});
// With a bundle
struct Camera3dBundle: Bundle<ProjectionMatrix, Transform, Camera> {
Camera3dBundle() :
Bundle(ProjectionMatrix::new_3d(/* ..*/), Transform {}, Camera {}) {}
};
world.spawn().insert(Camera3dBundle {});

Component Hooks

Vecs provides a simple interface for executing code when components are added, inserted, or removed from entities in the ECS world.

Supported Hooks:

There are three hooks you can use:

  • on_add: Runs when a component is added to an entity that didn't already have it
  • on_insert: Runs every time a component is inserted into an entity (including re-inserts)
  • on_remove: Runs when a component is removed or its entity is despawned

Custom Components

User-defined component types support hooks via static functions or instance methods:

struct MyComponent {
float data {0.f};
// Static hook - runs when component is added
static void on_add(World& world, Entity e) {
printf("MyComponent was added to an entity\n");
// Access component data if needed:
// MyComponent* comp = world.entity(e).get_component<MyComponent>();
}
// Instance method hook - runs when component is added
// Has direct access to component data
void on_insert(World& world, Entity e) {
printf("MyComponent was inserted with data: %f\n", data);
}
// Instance method hook - runs when component is removed
// Has direct access to component data before deletion
void on_remove(World& world, Entity e) {
printf("MyComponent was removed, had data: %f\n", data);
}
};
// Examples that trigger hooks:
auto entity = world.spawn(); // EntityBuilder
entity.insert(MyComponent {1.f}); // Triggers on_add and on_insert
entity.insert(MyComponent {2.f}); // Triggers only on_insert
entity.remove<MyComponent>(); // Triggers on_remove
world.despawn(entity); // Triggers on_remove if component exists

Built-In Types

For types you can't modify (like built-in or third-party types), you can still add hooks by specializing the ComponentHooks template:

template<>
struct vecs::ComponentHooks<int> {
static void on_add(World& world, Entity e) {
printf("int was added to an entity\n");
}
}
world.spawn().insert(123);
Definition vecs.h:2537

Note: Component hooks using ComponentHooks template specialization must be static functions as there's no component instance available when specializing templates.

Entity Hierarchy

Easily manage entity hierarchy:

Add children with EntityBuilder in lambda:

world.spawn()
.insert(Position {}, Velocity {}, Tag {"Parent"})
.with_children([](EntityBuilder& builder) {
return builder.insert(Position {}, Velocity {}, Tag {"Child"});
});

Add children after insertion:

EntityBuilder parent = world.spawn().insert(Position {}, Velocity {}, Tag {"Parent"});
EntityBuilder child = world.spawn().insert(Position {}, Velocity {}, Tag {"Child"});
parent.add_child(child); // EntityBuilder is implicitly converted to Entity here

Insert new child in parent entity with components:

world.spawn()
.insert(Position {}, Velocity {}, Tag {"Parent"})
.insert_child(Position {}, Velocity {}, Tag {"Child"});

Query for parent and their components:

world.add_system([](
Query<Parent>& query,
Query<Transform>& transform_query
) {
for (auto [parent] : query) {
auto transform = transform_query.get(parent);
if (transform) {
printf("Parent translation: (%f, %f)\n", transform->translation.x, transform->translation.y);
}
}
});

Getting Started

A C++ compiler with at least support for C++17 is required. The easiest way to get started is by adding CPM.cmake to your project:

include(cmake/CPM.cmake)
CPMAddPackage("gh:twct/vecs#git_tag_or_rev_sha")
target_link_libraries(MyTarget PRIVATE vecs)

Build & run unit tests

$ mkdir build && cd build
$ cmake -G Ninja -DVECS_BUILD_TESTS=on ..
$ ninja test
# or run specific test
$ ./ecs_test_runner --test-case="test name"

Build docs

$ cd docs && make
# bundle as zip
$ make zip

Benchmarks

A basic benchmarking program is included to offer insight into potential performance expectations:

for (size_t i = 0; i < NUM_ENTITIES; ++i) {
world.spawn().insert(Position {}, Velocity {});
}
world.add_system([](const Time& time, Query<Position, const Velocity>& query) {
for (auto [p, v] : query) {
p.x += v.x * time.delta;
p.y += v.y * time.delta;
}
});
for (size_t i = 0; i < NUM_ITERATIONS; ++i) {
world.progress();
}

Environment:

  • CPU: 12th Gen Intel i5-12600K (16) @ 4.900GHz
  • Compiler: GCC 14.2.1 (Arch), single-core execution, optimized with -O3

Execution time @ 1k iterations:

Entities Execution Time Cost-per-Entity (CPE)
1k entities 764.14 µs 0.76 ns
100k entities 56.51 ms 0.57 ns
1M entities 989.39 ms 0.99 ns

Compatibility

Vecs has been tested with the following compilers:

  • Clang 15.0.0 (macOS)
  • GCC 11.4.0 (Linux)
  • MSVC 19.41.34120 (Windows)

GitHub Repository

The source code for Vecs is available on GitHub.