Skip to content

foured/fecs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

23 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

fecs โ€” Fast ECS for C++

License: MIT WIP

fecs is a minimal and efficient Entity Component System (ECS) library for C++, designed to be simple, fast, and easy to integrate into any project.

๐Ÿšง Work in Progress: This library is still in early development and not yet feature complete.

โ˜„๏ธ In this small documentation I tried to explain, among other things, how and why some parts of the system work the way they do.


โœจ Features

โœ… Core Functionality

  • ๐Ÿงฑ Entity creation โ€” lightweight uint32_t-based entities
  • ๐Ÿงฉ Component management โ€” add/remove with optional constructor args
  • ๐Ÿงน Component cleanup โ€” auto-removal on entity destruction
  • ๐Ÿ— Entity builder โ€” simplifies adding multiple components
  • ๐Ÿ•น๏ธ Component management โ€” reservation

๐Ÿ” Iteration Queues

  • ๐Ÿ” Simple queues โ€” view, runner, direct_for_each for lightweight iteration
  • โšก Fast owning queues โ€” group, group_slice for cache-friendly iteration
  • ๐Ÿ‘€ View support in groups โ€” combine owned + viewed components
  • ๐Ÿšซ Excluder โ€” filter out specific component types from iteration

๐Ÿš€ How to Add to Project

๐Ÿ”ง Step 1: Clone the Repository

Clone this GitHub repository to a desired folder:

git clone https://github.com/foured/fecs.git

โš™๏ธ Step 2: Include in CMake

Add the repository to your CMake project:

add_subdirectory("somefolder/fecs")

๐Ÿ”— Step 3: Link the Library

Link fecs to your target:

target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE fecs)

๐Ÿ›  Getting Started

To begin using fecs, you need to create the main object โ€” the registry:

#include <fecs/core/registry.h>

int main() {
    fecs::registry registry;
    return 0;
}

This registry will manage all your entities and components.


๐Ÿงฑ Entity Management

โž• Creating Entities

fecs::entity_t e = registry.create_entity();

Entities in fecs are just uint32_t, making them lightweight and efficient.

๐Ÿ’ก It's recommended to pass entities by value, since they are only 4 bytes, whereas pointers or references typically take 8 bytes.

โŒ Destroying Entities

registry.destroy_entity(e);

This will automatically remove all components attached to the entity.


๐Ÿงฉ Component Management

๐Ÿงฌ Defining a Component

struct component_1 {
    int bullets;
    float lifetime;
};

โž• Adding a Component

registry.add_component<component_1>(e);

If your component has a constructor, you can pass arguments to add_component:

struct component_2 {
    component_2(const std::string& s)
        : val(std::atoi(s.c_str())) {}

    int val;
};

registry.add_component<component_2>(e, "123456789");

If you need to add a lot of components and you don't want to pass entity every time

fecs::entity_builder builder(registry);
builder.add_component<component_1>(1, 0.1f);
fecs::entity_t e = builder.get();

Or simpler

fecs::entity_t e = fecs::entity_builder(registry).with<component_1>(1, 0.1f).get();

โž• Bulk Component Addition

std::vector<fecs::entity_t> entities;
registry.add_component<component_1>(entities);

โŒ Removing a Component

registry.remove_component<component_1>(e);
registry.remove_component<component_2>(entities);

๐Ÿ” Checking for Component Presence

if (registry.has_component<component_1>(e)) {
    // ...
}

โš™๏ธ Component Processing

๐Ÿ’ก Concept

fecs does not enforce a strict "systems" abstraction, as optimal processing patterns vary by project.
Instead, it provides a flexible set of queues to iterate over and process components.

โ“ Queues are helper classes that simplify iteration and logic application on entities and their components.


๐Ÿ“š Types of Queues

There are two main categories of queues:

1. Single Component Iteration

  • runner
  • direct_for_each

2. Multi-Component Iteration

  • group
  • group_slice
  • view

๐Ÿง  Why Two Types?

Because fecs stores each component type in a separate sparse set โ€” a cache-friendly data structure that enables fast iteration and lookup.

โ„น๏ธ Sparse sets are similar to maps, but store data contiguously in memory for better performance.

When iterating over one component type, it's straightforward and fast.
But when working with multiple components, there are two strategies:

โšก Strategy 1: Fast Grouped Access (Preferred)

Align all sparse sets so that component data for entity X is stored at the same index Y across sets.
This allows fast access via shared index.

  • Implemented via: group, group_slice
  • Requires owned components

โš ๏ธ Only one group can own a given component type.

๐Ÿงฉ Strategy 2: Dynamic Lookup (Fallback)

Look up each component individually before applying logic.

  • Implemented via: view
  • Slower, but works in all cases

๐Ÿ†š Queue Differences

๐Ÿงฎ runner vs direct_for_each

Feature runner direct_for_each
Type Class Registry method
Caching โœ… Yes โŒ No
Suitable for Repeated logic One-time initialization
Under the hood Uses direct_for_each Simple loop

Guideline:

  • Use direct_for_each for simple initialization logic.
  • Use runner when executing the same logic repeatedly โ€” it caches iteration data for better performance.

๐Ÿ”ฌ group vs group_slice

Both allow multi-component iteration with fast access to owned and viewed components, but differ in flexibility:

Feature group group_slice
Iteration scope All owned components Only a selected subset of owned components
Filtering capability โŒ No filtering โ€” uses all owned โœ… Select only specific owned components
Viewed components โœ… Supported โœ… Supported
Performance tuning โŒ Less flexible โœ… More granular and optimized
Use case Full group logic Targeted or partial logic within the same group setup

๐Ÿ’ก group_slice can only be used after a group that owns all of the sliceโ€™s owned components has been created.

Guideline:

  • Use group for full access to all components in the group.
  • Use group_slice when you only need to operate on a few components from the group โ€” it avoids unnecessary unpacking.

โšก Usage Examples

๐Ÿ’ก All for_each functions can optionally take fecs::entity_t as the first parameter.

direct_for_each

registry.direct_for_each<component_1>([](component_1& c) {
    // ...
});

registry.direct_for_each<component_1>([](fecs::entity_t e, component_1& c) {
    // ...
});

runner

auto r = registry.runner<component_1>();
r.for_each([](fecs::entity_t e, component_1& c) {
    // ...
});

group

Only owning group:

registry.create_group<component_1, component_2>();
auto g = registry.group<component_1, component_2>();
g->for_each([](component_1& c1, component_2& c2) {
    // ...
});

Owning + viewed components:

registry.create_group<component_1, component_2>(fecs::view_part<component_3>{});
auto g = registry.group<component_1, component_2>(fecs::view_part<component_3>{});
g->for_each([](component_1& c1, component_2& c2, component_3& c3) {
    // ...
});

Shorter with descriptor:

using group_desc = fecs::queue_args_descriptor<
    fecs::pack_part<component_1, component_2>,
    fecs::view_part<component_3>
>;

registry.create_group(group_desc{});
auto g = registry.group(group_desc{});
g->for_each([](component_1& c1, component_2& c2, component_3& c3) {
    // ...
});

group_slice

group_slice is used when you want to iterate over only some of the owned components from a group:

registry.create_group<component_1, component_2, component_3>(fecs::view_part<component_4>{});

auto gs = registry.group_slice<component_1, component_3>(fecs::view_part<component_5>{});
gs.for_each([](fecs::entity_t e, component_1& c1, component_3& c3, component_5& c5) {
    // ...
});

You can also construct it via queue_args_descriptor:

using slice_desc = fecs::queue_args_descriptor<
    fecs::pack_part<component_1, component_3>,
    fecs::view_part<component_5>
>;

auto gs = registry.group_slice(slice_desc{});
gs.for_each([](fecs::entity_t e, component_1& c1, component_3& c3, component_5& c5) {
    // ...
});

About

Fast ECS for C++

Resources

License

Stars

Watchers

Forks