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_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()) {
}
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 {});
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 && 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.