ステートベースAI
サンプル
ステートベースのサンプルコードを用意しています。
●リンク
サンプル
●環境
VisualStudio2015
●内容
StateBaseAI
単純なStateBaseAIの実装
StateBaseAI02
関数ポインタによる状態管理
StateBaseAI03
Stateパターンによる状態管理
StateBaseAI04
階層ステートマシン
■概要
ステートベースAIはキャラクターなどの挙動を状態毎に分ける手法です。
各状態は別の状態へ遷移する条件があり、ある状態から別の状態へ遷移することで、
キャラクターが思考しているようにユーザーに思わせることができます。
このような状態が遷移するステートベースの手法をステートマシーンと呼びます。
●遷移
ステートマシーンは現在の状態から別の状態へ遷移することで
キャラクターなどの行動を表現します。
遷移をするためには条件が必要で、上の図でいうと
「待機=>移動」の場合「待機状態で一定時間経過」、
「移動=>追跡」の場合「プレイヤーが一定範囲内に入る」などです。
条件にはこれといった決まりはなくゲームの仕様で決まります。
■switch、ifによる実装
ステートベースをステートマシーンの方法で実装する場合、
最も単純な実装方法はif、またはswitchによる実装です。
状態が少ない場合は処理コストが低いので簡単に実装できますが、
状態が多くなってくると冗長なif、swtichになってしまうので、
保守や可読の面で適しているとはあまりいえません。
●例
switch (今の状態)
{
case 状態1:
状態1の処理と遷移処理
break;
case 状態2:
状態2の処理と遷移処理
break;
case 状態3:
状態3の処理と遷移処理
break;
・
・
・
}
●具体例
各状態毎の分岐を作成したら内部処理は関数呼び出しを行うようにすべきです。
理由はすべてifやswitchの中でコーディングしていたら
すぐに長くなり見ずらい関数になってしまうからです。
/*
更新
状態毎に呼び出す関数を変える
*/
void Enemy::Update()
{
if (m_State == STATE::WAIT)
{
Wait();
} else if (m_State == STATE::MOVE) {
Move();
} else if (m_State == STATE::CHASE) {
Chase();
} else if (m_State == STATE::ATTACK) {
Attack();
}
}
/*
状態:待機
一定時間待機 => 状態を移動へ
プレイヤーが一定距離内に侵入 => 状態を追跡へ
*/
void Enemy::Wait()
{
m_Timer++;
if (m_Timer >= 10)
{
printf("\tWait => Move\n");
m_Timer = 0;
m_State = STATE::MOVE;
} else if (GetDistance() < ChaseDistance) {
printf("\tWait => Chase\n");
m_Timer = 0;
m_State = STATE::CHASE;
}
}
■Stateパターン
更新関数内で状態毎に関数を呼び出すためにifやswitchを使用した場合
内部処理を関数化や関数ポインタ化することで可読性を上げたとしても
数が増えるごとに可読性が下がっていきます。
そこで呼び出し側の内容を簡略化する手法としてデザインパターンの
1つであるStateパターンがあります。
●クラス図
●特性
サンプルのStateBaseAIとStateBaseAI03をのEnemyのUpdateを
比較すればわかりやすいと思いますが、Stateパターンを使用することで
Enemyで行っていた状態毎の分岐や判定をすべてStateクラスが
担当することでUpdateの中身がシンプルになっています。
// StateBaseAI
void Enemy::Update()
{
if (m_State == STATE::WAIT)
{
Wait();
} else if (m_State == STATE::MOVE) {
Move();
} else if (m_State == STATE::CHASE) {
Chase();
} else if (m_State == STATE::ATTACK) {
Attack();
}
}
// StateBaseAI03
void Enemy::Update()
{
m_State->Update(this);
}
このようにifやswitchを無くす事ができるのがStateパターンの特性です。
●実装
・宣言
クラスの宣言はすべての状態が実行する
純粋仮想関数を定義した抽象クラスを作成し、
各状態クラスはそれを継承します。
// 状態基底クラス
class State
{
public:
virtual ~State() {}
// 初期化
virtual void Init(Enemy *enemy) = 0;
// 更新
virtual void Update(Enemy *enemy) = 0;
// 状態表示(デバッグ用)
virtual void PrintState(void) = 0;
};
・呼び出し側
呼び出し側は状態クラスの実行関数を呼び出すだけですが
状態クラス内で使用される関数の実装は別途必要です。
また、状態変数(m_State)を変更する手段を用意する必要があります。
サンプルではChangeStateで変更するようにしていますが、
戻り値を使用して切り替える方法もあります。
// 実行
void Enemy::Update()
{
m_State->Update(this);
}
// 状態遷移
void Enemy::ChangeState(State *state)
{
m_State = state;
m_State->Init(this);
m_State->PrintState();
}
・状態クラス定義
状態クラスでは純粋仮想関数になっている関数を定義します。
そこで、初期化や状態の実行処理などを実装していきます。
void WaitState::Init(Enemy *enemy)
{
enemy->SetTimer(0);
}
void WaitState::Update(Enemy *enemy)
{
if (enemy->AddTimer(1) >= 10)
{
enemy->ChangeState(MoveState::GetInstance());
} else if (enemy->IsDistanceToPlayer(10.0f)) {
enemy->ChangeState(ChaseState::GetInstance());
}
}
●サンプル
サンプルのStateBaseAI03では各状態クラスは遷移の際に作成するのではなく、
シングルトンパターンで作成しています。
これは敵AIなどは目まぐるしく状態が切り替わるので
そのたびにnew、deleteが呼ばれることでパフォーマンスが
下がる可能性を考慮しているからです。
シングルトンパターンならどれだけの敵を作成して状態が切り替わっても問題はありません。
■階層ステートマシン
状態の数が増えるとその分だけ遷移が発生して管理が大変になります。
そこで、状態を階層分けすることで遷移の流れを整理することができます。
上の図のように階層ステートマシーンは状態を階層分けして、
階層単位で状態遷移を実行する手法です。
巡回状態の場合は「待機」と「移動」を繰り返し、
階層状態が発見に遷移したら「追跡」「攻撃」「攻撃待機」が実行されます。
●注意点
階層ステートマシーンでは階層間で状態で遷移する場合
最初に実行する階層内の状態は基本的に決めておきます。
そうしないとせっかく階層分けをして整理した状態遷移の流れが
めちゃくちゃになってしまいます。
上の図では巡回状態から発見状態に遷移する場合は「追跡状態」、
発見状態から巡回状態に遷移する場合は「発見状態」を開始状態にしています。
●サンプル
基本はステートパターンを使用しており、
階層用の状態クラスと階層内用の状態クラスを用意した実装をしています。