Generic Entities.ForEach or other Alternatives?

G’day,

I’m having to duplicate most of the code in my game because we can’t use Entities.WithAll, which is getting really frustrating. My games has lots of units from 2 sides/teams, and a lot of systems that do something for each team (often using both ‘my’ team and the ‘enemy’ team).

Here’s one of the smaller examples from my game:

public class ShieldDamageSystem : SystemBase
{
    private EntityQuery side1DamageQuery;
    private EntityQuery side1ShieldStrengthQuery;
    private EntityQuery side2DamageQuery;
    private EntityQuery side2ShieldStrengthQuery;
    protected override void OnCreate()
    {
        side1ShieldStrengthQuery = EntityManager.CreateEntityQuery(
            ComponentType.ReadOnly<Side1>(),
            ComponentType.ReadOnly<ShieldStrength>()
            );
        side2ShieldStrengthQuery = EntityManager.CreateEntityQuery(
            ComponentType.ReadOnly<Side2>(),
            ComponentType.ReadOnly<ShieldStrength>()
        );
        base.OnCreate();
    }

    protected override void OnUpdate()
    {
        UpdateSide1();
        UpdateSide2();
    }

    void UpdateSide1()
    {
        float totalDamage = 0;
        Entities
            .WithAll<Side1>()
            .WithStoreEntityQueryInField(ref side1DamageQuery)
            .ForEach((ref ShieldDamage shieldDamage) =>
        {
            totalDamage += shieldDamage.Value;
        }).Run();
        var shieldStrength = side1ShieldStrengthQuery.GetSingleton<ShieldStrength>();
        shieldStrength.Value -= totalDamage;
        side1ShieldStrengthQuery.SetSingleton<ShieldStrength>(shieldStrength);
        EntityManager.DestroyEntity(side1DamageQuery);
    }
    void UpdateSide2()
    {
        float totalDamage = 0;
        Entities
            .WithAll<Side2>()
            .WithStoreEntityQueryInField(ref side2DamageQuery)
            .ForEach((ref ShieldDamage shieldDamage) =>
        {
            totalDamage += shieldDamage.Value;
        }).Run();
        var shieldStrength = side2ShieldStrengthQuery.GetSingleton<ShieldStrength>();
        shieldStrength.Value -= totalDamage;
        side2ShieldStrengthQuery.SetSingleton<ShieldStrength>(shieldStrength);
        EntityManager.DestroyEntity(side2DamageQuery);
    }
}

If we could use generic functions, this could be reduced to at most:

    public class ShieldDamageSystem : SystemBase
    {
        private EntityQuery side1DamageQuery;
        private EntityQuery side1ShieldStrengthQuery;
        private EntityQuery side2DamageQuery;
        private EntityQuery side2ShieldStrengthQuery;
        protected override void OnCreate()
        {
            side1ShieldStrengthQuery = EntityManager.CreateEntityQuery(
                ComponentType.ReadOnly<Side1>(),
                ComponentType.ReadOnly<ShieldStrength>()
                );
            side2ShieldStrengthQuery = EntityManager.CreateEntityQuery(
                ComponentType.ReadOnly<Side2>(),
                ComponentType.ReadOnly<ShieldStrength>()
            );
            base.OnCreate();
        }

        protected override void OnUpdate()
        {
            UpdateSide<Side1>(ref side1DamageQuery, ref side1ShieldStrengthQuery);
            UpdateSide<Side2>(ref side2DamageQuery, ref side2ShieldStrengthQuery);
        }

        void UpdateSide<TSide>(ref EntityQuery damageQuery, ref EntityQuery shieldQuery)
        {
            float totalDamage = 0;
            Entities
                .WithAll<TSide>()
                .WithStoreEntityQueryInField(ref damageQuery)
                .ForEach((ref ShieldDamage shieldDamage) =>
            {
                totalDamage += shieldDamage.Value;
            }).Run();
            var shieldStrength = shieldQuery.GetSingleton<ShieldStrength>();
            shieldStrength.Value -= totalDamage;
            shieldQuery.SetSingleton<ShieldStrength>(shieldStrength);
            EntityManager.DestroyEntity(damageQuery);
        }
}

If we could use generic base systems, it could be further reduced to:

public class Side1ShieldDamageSystem : ShieldDamageSystem<Side1> { }
public class Side2ShieldDamageSystem : ShieldDamageSystem<Side2> { }
public abstract class ShieldDamageSystem<TSide> : SystemBase
{
    private EntityQuery damageQuery;
    private EntityQuery shieldStrengthQuery;
    protected override void OnCreate()
    {
        shieldStrengthQuery = EntityManager.CreateEntityQuery(
            ComponentType.ReadOnly<TSide>(),
            ComponentType.ReadOnly<ShieldStrength>()
            );
        base.OnCreate();
    }

    protected override void OnUpdate()
    {
        float totalDamage = 0;
        Entities
            .WithAll<TSide>()
            .WithStoreEntityQueryInField(ref damageQuery)
            .ForEach((ref ShieldDamage shieldDamage) =>
            {
                totalDamage += shieldDamage.Value;
            }).Run();
        var shieldStrength = shieldStrengthQuery.GetSingleton<ShieldStrength>();
        shieldStrength.Value -= totalDamage;
        shieldStrengthQuery.SetSingleton<ShieldStrength>(shieldStrength);
        EntityManager.DestroyEntity(damageQuery);
    }
}

It’s not too bad in this case, but some systems like finding nearest enemies have 100+ lines of duplicate code with 10 queries containing both Side1 and Side2 (which if they were generic would be TSide and TEnemySide). I’m using the Side1 and Side2 tags to separate everything because it’s much faster for a lot of queries.

Any chance we’ll ever be able to do either of these? Or does anyone know a current way I can avoid duplicating code in this situation?

You could store the Player ID as a component value rather than a unique component per ID.
And if you ever just want to update a specific player ID, then you could maybe use a shared component for this instead, so you can filter: Entities.WithSharedComponentFilter(new PlayerID { Value - 2} )

@Lecks_1 have you actually tested your code?
You writing to the same value in parallel. How that suppose to work in your opinion?
I think you need rethink, how your systems are designed, if you start duplicating code.

@charleshendry I have considered/tried this, just it doesn’t work in some situations where i’m already using a SharedComponentFilter. Still, it might be worth doing in other cases, thanks.

@Antypodish there’s no parallel code in this example. It all runs on the main thread (because it all needs to write to the same value as you said). I realize I need to rethink how the systems are designed, just it seems like it would be a lot cleaner if we could use generics in Entities.ForEach.

IJobEntity will eventually solve this. In the meantime, shared components or a shared static method are your two best options.

Ah I see, you just going to use Run(). That fair point.