📗

【Unity】ゲームやキャラの状態を管理しよう

2024/12/13に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 ゲームやキャラの状態を管理
する方法について紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

状態管理パターン(State Pattern) について

状態管理パターンは、オブジェクトの状態をオブジェクト自身から分離し、それぞれの状態ごとに異なる動作を実現できるようにするデザインパターンです。
ゲーム開発においては、プレイヤーや敵キャラクター、NPCなどのオブジェクトが特定の状態(例: 攻撃中、ダメージ中、死亡)に応じて異なる振る舞いを行う場合使います!

状態管理パターンを使用することでの利点

  • 各状態が独立したクラスとして分離されるため、コードが見やすい
  • 新しい状態を追加しやすい
  • 状態ごとの振る舞いを簡単に変更できる

補足

今回の実装例では、
UniRxとUniTaskを使用します。
導入は、こちらを参考にしてください
https://zenn.dev/tmb/articles/e4fb3fe350852f

状態管理パターンの実装例

状態を表するインターフェース

IInGameState.cs
using Cysharp.Threading.Tasks;

namespace InGame
{
    /// <summary>
    /// 状態を表すインターフェース
    /// </summary>
    public interface IInGameState
    {
        /// <summary>
        /// 初期化
        /// </summary>
        public abstract void Initialize();

        /// <summary>
        /// 状態に入ったとき
        /// </summary>
        public abstract UniTask StartStateAsync();

        /// <summary>
        /// 状態を離れるとき
        /// </summary>
        public abstract UniTask EndStateAsync();
    }
}

インターフェースを継承したベースクラス

InGameStateView.cs
using System;
using UnityEngine;
using Cysharp.Threading.Tasks;

namespace InGame
{
    /// <summary>
    /// 各状態のベースクラス
    /// </summary>
    public abstract class InGameStateView : IInGameState, IDisposable
    {
        /// <summary>
        /// 初期化
        /// </summary>
        public virtual void Initialize()
        {
            Debug.Log("InGameStateView Initialize");
        }

        /// <summary>
        /// 破棄
        /// </summary>
        public void Dispose()
        {
            Debug.Log($"{this.GetType().Name} Dispose");
        }

        /// <summary>
        /// 状態に入ったとき
        /// </summary>
        public virtual async UniTask StartStateAsync()
        {
            Debug.Log("InGameStateView StartState");
            await UniTask.CompletedTask;
        }

        /// <summary>
        /// 状態を離れるとき
        /// </summary>
        public virtual async UniTask EndStateAsync()
        {
            Debug.Log("InGameStateView StartState");
            await UniTask.CompletedTask;
        }
    }
}

状態と紐づくタイプ

InGameState.cs
namespace InGame
{
    /// <summary>
    /// 状態と紐づくタイプ
    /// </summary>
    public enum InGameState
    {
        /// <summary> 起動時 </summary>
        Boot,

        /// <summary> 待機 </summary>
        Wait,

        /// <summary> 攻撃 </summary>
        Attack,

        /// <summary> ダメージ </summary>
        Damage,
    }
}

各状態を処理するクラス

BootStateView.cs
■起動時の状態
using UnityEngine;
using Cysharp.Threading.Tasks;

namespace InGame
{
    /// <summary>
    /// 起動時状態
    /// </summary>
    public sealed class BootStateView : InGameStateView
    {
        /// <summary>
        /// 初期化
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();
            Debug.Log("BootStateView Initialize");
        }

        /// <summary>
        /// 状態に入ったとき
        /// </summary>
        public override async UniTask StartStateAsync()
        {
            await base.StartStateAsync();
            Debug.Log("BootStateView StartState");
            await UniTask.CompletedTask;
        }

        /// <summary>
        /// 状態を離れるとき
        /// </summary>
        public override async UniTask EndStateAsync()
        {
            await base.EndStateAsync();
            Debug.Log("BootStateView EndState");
            await UniTask.CompletedTask;
        }
    }
}

■待機中の状態

WaitStateView.cs
using UnityEngine;
using Cysharp.Threading.Tasks;

namespace InGame
{
    /// <summary>
    /// 待機状態
    /// </summary>
    public sealed class WaitStateView : InGameStateView
    {
        /// <summary>
        /// 初期化
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();
            Debug.Log("WaitStateView Initialize");
        }

        /// <summary>
        /// 状態に入ったとき
        /// </summary>
        public override async UniTask StartStateAsync()
        {
            await base.StartStateAsync();
            Debug.Log("WaitStateView StartState");
            await UniTask.CompletedTask;
        }

        /// <summary>
        /// 状態を離れるとき
        /// </summary>
        public override async UniTask EndStateAsync()
        {
            await base.EndStateAsync();
            Debug.Log("WaitStateView StartState");
            await UniTask.CompletedTask;
        }
    }
}

■攻撃したときの状態

AttackStateView.cs
using UnityEngine;
using Cysharp.Threading.Tasks;

namespace InGame
{
    /// <summary>
    /// 攻撃したときの状態
    /// </summary>
    public sealed class AttackStateView : InGameStateView
    {
        /// <summary>
        /// 初期化
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();
            Debug.Log("AttackStateView Initialize");
        }

        /// <summary>
        /// 状態に入ったとき
        /// </summary>
        public override async UniTask StartStateAsync()
        {
            await base.StartStateAsync();
            Debug.Log("AttackStateView StartState");
            await UniTask.CompletedTask;
        }

        /// <summary>
        /// 状態を離れるとき
        /// </summary>
        public override async UniTask EndStateAsync()
        {
            await base.EndStateAsync();
            Debug.Log("AttackStateView StartState");
            await UniTask.CompletedTask;
        }
    }
}

■ダメージが入ったときの状態

DamageStateView.cs
using UnityEngine;
using Cysharp.Threading.Tasks;

namespace InGame
{
    /// <summary>
    /// ダメージが入ったときの状態
    /// </summary>
    public sealed class DamageStateView : InGameStateView
    {
        /// <summary>
        /// 初期化
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();
            Debug.Log("DamageStateView Initialize");
        }

        /// <summary>
        /// 状態に入ったとき
        /// </summary>
        public override async UniTask StartStateAsync()
        {
            await base.StartStateAsync();
            Debug.Log("DamageStateView StartState");
            await UniTask.CompletedTask;
        }

        /// <summary>
        /// 状態を離れるとき
        /// </summary>
        public override async UniTask EndStateAsync()
        {
            await base.EndStateAsync();
            Debug.Log("DamageStateView StartState");
            await UniTask.CompletedTask;
        }
    }
}

状態を管理するマネージャー

DamageStateView.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Cysharp.Threading.Tasks;

namespace InGame
{
    public class InGameStateManager
    {
        static private InGameStateManager _instance;
        static public InGameStateManager Instance
        {
            get
            {
                _instance ??= new InGameStateManager();
                return _instance;
            }
        }

        private ReactiveProperty<InGameState> _inGameState;
        private Dictionary<InGameState, InGameStateView> _stateViewDic;

        /// <summary> 現在の状態 </summary>
        public InGameState InGameState { get { return _inGameState.Value; } }

        /// <summary>
        /// 初期化
        /// </summary>
        public void Initialize()
        {
            Debug.Log("InGameStateManager Initialize");

            _stateViewDic = new Dictionary<InGameState, InGameStateView>();

            // 状態を登録
            RegisterState(InGameState.Boot, new BootStateView());
            RegisterState(InGameState.Wait, new WaitStateView());
            RegisterState(InGameState.Attack, new AttackStateView());
            RegisterState(InGameState.Damage, new DamageStateView());

            _inGameState = new ReactiveProperty<InGameState>(InGameState.Boot);

            // InGameStateが更新されたら通知
            _inGameState.Pairwise().Subscribe(async states => await OnChangeStateAsync(states.Previous, states.Current));
        }

        /// <summary>
        /// 破棄
        /// </summary>
        public void Dispose()
        {
            Debug.Log("InGameStateManager Dispose");

            foreach (InGameStateView stateView in _stateViewDic.Values)
            {
                (stateView as IDisposable)?.Dispose();
            }
            _inGameState?.Dispose();
        }

        /// <summary>
        /// 状態を登録
        /// </summary>
        public void RegisterState(InGameState state, InGameStateView stateView)
        {
            if (! _stateViewDic.ContainsKey(state))
            {
                _stateViewDic[state] = stateView;
            }
            _stateViewDic[state].Initialize();
        }

        /// <summary>
        /// 状態を設定
        /// </summary>
        public void UpdateState(InGameState state)
        {
            Debug.Log($"InGameStateManager UpdateState: {state}");

            _inGameState.Value = state;
        }

        /// <summary>
        /// 状態を実行
        /// </summary>
        private async UniTask OnChangeStateAsync(InGameState oldState, InGameState newState)
        {
            Debug.Log($"InGameStateManager OnChangeState: {oldState} -> {newState}");

            await _stateViewDic[oldState].EndStateAsync();
            await _stateViewDic[newState].StartStateAsync();
        }
    }
}

使用例

InGameController.cs
using UnityEngine;
using UniRx;
using UniRx.Triggers;

namespace InGame
{
    public class InGameController : MonoBehaviour
    {
        /// <summary>
        /// 開始
        /// </summary>
        private void Start()
        {
            // InGameStateManagerを初期化
            InGameStateManager.Instance.Initialize();

            // 状態をWaitに変更
            InGameStateManager.Instance.UpdateState(InGameState.Wait);

            // InGameControllerの破棄が通知された時に、InGameStateManagerを破棄する
            gameObject.OnDestroyAsObservable().Subscribe(_ => Dispose()).AddTo(this);
        }

        /// <summary>
        /// 破棄
        /// </summary>
        private void Dispose()
        {
            InGameStateManager.Instance.Dispose();
        }

        /// <summary>
        /// 更新
        /// </summary>
        private void Update()
        {
            if (Input.GetKeyUp(KeyCode.A))
            {
                // 攻撃の状態に変更
                InGameStateManager.Instance.UpdateState(InGameState.Attack);
            }
        }
    }
}

Discussion