2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UnityAdvent Calendar 2024

Day 6

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

Last updated at Posted at 2024-12-06

環境

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

エラーが発生するプログラムの例

以下は、チェスのようなゲームで、クールタイムが終わったタイミングで隣の列にいる駒をする処理のコードです。
ただしこれはエラーが発生します。

サンプルコード
        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();
            }
        }
使用したコンポーネント
    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を用いて先に配列を用意するようにしました。

サンプルコード
        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で実行できるため高速になります。

サンプルコード
        [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;
                    }
                }
                
            }
        }

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?