UnityECSで状態管理
UnityECSで状態管理をしようとすると以下のようにコンポーネントにEnumを取り入れてif文などで処理を分岐する方法が一般的だと思います。
public struct PlyaerStatus :IComponentData
{
public PlayerState PlayerState;
}
public enum PlayerState
{
Normal = 0,
Attack = 1,
Dodge = 2,
Damage = 3,
Die = 4,
}
[BurstCompile]
partial struct TestStateSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// 状態によって入力値を変えたいときとか
foreach (var (playerStatus, input) in
SystemAPI.Query<PlayerStatus, PlayerInputs>())
{
if(playerStatus.PlayerState == PlayerState.Normal)
{
// 状態がNormalの時のinput処理など
}
else if(playerStatus.PlayerState == PlayerState.Attack)
{
// 状態がAttackの時のinput処理など
}
}
}
}
という感じでif
やswitch
で分岐していきます。
状態がいろいろ組合された場合
普通に大元の状態が単一で複数の状態が複合しないという部分には上記で十分ですが、ゲームによっていろいろな状態が重なり合うようなものもあると思います。
例えば、毒状態で体力がだんだん減っていくうえに、混乱状態になって入力の値がランダムになり、その上現在は水の中にいるので水中での処理が走ってほしい。みたいなときとかです。
ここで一つ立ち戻ってもらいたいのですがこのECSというフレームワーク実はこれが得意なのです。
それぞれの状態をComponent
として、そのそれぞれの状態の時のシステムを個別に作っておけば、そのEntity
についてるComponent
にそれぞれの処理が走るので簡単に複合できるわけです。
具体的に言うとPoison
,Confusion
,Swim
コンポーネントとそれぞれに対するSystem
を作っておけば、Poison
とConfusion
のコンポーネントがついたエンティティはPoisonSystem
,ConfusionSystem
で処理され、Poison
,Swim
のコンポーネントがついたエンティティはPoisonSystem
,Swim
システムで処理されます。
もちろんSwim
しかついていないエンティティはSwim
のみで処理されます。
こういうことなので、状態を持てないといわれるECSですが、実はばっちり状態を持って処理することができるのです。
またConfusion
とPoison
が競合していたら動かしたくない場合はforeach
にWithNone<Poison>()
などを付ければいいわけです。
foreach (var (poison, entity) in
SystemAPI.Query<Poison>()
.WithNone<Confusion>().WithEntityAccess())
{
// Poison状態のときのみの処理
}
実際の例
では実際に書いていきます。
public struct Poison : IComponentData
{ }
public struct Confusion : IComponentData
{ }
public struct Swim : IComponentData
{ }
Component
はこんな形です。
他に必要なデータがあれば記述しても構いませんが、今回は分岐処理(ECSで言うQuery処理)のためのComponent
なので何も入っていなくても問題ありません。
自分はこの状態のことをただタグ
を付けてるようなものなのでタグ
と言っています。(公式の呼び方ではないと思う)
[BurstCompile]
partial struct PoisonSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
foreach (var (poison, hp) in
SystemAPI.Query<Poison, PlayerHp>())
{
// PlayerのHPが減っていく処理
//hp.currentHP -= 1;など
}
}
}
[BurstCompile]
partial struct ConfusionSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
foreach (var (confusion, input) in
SystemAPI.Query<Confusion, PlayerInput>())
{
// Playerの入力がランダムになるなど
// input.MoveVector = 〇〇...
}
}
}
[BurstCompile]
partial struct SwimSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
foreach (var (swim, control) in
SystemAPI.Query<Swim, CharacterControl>())
{
// 水の中の処理
// control.Velocity = 〇〇...など
}
}
}
これでEntity
についてる状態タグ
によってそれぞれの処理が実行されるようになりました。
状態変化を起こさせる
この状態になったときの処理は上記で書きました。
これをどう発生させるかですが、普通にAddComponent()
でよいです。
AddComponent()
などEntity
にComponent
を追加する方法はHybridAnimation
の時やりましたね.
// EntityCommandBufferを作成
EntityCommandBuffer ecb = SystemAPI.GetSingletonRW<EndSimulationEntityCommandBufferSystem.Singleton>().ValueRW.CreateCommandBuffer(World.Unmanaged);
foreach (var (hybridData, entity) in
SystemAPI.Query<CharacterHybridData>()
.WithNone<CharacterHybridLink>().WithEntityAccess())
{
// EntityCommandBuffer.AddComponent({Entity}, new {Component});で`タグ`を追加する。
ecb.AddComponent(entity, new Poison); or ecb.AddComponent<Poison>(entity);
// 削除するとき(状態から抜けるとき)はEntityCommandBufferで
// RemoveComponent<{Component}>({Entity});を呼ぶとコンポーネントが外れる
ecb.RemoveComponent<Poision>(entity);
}
ecb.Dispose();
削除するとき(状態を抜けるとき)はEntityCommandBuffer.RemoveCompoent<>()
を呼びましょう。
JobSystem
でコンポーネントの追加や削除をするときは以下のようにします。
public partial struct ExsamplePoisonJob : IJobEntity
{
// Update()から入れ込まれてくるEntityCommandBufferを保持する
// Jobは並列処理なのでParallelWriterにする必要がある
internal EntityCommandBuffer.ParallelWriter ecbParallel;
// JobSystemのQuery([ChunkIndexInQuery]がAddComponentするには必要)
void Execute(RefRW<PlayerComponent> player, Entity entity, [ChunkIndexInQuery] int sortKey)
{
// Posionコンポーネントを追加する(sortKeyがいるので注意)
ecbParallel.AddComponent<Poison>(sortKey,entity);
// Poisonコンポーネントを削除する(sortKeyがいるので注意)
ecbParallel.RemoveComponent<Poison>(sortKey,entity);
}
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// UpdateでEntityCommandBufferを作成し
var ecb = new EntityCommandBuffer(Allocator.TempJob);
// EntityCommandBufferをParallelWriterにしてJobに投げる
new ExsamplePoisonJob
{
ecbParallel = ecb.AsParallelWriter()
}
.Schedule();
state.Dependency.Complete();
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
これで何かのイベントとかアクションとか状況から状態を変化させることができます。
注意点
EntityCommandBuffer.AddComponent
は別に少量使うには問題ないのですが、一応内部ではArcheType
を書き換えたりいろいろやってますので、毎フレーム状態を変えるなどの使用は避けた方がいいと思います。