はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン(本記事)
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
State パターンについて
Stateパターンは名前の通り 状態変化
を扱うことに適したデザインパターンです。
状態
だけをintやEnumで管理するだけでは、その状態になった時
だったり その状態中どうするか?
などの振る舞いは管理できません。
そこで、この状態と振る舞いをまとめてクラスに(=カプセル化)してしまうことで処理を含めた共通化ができるようになります。
ざっくりとしたStateクラスのインターフェースを定義するなら以下のように作るのが良いでしょう。
Enum でStateが定義できるなら管理が楽なのでEnumを使うことを推奨しますが、言語的だったり、Stateの変更をしやすさでいえば、const int で管理して先のインターフェースのEnum部分をint に変えたものを使うと良いでしょう。
ゲームにおける State パターン
ゲームは状態遷移のオンパレードです。
敵/味方AIの行動制御
, インタラクティブミュージック
, マルチプレイのネットワーク接続管理
, ステージ(Level)遷移・切り替え
, ノードベースエディタの実行状況表示
といった具合に、特定の分野で利用されているのではなく、ほとんど全ての分野で使われます。
そのためゲーム開発において、「俺AI制御やらないから」とか「サウンドは関係ない」と言った言い訳は基本的には通用しないためしっかり学んでおきましょう。
ここからは、先ほどあげたような実例を用いてもう少し詳しく説明していきます。
行動制御とStateパターン
まずは単純な例として、敵AIの行動制御のためのStateパターンを考えてみます。
敵の状態としては 待機中(Idle)
, 移動中(Move)
, 攻撃(Attack)
, 防御(Defense)
, 死亡(Dead)
の5パターンがあるとしましょう。
そうするとクラス図的には先ほどのものに加えると以下のようになります。
これらを踏まえてサンプルのコードを書くなら以下のようになります。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Dummy.StatePattern
{
public enum StateType
{
UNDEFINED,
IDLE,
MOVE,
ATTACK,
DEFENSE,
DEAD,
}
public interface IState
{
StateType GetCurrentState { get; }
bool ChangeState(IState nextState);
void OnStateChanged();
void OnStateBegin();
void OnStateEnd();
void Update(float deltaTime);
void SetNextState(IState nextState);
IState GetNextState();
}
public class IdleState : IState
{
private IState m_nextState =null;
public bool IsEndState { get; protected set; } = false;
#region ===== IState =====
StateType IState.GetCurrentState { get; } = StateType.IDLE;
bool IState.ChangeState(IState nextState)
{
IState state = this;
state.OnStateEnd();
if (nextState == null) return false;
nextState.OnStateChanged();
nextState.OnStateBegin();
return true;
}
void IState.OnStateChanged()
{
// Initialize
}
void IState.OnStateBegin()
{
IsEndState = false;
}
void IState.OnStateEnd()
{
IsEndState = true;
}
void IState.Update(float deltaTime)
{
if (IsEndState) return;
if (m_nextState != null)
{
(this as IState).ChangeState(m_nextState);
return;
}
}
void IState.SetNextState(IState nextState) { m_nextState = nextState;}
IState IState.GetNextState() { return m_nextState; }
#endregion //) ===== IState =====
}
public class MoveState : IState
{
protected Transform m_moveTarget = null;
protected Vector3 m_moveDestination = Vector3.zero;
protected float m_moveSpeed = 0.0f;
private IState m_nextState =null;
public bool IsEndState { get; protected set; } = false;
#region ===== IState =====
StateType IState.GetCurrentState { get; } = StateType.MOVE;
bool IState.ChangeState(IState nextState)
{
IState state = this;
state.OnStateEnd();
if (nextState == null) return false;
nextState.OnStateChanged();
nextState.OnStateBegin();
return true;
}
void IState.OnStateChanged()
{
// Initialize
}
void IState.OnStateBegin()
{
IsEndState = false;
}
void IState.OnStateEnd()
{
IsEndState = true;
}
void IState.Update(float deltaTime)
{
if (IsEndState) return;
if (m_nextState != null)
{
(this as IState).ChangeState(m_nextState);
return;
}
if (m_moveTarget == null) return;
Vector3 currentPosition = m_moveTarget.position;
Vector3 moveVec = (m_moveDestination - currentPosition).normalized;
m_moveTarget.position = currentPosition + moveVec * (m_moveSpeed * deltaTime);
}
void IState.SetNextState(IState nextState) { m_nextState = nextState;}
IState IState.GetNextState() { return m_nextState; }
#endregion //) ===== IState =====
public void SetTarget(Transform target, Vector3 destination, float speedPerSec)
{
m_moveTarget = target;
m_moveDestination = destination;
m_moveSpeed = speedPerSec;
}
}
public class StateController : MonoBehaviour
{
private IState currentState = new IdleState();
private void Update()
{
currentState.Update(Time.deltaTime);
}
}
}
C#のピュアクラスでは毎フレームの更新ができないため、 IState.Update(float deltaTime) をMonoBehaviour継承クラスのどこかから読んでもらう必要はあります。
また、Stateの切り替えロジックについては今回は割愛しますが、ここでいえば、StateControllerのcurrentState に対してSetNextStateをコールすることで切り替わりが実現できます。
ノードベースエディタでのStateパターン
Unity のノードベースエディタといえば、StateMachineである Mecanim (Animator ウィンドウで制御するあれ)や、ShaderGraph, VisualScriptがあります。
これらに共通する機能としてはEditor再生中に現在どのNodeが実行されているのか?を可視化しています。
大抵は枠で囲われたり、点滅を繰り返す等で表現されますが、この どのノードが実行されているのか?
はStateパターンによって管理されてない限り実現は出来ません。
一応連結リストによる表現も可能といえば可能ですが、分岐するような表現は苦手であるため、柔軟性を持たせるにはStateパターンを利用して管理し、今回でいえば if(currentState == this) Node枠を表示する()
のような処理を行うことでノードベースエディタの表現が可能です。
まとめ
ゲーム開発においては切っても切れないほど頻繁に出てくるのが状態遷移。
その状態遷移を表現するためのデザインパターンとしてStateパターンがあります。
おそらくゲームほど複雑で多数の状態が定義されている開発はなかなかないと思うので、ゲーム開発を行う人はたとえ、ゲーム外ツールであっても必要になる場面が必ず出てくるので覚えておくとよいでしょう。