Confused about some DOTS/ECS practices and syntax

Disclaimer: There will be lots of code
TL;DR: Im very confused about correct System/Jobs syntax.

Hi, I just started a test project to learn about DOTS.
I decided to go with a Tower Defense Prototype and started working on basic concepts.

Questions (Architecture/Syntax):

  • What is the correct architecture for this, Am I doing this correctly or is Jobs something completely unrelated and should not be used in that way?
  • What is the correct way to reference “external” entities, for example, a tower that would want to know which enemy is aiming at? And yes, there’s no need to actually do that instead of just calculating the target when its going to attack, BUT. Its not an uncommon case**. Should I add a public Entity to the TargetingComponent and store the entity there?**

Questions(Jobs):

  • Are the 3 different ways to write the code inside the system all the same? does foreach generate a job inside burst compiling?
  • Code inside systemcan use SystemAPI.Query, and Entities.ForEach had their own way to reference objects, how can I add something like I mentioned previously (all towers, all enemies) like diferent aspects inside a jobEntity.
  • Is it the correct behaviour to iterate all things that much? or is the logic to do things like “cooldown system” and when its ready it adds a tag (empty component), so the “next” job only searches for that tag?

So guiding me with a Code Monkey video I started by adding an enemy that does a simple move.
For that I added a MovementComponent which initially only has speed.

public struct MovementComponent : IComponentData
    {
        public float speed;
    }

For that to be added to the Entity I understood that I have to add a Baker and an Authoring (Im not entirely sure if thats only for a visual feedback)

    public class MovementAuthoring : MonoBehaviour
    {
        public float speed;
    }

    public class MovementSpeedBaker : Baker<MovementAuthoring>
    {
        public override void Bake(MovementAuthoring authoring)
        {
            AddComponent(new MovementComponent { speed = authoring.speed });
        }
    }

Then I need to start with a System
The system would be something simple like this:

    [BurstCompile]
    public partial struct MovementSystem : ISystem
    {
        public void OnCreate(ref SystemState state)
        {}

        public void OnDestroy(ref SystemState state) { }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            new ProcessMovementJob { deltaTime = SystemAPI.Time.DeltaTime}.ScheduleParallel();
        }
    }

And finally the job would go something like this:

    [BurstCompile]
    public partial struct ProcessMovementJob : IJobEntity
    {
        public float deltaTime;
        private void Execute(EnemyAspect enemy)
        {
            if (!enemy.localTransform.IsValid) return;
            enemy.localTransform.ValueRW.Position += new float3(deltaTime*enemy.Movement.ValueRO.speed, 0, 0);
        }
    }

But then I went to a targetting system with multiple jobs

[BurstCompile]
    public partial struct TargetingSystem : ISystem
    {
        public void OnCreate(ref SystemState state) { }

        public void OnDestroy(ref SystemState state) { }
     
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            new ProcessClosestTargetStrategyJob().ScheduleParallel();
            new ProcessMostHealthTargetStrategyJob().ScheduleParallel();
        }
    }
}

and I was not able to understand how to pass multiple aspects, for example, the closest strategy would be implemented in something like these.

[BurstCompile]
    public partial struct TargetingSystem : ISystem
    {
        public void OnCreate(ref SystemState state) { }

        public void OnDestroy(ref SystemState state) { }
     
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            foreach (var tower in SystemAPI.Query<TowerAspect>())
            {
                var towerPosition = tower.localTransform.ValueRO.Position;
                var currentTarget = tower.targetingComponent.ValueRO.target;
                var closestDistance = 1000000f;
            
                if (currentTarget.HasValue)
                {
                    var targetTransform = SystemAPI.GetComponent<LocalTransform>(currentTarget.Value);
                    closestDistance = math.distancesq(towerPosition, targetTransform.Position);
                }
                foreach (var enemy in SystemAPI.Query<EnemyAspect>())
                {
                    if (!enemy.localTransform.IsValid)
                        continue;
                 
                    var distance = math.distancesq(towerPosition, enemy.localTransform.ValueRO.Position);
                    if (distance > tower.attackComponent.ValueRO.attackRange)
                        continue;
                    if (distance > closestDistance)
                        continue;
                    closestDistance = distance;
                    currentTarget = enemy.Entity;
                }
                tower.targetingComponent.ValueRW.target = currentTarget;
            }
        }

And I would like to move that logic to a job but it seems that I cannot use
SystemAPI, I tried to get that list and pass it as a parameter to the job but it also dont seem to work.
I understood that you can pass parameters in the execute method like this

private void Execute(TowerAspect tower)

but I was not able to pass something like 1 tower, all enemies.
Thus I ended up with multiple targeting systems.

Another thing I noticed mostly on older videos (more than 6 months)
Is that systems can use 3 different ways to write jobs, and i didnt understand what difference on that.
it seems that

foreach (var tower in SystemAPI.Query<TowerAspect>()){}
Entities.ForEach(()=>{}).ScheduleParallel();

And the job interface IJobEntity are exactly the same

Entities.ForEach is the precursor to idiomatic foreach, can’t be nested and it will(according to the devs) kill your iteration time with slower codegen. It can be used as Run on the mainthread, Schedule on a single job thread or ScheduleParallel.
Ideally you shouldn’t even use EFE as even though it missed the cutoff date for being marked obsolete, it kinda sorta is. My understanding is its kept around because some projects in production are so deep that they cant or wont switch over so Unity decided to give them an out and keep this thing around(even though it adds to confusion now and 1.0 was the perfect time to mark it obsolete).

Idiomatic foreach can only run on the mainthread. You can nest idiomatic queries inside each other(as you have already done so)
Lastly IJE codegens to an IJobChunk can be Run, Schedule single or ScheduleParallel. They cant be nested.

For your problem of translating that into a IJE, you can get entities or component data from queries to use inside of jobs, using ToEntityArray or ToComponentDataArray and the TempJob allocator, as well as ComponentLookup or BufferLookup’s to do random data access.
job code idea:

partial struct TowerJob : IJobEntity
{
    [DeallocateOnJobCompletion][ReadOnly] public NativeArray<Entity> EnemyEntities;
    [ReadOnly] public ComponentLookup<LocalTransform> LocalTransformLookup;

    public void Execute(ref TowerAspect tower)
    {
        var towerPosition = tower.LocalTransform.ValueRO.Position;
        var currentTarget = tower.TargetingComponent.ValueRO.Target;
        var closestDistance = 1000000f;

        if (currentTarget.HasValue)
        {
            var targetTransform = SystemAPI.GetComponent<LocalTransform>(currentTarget.Value);
            closestDistance = math.distancesq(towerPosition, targetTransform.Position);
        }

        for (var index = 0; index < EnemyEntities.Length; index++)
        {
            var enemy = EnemyEntities[index];
            if (!LocalTransformLookup.HasComponent(enemy))
                continue;
            var enemyTransform = LocalTransformLookup[enemy];

            var distance = math.distancesq(towerPosition, enemyTransform.Position);
            if (distance > tower.AttackComponent.ValueRO.attackRange)
                continue;
            if (distance > closestDistance)
                continue;
            closestDistance = distance;
            currentTarget = enemy;
        }

        tower.TargetingComponent.ValueRW.Target = currentTarget;
    }
}

If you have tons of enemies, given you have random lookups when iterating over enemy transforms you may want to invert the loops so you would iterate over the enemies in a parallel job and look up the tower instead.

Thanks for the reply, one thing I noticed is that ComponentLookup was throwing me an error because it cannot be added to ScheduleParallel but only on single thread.

Im not entirely sure if I understood the last part of your reply correctly how would you iterate through enemies in parallel but do the lookup? is there a diferent way thats not ComponentLookup?

You can look up components and buffers inside a parallel job; you just need to mark the lookup data structure in the job as [ReadOnly]. If you want to write in parallel back to the data a lookup refers to, then defer the write operations until you reach a sync point using an EntityCommandBuffer.ParallelWriter.

You can write back to ComponentLookup’s and nativearray’s in parallel using the NativeDisableParallelForRestriction, which disables the safety system. Doing so does mean you assume responsibility for race conditions yourself.