状態遷移でモンスターを管理
ゲーム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文だけでは、行動が増えるほど管理しづらくなる
状態とは「今何をしているか」を表す情報
状態遷移とは「条件によって状態が切り替わること」
視覚判定は、状態遷移の条件として使える
状態管理を使うと、見失う・探す・戻るなどの自然な行動を作りやすい
No Comments