0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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処理など
            }
        }
    }
}

という感じでifswitchで分岐していきます。

状態がいろいろ組合された場合

普通に大元の状態が単一で複数の状態が複合しないという部分には上記で十分ですが、ゲームによっていろいろな状態が重なり合うようなものもあると思います。
例えば、毒状態で体力がだんだん減っていくうえに、混乱状態になって入力の値がランダムになり、その上現在は水の中にいるので水中での処理が走ってほしい。みたいなときとかです。

ここで一つ立ち戻ってもらいたいのですがこのECSというフレームワーク実はこれが得意なのです。
それぞれの状態をComponentとして、そのそれぞれの状態の時のシステムを個別に作っておけば、そのEntityについてるComponentにそれぞれの処理が走るので簡単に複合できるわけです。
具体的に言うとPoison,Confusion,Swimコンポーネントとそれぞれに対するSystemを作っておけば、PoisonConfusionのコンポーネントがついたエンティティはPoisonSystem,ConfusionSystemで処理され、Poison,SwimのコンポーネントがついたエンティティはPoisonSystem,Swimシステムで処理されます。
もちろんSwimしかついていないエンティティはSwimのみで処理されます。

こういうことなので、状態を持てないといわれるECSですが、実はばっちり状態を持って処理することができるのです。

またConfusionPoisonが競合していたら動かしたくない場合はforeachWithNone<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()などEntityComponentを追加する方法は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を書き換えたりいろいろやってますので、毎フレーム状態を変えるなどの使用は避けた方がいいと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?