Unity ECSで他のクエリのEntityにアクセスする方法

# 環境
Unity 6000.0.25f1
Entities 1.3.5

# どういうトラブルが発生したか
2つの別々のクエリからのエンティティを比較したいが、`SystemAPI.Query`の中で別の`SystemAPI.Query`を呼び出すとエラーが発生する。
```:エラー内容
InvalidOperationException: No suitable code replacement generated, this is either due to generators failing, or lack of support in your current context
```

# エラーが発生するプログラムの例
以下は、チェスのようなゲームで、クールタイムが終わったタイミングで隣の列にいる駒をする処理のコードです。
ただしこれはエラーが発生します。
``` C#:サンプルコード
        public void OnUpdate(ref SystemState state)
        {
            var deltaTime = SystemAPI.Time.DeltaTime;

            foreach (var (coolTime, position, entity) in SystemAPI.Query<RefRW<AttackCoolTime>, RefRO<PlacePosition>>()
                         .WithAll<ExistTag>()
                         .WithEntityAccess())
            {
                // クールタイムの加算
                coolTime.ValueRW.currentTime += deltaTime;
                coolTime.ValueRW.isReady = coolTime.ValueRW.currentTime >= coolTime.ValueRW.coolTime;
                
                // スキル発動可能な時
                if (!coolTime.ValueRW.isReady) continue;
                
                // ★ 隣にいるEntityを取得しようとするとエラーが発生
                var targets = SystemAPI.Query<RefRO<PlacePosition>>()
                    .WithAll<ExistTag>()
                    .WithEntityAccess();
            }
        }
```

```C#: 使用したコンポーネント
    public struct AttackCoolTime : IComponentData
    {
        public AttackCoolTime(float limitCoolTime)
        {
            this.limitCoolTime = limitCoolTime;
            this.currentTime = 0;
            this.isReady = false;
        }

        public float limitCoolTime; // クールタイムの長さ
        public float currentTime; // 現在の経過時間
        public bool isReady; // スキルが準備完了か
    }
    
    public struct PlacePosition : IComponentData
    {
        public PlacePosition(int row, int column)
        {
            this.row = row;
            this.column = column;
        }
        public int row;     // 行
        public int column;  // 列
    }

    public struct ExistTag : IComponentData
    {
    }
    
    public struct Attacking : IComponentData
    {
        public Attacking(Entity target)
        {
            this.target = target;
        }

        public Entity target;
    }
```

# どのように解決したか？
先にコンポーネントとエンティティを配列にコピーし、その配列のをfor文で比較するという方法で解決しました。
そのために`QueryBuilder`を用いて先に配列を用意するようにしました。
``` C#:サンプルコード
        public void OnUpdate(ref SystemState state)
        {
            var ecb = new EntityCommandBuffer(Allocator.TempJob);
            var deltaTime = SystemAPI.Time.DeltaTime;

            //★ 先に位置情報を集める
            var positionQuery = SystemAPI.QueryBuilder().WithAll<PlacePosition, ExistTag>().Build();
            // コンポーネントの配列
            var positions = positionQuery.ToComponentDataArray<PlacePosition>(Allocator.Temp);
            // エンティティの配列
            var positionEntities = positionQuery.ToEntityArray(Allocator.Temp);

            // クールタイムが終われば攻撃する処理
            foreach (var (coolTime, position, entity) in SystemAPI.Query<RefRW<AttackCoolTime>, RefRO<PlacePosition>>()
                         .WithAll<ExistTag>()
                         .WithEntityAccess())
            {
                // クールタイムの加算
                coolTime.ValueRW.currentTime += deltaTime;
                coolTime.ValueRW.isReady = coolTime.ValueRW.currentTime >= coolTime.ValueRW.coolTime;
                
                // スキル発動可能な時
                if (!coolTime.ValueRW.isReady) continue;
                //★ 先ほど集めたコンポーネントの配列とエンティティの配列から確認する
                for (var i = 0; i < positions.Length; i++)
                {
                    var otherPos = positions[i];
                    var otherEnt = positionEntities[i];
                    // 隣のセルを探索する
                    if (math.abs(otherPos.column - position.ValueRO.column) == 1 &&
                        otherPos.row == position.ValueRO.row)
                    {
                        // 対象が見つかれば攻撃状態にする
                        ecb.AddComponent(entity, new Attacking(otherEnt));
                    }
                }
                
                // 最後にクールタイムをリセットする
                coolTime.ValueRW.currentTime = 0;
                coolTime.ValueRW.isReady = false;
            }

            ecb.Playback(state.EntityManager);
            ecb.Dispose();
        }
```
これでエラーが発生することなく別のクエリの情報を利用できます。

# IJobChunkを用いた方法
先ほどの実装方法はシンプルですが、全探索を行うため少しコストのある処理となっております。
なので、パフォーマンスが気になる方のためにIJobChunk(IJobEntityと混同しやすいので注意)を用いたサンプルも用意しました。
この場合だとBurstCompileで実行できるため高速になります。

```C#:サンプルコード
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // 先に集めておくQueryを用意する
            var positionQuery = SystemAPI.QueryBuilder().WithAll<PlacePosition, ExistTag>().Build();
            
            // IJobChunkを開始する
            new AttackingJob
            {
                deltaTime = SystemAPI.Time.DeltaTime,
                ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter(),
                placePositionTypeHandle = SystemAPI.GetComponentTypeHandle<PlacePosition>(true),
                attackCoolTimeHandle = SystemAPI.GetComponentTypeHandle<AttackCoolTime>(false),
                entityTypeHandle = SystemAPI.GetEntityTypeHandle(), // 同時ジョブ間の潜在的な競合状態を検出ために必要
                otherChunks = positionQuery.ToArchetypeChunkArray(state.WorldUpdateAllocator)
            }.ScheduleParallel(positionQuery, state.Dependency).Complete();
        }
        
        [BurstCompile]
        private struct AttackingJob : IJobChunk
        {
            [ReadOnly] public float deltaTime;
            public EntityCommandBuffer.ParallelWriter ecb;
            [ReadOnly] public ComponentTypeHandle<PlacePosition> placePositionTypeHandle;
            public ComponentTypeHandle<AttackCoolTime> attackCoolTimeHandle;
            public EntityTypeHandle entityTypeHandle;
            [ReadOnly] public NativeArray<ArchetypeChunk> otherChunks;
            
            public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
            {
                // もしdisabledなコンポーネントがきた時を検出する
                Assert.IsFalse(useEnabledMask);
                
                // もし存在タグがついていないならスキップ
                if (!chunk.Has<ExistTag>()) return;
                
                // 必要な配列を集める
                var positions = chunk.GetNativeArray(ref placePositionTypeHandle);
                var attackCoolTimes = chunk.GetNativeArray(ref attackCoolTimeHandle);
                var entities = chunk.GetNativeArray(entityTypeHandle);
                
                for (var i = 0; i < positions.Length; i++)
                {
                    var position = positions[i];
                    var coolTime = attackCoolTimes[i];
                    var entity = entities[i];
                    
                    // クールタイムの加算
                    coolTime.currentTime += deltaTime;
                    coolTime.isReady = coolTime.currentTime >= coolTime.limitCoolTime;
                    
                    // コンポーネントはstructなので値渡し、元の配列に戻して反映させる
                    attackCoolTimes[i] = coolTime;
                    
                    // クールタイムの準備ができていなければ次に
                    if (!coolTime.isReady) continue;

                    var isAttacked = false;
                    
                    // 別のチャンクと比較する
                    foreach (var otherChunk in otherChunks)
                    {
                        var otherPositions = otherChunk.GetNativeArray(ref placePositionTypeHandle);
                        var otherEntities = otherChunk.GetNativeArray(entityTypeHandle);
                        
                        // 別チャンクの中身を探索する
                        for (var k = 0; k < otherChunk.Count; k++)
                        {
                            var otherPos = otherPositions[k];
                            var otherEnt = otherEntities[k];
                            // 横のセルかどうか
                            if (math.abs(otherPos.column - position.column) != 1 || otherPos.row != position.row) continue;
                            // 対象が見つかれば攻撃状態にする
                            ecb.AddComponent(unfilteredChunkIndex, entity, new Attacking(otherEnt));
                            isAttacked = true;
                        }
                    }

                    // もし攻撃していたらクールタイムを元に戻す
                    if (isAttacked)
                    {
                        coolTime.currentTime = 0;
                        coolTime.isReady = false;
                        attackCoolTimes[i] = coolTime;
                    }
                }
                
            }
        }

```