Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Element

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::Registry owns entities, components, resources, and archetype storage.
  • elm::System wraps a callable plus the component/resource access metadata needed by a scheduler.
  • elm::EventQueue is 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.

Build

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 build

Then 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

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 build

For an explicit test build:

meson setup build -Dtests=enabled

Basic Use

#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

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.

Events

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.

Scheduling

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.

Oak

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.

About

Element is a reusable ECS framework. The ECS core is header-only C++20 and integrates with a scripting language so you can author your systems in script.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages