sschmid / Entitas

Entitas is a super fast Entity Component System (ECS) Framework specifically made for C# and Unity
MIT License
7.08k stars 1.11k forks source link

Is it possible to "activate" reactive systems before running init systems? #971

Closed matthiashermsen closed 3 years ago

matthiashermsen commented 3 years ago

Hi, given the following example:

There is a unique component describing the amount of items

    [Game, Unique]
    public sealed class ItemAmountComponent : IComponent
    {
        public uint value;
    }

The first init system sets the value

    public sealed class FirstInitSystem : IInitializeSystem
    {
        private readonly Contexts _contexts;

        public FirstInitSystem(Contexts contexts)
        {
            _contexts = contexts;
        }

        public void Initialize()
        {
            _contexts.Game.ReplaceItemAmount(10);
        }
    }

and imagine the value 10 is too big. This would break the game, the maximum would be 5. (In my project this is not a constant value, it's based on a calculation with different components)

The second init system deals with that value but crashes if that value is invalid. So I would have to make a check before

    public sealed class SecondInitSystem : IInitializeSystem
    {
        private readonly Contexts _contexts;

        public SecondInitSystem(Contexts contexts)
        {
            _contexts = contexts;
        }

        public void Initialize()
        {
            if (_contexts.Game.ItemAmount.value > 5)
            {
                Debug.LogError("Congrats. The game crashed.");
                return;
            }
        }
    }

I could avoid this check by creating a reactive system clamping that value whenever it changes

    public sealed class TheReactiveSystem : ReactiveSystem<GameEntity>
    {
        public TheReactiveSystem(Contexts contexts) : base(contexts.Game)
        {
        }

        protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
            => context.CreateCollector(GameMatcher.ItemAmount);

        protected override bool Filter(GameEntity entity)
            => entity.HasItemAmount;

        protected override void Execute(List<GameEntity> entities)
        {
            foreach (GameEntity gameEntity in entities)
            {
                if (gameEntity.ItemAmount.value > 5)
                    gameEntity.ReplaceItemAmount(5);
            }
        }
    }

The problem is that the second init system runs before the reactive system. Of course I could clamp the value in the first init system or make the check in the second init system. But it would be nice to encapsulate this logic.

Is there a way I can activate the reactive system before the second init system runs? Or observe the component with another approach?

rglobig commented 3 years ago

I guess you have these options:

  1. Call the reactive System in your first System:

    public sealed class FirstInitSystem : IInitializeSystem
    {
        private readonly Contexts _contexts;
        private readonly TheReactiveSystem s;
    
        public FirstInitSystem(Contexts contexts, TheReactiveSystem s)
        {
            _contexts = contexts;
        }
    
        public void Initialize()
        {
            _contexts.Game.ReplaceItemAmount(10);
            s.Execute();
        }
    }
  2. Write some static pure Helper Method you can call on both Systems:
    public static class Service {
    public int ClampItemAmount(int amount) => amount > 5 ? 5 : amount;
    }
  3. Do 2. but as interface and pass it to both ctor

I would go with 2. or 3.

matthiashermsen commented 3 years ago

I think I'll go with 3 :)