A modern, manager-based, and data-oriented C++17 Entity Component System (ECS) framework designed for performance and ease of use in real-time applications.
FECS is built on a modular, manager-based architecture that promotes clean, decoupled code. It leverages modern C++17 features and a fluent builder API to provide an expressive and type-safe way to build systems. The design prioritizes data-oriented principles for high performance in demanding applications like games and simulations, while remaining flexible and easy to extend.
- Modern C++17 Design: Utilizes modern C++ features for a clean and efficient implementation.
- Manager-Based Architecture: Decoupled managers for Entities, Components, Systems, and more.
- Fluent Builder APIs: Expressive and type-safe APIs for creating entities and defining systems.
- Powerful System Scheduling: Organize systems into ordered sets with support for
Startup,Update,Fixed, andTimedexecution. - Data-Oriented: High-performance component storage using sparse sets.
- Resource Management: A dedicated manager for global, non-entity data.
- C++17 compatible compiler (GCC, Clang, MSVC)
- CMake >= 3.16
To build the project and its examples, use the following commands:
mkdir build
cd build
cmake ..
cmake --build .or, Using FetchContent via CMake
include(FetchContent)
FetchContent_Declare(
FECS
GIT_REPOSITORY https://github.com/Akihiro120/FECS
GIT_TAG main
)
FetchContent_MakeAvailable(FECS)
target_link_libraries(${PROJECT_NAME} PRIVATE FECS)The following example demonstrates how to create a simple application with moving entities and a system to update them.
#include <FECS/FECS.h>
#include <iostream>
// Define components
struct Position { float x, y; };
struct Velocity { float dx, dy; };
// A system that moves entities
void MoveSystem(FECS::Query<Position, Velocity> query)
{
query.Each([](Position& pos, Velocity& vel)
{
pos.x += vel.dx;
pos.y += vel.dy;
});
}
int main()
{
// Create a world
FECS::World world;
// Create an entity with Position and Velocity components
world.Entities().Create()
.Attach<Position>({ 0.0f, 0.0f })
.Attach<Velocity>({ 1.0f, 0.5f })
.Build();
// Add a system to update the position
world.Scheduler()
.AddSystem()
.WithQuery<Position, Velocity>()
.Build(MoveSystem);
// Run the scheduler for one frame
world.Scheduler().Run(0.016f); // Pass delta time
// You can also query manually
world.View().Query<Position>().Each([](FECS::Entity id, Position& pos)
{
std::cout << "Entity " << id << " Position: " << pos.x << ", " << pos.y << std::endl;
});
return 0;
}The FECS::World class is the heart of the ECS. It acts as a central hub, providing access to all the underlying managers:
FECS::World world;
auto& entityManager = world.Entities();
auto& componentManager = world.Components();
auto& resourceManager = world.Resources();
auto& scheduleManager = world.Scheduler();
auto& viewManager = world.View();You can create entities using the EntityManager, accessed via world.Entities(). The EntityBuilder provides a fluent API for composing entities.
// Create a new entity and attach components
world.Entities().Create()
.Attach<Position>({ 0.0f, 0.0f })
.Attach<Velocity>({ 1.0f, 1.0f })
.Build();The EntityBuilder supports a variety of methods for more complex entity creation:
struct Player {};
world.Entities().Create()
// Emplace a component in-place
.Emplace<Position>(0.0f, 0.0f)
// Add a tag (a component with no data)
.Tag<Player>()
// Conditionally attach components
.When(isGodMode, [](FECS::EntityBuilder& builder)
{
builder.Attach(Invincibility{});
})
// Ensure a component exists, adding a default if it doesn't
.Ensure(Health{100})
// Modify an existing component
.Patch<Position>([](Position& pos)
{
pos.x = 10.0f;
})
// Apply a custom function to the builder
.Apply([](FECS::EntityBuilder& builder)
{
// ... custom logic ...
})
// Detach a component
.Detach<UselessComponent>()
.Build();While the EntityBuilder is the primary way to work with components, you can also use the ComponentManager directly:
auto& componentManager = world.Components();
FECS::Entity entity = world.Entities().Create().Build();
// Attach a component
componentManager.Attach<Position>(entity, {0.0f, 0.0f});
// Check if an entity has a component
if (componentManager.Has<Position>(entity))
{
// Get a component
Position& pos = componentManager.Get<Position>(entity);
}
// Detach a component
componentManager.Detach<Position>(entity);Systems contain the logic of your application. The ScheduleManager (world.Scheduler()) is used to define systems and their execution properties.
The SystemBuilder provides a fluent API for defining systems:
// A system that prints the position of entities
void PrintSystem(FECS::Query<Position> query)
{
query.Each([](Position& pos)
{
std::cout << "Position: " << pos.x << ", " << pos.y << std::endl;
});
}
world.Scheduler()
.AddSystem()
.WithQuery<Position>()
.Build(PrintSystem);Systems can depend on resources. Use Read<T> for read-only access and Write<T> for read-write access.
struct GameTime { float totalTime; };
void TimeSystem(GameTime& time)
{
time.totalTime += 0.016f;
}
world.Scheduler()
.AddSystem()
.Write<GameTime>()
.Build(TimeSystem);You can control when a system runs:
Startup(): Runs once before the main loop.Update(): Runs every frame.Fixed(): Runs at a fixed time step.Timed(interval): Runs at a specific interval.
world.Scheduler().SetFixedStep(1.0f / 60.0f);
world.Scheduler().AddSystem().Startup().Build(InitGame);
world.Scheduler().AddSystem().Update().Build(HandleInput);
world.Scheduler().AddSystem().Fixed().Build(PhysicsUpdate);
world.Scheduler().AddSystem().Timed(1.0f).Build(SlowUpdate);Systems can be organized into groups to control the execution order.
enum SystemGroupID { INPUT = 0, PHYSICS = 1, RENDER = 2 };
world.Scheduler().SetExecutionOrder({
SystemGroupID::INPUT,
SystemGroupID::PHYSICS,
SystemGroupID::RENDER,
});
world.Scheduler().AddSystem()
.In(SystemGroupID::RENDER)
.Build(RenderSystem);The ViewManager (world.View()) is the entry point for querying entities.
// Get a query for all entities with Position and Velocity
auto query = world.View().Query<Position, Velocity>();
// Iterate over the entities
query.Each([](FECS::Entity id, Position& pos, Velocity& vel)
{
// ... logic ...
});The ResourceManager (world.Resources()) allows you to store and access global, non-entity data.
// Define a resource
struct GameState { float isRunning = true; };
// Add the resource to the world
world.Resources().Add(GameState{});
// Access the resource directly
GameState& state = world.Resources().Get<GameState>();
state.isRunning = false;
// Access the resource in a system
void ControlSystem(GameState& state)
{
if (/* some condition */)
{
state.isRunning = false;
}
}
world.Scheduler()
.AddSystem()
.Write<GameState>()
.Build(ControlSystem);Benchmarking was performed under normal conditions where component storages are packed.
Hardware: Ryzen 5600G 3.9GHz 6 Cores
| Operation | 100 Entities (ms) | 10,000 Entities (ms) | 1,000,000 Entities (ms) |
|---|---|---|---|
| Create Entity | 0.0021 | 0.0604 | 3.4217 |
| Add Component | 0.0145 | 0.1238 | 7.8212 |
| Get Component | 0.0003 | 0.0136 | 1.3510 |
| Remove Component | 0.0033 | 0.0549 | 5.9727 |
| Delete Entity | 0.0131 | 0.0676 | 5.9905 |
| Query 2 Components | 0.0003 | 0.0244 | 2.6986 |
| Get 2 Components | 0.0004 | 0.0249 | 2.4843 |
| Query 4 Components | 0.0015 | 0.0348 | 3.1150 |
| Get 4 Components | 0.0005 | 0.0497 | 4.9651 |
The project includes several examples in the examples directory:
- boids: A classic boids simulation, demonstrating advanced features like system ordering and spatial hashing.
- scheduling: A simple example showcasing the system scheduler.
- benchmark: A set of benchmarks for various ECS operations.
- tests: A collection of tests for various features, and a good place to see the
EntityBuilderin action.
Distributed under the MIT License. See LICENSE for details.