Coder Social home page Coder Social logo

ecslite's Introduction

LeoEcsLite - Lightweight C# Entity Component System framework

Performance, 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! LeoEcsLite API is not tread safe and will never be! If you need multithread-processing - you should implement it on your side as part of ecs-system.

Table of content

Socials

discord

Installation

As unity module

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

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

By default last released version will be used. If you need trunk / developing version then develop name of branch should be added after hash:

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

As source

If you can't / don't want to use unity modules, code can be cloned or downloaded as archive from releases page.

Main parts of ecs

Entity

Сontainer for components. Implemented as int:

// Creates new entity in world context.
int entity = _world.NewEntity ();

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

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 / requested / removed through component pools.

System

Сontainer for logic for processing filtered entities. User class should implement IEcsInitSystem, IEcsDestroySystem, IEcsRunSystem (or other supported) interfaces:

class UserSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsRunSystem, IEcsDestroySystem, IEcsPostDestroySystem {
    public void PreInit (EcsSystems systems) {
        // Will be called once during EcsSystems.Init() call and before IEcsInitSystem.Init().
    }
    
    public void Init (EcsSystems systems) {
        // Will be called once during EcsSystems.Init() call and after IEcsInitSystem.PreInit().
    }
    
    public void Run (EcsSystems systems) {
        // Will be called on each EcsSystems.Run() call.
    }

    public void Destroy (EcsSystems systems) {
        // Will be called once during EcsSystems.Destroy() call and before IEcsInitSystem.PostDestroy().
    }
    
    public void PostDestroy (EcsSystems systems) {
        // Will be called once during EcsSystems.Destroy() call and after IEcsInitSystem.Destroy().
    }
}

Data sharing

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

class SharedData {
    public string PrefabsPath;
}
...
SharedData sharedData = new SharedData { PrefabsPath = "Items/{0}" };
EcsSystems systems = new EcsSystems (world, sharedData);
systems
    .Add (new TestSystem1 ())
    .Init ();
...
class TestSystem1 : IEcsInitSystem {
    public void Init(EcsSystems systems) {
        SharedData shared = systems.GetShared<SharedData> (); 
        string prefabPath = string.Format (shared.PrefabsPath, 123);
        // prefabPath = "Items/123" here.
    } 
}

Special classes

EcsPool

Container for components, provides api for adding / requesting / removing components on entity:

int entity = world.NewEntity ();
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 (entity);

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

// Del() removes component from entity. If it was last component - entity will be removed automatically too.
pool.Del (entity);

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

EcsFilter

Container for keeping filtered entities with specified component list:

class WeaponSystem : IEcsInitSystem, IEcsRunSystem {
    public void Init (EcsSystems systems) {
        // We want to get default world instance...
        EcsWorld world = systems.GetWorld ();
        
        // and create test entity...
        int entity = world.NewEntity ();
        
        // with "Weapon" component on it.
        var weapons = world.GetPool<Weapon>();
        weapons.Add (entity);
    }

    public void Run (EcsSystems systems) {
        EcsWorld world = systems.GetWorld ();
        // We want to get entities with "Weapon" and without "Health".
        // You can cache this filter somehow if you want.
        var filter = world.Filter<Weapon> ().Exc<Health> ().End ();
        
        // We want to get pool of "Weapon" components.
        // You can cache this pool somehow if you want.
        var weapons = world.GetPool<Weapon>();
        
        foreach (int entity in filter) {
            ref Weapon weapon = ref weapons.Get (entity);
            weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1);
        }
    }
}

Additional constraints can be added with Inc<>() / Exc<>() methods.

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

EcsWorld

Root level container for all entities / components, works like isolated environment.

Important: Do not forget to call EcsWorld.Destroy() method if instance will not be used anymore.

EcsSystems

Group of systems to process EcsWorld instance:

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 () {
        // process all dependent systems.
        _systems?.Run ();
    }

    void OnDestroy () {
        // destroy systems logical group.
        if (_systems != null) {
            _systems.Destroy ();
            _systems = null;
        }
        // destroy world.
        if (_world != null) {
            _world.Destroy ();
            _world = null;
        }
    }
}

Important: Do not forget to call EcsSystems.Destroy() method if instance will not be used anymore.

Engine integration

Unity

Tested on unity 2020.3 (but not dependent on it) and contains assembly definition for compiling to separate assembly file for performance reason.

Not ready yet.

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 Leopotam.EcsLite;

class EcsStartup {
    EcsWorld _world;
    EcsSystems _systems;

    // Initialization of ecs world and systems.
    void Init () {        
        _world = new EcsWorld ();
        _systems = new EcsSystems (_world);
        _systems
            // register additional worlds here.
            // .AddWorld (customWorldInstance, "events")
            // register your systems here, for example:
            // .Add (new TestSystem1 ())
            // .Add (new TestSystem2 ())
            
            // register components for removing here
            // position in registration is important,
            // should be after all AddWorld() registration, for example:
            // .DelHere<TestComponent1> ()
            // .DelHere<TestComponent2> ("events")
            
            .Init ();
    }

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

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

Projects powered by LeoECS Lite

With sources

Extensions

License

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

No personal support or any guarantees.

FAQ

I want to process one system at MonoBehaviour.Update() and another - at MonoBehaviour.FixedUpdate(). How can I do it?

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

EcsSystems _update;
EcsSystems _fixedUpdate;

void Start () {
    EcsWorld 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 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.

I use components as events that work only one frame, then remove it at last system in execution sequence. It's boring, how can I automate it?

If you want to remove one-frame components without additional custom code, you can register them at EcsSystems:

struct MyOneFrameComponent { }

EcsSystems _update;

void Start () {
    EcsWorld world = new EcsWorld ();
    _update = new EcsSystems (world);
    _update
        .Add (new CalculateSystem ())
        .Add (new UpdateSystem ())
        .DelHere<MyOneFrameComponent> ()
        .Init ();
}

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

important: All one-frame components should be registered with DelHere() after all worlds registration through AddWorld(). Important: All one-frame components with specified type will be removed at position in execution flow where this component was registered with DelHere() call.

I want to keep references to entities in components, but entity can be killed at any system and I need protection from reusing the same ID. How can I do it?

For keeping entity somewhere you should pack it to special EcsPackedEntity or EcsPackedEntityWithWorld types:

EcsWorld world = new EcsWorld ();
int entity = world.NewEntity ();
EcsPackedEntity packed = world.PackEntity (entity);
EcsPackedEntityWithWorld packedWithWorld = world.PackEntityWithWorld (entity);
...
if (packed.Unpack (world, out int unpacked)) {
    // unpacked is valid and can be used.
}
if (packedWithWorld.Unpack (out EcsWorld unpackedWorld, out int unpackedWithWorld)) {
    // unpackedWithWorld is valid and can be used.
}

I want to add some reactive behaviour on world changes, how I can do it?

You can use LEOECSLITE_WORLD_EVENTS definition to enable custom event listeners support on worlds:

class TestWorldEventListener : IEcsWorldEventListener {
    public void OnEntityCreated (int entity) {
        // entity created - raises on world.NewEntity().
    }

    public void OnEntityChanged (int entity) {
        // entity changed - raises on pool.Add() / pool.Del().
    }

    public void OnEntityDestroyed (int entity) {
        // entity destroyed - raises on world.DelEntity() or last component removing.
    }

    public void OnFilterCreated (EcsFilter filter) {
        // filter created - raises on world.Filter().End() for brand new filter.
    }

    public void OnWorldResized (int newSize) {
        // world resized - raises on world/pools resizing when no room for entity at world.NewEntity() call.
    }

    public void OnWorldDestroyed (EcsWorld world) {
        // world destroyed - raises on world.Destroy().
    }
}
...
var world = new EcsWorld ();
var listener = new TestWorldEventListener ();
world.AddEventListener (listener);

ecslite's People

Contributors

7bpencil avatar caxapexac avatar leopotam avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.