Element is a small C++20 ECS library. It is header-only, uses the elm
namespace, and ships Meson metadata so it can be used as a subproject or an
installed dependency.
The core is intentionally narrow:
elm::Registryowns entities, components, resources, and archetype storage.elm::Systemwraps a callable plus the component/resource access metadata needed by a scheduler.elm::EventQueueis a typed per-frame event buffer.- The public headers live under
include/elm.
Oak scripting support is kept out of this package. Use
element-oak-bridge
when you want Oak scripts to produce elm::System values.
As a Meson subproject:
element_dep = dependency('element', fallback: ['element', 'element_dep'])
executable('game',
'main.cpp',
dependencies: element_dep,
)If the source tree is already available as a subproject:
element_project = subproject('element')
element_dep = element_project.get_variable('element_dep')Install it for use from another project:
meson setup build -Dtests=disabled
meson install -C buildThen consume it with:
element_dep = dependency('element', required: true)Point PKG_CONFIG_PATH at the install prefix's pkg-config directory if Meson
cannot find the installed dependency.
Tests are enabled when Element is configured as the top-level project and disabled by default when it is used as a subproject.
meson setup build
meson compile -C build
meson test -C buildFor an explicit test build:
meson setup build -Dtests=enabled#include <elm/registry.hpp>
#include <elm/system.hpp>
struct Position {
float x = 0.0f;
float y = 0.0f;
};
struct Velocity {
float x = 0.0f;
float y = 0.0f;
};
int main()
{
elm::Registry registry;
auto entity = registry.create();
registry.emplace<Position>(entity, 0.0f, 0.0f);
registry.emplace<Velocity>(entity, 1.0f, 0.0f);
auto movement = elm::make_system(
"movement",
[](Position& position, const Velocity& velocity) {
position.x += velocity.x;
position.y += velocity.y;
});
movement.run(registry);
}Systems may also receive elm::Entity, elm::Registry&, and optional component
pointers:
struct Health {
float hp = 100.0f;
};
struct Shield {
float strength = 0.0f;
};
auto damage = elm::make_system(
"damage",
[](elm::Entity entity, Health& health, const Shield* shield) {
health.hp -= shield ? 1.0f : 5.0f;
});Required component references decide which entities a system visits. Non-const references are treated as writes; const references and const pointers are treated as reads.
Resources are registry-owned singletons keyed by C++ type. They are useful for frame time, input snapshots, configuration, event queues, or services owned by the host application.
struct Time {
float delta_seconds = 0.0f;
};
registry.set_resource<Time>(1.0f / 60.0f);
auto tick = elm::make_system("tick", [](elm::Registry& registry) {
auto& time = registry.resource<Time>();
// use time.delta_seconds
});resource<T>() asserts if the value has not been set. Use
has_resource<T>() when a resource is optional.
Systems that take elm::Registry& can declare resource access explicitly:
auto tick = elm::make_system(
"tick",
elm::ReadableComponents<> {},
elm::WritableComponents<Position> {},
elm::ReadableResources<Time> {},
elm::WritableResources<> {},
[](elm::Registry& registry) {
auto& time = registry.resource<Time>();
// update positions
});That access metadata is what elm::conflicts(...) uses for scheduler decisions.
elm::EventQueue stores events by C++ type and supports emit, each,
drain, clear, clear_all, empty, and size.
#include <elm/event_queue.hpp>
struct DamageEvent {
elm::Entity target;
float amount = 0.0f;
};
struct Health {
float hp = 100.0f;
};
registry.set_resource<elm::EventQueue>();
auto apply_damage = elm::make_system("apply_damage", [](elm::Registry& registry) {
auto& events = registry.resource<elm::EventQueue>();
events.drain<DamageEvent>([&](const DamageEvent& event) {
if (registry.alive(event.target) && registry.has<Health>(event.target)) {
registry.get<Health>(event.target).hp -= event.amount;
}
});
});The queue is not double-buffered. In frame-based code, drain each event type at a defined point in the frame.
Element does not ship a scheduler. Keep systems in whatever order your
application needs and call run(registry):
std::vector<elm::System> schedule;
schedule.push_back(movement);
schedule.push_back(apply_damage);
for (auto& system : schedule) {
system.run(registry);
}For parallel execution, group systems whose access sets do not conflict:
if (!elm::conflicts(a.access, b.access)) {
// a and b can run in the same batch
}Schedulers are application policy. Element only provides the system wrapper and the read/write metadata.
The Oak bridge is a separate header-only package. It registers native component
types with Oak, loads scripts, and converts @ElementSystem functions into
normal elm::System values.
See element-oak-bridge for the integration guide.