Skip to main content

状態遷移でモンスターを管理

ゲームAI課題:状態遷移でモンスターの行動を管理しよう

「今、モンスターは何をしているのか?」を設計する


1. 今日のテーマ

前回は、モンスターに「視覚」を与える考え方を学びました。

isFindPlayer = CheckCanSeePlayer();

このように、モンスターがプレイヤーを見つけているかどうかを判定しました。

そして、その結果を使って行動を変えました。

if (isAttackRange)
{
    Attack();
}
else if (isFindPlayer)
{
    ChasePlayer();
}
else
{
    Idle();
}

この書き方でも、最低限のAIは作れます。

しかし、行動が増えてくると問題が出てきます。


2. if文だけのAIの問題点

問題1:条件が増えると分かりにくい

例えば、モンスターに次の行動を追加したいとします。

  • 巡回する
  • プレイヤーを見つけたら追いかける
  • 近づいたら攻撃する
  • 見失ったら探す
  • しばらく探して見つからなければ巡回に戻る
  • HPが少なくなったら逃げる

これを if 文だけで書くと、次のように複雑になります。

if (isLowHp)
{
    Escape();
}
else if (isAttackRange)
{
    Attack();
}
else if (isFindPlayer)
{
    ChasePlayer();
}
else if (isLostPlayer)
{
    Search();
}
else if (isFarFromHome)
{
    ReturnHome();
}
else
{
    Patrol();
}

一見よさそうに見えます。

しかし、行動が増えるほど、

どの条件を先に書くべきか
今どの行動中なのか
行動の途中で何をすればよいのか

が分かりにくくなります。


問題2:「今何をしているか」が分かりにくい

例えば、モンスターが攻撃中だとします。

攻撃モーションには時間がかかります。

攻撃開始
↓
攻撃モーション中
↓
ダメージ発生
↓
攻撃終了

しかし、毎フレーム if 文だけで行動を決めていると、

if (isAttackRange)
{
    Attack();
}

が何度も呼ばれて、攻撃モーションが毎回最初から始まってしまうことがあります。

このように、

今は攻撃中である
今は探索中である
今は巡回中である

という情報を持たないと、行動を管理しづらくなります。


問題3:「見失った後」の行動が作りにくい

視覚判定だけで作ると、プレイヤーが見えなくなった瞬間に待機してしまいます。

if (isFindPlayer)
{
    ChasePlayer();
}
else
{
    Idle();
}

これだと不自然です。

本当は、次のような流れにしたいはずです。

プレイヤーを見つける
↓
追いかける
↓
壁の向こうに逃げられる
↓
見失う
↓
最後に見た場所へ行く
↓
少し探す
↓
見つからなければ巡回に戻る

このような「流れのある行動」を作るには、状態管理が必要です。


3. 状態とは何か

状態とは、

今、そのキャラクターが何をしているか

を表すものです。

例えば、モンスターには次のような状態があります。

状態 意味
Idle その場で待っている
Patrol 巡回している
Chase プレイヤーを追いかけている
Attack 攻撃している
Search プレイヤーを探している
Return 持ち場に戻っている
Escape 逃げている

このように、行動を状態として整理します。


4. enum class で状態を作る

C++では、状態を enum class で作ると分かりやすくなります。

enum class EnemyState
{
    Idle,
    Patrol,
    Chase,
    Attack,
    Search,
    Return,
    Escape
};

そして、敵が現在どの状態なのかを変数で持ちます。

EnemyState state = EnemyState::Patrol;

これで、モンスターは

今は Patrol 状態である
今は Chase 状態である
今は Attack 状態である

という情報を持てるようになります。


5. 状態ごとに行動を分ける

状態を持ったら、switch 文で行動を分けます。

switch (state)
{
case EnemyState::Idle:
    Idle();
    break;

case EnemyState::Patrol:
    Patrol();
    break;

case EnemyState::Chase:
    ChasePlayer();
    break;

case EnemyState::Attack:
    Attack();
    break;

case EnemyState::Search:
    Search();
    break;

case EnemyState::Return:
    ReturnHome();
    break;

case EnemyState::Escape:
    Escape();
    break;
}

これで、

現在の状態に応じて
呼び出す行動を変える

ことができます。


6. 状態遷移とは何か

状態遷移とは、

ある状態から、別の状態に切り替わること

です。

例えば、

Patrol
↓ プレイヤーを見つけた
Chase

これは、

if (state == EnemyState::Patrol)
{
    if (isFindPlayer)
    {
        state = EnemyState::Chase;
    }
}

と書けます。


7. 状態遷移の例

モンスターの基本的な状態遷移を考えると、次のようになります。

Patrol
  ↓ プレイヤーを見つけた
Chase
  ↓ 攻撃範囲に入った
Attack
  ↓ 攻撃範囲から出た
Chase
  ↓ プレイヤーを見失った
Search
  ↓ 見つからなかった
Patrol

このように、

何が起きたら
どの状態に変わるのか

を考えるのが、状態遷移の設計です。


8. 視覚判定と状態遷移をつなげる

前回作った視覚判定を使います。

isFindPlayer = CheckCanSeePlayer();

これを状態遷移に使います。

if (state == EnemyState::Patrol)
{
    if (isFindPlayer)
    {
        state = EnemyState::Chase;
    }
}

つまり、

視覚で見つける
↓
状態を切り替える
↓
行動が変わる

という流れになります。


9. 状態遷移表

状態遷移は、表で考えると分かりやすくなります。

現在の状態 条件 次の状態
Patrol プレイヤーを見つけた Chase
Chase 攻撃範囲に入った Attack
Chase プレイヤーを見失った Search
Attack 攻撃範囲から出た Chase
Search プレイヤーを見つけた Chase
Search 3秒探して見つからない Patrol

プログラムを書く前に、この表を作ると整理しやすくなります。


10. 状態遷移図

表だけでなく、図にするとさらに分かりやすくなります。

          プレイヤー発見
   +--------------------+
   |                    v
+--------+          +--------+
| Patrol |          | Chase  |
+--------+          +--------+
   ^                    |
   |                    | 攻撃範囲
   | 3秒探して          v
   | 見つからない    +--------+
+--------+          | Attack |
| Search |          +--------+
+--------+             |
   ^                    |
   | 見失った           | 範囲外
   +--------------------+

このように、状態同士のつながりを線で表します。


11. 今日の課題

課題テーマ

見失ったら探すモンスターAIを設計しよう

モンスターの仕様

今回設計するモンスターは、次のように動きます。

1. 普段は決められた場所を巡回している
2. プレイヤーが視界に入ったら追いかける
3. プレイヤーが攻撃範囲に入ったら攻撃する
4. プレイヤーが攻撃範囲から出たらまた追いかける
5. プレイヤーを見失ったら、その場で探す
6. 3秒探しても見つからなければ巡回に戻る

12. 使用してよい状態

今回は、次の状態を使います。

enum class EnemyState
{
    Patrol,
    Chase,
    Attack,
    Search
};

それぞれの意味は次の通りです。

状態 内容
Patrol 決められた場所を巡回する
Chase プレイヤーを追いかける
Attack プレイヤーを攻撃する
Search プレイヤーを探す

13. 使用してよいフラグ

今回は、次のフラグを使います。

bool isFindPlayer;      // プレイヤーを見つけている
bool isAttackRange;     // 攻撃範囲に入っている
bool isSearchTimeOver;  // 探索時間が終わった

フラグの意味

フラグ 意味
isFindPlayer 視界にプレイヤーがいる
isAttackRange プレイヤーが攻撃できる距離にいる
isSearchTimeOver 探索時間が終わった

14. 課題1:状態の意味を説明する

次の状態について、自分の言葉で説明しなさい。

状態 自分の説明
Patrol
Chase
Attack
Search

15. 課題2:状態遷移表を完成させる

次の表を完成させなさい。

現在の状態 条件 次の状態
Patrol isFindPlayer == true
Chase isAttackRange == true
Chase isFindPlayer == false
Attack isAttackRange == false
Search isFindPlayer == true
Search isSearchTimeOver == true

16. 課題3:状態遷移図を描く

次の状態を使って、状態遷移図を描きなさい。

Patrol
Chase
Attack
Search

矢印には、状態が切り替わる条件を書きなさい。

例:

Patrol -- isFindPlayer --> Chase

17. 課題4:状態遷移の疑似コードを書く

次のひな形を使って、状態遷移の疑似コードを書きなさい。

if (state == EnemyState::Patrol)
{
    if (__________)
    {
        state = EnemyState::__________;
    }
}
else if (state == EnemyState::Chase)
{
    if (__________)
    {
        state = EnemyState::__________;
    }
    else if (__________)
    {
        state = EnemyState::__________;
    }
}
else if (state == EnemyState::Attack)
{
    if (__________)
    {
        state = EnemyState::__________;
    }
}
else if (state == EnemyState::Search)
{
    if (__________)
    {
        state = EnemyState::__________;
    }
    else if (__________)
    {
        state = EnemyState::__________;
    }
}

18. 課題5:行動処理の疑似コードを書く

状態ごとに、どの関数を呼び出すかを書きなさい。

switch (state)
{
case EnemyState::Patrol:
    __________();
    break;

case EnemyState::Chase:
    __________();
    break;

case EnemyState::Attack:
    __________();
    break;

case EnemyState::Search:
    __________();
    break;
}

使ってよい関数名:

Patrol();
ChasePlayer();
Attack();
Search();

19. 課題6:最終的なUpdate処理を組み立てる

次の流れになるように、Update() の疑似コードを完成させなさい。

1. 視覚判定を行う
2. 攻撃範囲判定を行う
3. 探索時間が終わったか調べる
4. 状態遷移を行う
5. 現在の状態に応じた行動を行う

ひな形

void Enemy::Update()
{
    // 1. 視覚判定
    isFindPlayer = ____________________;

    // 2. 攻撃範囲判定
    isAttackRange = ____________________;

    // 3. 探索時間判定
    isSearchTimeOver = ____________________;

    // 4. 状態遷移
    if (state == EnemyState::Patrol)
    {
        if (__________)
        {
            state = EnemyState::__________;
        }
    }
    else if (state == EnemyState::Chase)
    {
        if (__________)
        {
            state = EnemyState::__________;
        }
        else if (__________)
        {
            state = EnemyState::__________;
        }
    }
    else if (state == EnemyState::Attack)
    {
        if (__________)
        {
            state = EnemyState::__________;
        }
    }
    else if (state == EnemyState::Search)
    {
        if (__________)
        {
            state = EnemyState::__________;
        }
        else if (__________)
        {
            state = EnemyState::__________;
        }
    }

    // 5. 状態ごとの行動
    switch (state)
    {
    case EnemyState::Patrol:
        __________();
        break;

    case EnemyState::Chase:
        __________();
        break;

    case EnemyState::Attack:
        __________();
        break;

    case EnemyState::Search:
        __________();
        break;
    }
}

20. 課題7:このAIの弱点を考える

このモンスターAIには、まだ弱点があります。

例えば、

  • Search状態でどこを探すのか決まっていない
  • 見失った瞬間のプレイヤー位置を覚えていない
  • 攻撃モーション中にすぐChaseへ戻るかもしれない
  • Patrolのルートが決まっていない
  • 壁を避けて移動できない

などです。

自分で、このAIの弱点を3つ書きなさい。


21. 課題8:改良案を考える

課題7で書いた弱点を1つ選び、改良案を書きなさい。

例:

弱点:
Search状態でどこを探すのか決まっていない。

改良案:
プレイヤーを最後に見た位置を lastPlayerPos として保存し、
Search状態ではその場所に移動してから周囲を探す。

22. 90分の進め方

時間 内容
0〜15分 資料前半を読む
15〜25分 課題1:状態の意味を説明する
25〜40分 課題2:状態遷移表を完成させる
40〜55分 課題3:状態遷移図を描く
55〜70分 課題4・5:疑似コードを書く
70〜85分 課題6:Update処理を組み立てる
85〜90分 課題7・8:弱点と改良案を書く

時間が足りない場合、課題8は宿題にしてもよい。


23. まとめ

今回の重要ポイントは次の通りです。

if文だけでは、行動が増えるほど管理しづらくなる
状態とは「今何をしているか」を表す情報
状態遷移とは「条件によって状態が切り替わること」
視覚判定は、状態遷移の条件として使える
状態管理を使うと、見失う・探す・戻るなどの自然な行動を作りやすい