Unity simple lightweight ECS framework LeoECS Chinese document

Posted by kittrellbj on Thu, 20 Jan 2022 07:05:46 +0100

LeoECS - simple lightweight C# entity component system framework

Performance, zero / small memory allocation / footprint, the main goal of this project - independent of any game engine.

**Important** It is a "structure based" version, if you search for a "class based" version - check Class based branching!

This framework requires C#7.3 or above.

**Important** Don't forget to use the debug version for development and release in the production environment: all internal error checking / exception throwing only works in the debug version and is deleted in the release environment for performance reasons.

**Important** ecs core API is unsafe and will never be safe! If you need multithreading - you should implement it in your ecs system.

download

As Unity module

This repository can be installed as a unity module directly from the git url. In this way, the new line should be added to "Packages/manifest.json":

"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git",

By default, the latest released version is used. If you need a relay / development version, add the "development" name of the branch after the hash:

"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git#develop",

As a source

If you cannot / do not want to use the unity module, you can download the code from the Releases page as the archive source of the required version.

Main components of ECS

Component

The user data container has no / little logic inside:

struct WeaponComponent {
    public int Ammo;
    public string GunName;
}

**Important** Don't forget to manually initialize all fields for each new component - they will be reset to the default values when recycled to the pool.

Entity

Component container. Implemented as "EcsEntity" to wrap the internal identifier:

//Create a new entity in the world context
EcsEntity entity = _world.NewEntity ();

//Get() returns the component on the entity. If the component does not exist - it will be added.
ref Component1 c1 = ref entity.Get<Component1> ();
ref Component2 c2 = ref entity.Get<Component2> ();

//Del() deletes the component from the entity.
entity.Del<Component2> ();

//You can replace a component with a new instance of the component. If the component does not exist - it will be added.
var weapon = new WeaponComponent () { Ammo = 10, GunName = "Handgun" };
entity.Replace (weapon);

//Use Replace() to create linked components:
var entity2 = world.NewEntity ();
entity2.Replace (new Component1 { Id = 10 }).Replace (new Component2 { Name = "Username" });

//Any entity can be copied with all components:
var entity2Copy = entity2.Copy ();

//Any entity can be merged / moved to another entity (the source will be destroyed):
var newEntity = world.NewEntity ();
entity2Copy.MoveTo (newEntity); 
//All components in entity2Copy have been moved to newEntity and entity2Copy has been destroyed.

//Any entity can be destroyed.
entity.Destroy ();

**Important** Entities that do not have components will be created at the last ecsentity Automatically deleted when del() is called.

System

A logical container for processing filtered entities. The user class shall implement "IECsInitSystem", "IEcsDestroySystem", "IEcsRunSystem" (or other supported) interfaces:

class WeaponSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsDestroySystem, IEcsPostDestroySystem {
    public void PreInit () {
        //Will be in ecssystems During init() call and iecsintsystem Init is called before.
    }

    public void Init () {
        //Will be in ecssystems Called once during init() call.
    }

    public void Destroy () {
        //Will be in ecssystems Called once during the destroy() call.
    }

    public void PostDestroy () {
        //Will be in ecssystems During the destroy() call and iecsdestroysystem Destroy is called once.
    }
}
class HealthSystem : IEcsRunSystem {
    public void Run () {
        //Will be in each ecssystems Called on the run() call.
    }
}

Data injection

All compatible "EcsWorld" and "EcsFilter" fields of ECS system will be initialized automatically (automatic injection):

class HealthSystem : IEcsSystem {
    //Automatically inject fields.
    EcsWorld _world = null;
    EcsFilter<WeaponComponent> _weaponFilter = null;
}

Instances of any custom type can be accessed through ecssystems Inject() method into all systems:

var systems = new EcsSystems (world)
    .Add (new TestSystem1 ())
    .Add (new TestSystem2 ())
    .Add (new TestSystem3 ())
    .Inject (a)
    .Inject (b)
    .Inject (c)
    .Inject (d);
systems.Init ();

Each system will be scanned to compatible fields with appropriate initialization (can contain all fields or none).

**Important** Any user type of data injection can be used to share external data between systems.

Data injection in multi ECS system

If you want to use multiple "ECS systems", you can find the strange behavior of DI

struct Component1 { }

class System1 : IEcsInitSystem {
    EcsWorld _world = null;

    public void Init () {
        _world.NewEntity ().Get<Component1> ();
    } 
}

class System2 : IEcsInitSystem {
    EcsFilter<Component1> _filter = null;

    public void Init () {
        Debug.Log (_filter.GetEntitiesCount ());
    }
}

var systems1 = new EcsSystems (world);
var systems2 = new EcsSystems (world);
systems1.Add (new System1 ());
systems2.Add (new System2 ());
systems1.Init ();
systems2.Init ();

You will get '0' in the console. The problem is that DI starts with the "Init()" method in each "EcsSystems". This means that any new instance of "EcsFilter" (with delayed initialization) will only be correctly injected into the current "EcsSystems".

To fix this behavior, modify the startup code as follows:

var systems1 = new EcsSystems (world);
var systems2 = new EcsSystems (world);
systems1.Add (new System1 ());
systems2.Add (new System2 ());
systems1.ProcessInjects ();
systems2.ProcessInjects ();
systems1.Init ();
systems2.Init ();

After repair, you should get "1" on the console.

Specify class

EcsFilter

Container to hold filtered entities with the specified component list:

class WeaponSystem : IEcsInitSystem, IEcsRunSystem {
    //Auto injection fields: EcsWorld instance and EcsFilter.
	EcsWorld _world=null;

	//We want to get entities with "WeaponComponent" but no "HealthComponent".
    EcsFilter<WeaponComponent>.Exclude<HealthComponent> _filter = null;

    public void Init () {
        _world.NewEntity ().Get<WeaponComponent> ();
    }

    public void Run () {
        foreach (var i in _filter) {
            //The entity that contains the WeaponComponent.
            ref var entity = ref _filter.GetEntity (i);

            //Get1 will return a link to the attached "WeaponComponent".
            ref var weapon = ref _filter.Get1 (i);
            weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1);
        }
    }
}

**Important** If you want to destroy part of this data (entity or component), you should not use the "ref" modifier on any filter data outside the foreach loop on this filter - this will break memory integrity.

All components in the filterInclude constraint can be accessed through ecsfilter Get1(),EcsFilter.Get2(), etc. - in the same order as used in the filter type declaration.

If fast access is not required (for example, for flag based components without data), the component can implement the "IEcsIgnoreInFilter" interface to reduce memory usage and improve performance:

struct Component1 { }

struct Component2 : IEcsIgnoreInFilter { }

class TestSystem : IEcsRunSystem {
    EcsFilter<Component1, Component2> _filter = null;

    public void Run () {
        foreach (var i in _filter) {
            //Its valid code.
            ref var component1 = ref _filter.Get1 (i);

            //Due to memory / performance reasons_ filter.Get2() has an invalid code due to its cache.
            ref var component2 = ref _filter.Get2 (i);
        }
    }
}

Important: any filter supports up to 6 component types, such as the "include" constraint, and up to 2 component types as the "exclude" constraint. Shorter constraints - better performance.

Important: if you try to use two filters with the same components but in different order, an exception will appear with details about the conflict type, but only in DEBUG mode. In "RELEASE" mode, all checks are skipped.

EcsWorld

The root level container of all entities / components, which works in a similar way to the isolated environment.

Important: when the instance is no longer used, don't forget to call ecsworld Destroy() method.

EcsSystems

System groups to process "EcsWorld" instances:

class Startup : MonoBehaviour {
    EcsWorld _world;
    EcsSystems _systems;

    void Start () {
        //Create ecs environment.
        _world = new EcsWorld ();
        _systems = new EcsSystems (_world)
            .Add (new WeaponSystem ());
        _systems.Init ();
    }
    
    void Update () {
        //Handle all related systems.
        _systems.Run ();
    }

    void OnDestroy () {
        //Destroy the system logical group.
        _systems.Destroy ();
        //World Destruction.
        _world.Destroy ();
    }
}

The EcsSystems instance can be used as a nested system (supports any type of IEcsInitSystem, IEcsRunSystem, ECS behavior):

//Initialization initialization.
var nestedSystems = new EcsSystems (_world).Add (new NestedSystem ());
//Do not call nestedsystems. Com here Init(), rootSystems will execute automatically.

var rootSystems = new EcsSystems (_world).Add (nestedSystems);
rootSystems.Init ();

//Update loop update loop.

//Do not call nestedsystems. Com here Run(), which rootSystems will execute automatically.
rootSystems.Run ();

// Destroying destroying
//Do not call nestedsystems. Com here Destroy(), rootSystems will execute automatically.
rootSystems.Destroy ();

It can be handled at run time to enable or disable any "IEcsRunSystem" or "EcsSystems" instance:

class TestSystem : IEcsRunSystem {
    public void Run () { }
}
var systems = new EcsSystems (_world);
systems.Add (new TestSystem (), "my special system");
systems.Init ();
var idx = systems.GetNamedRunSystem ("my special system");

//The status here is true. By default, all systems are active.
var state = systems.GetRunSystemState (idx);

//System execution is prohibited.
systems.SetRunSystemState (idx, false);

Engine integration

Unity

Test on unity 2019.1 (independent of it) and include assembly definitions for compiling into separate assembly files (for performance reasons).

The Unity editor integration includes a code template and a world debug viewer.

Custom engine

Code example - each part should be integrated in the appropriate place of the engine execution flow

using Leopotam.Ecs;

class EcsStartup {
    EcsWorld _world;
    EcsSystems _systems;

    //ecs world and system initialization.
    void Init () {        
        _world = new EcsWorld ();
        _systems = new EcsSystems (_world);
        _systems
            // Register the system here, for example:
            // .Add (new TestSystem1 ())
            // .Add (new TestSystem2 ())
            
            // Register a frame component (order is important), for example:
            // .OneFrame<TestComponent1> ()
            // .OneFrame<TestComponent2> ()
            
            // Insert service instances here (order is not important), for example
            // .Inject (new CameraService ())
            // .Inject (new NavMeshSupport ())
            .Init ();
    }

    //Engine update cycle.
    void UpdateLoop () {
        _systems?.Run ();
    }

    //clear.
    void Destroy () {
        if (_systems != null) {
            _systems.Destroy ();
            _systems = null;
            _world.Destroy ();
            _world = null;
        }
    }
}

Projects supported by LeoECS

With sources:

Released Games:

Extend Extensions

Frequently asked questions (FAQs)

Structure based, class based versions? Which is better? Why?

Class based versions are stable, but no longer stable in an active development environment -- except for bug fixes (found in the "class based" branch).

The structure is based on only one version under development. It should be faster than the class based version, easier to clean up components, and you can more easily switch to "unity ecs" later if you like. Even after the release of "unity ecs", the framework will still be in the development stage.

I want to know - whether the component has been added to the entity and get it / add a new component, otherwise, how do I do it?

If you don't care if the component has been added, and you just want to ensure that the entity contains the component - just call ecsententity Get < T > - it will return the existing component. If it does not exist, a new component will be added.

If you want to know if the component exists (use it later in the custom logic), use ecsententity Has < T > method, which will return the fact added before the component.

I want to be in monobehavior Update() handles a system in monobehavior Fixedupdate() handles another system. What shall I do?

For splitting systems by MonoBehaviour-method multiple EcsSystems logical groups should be used:

The monobehavior method of multiple "EcsSystems" logical groups should be used to split the system:

EcsSystems _update;
EcsSystems _fixedUpdate;

void Start () {
    var world = new EcsWorld ();
    _update = new EcsSystems (world).Add (new UpdateSystem ());
    _update.Init ();
    _fixedUpdate = new EcsSystems (world).Add (new FixedUpdateSystem ());
    _fixedUpdate.Init ();
}

void Update () {
    _update.Run ();
}

void FixedUpdate () {
    _fixedUpdate.Run ();
}

I like how dependency injection works, but I want to skip some fields in initialization. What shall I do?

You can use the [EcsIgnoreInject] attribute on any field of the system:

...//Will be injected. EcsFilter<C1> _ filter1 = null;// Will skip. [EcsIgnoreInject]EcsFilter<C2> _ filter2 = null;

I don't like foreach loops. I know for loops are faster. How do I use it?

The current implementation of foreach loop is fast enough (custom enumerator, no memory allocation), and small performance differences can be found on 10k items and more. for loop iteration is no longer supported in the current version.

I copy and paste my reset component code again and again. How can I do it in other ways?

If you want to simplify the code and keep the reset/init code in one place, you can set up a custom handler to handle the cleaning / initialization of components:

struct MyComponent : IEcsAutoReset<MyComponent> {    public int Id;    public object LinkToAnotherComponent;    public void AutoReset (ref MyComponent c) {        c.Id = 2;        c.LinkToAnotherComponent = null;    }}

For new component instances, this method will be called automatically after removing the component from the entity and before recycling to the component pool.

Important: for the custom "AutoReset" behavior, there is no additional check for the reference type field. You should provide the correct cleanup/init behavior without memory leakage.

I use the component as an event that works for only one frame and then delete it in the last system in the execution sequence. It's so boring. How can I automate it?

If you want to delete single frame components without attaching custom codes, you can register them in EcsSystems:

struct MyOneFrameComponent { }EcsSystems _update;void Start () {    var world = new EcsWorld ();    _update = new EcsSystems (world);    _update        .Add (new CalculateSystem ())        .Add (new UpdateSystem ())        .OneFrame<MyOneFrameComponent> ()        .Init ();}void Update () {    _update.Run ();}

Important: all single frame components with the specified type will be deleted where OneFrame() is called to register the component in the execution flow.

I need more control over the default cache size of the internal structure. What should I do?

You can use the EcsWorldConfig instance to set a custom cache size:

var config = new EcsWorldConfig() {    // World.Entities default cache size.    WorldEntitiesCacheSize = 1024,    // World.Filters default cache size.    WorldFiltersCacheSize = 128,    // World.ComponentPools default cache size.    WorldComponentPoolsCacheSize = 512,    // Entity.Components default cache size (not doubled).    EntityComponentsCacheSize = 8,    // Filter.Entities default cache size.    FilterEntitiesCacheSize = 256,    };var world = new EcsWorld(config);...

I need more than 6 "include" or more than 2 "exclude" in the filter component. What do I do?

You can use EcsFilter autogen-tool and replace EcsFilter.cs file with brand new generated content.

I want to add some changes in response behavior to the filter item. What do I do?

Leoecs can be used_ FILTER_ Events definition to enable custom event listener support for filters:

class CustomListener: IEcsFilterListener {    public void OnEntityAdded (in EcsEntity entity) {        // reaction on compatible entity was added to filter.         Reactions to compatible entities have been added to the filter.} Public void onentityremoved (in ecdentity entity) {/ / reaction on incompatible entity was removed from filter. / / reaction on incompatible entity was removed from filter.}} Class mysystem: iecsinitsystem, iecsdestroysystem {readonly ecsfilter < component1 > _filter = null; readonly customlistener _listener = new customlistener(); public void init() {/ / subscribe listener to filter events. / / subscribe listener to filter events. _filter. AddListener (_listener);} Public void destroy() {/ / unsubscribe listener to filter events. / / unsubscribe listener to filter events. _filter.RemoveListener (_listener);}}

Topics: Unity Framework Unity3d ECS