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);
});
world.add_system([](const Resource<int>& i) {
printf("The int resource value is: %i\n", *i);
});
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;
}
});
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;
}
});
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);
}
});
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()) {
}
for (const auto& entity : observer.inserted()) {
}
for (const auto& entity : observer.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 {};
}
};
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).
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.
world.spawn().insert(Transform {}, ProjectionMatrix::new_3d(), Camera {});
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 void on_add(World& world, Entity e) {
printf("MyComponent was added to an entity\n");
}
void on_insert(World& world, Entity e) {
printf("MyComponent was inserted with data: %f\n", data);
}
void on_remove(World& world, Entity e) {
printf("MyComponent was removed, had data: %f\n", data);
}
};
auto entity = world.spawn();
entity.insert(MyComponent {1.f});
entity.insert(MyComponent {2.f});
entity.remove<MyComponent>();
world.despawn(entity);
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<>
static void on_add(World& world, Entity e) {
printf("int was added to an entity\n");
}
}
world.spawn().insert(123);
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);
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.