AI モンスターの知能
モンスターに視覚を与える
isFindPlayer は誰が決めているのか?
1. 前回までの確認
前回、モンスターの行動を if文 で切り替える考え方を学びました。
例えば、次のような処理です。
if (isAttackRange)
{
Attack();
}
else if (isFindPlayer)
{
ChasePlayer();
}
else
{
Idle();
}この処理では、モンスターは次のように行動します。
条件 | 行動 |
|---|---|
攻撃範囲にいる | 攻撃する |
プレイヤーを見つけている | 追いかける |
どちらでもない | 待機する |
このように、条件によって行動を変えることで、モンスターが考えて動いているように見えます。
2. ここで問い
次の変数に注目してください。
isFindPlayerこれは、
プレイヤーを見つけているか?を表すフラグです。
しかし、ここで考えるべきことがあります。
isFindPlayer は誰が決めているのか?
プログラムの中で、
if (isFindPlayer)と書くことはできます。
しかし、isFindPlayer が true になる理由を作らなければ、モンスターはプレイヤーを見つけることができません。
つまり、
isFindPlayer = true;になる条件を、こちらで作る必要があります。
3. ゲームAIには「知覚」が必要
モンスターが行動を決めるには、まずゲームの状況を知る必要があります。
例えば、
- プレイヤーが近くにいる
- プレイヤーが前方にいる
- プレイヤーとの間に壁がない
- プレイヤーが音を立てた
- プレイヤーが攻撃してきた
などです。
このように、AIがゲームの状況を知る仕組みを 知覚 と呼びます。
4. 人間の「見る」とAIの「見る」は違う
人間は目で映像を見ています。
しかし、ゲームAIは本当に画面を見ているわけではありません。
ゲームAIは、次のような数値や条件を使って「見えているか」を判定します。
距離は近いか?
前方にいるか?
壁に隠れていないか?つまり、ゲームAIの視覚とは、
画像を見ることではなく、
条件を満たしたら「見えている」とする処理です。
5. まず一番簡単な視覚を作る
最初は、距離だけで考えます。
例えば、
プレイヤーが10m以内にいたら発見というルールにします。
この場合、モンスターはプレイヤーとの距離を調べます。
VECTOR toPlayer = VSub(playerPos, enemyPos);
float distance = VSize(toPlayer);
if (distance < VIEW_DISTANCE)
{
isFindPlayer = true;
}
else
{
isFindPlayer = false;
}ここで、VIEW_DISTANCE は視界距離です。
const float VIEW_DISTANCE = 10.0f;6. 距離だけの視覚の問題点
距離だけで判定すると、かなり単純なAIになります。
例えば、次のような問題があります。
問題1:後ろにいても見つかる
距離だけで判定すると、プレイヤーがモンスターの後ろにいても見つかってしまいます。
プレイヤー
↓
モンスター → 前を向いている本当なら、モンスターの後ろにいるプレイヤーは見えないはずです。
でも距離だけで判定していると、近ければ見つかります。
問題2:壁越しでも見つかる
距離だけで判定すると、壁の向こうにいるプレイヤーも見つかってしまいます。
モンスター |壁| プレイヤー本当なら、壁があるので見えないはずです。
でも距離だけで判定していると、近ければ見つかります。
7. 視覚に必要な条件
より自然な視覚を作るには、次の3つを考えます。
1. 距離
2. 向き
3. 障害物8. 条件1:距離
まずは距離です。
プレイヤーが遠すぎるなら見えないこれは分かりやすい条件です。
if (distance > VIEW_DISTANCE)
{
isFindPlayer = false;
}9. 条件2:向き
次に、プレイヤーがモンスターの前方にいるかを調べます。
モンスターには向きがあります。
モンスター → 前方プレイヤーが前方にいれば見える。
モンスター → プレイヤーしかし、プレイヤーが後ろにいるなら見えません。
プレイヤー ← モンスター10. 前方にいるかをどう調べるか
ここで使うのが 内積 です。
必要なベクトルは2つです。
モンスターの前方向
モンスターからプレイヤーへの方向11. モンスターからプレイヤーへの方向
まず、モンスターからプレイヤーへのベクトルを求めます。
VECTOR toPlayer = VSub(playerPos, enemyPos);次に、長さを1にします。
float distance = VSize(toPlayer);
VECTOR dirToPlayer = VScale(toPlayer, 1.0f / distance);この dirToPlayer が、モンスターから見たプレイヤーの方向です。
12. モンスターの前方向
次に、モンスターの前方向を用意します。
例として、Y軸回転から前方向を作るなら、次のようになります。
VECTOR forward =
{
sinf(enemyRotY),
0.0f,
cosf(enemyRotY)
};ただし、モデルの正面方向や座標系によって、sinf と cosf の使い方は変わることがあります。
13. 内積で角度を調べる
2つのベクトルの内積を取ります。
float dot = VDot(forward, dirToPlayer);dot の値は、だいたい次のように考えられます。
dotの値 | 意味 |
|---|---|
1.0 に近い | 正面にいる |
0.0 に近い | 横にいる |
-1.0 に近い | 後ろにいる |
14. 視野角の判定
例えば、次のようにします。
const float VIEW_DOT = 0.5f;
if (dot > VIEW_DOT)
{
isFindPlayer = true;
}
else
{
isFindPlayer = false;
}dot > 0.5f なら、前方のある範囲内にいると判断できます。
15. VIEW_DOT の目安
条件 | 見える範囲の目安 |
|---|---|
| 前方約120度 |
| 前方約90度 |
| 前方約60度 |
数値を大きくすると、視野が狭くなります。
数値を小さくすると、視野が広くなります。
16. 距離と向きを組み合わせる
距離と向きを組み合わせると、次のような判定になります。
bool CheckCanSeePlayer()
{
VECTOR toPlayer = VSub(playerPos, enemyPos);
float distance = VSize(toPlayer);
if (distance > VIEW_DISTANCE)
{
return false;
}
if (distance < 0.001f)
{
return true;
}
VECTOR dirToPlayer = VScale(toPlayer, 1.0f / distance);
VECTOR forward =
{
sinf(enemyRotY),
0.0f,
cosf(enemyRotY)
};
float dot = VDot(forward, dirToPlayer);
if (dot < VIEW_DOT)
{
return false;
}
return true;
}この関数は、
プレイヤーが見えているなら true
見えていないなら falseを返します。
17. 条件3:障害物
まだ問題があります。
距離が近くて、前方にいても、壁の向こうにいるなら本当は見えません。
モンスター ---- 壁 ---- プレイヤーこの場合は、見えないようにしたいです。
18. 壁で見えない処理
考え方は単純です。
モンスターからプレイヤーまで線を引く
↓
途中に壁があるか調べる
↓
壁があれば見えない
↓
壁がなければ見えるこのような判定を、レイ判定やライン判定と呼びます。
19. 疑似コード
bool isHitWall = CheckLineHitWall(enemyPos, playerPos);
if (isHitWall)
{
return false;
}CheckLineHitWall は、敵とプレイヤーの間に壁があるか調べる処理だと考えてください。
20. 最終的な視覚判定
距離、向き、壁を組み合わせると、次のような流れになります。
bool CheckCanSeePlayer()
{
VECTOR toPlayer = VSub(playerPos, enemyPos);
float distance = VSize(toPlayer);
// 遠すぎるなら見えない
if (distance > VIEW_DISTANCE)
{
return false;
}
// ほぼ同じ位置なら見えている扱い
if (distance < 0.001f)
{
return true;
}
// 方向を求める
VECTOR dirToPlayer = VScale(toPlayer, 1.0f / distance);
// モンスターの前方向
VECTOR forward =
{
sinf(enemyRotY),
0.0f,
cosf(enemyRotY)
};
// 前方にいるか調べる
float dot = VDot(forward, dirToPlayer);
if (dot < VIEW_DOT)
{
return false;
}
// 壁があれば見えない
if (CheckLineHitWall(enemyPos, playerPos))
{
return false;
}
return true;
}21. 前回のAIに接続する
視覚判定ができたら、前回のAIに接続できます。
isFindPlayer = CheckCanSeePlayer();
if (isAttackRange)
{
Attack();
}
else if (isFindPlayer)
{
ChasePlayer();
}
else
{
Idle();
}これで、
見えたら追いかける
見えなければ待機するというモンスターになります。
22. 何がAIらしくなるのか
距離だけで判定していたときは、
近ければ必ず見つかるという動きでした。
しかし、視野角や壁判定を入れると、次のようになります。
後ろに回ると気づかれない
壁に隠れると見つからない
正面に出ると追いかけてくるこのようなルールがあると、プレイヤーは、
敵に見つかった
敵から隠れた
敵の背後を取ったと感じやすくなります。
つまり、AIが少し賢く見えます。
23. 重要な考え方
ゲームAIは、本当に人間のように世界を理解しているわけではありません。
しかし、次のような情報を使うことで、考えているように見せることができます。
距離
方向
角度
障害物
時間
状態AIは、世界をそのまま理解しているのではなく、ゲームに必要な情報だけを取り出して判断しています。
まとめ
今回のポイントは次の通りです。
isFindPlayer は勝手に決まらないAIには知覚が必要ゲームAIの視覚は画像を見ることではない距離・向き・壁で「見えている」を作る見えているかどうかをフラグにして、行動分岐に使う最後の確認問題
問1
次のフラグは何を表しているか説明しなさい。
bool isFindPlayer;問2
距離だけでプレイヤーを発見するAIには、どんな問題がありますか。
2つ書きなさい。
問3
モンスターの前方にプレイヤーがいるかを調べるために使う計算は何ですか。
問4
壁の向こうにいるプレイヤーを見えないようにするには、どのような判定が必要ですか。
問5
次の処理の意味を説明しなさい。
isFindPlayer = CheckCanSeePlayer();発展課題
次のようなモンスターを考えなさい。
普段は巡回している。
プレイヤーが視界に入ったら追いかける。
壁の向こうに逃げられたら見失う。
見失ったら、その場で3秒間探す。
それでも見つからなければ巡回に戻る。このモンスターに必要なフラグを考えなさい。
例:
bool isFindPlayer;
bool isLostPlayer;
bool isSearching;
bool isPatrol;さらに、どのような順番で if文 を書けばよいか考えなさい。
No Comments