fennecs 0.0.4-pre

This is a prerelease version of fennecs.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package fennecs --version 0.0.4-pre                
NuGet\Install-Package fennecs -Version 0.0.4-pre                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="fennecs" Version="0.0.4-pre" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add fennecs --version 0.0.4-pre                
#r "nuget: fennecs, 0.0.4-pre"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install fennecs as a Cake Addin
#addin nuget:?package=fennecs&version=0.0.4-pre&prerelease

// Install fennecs as a Cake Tool
#tool nuget:?package=fennecs&version=0.0.4-pre&prerelease                

... the tiny, tiny, high-energy Entity Component System!

What the fox!? Another ECS?

We know... oh, we know. 😩️

But in a nutshell, fennecs is...

🐾 zero codegen 🐾 minimal boilerplate 🐾 archetype-based 🐾 intuitively relational 🐾 lithe and fast

fennecs is a re-imagining of RelEcs/HypEcs, extended and compacted until it feels just right for high performance game development in any modern C# engine. Including, of course, the fantastic Godot 4.x!

Quickstart: Let's go!

πŸ“¦> dotnet add package fennecs

At the basic level, all you need is a 🧩component type, a number of small foxes 🦊entities, and a query to βš™οΈiterate and modify components, occasionally passing in some uniform πŸ’Ύdata.

// Declare your own component types. (you can also use most existing value or reference types)
using Position = System.Numerics.Vector3;

// Create a world. (fyi, World implements IDisposable)
var world = new ECS.World();

// Spawn an entity into the world with a choice of components. (or add/remove them later)
var entity = world.Spawn().Add<Position>().Id();

// Queries are cached, just build them right where you want to use them.
var query = world.Query<Position>().Build();

// Run code on all entities in the query. (omit chunksize to parallelize only by archetype)
query.RunParallel((ref Position position, in float dt) => {
    position.Y -= 9.81f * dt;
}, uniform: Time.Delta, chunkSize: 2048);

πŸ’’... when we said minimal boilerplate, we foxing meant it.

Even using the strictest judgment, that's no more than 2 lines of boilerplate! Merely instantiating the world and building the query aren't directly moving parts of the actor/gravity feature we just built, and should be seen as "enablers" or "infrastructure".

The πŸ’«real magicπŸ’« is that none of this brevity compromises on performance.

Features: What's in the box?

fennECS is a tiny, tiny ECS with a focus on performance and simplicity. And it cares enough to provide a few things you might not expect. Our competition sure didn't.

Pile it on: Comparison Matrix

πŸ₯‡πŸ₯ˆπŸ₯‰ECS Comparison Matrix<br/><b>Foxes are soft, choices are hard</b> - Unity dumb; .NET 8 really sharp.

Here are some of the key properties where fennECS might be a better or worse choice than its peers. Our resident fennecs have worked with all of these ECSs, and we're happy to answer any questions you might have.

fennECS HypEcs Entitas Unity DOTS DefaultECS
Boilerplate-to-Feature Ratio 3-to-1 5-to-1 12-to-1 27-to-1 😱 7-to-1
Entity-Target Relations βœ… βœ… ❌ ❌ ❌
Target Querying<br/>(find all targets of relations of type) βœ… ❌ ❌ ❌ ❌
Entity-Component Queries βœ… βœ… βœ… βœ… βœ…
Add Shared Components βœ… ❌ ❌ 🟨 βœ…
Change Shared Components βœ… ❌ ❌ ❌ βœ…
Entity-Type-Relations ❌ βœ… ❌ ❌ ❌
Entity-Target-Querying βœ… ❌ ❌ ❌ ❌
Arbitrary Component Types βœ… βœ… ❌ ❌ βœ…
Structural Change Responders 🟨<br/>(coming soon) ❌ βœ… ❌ ❌
Automatic Thread Scheduling 🟨<br/>(coming soon) ❌ ❌ βœ…<br/>(highly static) βœ…
No Code Generation Required βœ… βœ… ❌ ❌ 🟨
Enqueue Structural Changes at Any Time βœ… βœ… βœ… 🟨 🟨
Apply Structural Changes at Any Time ❌ ❌ βœ… ❌ ❌
C# 12 support βœ… ❌ ❌ ❌ ❌
Parallel Processing ⭐⭐ ⭐ ❌ ⭐⭐⭐ ⭐⭐
Singleton / Unique Components 🟨<br/>(ref types only) ❌ βœ… 🟨<br/>(per system) βœ…
Journaling ❌ ❌ 🟨 βœ… ❌

Highlights / Design Goals

  • Entity-Entity-Relations with O(1) lookup time complexity.

  • Entity-Component Queries with O(1) lookup time complexity.

  • Entity Spawning and De-Spawning with O(1) time complexity.

  • Entity Structural Changes with O(1) time complexity (per individual change).

  • Workloads can be parallelized across Archetypes (old) and within Archetypes (new).

  • Unit Test coverage.

  • Benchmarking suite.

  • Modern C# 12 codebase, targeting .NET 8.

  • Godot 4.x Sample Integrations.

Future Roadmap

  • Unity Support: Planned for when Unity is on .NET 7 or later, and C# 12 or later.
  • fennECS as a NuGet package
  • fennECS as a Godot addon

Already plays well with Godot 4.x!

<img src="Documentation/Logos/godot-icon.svg" width="128px" alt="Godot Engine Logo, Copyright (c) 2017 Andrea CalabrΓ³" />

Legacy Documentation

Components

// Components are simple structs.
struct Position { public int X, Y; }
struct Velocity { public int X, Y; }

Systems

// Systems add all the functionality to the Entity Component System.
// Usually, you would run them from within your game loop.
public class MoveSystem : ISystem
{
    public void Run(World world)
    {
        // iterate sets of components.
        var query = world.Query<Position, Velocity>().Build();
        query.Run((count, positions, velocities) => {
            for (var i = 0; i < count; i++)
            {
                positions[i].X += velocities[i].X;
                positions[i].Y += velocities[i].Y;
            }
        });
    }
}

Spawning / De-Spawning Entities

public void Run(World world)
{
    // Spawn a new entity into the world and store the id for later use
    Entity entity = world.Spawn().Id();
    
    // Despawn an entity.
    world.Despawn(entity);
}

Adding / Removing Components

public void Run(World world)
{
    // Spawn an entity with components
    Entity entity = world.Spawn()
        .Add(new Position())
        .Add(new Velocity { X = 5 })
        .Add<Tag>()
        .Id();
    
    // Change an Entities Components
    world.On(entity).Add(new Name { Value = "Bob" }).Remove<Tag>();
}

Relations

// Like components, relations are structs.
struct Apples { }
struct Likes { }
struct Owes { public int Amount; }
public void Run(World world)
{
    var bob = world.Spawn().Id();
    var frank = world.Spawn().Id();
    
    // Relations consist of components, associated with a "target".
    // The target can either be another component, or an entity.
    world.On(bob).Add<Likes>(typeof(Apples));
    //   Component           ^^^^^^^^^^^^^^
    
    world.On(frank).Add(new Owes { Amount = 100 }, bob);
    //                                      Entity ^^^
    
    // if you want to know if an entity has a component
    bool doesBobHaveApples = world.HasComponent<Apples>(bob);
    // if you want to know if an entity has a relation
    bool doesBobLikeApples = world.HasComponent<Likes>(bob, typeof(Apples));
    
    // Or get it directly.
    // In this case, we retrieve the amount that Frank owes Bob.
    var owes = this.GetComponent<Owes>(frank, bob);
    Console.WriteLine($"Frank owes Bob {owes.Amount} dollars");
}

Queries

public void Run(World world)
{
    // With queries, we can get a list of components that we can iterate through.
    // A simple query looks like this
    var query = world.Query<Position, Velocity>().Build();
    
    // Now we can loop through these components
    query.Run((count, positions, velocities) => 
    {
        for (var i = 0; i < count; i++)
        {
            positions[i].X += velocities[i].X;
            positions[i].Y += velocities[i].Y;
        }
    });
    
    // we can also iterate through them using multithreading!
    // for that, we simply replace `Run` with `RunParallel`
    // note that HypEcs is an arche type based ECS.
    // when running iterations multithreaded, that means we parallelise each *Table* in the ecs,
    // not each component iteration. This means MultiThreading benefits from archetype fragmentation,
    // but does not bring any benefits when there is only one archetype existing in the ecs that is iterated.
    query.RunParallel((count, positions, velocities) => 
    {
        for (var i = 0; i < count; i++)
        {
            positions[i].X += velocities[i].X;
            positions[i].Y += velocities[i].Y;
        }
    });
    
    // You can create more complex, expressive queries through the QueryBuilder.
    // Here, we request every entity that has a Name component, owes money to Bob and does not have the Dead tag.
    var appleLovers = world.QueryBuilder<Entity, Name>().Has<Owes>(bob).Not<Dead>().Build();
    
    // Note that we only get the components inside Query<>.
    // Has<T>, Not<T> and Any<T> only filter, but we don't actually get T in the loop.
    appleLovers.Run((count, entities, names) => 
    {
        for (var i = 0; i < count; i++)
        {
            Console.WriteLine($"Entity {entities[i]} with name {names[i].Value} owes bob money and is still alive.")
        }
    });
}

Creating a World

// A world is a container for different kinds of data like entities & components.
World world = new World();

Running a System

// Create an instance of your system.
var moveSystem = new MoveSystem();

// Run the system.
// The system will match all entities of the world you enter as the parameter.
moveSystem.Run(world);

// You can run a system as many times as you like.
moveSystem.Run(world);
moveSystem.Run(world);
moveSystem.Run(world);

// Usually, systems are run once a frame, inside your game loop.

SystemGroups

// You can create system groups, which bundle together multiple systems.
SystemGroup group = new SystemGroup();

// Add any amount of systems to the group.
group.Add(new SomeSystem())
     .Add(new SomeOtherSystem())
     .Add(new AThirdSystem());

// Running a system group will run all of its systems in the order they were added.
group.Run(world);

Example of a Game Loop

// In this example, we are using the Godot Engine.
using Godot;
using HypEcs;
using World = HypEcs.World; // Godot also has a World class, so we need to specify this.

public class GameLoop : Node
{
    World world = new World();

    SystemGroup initSystems = new SystemGroup();
    SystemGroup runSystems = new SystemGroup();
    SystemGroup cleanupSystems = new SystemGroup();

    // Called once on node construction.
    public GameLoop()
    {
        // Add your initialization systems.
        initSystem.Add(new SomeSpawnSystem());

        // Add systems that should run every frame.
        runSystems.Add(new PhysicsSystem())
            .Add(new AnimationSystem())
            .Add(new PlayerControlSystem());
        
        // Add systems that are called once when the Node is removed.
        cleanupSystems.Add(new DespawnSystem());
    }

    // Called every time the node is added to the scene.
    public override void _Ready()
    {
        // Run the init systems.
        initSystems.Run(world);   
    }

    // Called every frame. Delta is time since the last frame.
    public override void _Process(float delta)
    {
        // Run the run systems.
        runSystems.Run(world);

        // IMPORTANT: For HypEcs to work properly, we need to tell the world when a frame is done.
        // For that, we call Tick() on the world, at the end of the function.
        world.Tick();
    }

    // Called when the node is removed from the SceneTree.
    public override void _ExitTree()
    {
        // Run the cleanup systems.
        cleanupSystems.Run(world);
    }
}

Acknowledgements

Many thanks to Byteron (Aaron Winter) for creating HypEcs and RelEcs, the inspiring libraries that fennECS evolved from.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories (2)

Showing the top 2 popular GitHub repositories that depend on fennecs:

Repository Stars
outfox/fennecs
... the tiny C# ECS that loves you back!
Doraku/Ecs.CSharp.Benchmark
Benchmarks of some C# ECS frameworks.
Version Downloads Last updated
0.5.14-beta 389 10/30/2024
0.5.13-beta 53 10/30/2024
0.5.12-beta 58 10/29/2024
0.5.11-beta 159 9/25/2024
0.5.10-beta 1,104 7/21/2024
0.5.9-beta 122 7/20/2024
0.5.8-beta 403 7/4/2024
0.5.7-beta 226 6/15/2024
0.5.6-beta 67 6/15/2024
0.5.5-beta 236 6/12/2024
0.5.4-beta 273 6/11/2024
0.5.3-beta 61 6/11/2024
0.5.2-beta 74 6/11/2024 0.5.2-beta is deprecated because it is no longer maintained.
0.5.1-beta 408 6/7/2024 0.5.1-beta is deprecated because it is no longer maintained.
0.5.0-beta 178 6/5/2024 0.5.0-beta is deprecated because it is no longer maintained.
0.4.6-beta 408 5/30/2024 0.4.6-beta is deprecated because it is no longer maintained.
0.4.5-beta 87 5/29/2024 0.4.5-beta is deprecated because it is no longer maintained.
0.4.2-beta 1,082 5/23/2024 0.4.2-beta is deprecated because it is no longer maintained.
0.4.1-beta 92 5/23/2024 0.4.1-beta is deprecated because it is no longer maintained.
0.4.0-beta 94 5/22/2024 0.4.0-beta is deprecated because it is no longer maintained.
0.3.5-beta 108 5/12/2024 0.3.5-beta is deprecated because it is no longer maintained.
0.3.0-beta 83 5/9/2024 0.3.0-beta is deprecated because it is no longer maintained.
0.2.0-beta 119 3/9/2024 0.2.0-beta is deprecated because it is no longer maintained.
0.1.1-beta 368 3/4/2024 0.1.1-beta is deprecated because it is no longer maintained.
0.1.0-beta 99 3/1/2024 0.1.0-beta is deprecated because it is no longer maintained.
0.0.7-pre 86 2/25/2024 0.0.7-pre is deprecated because it is no longer maintained.
0.0.6-pre 80 2/25/2024 0.0.6-pre is deprecated because it is no longer maintained.
0.0.5-pre 82 2/23/2024 0.0.5-pre is deprecated because it is no longer maintained.
0.0.4-pre 84 2/21/2024 0.0.4-pre is deprecated because it is no longer maintained.
0.0.3-pre 87 2/9/2024 0.0.3-pre is deprecated because it is no longer maintained.
0.0.2-pre 80 2/9/2024 0.0.2-pre is deprecated because it is no longer maintained.
0.0.1-pre 96 2/6/2024 0.0.1-pre is deprecated because it is no longer maintained.