Alternative managed wrapper around flecs-cs bindings for using the ECS framework Flecs in modern .NET.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

130 lines
5.3 KiB

# gaemstone.ECS
.. is a medium-level managed wrapper library around the [flecs-cs] bindings for the amazing [Entity Component System (ECS)][ECS] framework [Flecs]. It is used as part of the [gæmstone] game engine, but may be used in other projects as well. To efficiently use this library, a thorough understanding of [Flecs] is required.
These classes have been split from the main [gæmstone] project. It is still a little unclear what functionality belongs where, and structural changes may occur. In its current state, I recommend to use this repository simply as a reference for building similar projects.
## Features
- Convenient wrapper types such as [Id], [Entity] and [EntityType].
- [EntityPath] uses a unix-like path, for example `/Game/Players/copygirl`.
- Fast type-to-entity lookup using generic context on [World]. (See below.)
- Define your own [Components] as both value or reference types.
- Query the ECS with [Iterators], [Filters], [Queries] and [Rules].
- Create [Systems] for game logic and [Observers] to act on changes.
[Id]: ./src/gaemstone.ECS/Id.cs
[Entity]: ./src/gaemstone.ECS/Entity.cs
[EntityType]: ./src/gaemstone.ECS/EntityType.cs
[EntityPath]: ./src/gaemstone.ECS/EntityPath.cs
[World]: ./src/gaemstone.ECS/World.cs
[Components]: ./src/gaemstone.ECS/Component.cs
[Iterators]: ./src/gaemstone.ECS/Iterator.cs
[Filters]: ./src/gaemstone.ECS/Filter.cs
[Queries]: ./src/gaemstone.ECS/Query.cs
[Rules]: ./src/gaemstone.ECS/Rule.cs
[Systems]: ./src/gaemstone.ECS/System.cs
[Observers]: ./src/gaemstone.ECS/Observer.cs
## Example
var world = new World<Program>();
var position = world
.New("Position") // Create a new EntityBuilder, and set its name.
.Symbol("Position") // Set entity's symbol. (Used in query expression.)
.Build() // Actually create the entity in-world.
// Finally, create a component from this entity.
// "Position" is defined at the bottom of this example.
// Create an "Entities" parent with two positioned entities inside.
var entities = world.New("Entities").Build();
entities.NewChild("One").Set(new Position(-5, 0)).Build();
entities.NewChild("Two").Set(new Position(10, 20)).Build();
// Changed my mind: Let's multiply each entity's position by 10.
foreach (var child in entities.GetChildren()) {
ref var pos = ref child.GetRefOrThrow<Position>();
pos = new(pos.X * 10, pos.Y * 10);
// The following systems run in the "OnUpdate"
// phase of the default pipeline provided by Flecs.
var dependsOn = world.LookupPathOrThrow("/flecs/core/DependsOn");
var onUpdate = world.LookupPathOrThrow("/flecs/pipeline/OnUpdate");
// Create a system that will move all entities with
// the "Position" component downwards by 2 every frame.
.Add(dependsOn, onUpdate)
.Build().InitSystem(new("Position"), iter => {
var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
ref var pos = ref posColumn[i];
pos = new(pos.X, pos.Y - 2);
// Create a system that will print out entities' positions.
.Add(dependsOn, onUpdate)
.InitSystem(new("[in] Position"), iter => {
var posColumn = iter.Field<Position>(1);
for (var i = 0; i < iter.Count; i++) {
var entity = iter.Entity(i);
var pos = posColumn[i];
Console.WriteLine($"{entity.Name} is at {pos}");
// Infinite loop that runs the "game" at 30 FPS.
while (true) {
var delta = TimeSpan.FromSeconds(1.0f / 30);
// Progress returns false if quit was requested.
if (!world.Progress(delta)) break;
record struct Position(int X, int Y);
## Instructions
# Clone the repository. Recurse into submodules to include flecs-cs and flecs.
git clone --recurse-submodules
# If you want to add it to your own repository as a submodule:
git submodule add
# To add a reference to this library to your .NET project:
dotnet add reference gaemstone.ECS/gaemstone.ECS.csproj
# To generate flecs-cs' bindings:
## On the `TContext` type parameter
Entities may be looked up simply by their type, once they're registered with `CreateLookup` or `InitComponent`. Under the hood, this is made possible using a nested static generic class that hold onto an `Entity` field. Theoretically this could be compiled into a simple field lookup and therefore be faster than dictionary lookups.
To support scenarios where multiple worlds may be used simultaneously, each with their own unique type lookups, we specify a generic type as that context.
In cases where only a single world is used, the amount of typing can be reduced by including a file similar to the following, defining global aliases:
global using Entity = gaemstone.ECS.Entity<Context>;
global using Id = gaemstone.ECS.Id<Context>;
global using Iterator = gaemstone.ECS.Iterator<Context>;
global using World = gaemstone.ECS.World<Context>;
// Add more aliases as you feel they are needed.
public struct Context { }