前回から引き続きAIの実装を行っております。
アルゴリズムの話がメインになっちゃうので、今回も特に見栄えある内容ではありませんがご了承を・・・
Contents
ユニットの行動実装
こんな感じに実装しています
- マスターデータに行動パターンごとの判定用パラメータ追加
- 索敵範囲内にあるオブジェクトを取得
- オブジェクトに対する行動パターンの優先度から最優先事項を決める(ファジー理論)
- 最優先事項に対して移動するか攻撃するか決定
- 移動するなら移動先、攻撃するなら攻撃先を決定
- 移動する場合は移動先までの経路をA*で求めて、初めの一歩の移動先を決める
- 求められた行動をコルーチンで処理する
行動パターン
行動パターンは、相手の種類とどういう動作かを指定するようにしました。
相手の種類
- 障害物
- 味方ユニット
- 敵ユニット
- ・・・
動作パターン
- 近づく
- 逃げる
- 攻撃する
- ・・・
このように定義しておくことで、とある味方なら近づくパターンとか、とある敵なら逃げるって感じに組めます。
この、「とある~」の部分が後述のファジー理論によって決定します。
ファジー理論
「お腹すいたからご飯食べよう」って時、どれくらいお腹すいたかをわざわざ胃の残留量10%以下だったら~なんて決めないですよね
普通は、だいたいこれくらいお腹すいたら何か食べようってくらい緩く行動決定するものです。
しかも、同時に「ちょっとトイレ行きたい」場合もあります。トイレ我慢して先に食べるのか、先にトイレ済ませておくか。
あるいは、
「おかえりなさいあなた。ご飯にする? それともお風呂にする? それとも・・・」
って時どれを選ぶかは、その人のパラメータ次第です。
そんな感じの緩い行動決定のさせ方をファジー理論というものを用いて行っていきます。
ゲームで言えば、そこそこ近いとか、そこそこ死にそうとか。
たとえば、死にそうな敵に攻撃したい、追い討ちかける系のゲスキャラがいたとして、
ゲスいので、自分のHPが低かったら敵から逃げつつ近くの障害物に身を潜めたりHPの高い味方に近づこうとするとか。ゲスい!
その味方が実は一匹狼みたいなタイプで、他の味方とつい距離をとっちゃうようなキャラだったら・・・
AI同士の行動の連鎖が産まれますね。
ファジー理論の要点は、
- 判定パラメータ(自分との距離、方向、自分の体力、相手の体力など)を決める
- 各パラメータに対して、横軸がパラメータの数値、縦軸が優先度のカーブデータを持たせる
- 1つの行動パターンに対して、複数の判定パラメータを設定する
- 1行動パターンごとに、判定パラメータをチェックしてもっとも優先度が低い数値を求める
- 全行動パターン内でもっとも優先度の高い行動を求める
一番面倒なのはカーブの設定なのですが、Unityには便利なAnimationCurveというものがあります。
これ使っておくだけでInspectorから設定できるので、あとは判定パラメータの数値をEvaluateメソッドに渡すだけで優先度が求められます。
AIの調整
なんでこのタイミングでそれをやるかなーチミは・・・
って時は、ひたすら行動パターンの調整をしていきます。
ひとつは、カーブのみで調整可能なタイプ。
たとえば、これくらい遠いなら無視して欲しいってときは、その距離なら優先度0にしてしまえばOK。
他の判定内容のカーブと比べつつ、このタイミングなら優先度高いと思う場所が一番盛り上がるように設定していきます。
あとは、判定パラメータを増やすタイプ。
一番近い障害物がターゲットになって欲しいけど、後ろにあるものは優先度低いよなーってとき。
方向のパラメータも判定することで、後ろのオブジェクトは近くても優先度が低くできます。
最後に、判定パラメータの種類を増やすタイプ。
敵が後ろ向いていたら~とか。
判定基準が足りていない場合もあるので、どの時にどの行動を選んで欲しいか仕様を明確にするのが一番です。
指示の優先度
まだ指示する処理ができていませんが、仕様だけ決めておきます。
指示もひとつの行動パターンなので、指示用行動パターンを別途用意しておきます。
ただし、指示の場合は、プレイヤーにとっては距離など関係なく優先して欲しいはずなので、
判定基準にちょっと小細工を入れる予定。
簡単に言うと、キャラっていうのは、ちょっと言うこと聞かないくらいが一番かわいいのですよ!
忘れっぽいとか、友好d・・・ゲフンゲフン
A*経路探索
ターゲットが決まって、最終的な移動先がわかったら、A*アルゴリズムを用いて経路を決定します。
今回のゲームは、最悪到達不可能な場所がターゲットになるちょっとおバカなAIなので、
自分の位置から決められたステップ数の範囲内でもっともターゲットに近い位置まで移動するようにしました。
A*の要点は、
- 各マスに対して、Nodeというクラスを用意する
・一歩前のNodeの参照(nullだったら開始位置)
・一歩前のNodeからの移動方向(初期位置は現在の向き)
・開始位置からどれくらいかかるか(コスト)で、歩数
・一歩前からの移動方向のコスト(前なら0、横なら1、後ろなら2)
・目標位置からどれくらい近いか(ヒューリスティック)で、xの距離+yの距離(障害物無視の最短移動経路)
・マスの位置
を格納する - 探索前のNodeを入れておくリストを用意する(OpenNodes)
- 全てのNodeを入れておくリストを用意する(Nodes)
Dictionary<マス座標,Node>で検索しやすくしておくといい - 開始位置をNodes、OpenNodesに登録
- ここからループ
- OpenNodesがないならループ終了
- OpenNodesからもっともスコア(コスト*3+方向コスト+ヒューリスティック)が少なかったNodeを取得する
- OpenNodesからそのNodeを削除する。
- そのNodeが目標位置ならループ終了(探索したNodeとして保持しておく)
- そのNodeのコストが指定位置以上なら無視
行けない場合にマップ全域調べないようにするため。
複雑な迷路でも必ず行って欲しいなら無視しちゃダメ - そのNodeの前後左右にある移動できるマス(もちろんNodesに登録されていないかも確認する)をNodes,OpenNodesに登録する
- ここまでループ
- 探索したNodeがないなら、一番近いNode(ヒューリスティックが最低)のうち、もっとも早く到達できる場所(コストが最低)を、探索したNodeとする
- 探索したNodeから一歩前のNodeをたどって、開始地点から1歩先の位置を返す
- 移動できないなら開始位置を返す
やってることは結構単純なんですが、文章にすると長くて複雑そうに見えるのが困りモノですね。
逃げる場合の移動先
これは簡単。
A*の計算を逆にしていくだけ。
- 方向コストを、前なら2、横なら1、後ろなら0
- スコアが多いほうに展開
- ヒューリスティックがもっとも多く、コストがもっとも少ない位置が最終的な移動先
頭悪くていいなら現在位置の前後左右のうち遠いほうに行くだけでもOKです。
まとめ
まだ移動しか組めてない・・・
今週終わる・・・
うへぇ・・・
でもがんばる!
Unityのアセット販売中!
ステートマシンの状態遷移やパラメータはエディタで編集でき、
ゲームロジックに依存するステートの挙動はスクリプトで記述可能なエディタ拡張。
RPGツクールVXやWOLF RPGエディターのオートタイルに準拠したエディタ拡張。