環境
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;
}
}
}
}