Dartk.EntitiesDotNet 0.1.0-alpha10

Prefix Reserved
This is a prerelease version of Dartk.EntitiesDotNet.
dotnet add package Dartk.EntitiesDotNet --version 0.1.0-alpha10                
NuGet\Install-Package Dartk.EntitiesDotNet -Version 0.1.0-alpha10                
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="Dartk.EntitiesDotNet" Version="0.1.0-alpha10" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Dartk.EntitiesDotNet --version 0.1.0-alpha10                
#r "nuget: Dartk.EntitiesDotNet, 0.1.0-alpha10"                
#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 Dartk.EntitiesDotNet as a Cake Addin
#addin nuget:?package=Dartk.EntitiesDotNet&version=0.1.0-alpha10&prerelease

// Install Dartk.EntitiesDotNet as a Cake Tool
#tool nuget:?package=Dartk.EntitiesDotNet&version=0.1.0-alpha10&prerelease                

EntitiesDotNet

Warning: This is an experimental project and is not ready for production use.

A fast and ergonomic general purpose Entity Component System library for .NET inspired by Unity Entities.

Installation

Add prerelease package Dartk.EntitiesDotNet.

dotnet add package Dartk.EntitiesDotNet --prerelease

(Optional) For EntityRef and Inline source generators add prerelease package Dartk.EntitiesDotNet.Generators

dotnet add package Dartk.EntitiesDotNet.Generators --prerelease

Entity Component System concepts

Entity

Entities in ECS are basic units. They are similar to objects in OOP, but unlike objects entities do not store any values themselves. Values are stored in components that are layed out in parallel arrays for efficient processing of components of the same type. Entity is just a lightweight key that is used to identify a position of components in the arrays.

Component

Components are what holds the data. They are stored in parallel arrays and can be efficiently processed sequentially or accessed randomly using Entity as a key.

System

Any function that manipulates entities.

EntitiesDotNet types

EntityId

A structure that represents an Entity and used as a key to index entity's components by EntityManager.

public readonly record struct EntityId(int Id, int Version);

Archetype

A combination of component types. Every unique combination is represented by a single instance of Archetype class.

var archetype0 = Archetype<Translation, Velocity, Acceleration>.Instance;
var archetype1 = Archetype.Instance<Translation, Velocity, Acceleration>();

Assert.IsTrue(object.ReferenceEquals(archetype0, archetype1));

IComponentArray

An interface that provides access to parallel arrays of components.

void UpdateTranslation(IComponentArray components, float deltaTime)
{
    if (!components.Archetype.Contains<Velocity, Translation>()) return;

    int count = components.Count;
    ReadOnlySpan<Velocity> velocities = components.GetReadOnlySpan<Velocity>();
    Span<Translation> translations = components.GetSpan<Translation>();
    
    for (var i = 0; i < count; ++i)
    {
        translations[i] += velocities[i] * deltaTime;
    }
}

An equivalent method using Read<...>.Write<...>.From:

void UpdateTranslation(IComponentArray components, float deltaTime)
{
    var (count, velocities, translations) = Read<Velocity>.Write<Translation>.From(components);
    for (var i = 0; i < count; ++i)
    {
        translations[i] += velocities[i] * deltaTime;
    }
}

EntityArrays

A collection of IComponentArray objects.

void UpdateTranslation(EntityArrays entities, float deltaTime)
{
	entities.ForEach((in Velocity velocity, ref Translation translation) =>
		translation += velocity * deltaTime);
}

EntityManager

Creates and destroys entities, stores components.

Components are stored in a dictionary of IComponentArray objects indexed by Archetype. Entities property returns an EntityArrays collection that holds all entities' components owned by the EntityManager.

var entityManager = new EntityManager();

// creates entity with Velocity and Translation components
var entity0 = entityManager.CreateEntity(Archetype<Velocity, Translation>.Instance);

// creates entity with Velocity and Translation components and sets their values
var entity1 = entityManager.CreateEntity(new Velocity(10), new Translation(0));

// updates translation
var deltaTime = 1f / 60f;
entityManager.Entities.ForEach((in Velocity velocity, ref Translation translation) =>
	translation += velocity * deltaTime);

Entity

A convenience structure, that holds EntityId and EntityManager and provides easy access to components.

public readonly record struct Entity : IDisposable
{
    public Entity(EntityManager entityManager, EntityId id);
    
    public readonly EntityId Id;
    public readonly EntityManager EntityManager;
    public ref T RefRW<T>();            // mutable component reference
    public ref readonly T RefRO<T>();   // read-only component reference
    public void Dispose();              // Destroys entity
}

Iterating over components

All of the examples below declare a static class UpdateTranslationSystem with a method Execute(EntityArrays entityArrays, float deltaTime) that reads Velocity and updates Translation components for every entity in the entityArrays argument.

static class UpdateTranslationSystem
{
    public static void Execute(EntityArrays arrays, float deltaTime);
}

foreach loop

Read<TR0, TR1, ...>.Write<TW0, TW1, ...>.From() methods can be used to access spans of components (Span<T> or ReadOnlySpan<T>) from EntityArrays and IComponentArray. The result can be deconstructed into a count of elements followed by spans of the specified components.

static class UpdateTranslationSystem
{
    public static void Execute(EntityArrays entities, float deltaTime)
    {
        foreach (var (count, velocities, translations) in
            Read<Velocity>.Write<Translation>.From(entities))
        {
            // velocities : ReadOnlySpan<Velocity>
            // translations : Span<Translation>
            
            for (var i = 0; i < count; ++i)
            {
                translations[i] += velocities[i] * deltaTime;
            }
        }
    }
}

Or

static class UpdateTranslationSystem
{
    public void Excecute(EntityArrays entities, float deltaTime)
    {
        foreach (IComponentArray components in entities)
        {
            var (count, v, t) = Read<Velocity>.Write<Translation>.From(components);
            // velocities : ReadOnlySpan<Velocity>
            // translations : Span<Translation>
            
            for (var i = 0; i < count; ++i)
            {
                translations[i] += velocities[i] * deltaTime;
            }
        }
    }
}

foreach loop with unmanaged function call

Spans of components that are returned from Read<TR0, TR1, ...>.Write<TW0, TW1, ...>.From() methods can be passed to an unmanaged function.

static class UpdateTranslationSystem
{
    public static unsafe void loop_native(EntityArrays entities, float deltaTime)
    {
        foreach (var (count, velocities, translations) in
            Read<Velocity>.Write<Translation>.From(entities))
        {
            fixed (Velocity* velocitiesPtr = velocities)
            fixed (Translation* translationsPtr = translations)
            {
                update_translation(count, velocitiesPtr, translationsPtr, deltaTime);
            }
        }
    }
    
    [DllImport("native_library.dll")]
    private static extern void update_translation(
        int count,
        Velocity* velocities,
        Translation* translations,
        float deltaTime);
}

ForEach extensions

ForEach extension methods for EntityArrays and IComponentArray take one of the ForEach_RR..WW..<T0, T1, ...> delegates to iterate over components.

namespace EntitiesDotNet.Delegates;

public delegate void Func_RW<T0, T1>(in T0 arg0, ref T1 arg1);
public delegate void Func_RW_I<T0, T1>(in T0 arg0, ref T1 arg1, int index);
static class UpdateTranslationSystem
{
    public static void Execute(EntityArrays entities, float deltaTime)
    {
        entities.ForEach((in Velocity v, ref Translation t) => t += v * deltaTime);
    }
}

ForEach extensions inlining

ForEach extension method calls can be inlined.

Info: This is one of the fastest ways to iterate over components

static partial class UpdateTranslationSystem
{
    [Inline] static void _Execute(EntityArrays entities, float deltaTime)
    {
        entities.ForEach((in Velocity v, ref Translation t) => t += v * deltaTime);
    }
}

<details>

<summary>Generated <code>UpdateTranslationSystem.Execute</code></summary>

#define SOURCEGEN
#pragma warning disable CS0105 // disables warning about using the same namespaces several times

using EntitiesDotNet;

static partial class UpdateTranslationSystem
{
    [EntitiesDotNet.GeneratedFrom(nameof(_Execute))]
    public static void Execute(EntityArrays entities, float deltaTime)
    {
        {
            var @this = entities;
            foreach (var __array in @this)
            {
                if (__array.Count == 0 || !__array.Archetype.Contains<Velocity, Translation>())
                {
                    continue;
                }

                var __count = __array.Count;
                ref var v = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(__array.GetSpan<Velocity>());
                ref var t = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(__array.GetSpan<Translation>());
                for (var __i = 0; __i < __count; v = ref System.Runtime.CompilerServices.Unsafe.Add(ref v, 1), t = ref System.Runtime.CompilerServices.Unsafe.Add(ref t, 1), ++__i)
                {
                    t += v * deltaTime;
                }
            }
        }
    }
}
#pragma warning restore CS0104

</details>

Iterating over components using EntityRef

.NET 7 introduced ref fields that can be used to access entity's components.

Warning: ref fields are not supported for runtime targets lower than .NET 7.

Create a ref partial struct with mutable ref fields of component types (ref itself can be readonly but the field must be mutable). Mark the struct with [EntityRef] attribute. The struct must be public or internal to support ForEach extension methods for EntityArrays and IComponentArray.

static partial class UpdateTranslationSystem
{
    [EntityRef]
    public ref partial struct ThisEntity
    {
        public ref Translation Translation;
        public ref readonly Velocity Velocity;   // mutable field, readonly reference
    }
}

foreach loop

EntityRef generator creates static method From(IComponentArray) that can be used with foreach loop:

Info: This is one of the fastest ways to iterate over components

static partial class UpdateTranslationSystem
{
    [EntityRef]
    ref partial struct ThisEntity
    {
        public ref Translation Translation;
        public ref readonly Velocity Velocity;
    }
    
    public static void Execute(EntityArrays entityArrays, float deltaTime)
    {
        foreach (var array in entityArrays)
        foreach (var entity in ThisEntity.From(array))
        {
            entity.Translation += entity.Velocity * deltaTime;
        }
    }
}

ForEach extensions

If the EntityRef struct is public or internal, then ForEach extension methods for EntityArrays and IComponentArray are generated:

static partial class UpdateTranslationSystem
{
    [EntityRef]
    public ref partial struct ThisEntity
    {
        public ref Translation Translation;
        public ref readonly Velocity Velocity;
    }

    public static void Execute(EntityArrays entities, float deltaTime)
    {
        entities.ForEach((in ThisEntity entity) =>
            entity.Translation += entity.Velocity * deltaTime);
    }
}

ForEach extensions inlining

ForEach extension method calls can be inlined.

Info: This is one of the fastest ways to iterate over components

static partial class UpdateTranslationSystem
{
    [EntityRef]
    public ref partial struct ThisEntity
    {
        public ref Translation Translation;
        public ref readonly Velocity Velocity;
    }

    [Inline] static void _Execute(EntityArrays entities, float deltaTime)
    {
        entities.ForEach((in ThisEntity entity) =>
            entity.Translation += entity.Velocity * deltaTime);
    }
}

<details>

<summary>Generated method <code>UpdateTranslationSystem.Execute</code></summary>

#define SOURCEGEN
#pragma warning disable CS0105 // disables warning about using the same namespaces several times

using EntitiesDotNet;

static partial class UpdateTranslationSystem
{
    [EntitiesDotNet.GeneratedFrom(nameof(_Execute))]
    public static void Execute(EntityArrays entities, float deltaTime)
    {
        {
            var @this = entities;
            UpdateTranslationSystem.ThisEntity entity = default;
            foreach (var __array in @this)
            {
                var(__count, __VelocitySpan, __TranslationSpan) = EntitiesDotNet.Read<Velocity>.Write<Translation>.From(__array);
                if (__count == 0)
                    continue;
                ref var __Velocity = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(__VelocitySpan);
                ref var __Translation = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(__TranslationSpan);
                for (var __i = 0; __i < __count; __Velocity = ref System.Runtime.CompilerServices.Unsafe.Add(ref __Velocity, 1), __Translation = ref System.Runtime.CompilerServices.Unsafe.Add(ref __Translation, 1), ++__i)
                {
                    entity.Velocity = ref __Velocity;
                    entity.Translation = ref __Translation;
                    entity.Translation += entity.Velocity * deltaTime;
                }
            }
        }
    }
}
#pragma warning restore CS0104

</details>

Inlining source generator

EntitiesDotNet.Generators includes a source generator that inlines a body of a lambda that is passed to ForEach extension method (for individual components and EntityRefs). The inlined methods do not allocate memory on the heap and iterate over components with near-native performance.

Given the following code:

static partial class InliningExample
{
    [Inline] static int _SumPositiveIntegers(EntityArrays entities)
    {
        var sum = 0;
        entities.ForEach((in int i) =>
        {
            if (i <= 0) return;
            sum += i;
        });

        return sum;
    }
}

Inlining generator will create a method called SumPositiveIntegers that will

Generated code:

#define SOURCEGEN
#pragma warning disable CS0105 // disables warning about using the same namespaces several times

using EntitiesDotNet;

static partial class InliningExample
{
    [EntitiesDotNet.GeneratedFrom(nameof(_SumPositiveIntegers))]
    public static int SumPositiveIntegers(EntityArrays entities)
    {
        var sum = 0;
        {
            var @this = entities;
            foreach (var __array in @this)
            {
                if (__array.Count == 0 || !__array.Archetype.Contains<int>())
                {
                    continue;
                }

                var __count = __array.Count;
                ref var i = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(__array.GetSpan<int>());
                for (var __i = 0; __i < __count; i = ref System.Runtime.CompilerServices.Unsafe.Add(ref i, 1), ++__i)
                {
                    {
                        if (i <= 0)
                            continue;
                        sum += i;
                    }
                }
            }
        }

        return sum;
    }
}
#pragma warning restore CS0104

Requirements

For an inlined method to be generated following conditions must be met:

  1. Containing type must be partial

    static partial class InliningExample
    
  2. The original method must be marked with one of the attributes:

    • [Inline]
    • [Inline.Private]
    • [Inline.Protected]
    • [Inline.Internal]
    • [Inline.Public]

    These attributes take a string? name = null argument that defines a name for a method that will be generated.

    [Inline.Public("InlinedSumPositiveIntegers")] static int SumPositiveIntegers(EntityArrays entities)
    // Inlined method name: 'InlinedSumPositiveIntegers'
    

    If the attribute's argument name is null then the generated method name will be determined by the following rule:

    If the original method's name starts with an underscore, then the name without an underscore will be used.

    [Inline.Public] static int _SumPositiveIntegers(EntityArrays entities)
    // Inlined method name: 'SumPositiveIntegers'
    

    If the original method's name does not start with an underscore, then the name with _Inlined at the end will be used.

    [Inline.Public] static int SumPositiveIntegers(EntityArrays entities)
    // Inlined method name: 'SumPositiveIntegers_Inlined'
    

Performance

The following benchmark measured a performance of updating Translation from Velocity for 2 x N entities.

struct float3
{
    public float X;
    public float Y;
    public float Z;
}

struct Velocity
{
    public float3 Float3;
}

struct Translation {
    public float3 Float3;
}

void UpdateTranslation(in Velocity v, ref Translation t, float deltaTime)
{
    t.Float3.X += v.Float3.X * deltaTime;
    t.Float3.Y += v.Float3.Y * deltaTime;
    t.Float3.Z += v.Float3.Z * deltaTime;
}

Component systems included in the benchmark:

Full code of the component systems: ComponentSystems.cs

The benchmark was run on a system:

BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19045.2728)
AMD Ryzen 5 5600U with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.101
[Host]     : .NET 7.0.1 (7.0.122.56804), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.1 (7.0.122.56804), X64 RyuJIT AVX2

2 x 1 000 Entities

Method Mean Error StdDev Ratio Allocated
native 1.612 μs 0.0036 μs 0.0032 μs 1.00 -
loop_native 1.858 μs 0.0019 μs 0.0018 μs 1.15 -
ext_inl 1.963 μs 0.0381 μs 0.0408 μs 1.22 -
ER_ext_inl 1.926 μs 0.0029 μs 0.0027 μs 1.19 -
ER_loop 1.931 μs 0.0070 μs 0.0065 μs 1.20 -
loop 2.217 μs 0.0072 μs 0.0060 μs 1.38 -
ext 3.784 μs 0.0480 μs 0.0513 μs 2.35 120 B
ER_ext 4.028 μs 0.0079 μs 0.0070 μs 2.50 120 B

alternate text is missing from this package README image

2 x 100 000 Entities

Method Mean Error StdDev Ratio Allocated
native 173.3 μs 0.58 μs 0.49 μs 1.00 -
loop_native 168.4 μs 0.40 μs 0.37 μs 0.97 -
ext_inl 174.4 μs 1.76 μs 1.37 μs 1.01 -
ER_ext_inl 174.7 μs 0.53 μs 0.44 μs 1.01 -
ER_loop 174.9 μs 0.64 μs 0.60 μs 1.01 -
loop 206.8 μs 0.71 μs 0.66 μs 1.19 -
ext 361.5 μs 1.58 μs 1.47 μs 2.09 120 B
ER_ext 415.8 μs 1.01 μs 0.94 μs 2.40 120 B

alternate text is missing from this package README image

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Dartk.EntitiesDotNet:

Package Downloads
Dartk.EntitiesDotNet.Generators

Source generators for EntitiesDotNet

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.1.0-alpha10 94 5/10/2023