/BitEcs

BitEcs - Lightweight C# Entity Component System framework

Primary LanguageC#MIT LicenseMIT

BitEcs - Lightweight C# Entity Component System framework

Ergonomic, performant, zero/small memory allocations/footprint, no dependencies on any game engine - main goals of this project.

Important! Don't forget to use DEBUG builds for development and RELEASE builds in production: all internal error checks / exception throwing works only in DEBUG builds and eleminated for performance reasons in RELEASE.

Important! BitEcs API is not thread safe and will never be! If you need multithread-processing - you should implement it on your side as part of ecs-system.

Basics

World

Container for all your data.

// Creates a new world.
EcsWorld world = new EcsWorld();

// Creates a new named world.
EcsWorld world = new EcsWorld("Name");

// Destroys a world;
world.Destroy();

Entity

Сontainer for components. Implemented as int:

// Creates new entity in world context.
EcsEntity entity = _world.Spawn();

// Any entity can be destroyed. 
// All components will be removed first, then entity will be destroyed. 
entity.Destoy();

Important! Entities can't live without components and will be killed automatically after last component removement.

Component

Container for user data without / with small logic inside:

struct Component1
{
    public int Id;
    public string Name;
}

Components can be added and removed directly through entities.

// creates entity
EcsEntity entity = world.Spawn();

// adds component to entity.
entity.Add<Component1>();

// uou can also add multiple components at once
entity.Add<Component2>().Add<Component3>();

// or add a predefined struct as component
entity.Add(new Component4("Value"));

// removing components is just as easy
entity.Remove<Component1>().Remove<Component4>();

if you have an EcsEntity in your hand, you can also get components from it.

// get a component from an entity
ref var c1 = ref entity.Get<Component1>();
ref var c2 = ref entity.Get<Component2>();

Important! If you try to get a Component the Entity does not have your game will crash!

System

Сontainer for logic for processing entities.

class CustomSystem : IEcsSystem
{
    public void Run(EcsWorld world)
    {
        // Will be called on each EcsSystems.Run() call.
    }
}

Run systems like so:

// Systems are run on Worlds.
EcsWorld world = new EcsWorld();

// create an instance of the system;
IEcsSystem system = new CustomSystem();

// run the system on a world
system.Run(world);

ForEach

to iterate components in your system you can use the ForEach function of EcsWorld

class CustomSystem : IEcsSystem
{
    public void Run(EcsWorld world)
    {
        // ForEach takes a Function, the components are defined through their Parameters
        world.ForEach((ref Position pos, ref Velocity vel) =>
        {
            pos.Value += vel.Value;
        });
    }
}

the functions can have a special parameter in case you also need the entity

class CustomSystem : IEcsSystem
{
    public void Run(EcsWorld world)
    {
        // ForEach takes a Function, the components are defined through their Parameters
        world.ForEach((EcsEntity entity, ref Position pos, ref Velocity vel) =>
        {
            // only apply vel if entity does not have a Paused component
            if (!entity.HasComponent<Paused>())
            {
                pos.Value += vel.Value;
            }
        });
    }
}

Query

another way to iterate components in you system is using queries.

class CustomSystem : IEcsSystem
{
    public void Run(EcsWorld world)
    {
        // create a query
        var query = world.Query<Component1>().End();
        
        // iterate the query
        // query iteration gives you entity ids.
        foreach (int entityId in query)
        {
            // fetch entity from entityId
            var entity = world.Entity(entityId);

            // get component from entity
            ref var c1 = ref world.Entity(entityId).Get<Component1>();

            // you can get any component from this entity, 
            // however only the ones you queried for are definitely present.
            // might crash if component is not present.
            ref var c2 = ref world.Entity(entityId).Get<Component2>(); 
        }
    }
}

You can put multiple constraints on your queries

// create a query with multiple constraints
var query = world.Query<Component1>().Inc<Component2>().Exc<Component3>().End();

Important: Any query supports any amount of components, include and exclude lists can't intersect and should be unique.

Resource

Instance of any custom type can be shared between all systems:

class CustomResource
{
    public string Path;
}
// create resource
CustomResource res = new CustomResource { Path = "Items/{0}" };

// add resource to world
world.AddResource(res);

// or add resource with default values
world.AddResource<CustomResource>();
using Bitron.Ecs.Resource;

class System : IEcsSystem
{
    public void Run(EcsWorld world)
    {
        // get resource from world
        CustomResource res = systems.GetResource<CustomResource>(); 
        
        string path = string.Format(res.Path, 123);
        // path == "Items/123" here.
    } 
}

Important: Resources are unique. If you try to add another instance of a same resource type, the present one will be overwritten by the new one.

Special classes

EcsPool

Container for components. EcsEntity and Queries wrap around these pools for a more ergonomic API, but at a performance cost. If you need more performance, you can optimize your code using pools directly.

int entityId = world.Spawn().GetId();
EcsPool<Component1> pool = world.GetPool<Component1>(); 

// Add() adds component to entity. If component already exists - exception will be raised in DEBUG.
ref Component1 c1 = ref pool.Add(entityId);

// Get() returns exist component on entity. If component does not exists - exception will be raised in DEBUG.
ref Component1 c1 = ref pool.Get(entityId);

// Del() removes component from entity. If it was last component - entity will be removed automatically too.
pool.Remove(entityId);
class CustomSystem : IEcsSystem
{
    public void Run(EcsWorld world)
    {
        // create a query
        var query = world.Query<Component1>();
        var pool = query.GetPool<Component1>();
        
        // iterate the query
        // query iteration gives you entity ids.
        foreach (int entityId in query)
        {
            // get components from pool directly
            ref var c1 = ref pool.Get<Component1>(entityId);
        }
    }
}

Important! After removing component will be pooled and can be reused later. All fields will be reset to default values automatically.

SystemGroup

You can group Systems together into an EcsSystemGroup

// create a new system group
EcsSystemGroup systemGroup = new EcsSystemGroup();

// add systems to the group
systemGroup
    .Add(new BirthSystem())
    .Add(new AgeSystem())
    .Add(new DeathSystem())
    .Add(new PhysicsSystem())

// like systems, system groups are run on a world
EcsWorld world = new EcsWorld();

// run all systems in the group
systemGroup.Run(world);

One Frame Systems

Sometimes it is useful to be able add a System that removes all components of a certain type from all entities. Therefore you can

EcsSystemGroup systemGroup = new EcsSystemGroup();

systemGroup
    .Add(new AttackSystem())
    .Add(new DamageSystem())
    .OneFrame<Damage>(); // removes any Damage components from all entities

If you don't use EcsSystemGroup, you still can use a predefined System for this

var system = new RemoveAllComponentsOfType<Damage>();
system.Run(world);

Custom engine

C#7.3 or above required for this framework.

Code example - each part should be integrated in proper place of engine execution flow.

using Bitron.Ecs;

class Engine
{
    EcsWorld _world = new EcsWorld();
    EcsSystemGroup _initSystems = new EcsSystemGroup();
    EcsSystemGroup _runSystems = new EcsSystemGroup();
    EcsSystemGroup _destroySystems = new EcsSystemGroup();

    // Initialization of ecs world and systems.
    void Init()
    {
        // add your systems
        _initSystems.Add(new SpawnPlayerSystem());
        _runSystems.Add(new UpdatePlayerSystem()).Add(new PhysicsSystem());
        _destroySytstems.Add(new DespawnPlayerSystem());

        // run init systems
        _initSystems?.Run(_world);
    }

    // Engine update loop.
    void UpdateLoop()
    {
        _runSystems?.Run(_world);
    }

    // Cleanup.
    void Destroy()
    {
        // run destroy systems
        _destroySystems?.Run(_world);
        _world?.Destroy();
    }
}

License

The software is released under the terms of the MIT license.

No personal support or any guarantees.

FAQ

I copy&paste my reset components code again and again. How can I do it in other manner?

If you want to simplify your code and keep reset/init code at one place, you can setup custom handler to process cleanup / initialization for component:

struct MyComponent : IEcsAutoReset<MyComponent>
{
    public int Id;
    public object LinkToAnotherComponent;

    public void AutoReset(ref MyComponent c)
    {
        c.Id = 2;
        c.LinkToAnotherComponent = null;
    }
}

This method will be automatically called for brand new component instance and after component removing from entity and before recycling to component pool.

Important: With custom AutoReset behaviour there are no any additional checks for reference-type fields, you should provide correct cleanup/init behaviour without possible memory leaks.