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_add_system([](Query<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);
}
});

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

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 && ninja test

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: Intel(R) Core(TM) i3-10110U CPU @ 2.10GHz
  • Compiler: GCC 11.4.0 (Ubuntu), single-core execution, optimized with -O3

Execution time @ 1k iterations:

Entities Execution Time Cost-per-Entity (CPE)
1k entities 660.00 µs 0.66 ns
100k entities 96.55 ms 0.97 ns
1M entities 2.03 s 2.03 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.