apecs: A Petite Entity Component System

Overview

apecs: A Petite Entity Component System

A header-only, very small entity component system with no external dependencies. Simply pop the header into your own project and off you go!

The API is very similar to EnTT, with the main difference being that all component types must be declared up front. This allows for an implementation that doesn't rely on type erasure, which in turn allows for more compile-time optimisations.

Components are stored contiguously in apx::sparse_set objects, which are essentially a pair of std::vectors, one sparse and one packed, which allows for fast iteration over components. When deleting components, these sets may reorder themselves to maintain tight packing; as such, sorting isn't currently possibly, but also shouldn't be desired.

This library also includes some very basic meta-programming functionality, found in the apx::meta namespace, as well as apx::generator, a generator built using the C++20 coroutine API.

This project was just a fun little project to allow me to learn more about ECSs and how to implement one, as well as metaprogramming and C++20 features. If you are building your own project and need an ECS, I would recommend you build your own or use EnTT instead.

The Registry and Entities

In apecs, an entity, apx::entity, is simply a 64-bit unsigned integer. All components attached to this entity are stored and accessed via the apx::registry class. To start, you can default construct a registry, with all of the component types declated up front

apx::registry registry;

Creating an empty entity is simple

apx::entity e = registry.create();

Adding a component is also easy

transform t = { 0.0, 0.0, 0.0 }; // In this example, a transform consists of just a 3D coordinate
registry.add(e, t);

// Or more explicitly:
registry.add(e, t);

Move construction is also allowed, as well as directly constructing via emplace

// Uses move constructor (not that there is any benefit with the simple trasnsform struct)
registry.add(e, {0.0, 0.0, 0.0});

// Only constructs one instance and does no copying/moving
registry.emplace(e, 0.0, 0.0, 0.0);

Removing is just as easy

registry.remove(e);

Components can be accessed by reference for modification, and entities may be queried to see if they contain the given component type

if (registry.has(e)) {
  auto& t = registry.get(e);
  update_transform(t);
}

There is also the noexcept version get_if which returns a pointer to the component, and nullptr if it does not exist

if (auto* t = registry.get_if(e)) {
  update_transform(*t);
}

Deleting an entity is also straightforward

registry.destroy(e);

Given that an apx::entity is just an identifier for an entity, it could be that an identifier is referring to an entity that has been destroyed. The registry provides a function to check this

registry.valid(e); // Returns true if the entity is still active and false otherwise.

The current size of the registry is the number of currently active entities

std::size_t active_entities = registry.size();

Finally, a registry may also be cleared of all entities with

registry.clear();

Iteration

Iteration is implmented using C++20 coroutines. There are two main ways of doing interation; iterating over all entities, and iterating over a view; a subset of the entities containing only a specific set of components.

Iterating over all

for (auto entity : registry.all()) {
  ...
}

Iterating over a view

for (auto entity : registry.view()) {
  ...
}

When iterating over all entities, the iteration is done over the internal entity sparse set. When iterating over a view, we iterate over the sparse set of the first specified component, which can result in a much faster loop. Because of this, if you know that one of the component types is rarer than the others, put that as the first component.

If you prefer a more functional approach, all and view may also accept lambdas:

registry.all([](auto entity) {
  ...
});

and

registry.view(auto entity) {
  ...
});

There is also an "extended" version of view to access the components more easily:

registry.view(auto entity, const transform& t, const mesh& m) {
  ...
});

Notification System

apecs, like EnTT, is mainly just a data structure for storing components, and does not have any built in features specifically for systems; they are left up to the user. However, apecs does also allow for registering callbacks so that systems can be notified whenever a component is created or destroyed. Callbacks have the signature void(apx::entity, const Component&). To be notified of a component being added, use on_add

registry.on_add([&](apx::entity entity, const transform& component) {
  ...
});

on_add callbacks are invoked after the component has been added. Similarly for removing, use on_remove

registry.on_remove([&](apx::entity entity, const transform& component) {
  ...
});

on_remove callbacks are invoked before the component has been removed. It is currently not possible to remove callbacks, but this may be added in the future.

If a registry is cleared, all on_remove callbacks are invoked for each entity along the way.

Entity Handle

To some, a call such as registry.add(entity, t) may feel unnatural and would prefer a more traditional object oriented interface such as entity.add(t). This is provided via apx::handle, a thin wrapper around a registry pointer and an entity. Given a reigstry and an entity, a handle can be created easily

apx::handle handle{®istry, entity};

To make your code prettier, a helper function is provided to create handles when creating a fresh entity

apx::handle entity = apx::create_from(registry);

As stated, this is a simple wrapper and provides the whole interface for a single entity

handle.valid();
handle.destroy();

handle.add(t);
handle.has();
handle.get();
handle.remove();

Metaprogramming

To implement many of these features, some metaprogramming techniques were required and are made available to users. First of all, apx::tuple_contains allows for checking at compile time if a given std::tuple type contains a specific type. This is used in the component getter/setter functions to give nicer compile errors if there is a type problem, but may be useful in other situations.

static_assert(apx::tuple_contains_v<int, std::tuple<float, int, std::string>> == true);
static_assert(apx::tuple_contains_v<int, std::tuple<float, std::string>> == false);

When destroying an entity, we also need to loop over all types to delete the components and to make sure any on_remove callbacks are invoked. This can be done with apx::for_each

apx::meta::for_each(tuple, [](auto&& element) {
  ...
});

This of course needs to be generic lambda as this gets invoked for each typle in the tuple.

In extension to the above, you may also find yourself needing to loop over all types within a reigstry. This can be achieved by creating a tuple of apx::tag types and extracting the type from those in a for loop. The library provides some helpers for this. In particular, each registry provides an inline static constexpr version of this tuple as registry::tags and there is apx::meta::tag::type() which is intended to be used in a declype expression to get the type:

apx::meta::for_each(registry.tags, [](auto&& tag) {
  using T = decltype(tag.type());
  ...
});

If your compiler supports explicit template types in lambdas (C++20 feature, not currently implemented by everyone), this can be simplified to

apx::meta::for_each(registry.tags, []<typename T>(apx::meta::tag) {
  ...
});

Upcoming Features

  • The ability to deregister callbacks.
  • A slower way of iterating components that allows for deleting and inserting new components. See apx::sparse_set::safe vs apx::sparse_set::fast for more info.
  • Potentially apx::handle based versions for registry::all and registry::view.
Comments
  • Incorrect use of move

    Incorrect use of move

    In member function registry<Comps...>::add(apx::entity entity, Comp&& component) the argument component is passed on with std::move() even though Comp could be some_component_type&.

    Since emplace() does it correctly you probably know that you just need to use std::forward instead, and can then remove the other overload of add(), the one with the const Comp&.

    opened by ColinH 3
  • Housekeeping

    Housekeeping

    Just some more small things that I noticed while going over the code some more. I hope they fit your line of thinking and that I didn't add a superfluous noexcept or nodiscard. And you should probably check whether all of the tests run for you.

    opened by ColinH 1
  • Several Fixes

    Several Fixes

    • Remove registry::add<Comp>(apx::entity, const Comp&) as we have registry::add<Comp>(apx::entity, Comp&&), and fix the latter to use std::forward.
    • When looping over the callbacks, use auto& to avoid unnecessary copies.
    • Fix a typo in registry::view<T>.
    • Make naming of variables in apx::handle consistent with the rest of the library, and use template keyword to allow for compiling with gcc. I'll be doing more testing on other platforms in the future.
    opened by fullptr 1
  • Missing variable name

    Missing variable name

    Line 573, which is in member function registry<Comps...>::view(), currently reads

                apx::entity = d_entities[index];
    

    which I needed to change to

                apx::entity entity = d_entities[index];
    

    in order to make it compile (with GCC10).

    opened by ColinH 1
  • Missing template keyword

    Missing template keyword

    Hi, really like the minimalist approach of apecs!

    I just tried compiling with GCC10 and stumbled over the following issue:

    In template class handle the type of member field registry depends on the template parameters of handle.

    In the member functions of handle that call registry->function<Comp>(...) it is therefore necessary to use the template keyword as in registry->template function<Comp>(...).

    opened by ColinH 1
  • Make Tests Runnable Again

    Make Tests Runnable Again

    • Fix up the cmake file to make it easy to build the unit tests, but this is off by default.
    • Fix a bug that the unit tests actually caught! Clearly not been run in a while.
    • Will make these run in travis in the future.
    opened by fullptr 0
  • Add Copy Function

    Add Copy Function

    • Adds apx::copy, a free function for copying entities. Can be used to duplicating an entity within a single registry, or for creating a copy of the entity in a different registry of the same type.
    opened by fullptr 0
  • Restructure

    Restructure

    • Move the header into include/apecs.
    • Modify cmake file to create an apecs library. This repo should now be usable as a git submodule and you can add it to your cmake project via add_subdirectory(apecs).
    opened by fullptr 0
  • Small Clean-up

    Small Clean-up

    • Simplified some of the functions that created some intermediate variables. Improved the readability.
    • Make the add and emplace functions more consistent with their use of std::remove_cvref.
    opened by fullptr 0
  • Remove `spkt::handle` and callback functionality

    Remove `spkt::handle` and callback functionality

    • After getting more usage out of my ECS, I've been finding the handle class to be a bit of an anti-pattern, preferring to use entity IDs directly everywhere. As such, I'm removing the handle class so that I don't have to keep its interface in sync with the registry.
    • Further, I'm also no longer using the callback functionality in my games, as systems are entirely stateless. Previously, certain systems kept maps of data for entities themselves, and the callbacks were used to keep them in sync. Now, components have "runtime elements" which get lazily initialized by systems, and I use RAII in those to do clean up when the components are deleted. This is nicer as the lifetime of any runtime data is explicitly tied to the lifetime of the component that it's on, so there is no need to rely on a reactive system to keep things in sync.
    • An example of the above is a physics engine. Typically the physics engine will need to store rigid bodies to do their simulations. Previously, these would be stored in the physics engine class and if a entity was deleted, the on_remove call would remove the rigid body from the physics engine. Instead, the rigid body can be stored in an opaque smart pointer on the component, and when the component gets deleted, the destructor for that smart pointer can do the clean up. This makes it far easier to read (in my opinion) as you don't need to look around for callbacks.
    opened by fullptr 0
  • Add `get_all` and `view_get`

    Add `get_all` and `view_get`

    • Mirroring has_all, registry::get_all return a tuple of references to components, allowing for statements such as auto [t, m] = registry.get_all<transform, mesh>(entity);.
    • Added registry::view_get, which pipes a registry::view object through get_all so that it yields tuples of components instead of the entity. Reduces a lot of repetition in for loops as component types only need to be spelled once.
    opened by fullptr 0
Owner
Matt Cummins
Matt Cummins
EntityX - A fast, type-safe C++ Entity-Component system

EntityX - A fast, type-safe C++ Entity Component System NOTE: The current stable release 1.0.0 breaks backward compatibility with < 1.0.0. See the cha

Alec Thomas 2k Dec 29, 2022
[WIP] Experimental C++14 multithreaded compile-time entity-component-system library.

ecst Experimental & work-in-progress C++14 multithreaded compile-time Entity-Component-System header-only library. Overview Successful development of

Vittorio Romeo 450 Dec 17, 2022
C++ single-header entity component system library

ECS This is a simple C++ header-only type-safe entity component system library. It makes heavy use of C++11 constructs, so make sure you have an up to

Sam Bloomberg 418 Dec 27, 2022
C++ entity-component system

CORGI Version 1.0.2 {#corgi_readme} CORGI is a C++ entity-component system library developed primarily for games that focus on simplicity and flexibil

Google 250 Nov 6, 2022
Entity-Component-System (ECS) with a focus on ease-of-use, runtime extensibility and compile-time type safety and clarity.

Kengine The Koala engine is a type-safe and self-documenting implementation of an Entity-Component-System (ECS), with a focus on runtime extensibility

Nicolas Phister 466 Dec 26, 2022
Thoughts about entity-component-system

About Warning: This is not a complete production-ready library for entity-component-system. This is only my thoughts about how the modern entity-compo

Sergey Makeev 179 Dec 7, 2022
An open source C++ entity system.

anax ARCHIVED: as I haven't maintained this library for at least a couple of years. I don't have the time or interest to work on this. Please use anot

Miguel Martin 456 Jan 4, 2023
A drop-in entity editor for EnTT with Dear ImGui

imgui_entt_entity_editor A drop-in, single-file entity editor for EnTT, with ImGui as graphical backend. demo-code (live) Editor Editor with Entiy-Lis

Erik Scholz 151 Jan 2, 2023
Simple ESPHome Wiegand custom component

esphome-wiegand Simple ESPHome Wiegand custom component Based on this code: https://github.com/Luisiado/wiegand_esphome_module To use: Drop wiegand_de

Av 24 Dec 26, 2022
A component based project manager.

Component Based Project Manager CBPM provides an interface to manage a component-based project. Build To build CBPM, you must install xmake: a build-s

null 1 Nov 2, 2021
Spin-off component from existing IBM/mcas open source project

PyMM PyMM is a python library that allows the storing and manipulation of existing heavily used types such as Numpy ndarray and PyTorch on Persistent

International Business Machines 16 Nov 20, 2022
bsdiff changed to remove bz2, the header and to allow streaming interfaces, to be used on the esp32 with idf as a component

bspatch for esp32 This project adds support for bspatch to the esp32 with some changes: no compression (bz2), no header and changed the interfaces to

Blockstream 11 Oct 24, 2022
Custom ESPHome Component for generic Sit-Stand-Desks

ESPHomeGenericSitStandDesk I have one of those generic relatively cheap Sit Stand Desks. In an effort to monitor my desk usage I developed this overki

Till 18 Dec 27, 2022
ESPHome component to send and receive HDMI-CEC messages.

HDMI-CEC ESPHome Component An ESPHome component that supports receiving and transmitting HDMI-CEC messages to connected HDMI devices. The ultimate goa

John Boiles 23 Dec 16, 2022
Operating system project - implementing scheduling algorithms and some system calls for XV6 OS

About XV6 xv6 is a modern reimplementation of Sixth Edition Unix in ANSI C for multiprocessor x86 and RISC-V systems.

Amirhossein Rajabpour 22 Dec 22, 2022
CQC (Charmed Quark Controller) a commercial grade, full featured, software based automation system. CQC is built on our CIDLib C++ development system, which is also available here on GitHub.

The CQC Automation System What It Is CQC is a commercial quality, software based automation system, suitable for residential or commercial application

Dean Roddey 61 Dec 13, 2022
KePOS is a 64-bit operating system. Design and implement your own operating system

KePOS is a 64-bit operating system. The purpose of this system is to combine the theoretical knowledge and practice of the operating system, and to deepen the understanding of the operating system.

null 65 Nov 9, 2022
Hobbyist Operating System targeting x86_64 systems. Includes userspace, Virtual File System, An InitFS (tarfs), Lua port, easy porting, a decent LibC and LibM, and a shell that supports: piping, file redirection, and more.

SynnixOS Epic Hobby OS targeting x86_64 CPUs, it includes some hacked together functionality for most essential OSs although, with interactivity via Q

RaidTheWeb 42 Oct 28, 2022