GithubHelp home page GithubHelp logo

fullptr / apecs Goto Github PK

View Code? Open in Web Editor NEW
17.0 3.0 0.0 97 KB

A petite entity component system

License: MIT License

CMake 2.20% C++ 97.80%
ecs cpp cpp20 entity-component-system data-oriented-design modern-cpp game-development entity-component ecs-framework header-only gamedev

apecs's Introduction

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.

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. This was originally implemented using coroutines, however the allocations caused too much overhead, so I replaced them with an iterator implemenetation, before realising that coroutines were the wrong tool to begin with, and the correct tool was C++20 ranges. Ultimately the coroutine implementation was just transforming and filtering vectors of entities and components, which is now expressed directly in the code and no longer uses superfluous dynamic memory allocations.

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<transform, mesh, light, physics, script> 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<transform>(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<transform>(e, {0.0, 0.0, 0.0});

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

Removing is just as easy

registry.remove<transform>(e);
registry.remove_all_components(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<transform>(e)) {
  auto& t = registry.get<transform>(e);
  update_transform(t);
}

There are varidic versions of the above in the form of *_all

if (registry.has_all<transform, mesh>(e)) {
  auto [t, m] = registry.get_all<transform, mesh>(e);
  update_transform(t);
}

If you want to know if an entity has at least one of a set of components:

registry.has_any<box_collider, sphere_collider, capsule_collider>(e);

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

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

Deleting an entity is also straightforward

registry.destroy(e);

You can also destroy any span of entities in a single call:

registry.destroy({e1, e2, e3, e4});

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 ranges. 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<transform, mesh>()) {
  ...
}

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.

It is common that the current entity is not actually of direct interest, and is only used to fetch components. For this, there is view_get which instead returns a tuple of components instead of the entity id:

for (auto [t, m] : registry.view_get<transform, mesh>()) {
  ...
}

// The above is clearer than the more verbose and error prone:
for (auto entity : registry.view<transform, mesh>()) {
  auto& t = registry.get<transform>(entity);
  auto& m = registry.get<mesh>(entity);
  ...
}

Note that you also don't need to care about constness in the view_get case. If the registry is not const, the values in the tuple will be references, and if you are accessing through a const& registry, the components will also be const&. If you dont have a const& to the registry, you can enforce it by creating one and viewing through that:

const auto& cregistry = registry;
for (auto [t, m] : cregistry.view_get<transform, mesh>()) {
  // t and m are const& in this context
}

Other Functionality

The registry also contains some other useful functions for common uses of views:

Finding an Entity via a Predicate

You can look up an entity that satisfies a callback via

registry.find([](auto entity) -> bool { ... });

By default this loops over all entities and returns the first one satisfying the given predicate, returning apx::null if there is no such entity. This can be optimized by looping over a view if desired:

registry.find<transform>([](auto entity) -> bool { ... });

Copying Entities

It might desirable to duplicate entities within a registry. More generally, given two different registries of the same templated type, it may also be useful to be able to copy an entity from one registry to another. For this, use apx::copy:

template <typename... Comps>
entity copy(entity entity, const registry<Comps...>& src, registry<Comps...>& dst);

The given entity must be a valid entity in the src registry.

Deleting Entities via a Predicate

Deleting entities in a loop is undefined behaviour as you could be modifying the container you are iterating over. To delete a set of entities safely

registry.destroy_if([](auto entity) -> bool { ... });

This can also take template parameters to do the loop over a view as well.

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::meta::tag<T> 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<Comps...>::tags:

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

Upcoming Features

  • The ability to specify const in the getters, such as registry.get<const transform, mesh>(entity).

apecs's People

Contributors

colinh avatar fullptr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

apecs's Issues

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&.

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).

Missing reference

Whenever you call the callbacks the code reads

        for (auto cb : std::get<std::vector<callback_t<Comp>>>(d_on_add)) {
            cb(entity, ret);
        }

where probably you'd want to use for( auto& cb : ... in order to not copy the callback object into cb.

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>(...).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.