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 andRELEASE
builds in production: all internal error checks / exception throwing works only inDEBUG
builds and eleminated for performance reasons inRELEASE
.
Important! LeoEcsLite 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.
- Socials
- Installation
- Main parts of ecs
- Data sharing
- Special classes
- Engine integration
- Projects powered by LeoECS Lite
- Extensions
- License
- FAQ
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",
If you can't / don't want to use unity modules, code can be cloned or downloaded as archive from releases
page.
С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.
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.
С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().
}
}
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.
}
}
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.
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.
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.
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.
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.
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 ())
.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;
}
}
}
- Extended filters
- Extended systems
- Threads support
- Unity editor integration
- Unity uGui bindings
- Unity jobs support
- UniLeo - Unity scene data converter
The software is released under the terms of the MIT license.
No personal support or any guarantees.
I prefer to name them lite
(ecs-lite) and classic
(ecs-full). Main differences between them (based on lite
):
- Codebase decreased in 2 times (easier to maintain and extend).
- No any static data in core anymore (except pooling for filter dynamic building feature, can be removed).
- No caches for components in filter (less memory consuming).
- Fast access to any component on any entity (with performance of cached filter components in
classic
). - No limits to amount of filter contraints (filter is not generic class anymore).
- Performance is similar to
classic
, maybe slightly better in some cases (worse in some corner cases on very huge amount of data). - Oriented to using multiple worlds at same time (can be useful for keep memory consuming low on huge amount of short living components like "events").
- No reflection in runtime (can be used with aggressive code stripping).
- No data injection through reflection (you can use custom shared class between systems with required data).
- Entities switched back to
int
(memory consuming decreased). Saving entity as component field supported through packing toclassic
EcsEntity
-similar struct. - Small core, all new features can be added through extension repos.
- All new features will be added to
lite
only,classic
looks stable and mature enough - no new features, bugfixes only.
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 ();
}
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 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.
}
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);