Skip to content
korchoonPublic

About

Async Routine for LeoECS Proto enables safe sync logic execution within ECS, avoiding scheduler issues and simplifying refactoring.

Resources

Stars

11 stars

Watchers

1 watching

Forks

Latest commit

 

History

14 Commits

Folders and files

Repository files navigation

AsyncSystem for ECS Leo Proto

Important

Due to license restrictions, this repository does not include the Leopotam.EcsProto and Leopotam.EcsProto.QoL packages (they are added to .gitignore).

You can read about EcsProto and learn how to get a copy on the author’s website.

The Problem Being Solved

In EcsProto (as in other ECS frameworks), it is not recommended to use async syntax (System.Threading.Task, UniTask, UnityEngine.Awaitable) when working with ProtoEntity, because these async types have an implicit scheduler that works outside the SystemGroup loop. This can lead to unpredictable errors when accessing ProtoEntity data.

AsyncSystem solves this problem by using its own Routine type, which:

  • does not use a scheduler,
  • is wrapped in a system and executed strictly within the system order,
  • before every await call checks that the entity matches the filter requirements, that the entity is alive, the world is alive, etc.

Thus, working with ProtoEntity remains safe throughout the entire async method, without extra boxing/unboxing, since all checks are performed before await is triggered (see AsyncSystem).

Additional Advantages

  • Reduces the number of auxiliary marker components needed only for waiting and not carrying business logic.
  • Allows expressing a Behaviour Tree with Routine<bool> and writing it in async/await syntax while keeping execution under control.

When to Use

1. Prototyping

  • Start with an async system for quick logic testing.
  • As things stabilize, move important components out and split them into regular ECS systems.

2. UI Logic

  • Interaction with UI is often complex and inconvenient to split into separate systems.
  • Since UI rarely requires mass processing, using async simplifies the code.
  • If needed, it can be easily refactored into regular systems.

Description

AsyncSystem is a base class that allows writing asynchronous logic in EcsProto in a linear style using async/await syntax. This simplifies the code by eliminating callback hell and allows using language features such as:

  • variable scope,
  • flow control,
  • exception handling (try/finally).

Adding a System

To add an AsyncSystem, create a class inherited from AsyncSystem<TSelf> and implement two methods:

class MySystem : AsyncSystem<MySystem> {
    // filter as a start condition for async logic
    protected override IProtoIt GetProtoIt () => new ProtoIt(It.Inc<SomeComponent>());
 
    protected override async Routine Run (ProtoEntity entity){
        await Routine.When(...)
        // ...
    }
}

Note

  • Under the hood, a custom Task-like type Routine is used, details in README_ROUTINE.md.
  • For deferred cleanup, the Scope type is used (similar to defer in Go), details in README_SCOPE.md.

Examples of Async Systems

Example 1: Unit State Machine Logic

  • Starts as soon as the entity gets the CUnit component (see filter).
  • Changes color depending on health (>=50 green, >0 yellow, red).
  • In parallel, updates the health bar with a slider and text (see parallel).
  • When health is 0, waits 3 seconds and removes the unit component.
class SysUnit : AsyncSystem<SysUnit> {
    [DI] GameAspect _counterAspect = default;
    [DI] SceneContext _scene = default;

    protected override IProtoIt GetProtoIt () => new ProtoIt (It.Inc<CUnit> ());

    protected override async Routine Run (ProtoEntity entity) {
        var scope = await Routine.GetScope ();

        var view = Object.Instantiate (_scene.UnitPrefab, _scene.UnitSpawn);
        scope.Add (() => Object.Destroy (view.gameObject));

        var parallel = await Routine.GetParallel ();
        parallel.Attach (ObserveAmount ()); 

        // healthy state
        view.Image.color = Color.green;

        await Routine.When (() => _counterAspect.CUnit.Get (entity).Health < 50);
        // injured state
        view.Image.color = Color.yellow;

        await Routine.When (() => _counterAspect.CUnit.Get (entity).Health <= 0);
        // dead state
        view.Image.color = Color.red;

        var timer = 3f;
        await Routine.When (() => {
            timer -= Time.deltaTime;
            return timer < 0f;
        });

        _counterAspect.CUnit.Del (entity);
        return;

        async Routine ObserveAmount () {
            while (true) {
                var cache = _counterAspect.CUnit.Get (entity).Health;
                view.Slider.value = cache;
                view.Text.text = cache.ToString ();
                await Routine.When (() => cache != _counterAspect.CUnit.Get (entity).Health);
            }
        }
    }
}

Example 2: Main Game Logic

  • Waits for StartBtn click to start the game.
  • Creates an entity with the CUnit (Health = 100) component.
  • Enables the attack button and subscribes logic to its click within using (unitAliveScope).
  • Ends the game when no units remain, waits for GameOverBtn click.
  • Repeats the process.
class SysGameFlow : AsyncSystem<SysGameFlow> {
    [DI] GameAspect _gameAspect = default;
    [DI] SceneContext _sceneContext = default;

    override IProtoIt GetProtoIt () => new ProtoIt (It.Inc<CGame> ());

    override async Routine Run (ProtoEntity entity) {
        _sceneContext.MenuRoot.SetActive (true);
        while (true) {
            using var roundScope = new Scope ();

            await _sceneContext.StartBtn.WaitForClick ();
            // game started
            _sceneContext.MenuRoot.SetActive (roundScope, false);
            _sceneContext.GameRoot.SetActive (roundScope, true);

            _gameAspect.CUnit.NewEntity () = new () { Health = 100 };

            using (var unitAliveScope = roundScope.NestedScope ()) {
                _sceneContext.ShootBtn.onClick.AddListener (unitAliveScope, Shoot);

                await Routine.When (() => _gameAspect.UnitIt.Len () == 0);
                // game over
            }

            await _sceneContext.GameOverBtn.WaitForClick ();
        }
    }

    void Shoot () {
        foreach (var unitE in _gameAspect.UnitIt) {
            _gameAspect.CUnit.Get (unitE).Health -= 10;
        }
    }
}

About

Async Routine for LeoECS Proto enables safe sync logic execution within ECS, avoiding scheduler issues and simplifying refactoring.

Resources

Stars

11 stars

Watchers

1 watching

Forks

Contributors

Languages