はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】状態の管理をStateパターンを用いてよりスマートに

はじめに

今回はStateパターンを用いて状態管理するという記事になります!

例えばプレイヤーが今どんな状態なのか。攻撃をしているのか、歩いているのか。

このような状態遷移を実装と思ったとき、まずはif文やswitch文で分岐させる方法が思いつくのではないでしょうか。

ただこの方法では状態が増えるごとにとても分かりづらくなってしまう傾向があります。

しかし、Stateパターンではこのif/switch文地獄から解放することができるのです。

どうやってif/switch文をなくすの?というのは当然の疑問ですが、私が思う一番重要なポイントはクラスのアップキャスト(ポリモーフィズム)を利用しているところだと思います。

百聞は一見に如かなので、早速みていきましょう!

その前に

この記事はこちらの記事をかなり参考にさせてもらっています。

befool.co.jp

ほとんどこちらの記事の通りなのですが、いくつか私なりに工夫?を施してみました。

ただそれに従ってC#6での機能が必要です。
Edit > Project Settings > Playerから、.NET 4.xを選択しましょう。

f:id:hanaaaaaachiru:20190324194408p:plain

実際のコード

今回は3つの状態を想定してみました。
・Idle
・Run
・Attack

これをStateパターンに当てはめて実装していきます。

CharacterState.cs
using UnityEngine;
using System;
using UniRx;

/// <summary>
/// キャラクターの状態(ステート)
/// </summary>
namespace CharacterState
{
    /// <summary>
    /// ステートの実行を管理するクラス
    /// </summary>
    public class StateProcessor
    {
        //ステート本体
        public ReactiveProperty<CharacterState> State { get; set; } = new ReactiveProperty<CharacterState>();

        //実行ブリッジ
        public void Execute() => State.Value.Execute();
    }

    /// <summary>
    /// ステートのクラス
    /// </summary>
    public abstract class CharacterState
    {
        //デリゲート
        public Action ExecAction { get; set; }

        //実行処理
        public virtual void Execute()
        {
            if (ExecAction != null) ExecAction();
        }

        //ステート名を取得するメソッド
        public abstract string GetStateName();
    }

    //=================================================================================
    //以下状態クラス
    //=================================================================================

    /// <summary>
    /// 何もしていない状態
    /// </summary>
    public class CharacterStateIdle : CharacterState
    {
        public override string GetStateName()
        {
            return "State:Idle";
        }
    }

    /// <summary>
    /// 走っている状態
    /// </summary>
    public class CharacterStateRun : CharacterState
    {
        public override string GetStateName()
        {
            return "State:Run";
        }
    }

    /// <summary>
    /// 攻撃している状態
    /// </summary>
    public class CharacterStateAttack : CharacterState
    {
        public override string GetStateName()
        {
            return "State:Attack";
        }

        public override void Execute()
        {
            Debug.Log("なにか特別な処理をしたいときは派生クラスにて処理をしても良い");
            if (ExecAction != null) ExecAction();
        }
    }
}
CharacterController.cs
using UnityEngine;
using CharacterState;
using UniRx;

public class CharacterController : MonoBehaviour
{
    //変更前のステート名
    private string _prevStateName;

    //ステート
    public StateProcessor StateProcessor { get; set; } = new StateProcessor();
    public CharacterStateIdle StateIdle { get; set; } = new CharacterStateIdle();
    public CharacterStateRun StateRun { get; set; } = new CharacterStateRun();
    public CharacterStateAttack StateAttack { get; set; } = new CharacterStateAttack();

    private void Start()
    {
        //ステートの初期化
        StateProcessor.State.Value = StateIdle;
        StateIdle.ExecAction = Idle;
        StateRun.ExecAction = Run;
        StateAttack.ExecAction = Attack;

        //ステートの値が変更されたら実行処理を行うようにする
        StateProcessor.State
            .Where(_ => StateProcessor.State.Value.GetStateName() != _prevStateName)
            .Subscribe(_ =>
            {
                Debug.Log("Now State:" + StateProcessor.State.Value.GetStateName());
                _prevStateName = StateProcessor.State.Value.GetStateName();
                StateProcessor.Execute();
            })
            .AddTo(this);
    }

    public void Idle()
    {
        Debug.Log("StateがIdleに状態遷移しました。");
    }

    public void Run()
    {
        Debug.Log("StateがRunに状態遷移しました。");
    }

    public void Attack()
    {
        Debug.Log("StateがAttackに状態遷移しました。");
    }
}

f:id:hanaaaaaachiru:20190420004532p:plain

CharacterControllerStateProcessor.State.Valueの値(状態)を変えると、それに応じた処理が自動で実行されます。

例えば、

StateProcessor.State.Value = StateRun;

とすることで、StateProcessor.Execute();つまりはRun()が呼ばれます。

ざっくりと言えば、デリゲートでメソッドを登録しておいて、状態に応じて実行をしているということになります。

さいごに

私なりの状態遷移を紹介してみました。
また状態遷移はキャラクターの状態のみならず、いろいろな応用が利くはずです。

あとマサカリも大大募集中です!