EcsRx / ecsrx.unity

A simple framework for unity using the ECS paradigm but with unirx for fully reactive systems.
MIT License
417 stars 65 forks source link

System question - access to entities in group #37

Closed Bezarius closed 7 years ago

Bezarius commented 7 years ago

How i can get access to entity group in system like this:

    public class SenseSystem :  IReactToGroupSystem
    {

        public IGroup TargetGroup
        {
            get
            {
                return new GroupBuilder()
                    .WithComponent<SomeComponent>()
                    .Build();
            }
        }

        public IObservable<IGroupAccessor> ReactToGroup(IGroupAccessor @group)
        {
            return Observable.Interval(TimeSpan.FromSeconds(1)).Select(x => @group);
        }

        public void Execute(IEntity entity)
        {
            // now i want to get acccess to another  entities from targetGroup(find nearest enemy)
        }
    }

Or may be i have to use a IManualSystem?

grofit commented 7 years ago

You can do it from in here, its just that generally it would be better to isolate that concern and inject it in. So for example lets say you wanted to do something simple like getting the closest 5 applicable entities within the group, you could make an:

public class EnemySearcher
{
   private IPool _enemyPool {get;set;}

   public EnemySearcher(IPool enemyPool)
   { _enemyPool = enemyPool; } 

   public IEnumerable<IEntity> FindNearestEnemies(Vector2 point, int maximumMatches)
   {
        return _enemyPool.Entities.Where(isEnemy)
            .OrderBy(distanceFromPoint)
            .Take(maximumMatches);
   }

   public bool isEnemy(IEntity entity)
   { return entity.HasComponent<Enemy>(); }

   public float distanceFromPoint(Vector2 point, IEntity entity)
   {
       var enemy = entity.GetComponent<Enemy>();
       return Vector2.Distance(point, enemy.Position);
   }
}

Then once you have an object which does this, set it up in DI like so:

var enemyPool = PoolManager.CreatePool(enemies);
var enemySearcher = new EnemySearcher(enemyPool);
Container.Bind<EnemySearcher>().FromInstance(enemySearcher);

Then you just inject that into your code:

public class SenseSystem :  IReactToGroupSystem
{
   public EnemySearcher EnemySearcher {get; private set;}

   public SenseSystem(EnemySearcher enemySearcher)
   { EnemySearcher = enemySearcher; }

   // your other stuff

   public void Execute(IEntity entity)
   {
        var position = entity.GetComponent<Player>().Position; // or whatever
        var closestEnemies = EnemySearcher.FindNearestEnemies(position, 5);
        // do stuff with closest enemies
   }
}

That way you dont need to know about the pool within your system directly AND your can re-use the logic elsewhere if needed. I did think about exposing an IQuery style object for doing custom queries into pools, which I may still do, as then you could inject your enemy pool in then call a query on it, but anyway CURRENTLY this should let you do what you want, its all off top of my head so the code may not be 100% working but hopefully the intent is clear.

Bezarius commented 7 years ago

Thank you for good example, the idea is clear :)

Bezarius commented 7 years ago

What do you think about this implementation?

public class SenseSystem : IReactToGroupSystem
    {
        private readonly IGroupAccessor _playerAllyUnitspAccessor;
        private readonly IGroupAccessor _playerEnemyUnitspAccessor;

        public IGroup TargetGroup
        {
            get
            {
                return new GroupBuilder()
                    .WithComponent<ViewComponent>()
                    .WithComponent<SenseComponent>()
                    .WithComponent<OwnerComponent>()
                    .WithComponent<HealthComponent>()
                    .Build();
            }
        }

        public SenseSystem(IPoolManager poolManager)
        {
            _playerAllyUnitspAccessor = poolManager.CreateGroupAccessor(new UnitGroup(PlayerGroup.Ally));
            _playerEnemyUnitspAccessor = poolManager.CreateGroupAccessor(new UnitGroup(PlayerGroup.Enemy));
        }

        public IObservable<IGroupAccessor> ReactToGroup(IGroupAccessor @group)
        {
            return Observable.Interval(TimeSpan.FromSeconds(1)).Select(x => @group);
        }

        public void Execute(IEntity entity)
        {
            var owner = entity.GetComponent<OwnerComponent>();
            var sense = entity.GetComponent<SenseComponent>();
            var position = entity.GetComponent<ViewComponent>().View.transform.position;
            var accessor = owner.PlayerGroup == PlayerGroup.Ally
                ? _playerEnemyUnitspAccessor
                : _playerAllyUnitspAccessor;

            var nearest = accessor.Entities.Select(x => new
            {
                @object = x,
                range = (x.GetComponent<ViewComponent>().View.transform.position - position).sqrMagnitude
            }).Where(w => w.range < sense.Radius).OrderBy(o => o.range).Select(s => s.@object);

            //todo: ....

        }
    }
grofit commented 7 years ago

its fine, however its not as easy to test your logic in isolation, i.e if you wanted to do a unit test where you wanted to do something like:

Given I have 5 entities at 10.0, 10.0, 10.0
And I have 5 entities at 1.0, 1.0, 1.0
When I search for closest 5 entities to 1.0, 1.0, 1.0
Then I should get 5 entities
And all the entities position is 1.0, 1.0, 1.0

You could test that acceptance criteria quite simply using the whole EnemySearcher class as you can just create some entities and check that you get the closest matches, its also more re-useable across other classes which may want to know about closest entities as you can easily make the class search for different components etc.

However one main thing for me is its isolation and testability, so in your example that logic is within a system, so to test that you would need to do a lot more legwork, i.e creating the system, the group accessors etc, so its far harder to test and see intent.

This being said if it works fine for you and makes sense, roll with it. It is better to ship than to waste time bogged down in the detail, and also as mine was a first stab you could possibly make a group with a predicate to give you a handle to the right sort of entities and you could even add a predicate to the group to provide some sort of constraint so your group contains only entities within 5.0 units of your character.

You have also given me an idea here around possibly adding a FilterableGroup notion or something so you could basically have another group which allows you to filter and order a group so when you try to get the entities you actually can constrain and order them ahead of time and keep them in that order.

Anyway hope my rambling makes sense, I am almost always on the gitter channel if you want to discuss in more detail around any of the points above.

Bezarius commented 7 years ago

Many thanks for the tips :)