AIで色々悩んでみた:EQS編
EQSは位置情報またはアクタ情報(これらをItemと呼ぶ)を生成し、それらに重み付けを行って最も数値の高いItemの情報をBlackboardに書き込む、ということを行う機能です。書いただけだとなんじゃそら、と思うかも知れませんが…
始めに言っておくことがあります。
EQSは決して難しくありません。
実験的な機能ではありますが、視覚化もしやすいですし、機能の理解自体はそれほど難しくないです。Materialノードなどで数列の扱い方がなんとなくでも分かっていれば、拍子抜けするぐらい簡単に、しかし複雑な挙動をAIにさせることができます。
まずEQSを扱うための下準備をします。EditorPreferences>General>Experimental>AI>EnvironmentQueryingSystemにチェックを入れます。
◎EQSTestingPawn
EQSTestingPawnというPawnをBlueprintから作成してViewportに設置してみましょう。
どうでしょう?まだ何も起こりませんね。これはEQSを視覚的に分かりやすくするためだけのものなのでそれで問題ありません。ゲームにも一切影響は与えないPawnです。次に進みます。
◎EQS
ではEQSを作成しましょう。ContentBrowserを右クリックしてメニューを開き、CreateAdvancedAsset>ArtificialIntelligenceの中身を見るとEnvironmentQueryという項目があります。これを選択することでEQSを作成することができます。
EQSの中身は初期ではこのようになっています。BehaviorTreeのエディタとかなり似ていますね。
◎Generator
見ての通りRootからノードを繋ぐ形式なわけですが、繋げるのはGeneratorsというカテゴリのノードのみとなっています。
これらはItem(位置情報またはアクタ情報)を生成する機能です。試しにですが、個人的には最も分かりやすいPoints:GridをViewport上で視覚化してみましょう。
追加したところです。Detailsは今はそのままで大丈夫です。これを先程Viewport上に配置したEQSTestingPawnのDetails>EQS>QueryTempleteに追加します。
さてViewportを見てみましょう。
EQSTestingPawnを中心に沢山の水色Sphereが生成されています。これがItemsが視覚化されたものです。基本的にPoints:~という名前のGeneratorは位置情報を生成します。それぞれの違いは生成の仕方です。紹介していきます。
・Points:Grid
中心からGridHalfSize*2の大きさの辺を持つ正方形内に、SpaceBetween間隔でグリッド状にItemsを生成します。
・Points:Circle
放射状にItemsを生成します。障害物に当たるとそこで止まります。生成間隔はSpaceBetweenで間隔指定かNumberOfPointsで数指定かPointOnCircleSpacingMethodで選択できます。またArcAngleとArcDirectionの組み合わせで敵などに向かって扇状にItemを飛ばすこともできます。(活用方法はちょっと思いつきませんが…)
Points:Cone
X軸方向に扇状にItemsを生成します。AlignedPointsDistanceで各々の軸(扇で言う所の"骨")に生成するItemsの間隔、ConeDegreeで角度指定、AngleStepで"骨"そのものの数の指定をします。
Points:Donut
ドーナツ状にItemsを生成します。これはパラメーターは比較的分かりやすいですね。UseSpiralPatternはチェックを入れると
こうなります。
・Points:PathingGrid
基本的にはPoints:Gridと変わらないのですが、こちらはNavMeshに沿ってグリッド状に生成します。Points:Gridだと段差のある所などに生成する時、生成されなかったり歪んだ形になってしまったりするのですが、こちらでは比較的綺麗に配置されていることが分かります。ただNavModifierVolumeを使用しても生成をコントロールすることはできないようです。
・CurrentLocation
これはPoints:~という名前は付いていませんが位置情報を扱うGeneratorです。生成の中心位置を返します。それだけです。
アクタ情報を扱うGeneratorは現状一つだけです。
・ActorsOfClass
Radius内のActorの情報を取得します。取得するActorの種類はSearchedActorClassで指定できます。
・Composite
2つ以上のGeneratorを組み合わせる時に使うノードです。Generatorsパラメーターで配列を追加して使用します。
◎Test
現状ではItemsが生成されましたが、これだけでは何の意味もありません。今度はこのItemsに"重み"を付けていきます。それにはTestを使用します。これはBehaviorTreeにおけるDecoratorまたはServiceのように、Generatorにくっつけるような形で機能するノードです。もちろん複数個くっつけることもできます。
いくつか種類が並んでいますが全てについて書いていくと長くなるのでやめにして、ドキュメントに投げます。
EQS ノードのリファレンス:テスト | Unreal Engine ドキュメント
個々のTestについて詳しくは上で見ていただくとして、共通パラメーターで重要なものがいくつかあります。まずはTestPurposeというパラメーターです。
これにはFilterとScore、あるいはその両方を選択するようになっています。
Scoreは言わば"重み付け"です。例えば0~1の間で条件に最も合うItemを1とし、最も合わないItemを0とします。それ以外のものはその間の値で推移します。
Filterは"分類"です。こちらはもっと単純に条件に合うItemは1とし、合わないItemは0とします。
もう一つはScoreカテゴリにあるパラメーター郡です。これはTestの種類やTestPurposeパラメーターによって変更できるものが変わってくるのですが、下の画像はGeneratorはPoints:Cone、Testの種類はDistance、TestPurposeはScoreOnlyで設定した時において表示されるパラメーターです。
ScoringEquationは補完の種類です。今はLinearなので発生源からの距離に対しての線形補完になっています。
ScoringFactorは複数のTest間で重みの調整を行う時に弄ることになります。
では具体的にViewport上でこれはどう見えるのでしょうか?それがこちらです。
見ての通り発生源からの距離に応じて0~1の数値が付与されているのが分かります。最も数値が高いのが扇状の外縁部なので、この場合EQSを呼び出すと外縁のItemsがBlackboardに書き込まれます。
Points:PathingGrid(Generator)とTrace(Test)とFilterOnly(TestPurpose)の組み合わせではこうなります。
赤くなっている部分が"使用可能な"Itemsです。青いItemsはこれ以降強制的に0になり無視されます。
EQSTestingPawnからLineTraceがヒットする部分は青く、ヒットしない部分は赤くなっているのが分かります。これを反転させるにはBoolMatchパラメーターのチェックを外します。
複数のTestを組み合わせると、例えば「プレイヤーから逃げる敵」「プレイヤーから隠れる敵」などを実装できます。もっと工夫すると「プレイヤーが見えている時にプレイヤーから逃げる敵」や「自分から見えている範囲内でプレイヤーから隠れる敵」なども作ることができます。
◎EnvironmentQueryContext
ここまで見てきた中で例えばPoints:GridのGenerateAroundというパラメーターに注目してみましょう。これはその名の通り生成の中心点を決めるパラメーターですが、なんだか見慣れないリストが出てきます。
これはEQS内でのこれらがEnvironmentQueryContext(以下EQC)というシステムによって返された位置あるいはActorの情報で指定されることになっているからです。このパラメーターではそのEQCの種類を指定しています。ここでは2つプリセットとしてEQSが登録されています。EnvQueryContext_QuerierはこのEQSを呼び出した本体の情報を返します。今まで何の気無しにItemsを生成していましたが、常にEQSTestingPawnを中心としてそれらが生成されていたのは、生成位置を決めるパラメーターにこのEQCが初期設定されていたからです。EnvQueryContext_Itemはそのまま各々のItemの情報を返します。しかしこれ以外にもEQCは自作拡張が容易にできます。ではこのEQCをどのように扱うのかを見ていきましょう。
まずはEnvQueryContext_BlueprintBaseというObjectを作成します。
作成した時点で既にEQCのリストには加えられています。早速中身を見てみましょう。
EventGraphには何もありません。このObjectはOverrideすることで活用します。OverrideできるFunctionは以下の通りです。
上からActor型配列、Vector型配列、Actor型単体変数、Vector型単体変数をそれぞれ返すためのFunctionです。いずれもInputノードとReturnノードを内包しており、これを繋げることでEQCを表現します。
例えば、ゲームに存在する全てのActorからPoints:ConeでItemsを生成させたい時はProvideActorSetをこのようにOverrideし…
Points:ConeのCenterActorパラメーターをこのEQCに変更すればそのように動きます(重いのでおすすめはしません)。
◎BehaviorTreeからEQSを呼び出す
さていよいよBehaviorTreeからのEQSを呼び出しを実装します。これにはBehaviorTreeのRunEQSQueryというTaskを使用します。
EQSカテゴリのQueryTemplateパラメーターに作成したEQSを設定します。次にRunModeの設定ですが…まあ書いてある通りなので迷うことはないでしょう。
最後にBlackboardのKeyを設定します。これでこのノードが走ったタイミングでEQSが呼び出され、Blackboardの値が書き換わります。
無事EQSが動作します。以上です。お疲れさまでした。
EQSはEQSTestingPawnのおかげで視覚化しやすく、触った時点では簡単な印象がありましたが、文章にすると結構かかりますね。まあ一つ一つの要素の関係性は分かりやすいのでそんなに難しさは感じないのではないかと思います。
あと書いている内に気付いてしまったんですがGeneratorも自作で作れそうですね…これはもうちょっと暇な時に調べようかな?(^_^;)
ご指摘、ご質問があれば是非お願いいたします。
アセットを読み解く:BallisticsFX ※訂正あり
タダより安いものはない!!!!
皆さん!毎月無料のUE4アセット、欠かさずにゲットしてますか!?僕のような貧乏人は毎月どころか毎日、いやなんなら毎時間、毎分、毎秒、別に更新されてるわけでもないのにチェックしておりますよ!
世間ではなんとかPayが何割還元とかで大騒ぎしておりますが、こちとら10割還元です!半額で2倍お得とか言ってる量販店も反省してください!2とか3とか本当にケチ臭くて嫌だ!こっちは無限倍お得!どころかゼロ除算で数学すら超越しているいるのですよ!
…え?UE4のアセットは自分にはレベルが高すぎて参考にならない?
…うん、実は俺もそうなの…
というわけで、レベルの高い(無料の!)アセットの中身をなんとか取っ掛かりを見つけて解析、学習しつつ、ついでにそれをブログに載せてアクセス稼いでやるぜゲヘゲヘ、ということをやっていきたいと思ってます。なんかレジ横の肉入れる用のビニール袋を大量に持っていく奴みたいだな、俺。
記念すべき最初のアセットはこちら…
BallisticsFX!
BallisticsFXはCubitStudiosというスタジオが開発したフォトリアル系のエフェクト集です。主に弾丸が様々なマテリアルに着弾した際のインタラクションが集まっています。
カバー範囲がかなり広く、砂や水、金属、草、マグマなどリアル系なら大半の素材は用意されています。この被弾サンプルのMaterialだけでも結構な価値がありますね。大きめの凝ったDemoMapが付属しているのでそこでドンパチやって遊んでいるだけでも楽しいです。
早速中身を見させてもらいましょう。
インタラクションの素材そのものはParticleやMaterialで実装されております。当たった時に吹き上がる土埃、マズルフラッシュなどはもちろんParticle、弾痕はNormalMapを含んだDecalの貼り付けで実装されているようです。
・インタラクション及び跳弾の実装
ここで注目したいのがBPです。インタラクションとは言いつつ被弾素材には一切ノードが実装されていません。じゃあどうやっているのかなと見てみると、実装はほとんど弾丸側で処理されています。
どうやら弾が物質に当たると
①ProjectileがImpactFXSpawnerというActorをSpawnしてそこにHitResultを送る
②ImpactFXSpawnerがHitResultからPhysicsMaterialを取得する
③PhysicsMaterialからSurfaceType(EPhysicalSurfaceというEnum型の変数)を参照する
④参照したSurfaceTypeからGetDataTableRawNameノードを用いて
FXMaterialDataTableというcsvファイルからデータを参照する
※訂正:これは間違いで実際に使用されていたノードはGetDataTableRowという名の別のノードでした。申し訳ありませんでした。
⑤FXMaterialDataTableにはProjectileが当たった時に発生させるべきParticleや貼り付けるべきDecalなどのデータが格納されているので、それに従って処理を行う
という処理を行っているようです。一応ProjectileがデータにないSurfaceTypeのPhysicsMaterialを持つ物質に当たったときのことも考えられており、GetDataTableRawNameの参照に失敗した場合にはDefault行のデータが参照され実行されるようになっています。
なるほど、こういう処理にすれば基本的にはPhysicsMaterial(にくっついているSurfaceType)にエフェクトが依存するので、素材側に余計な設定をせずに済むのですね。
ちょっと補足としてSurfaceTypeの型であるEPhysicalSurfaceですが、DefaultでPhysicsMaterialに付属している変数で、これの設定はProjectSetting>Physics>PhysicalSurfaceで行います。
一応62個まで設定できるようです。
ちなみにこのアセットでは一定確率で跳弾するRicochet(もしくはBouce)という機能がありますが、これも実装は似たような感じです。
こちらの場合はProjectileそのものがSurfaceTypeに従ってMaterialProjectileDataTableというcsvファイルを参照します。そこには素材による跳弾率や跳弾した際の速度変化率などが格納されており、これをもとに跳弾の計算をします。
・WorldSupport
このアセットでは支柱や扉などを撃って一定量ダメージが蓄積されると破壊されてバラバラになります。もちろんこれはDestructibleMeshComponentを用いて実装されているわけですが、最初なぜ一部だけが崩落したような状態になるのか良く分かりませんでした。自分が実験していた時はそういう破壊のされ方はしなかったからです。中身を見てみるとDestructibleMeshのパラメーターであるWorldSupportにチェックが入っているようです。
どうやら…
こういうことのようです。
AIで色々悩んでみた:AI知覚編
AIの知覚とそれに伴う様々な要素のお話。
当然のことですが、AIはAIにとっての敵(プレイヤー)やその他障害物を知覚する要素というものが備わっています。BehaviorTreeでもそれらの要素によって状況判断をしたり、Blackboardに記憶してそれに応じたアクションを起こしたりします。
「極め本」ではLineTraceを使ってこの情報を取得していました。これはこれで一つの道ではありますが、普通にやると直線でしか情報を取得できないですし、じゃあこれを発展させて"視界"のようなものを作ろうとなるとかなり専門的な知識が必要になります。(最初期にそうしようとして色々調べてみたらQuaternionが出てきて諦めた)
そこでUE4にはこれらの知覚をサポートするコンポーネントがいくつか備わっています。それらを一つずつ見ていきましょう。
・PawnSensing
まずはこれです。やや拡張性に欠け、ちょっと使いづらい面もありますが、スタンダードな視覚と聴覚をサポートしてくれます。
Characterにアタッチしたところです。見やすいようにパラメーターは少しいじっています。緑色のコーン型のガイドが視覚、青のSphere型が聴覚、黄色が「間に何も障害物がない場合の」聴覚範囲(LOSHearingThreshold)です。
感知にはIntervalがあるので注意しましょう。まあHearingMaxSoundAgeと一緒に調整すればそんなに気にならないでしょうが。
名前の通り視覚では基本的にPawnを感知します。OnlySensePlayersにチェックを入れておけばプレイヤーであるPawnだけを感知します。聴覚の方はちょっとだけ特殊で感知される側にPawnNoiseEmitterというコンポーネントが必要です。
このコンポーネントからMakeNoise関数を呼び出して音を出します。ちなみにAIカテゴリに"普通の"MakeNoise関数もありますがこちらでは駄目です。
上の画像だと一番右のノードだけがPawnSensingで知覚できる音を発生させられます。
さて、PawnSensingが視覚から情報を感知した場合OnSeePawnEventが、聴覚の場合はOnHearNoiseEventが呼び出されます。
PawnSensingの利点は実装が比較的簡単なことと、最も重要なのはViewportにおいてガイドが出るので視界などのイメージがとても分かりやすい、ということです。なので単純なAIでしたらこちらでも問題無いと思います。
・AIPerception
さて、こちらはガイドがありません。それどころか初期状態では何も感知できません。その代わり拡張が可能なコンポーネントです。そしてこちらは情報が"入った時"以外にも"失った時"も検知することができます。
いきなり注意なのですが、このコンポーネントは基本的にはAIControllerに付けます。実はPawn自体に付けても動くは動くのですが、一部の機能が使えなくなるようです。
AIPerceptionの各感知は配列になっており、SensesConfigパラメーターから感知を追加していく形で実装します。こういった形なので例えば一つのキャラクターが複数の視野の異なる視覚を持ったり、などといった動作が容易に実現できます。
さて色々知覚の種類が並んでいますね。これについては後で説明するとして、知覚した場合、OnTargetPerceptionUpdatedEventまたはOnPerceptionUpdatedEventが呼び出されます。
情報の受け取りはAIStimulus型というStructureで行います。ノードのOutputはこのようになっています。
ちょっと注意すべきこととしてAIStimulusの情報の受け取り方にはクセがあります。例としてOnPerceptionUpdatedEventからのスタンダードなノードの組み方を載せておきます。
このようにすることで知覚された各TargetのSensesConfig配列の0~2までに入っている知覚に関する各情報を取り出すことができます。
さて次はそれぞれの知覚について紹介します。
・AI Sight config
まずは視覚から。
一覧からAI Sight configを選択すると、上のようなパラメーターが現れます。PawnSensingの視覚と違う部分を少し説明します。
LoseSightRadiusはSightRadiusに対象が入った後に、補足していた対象を見失う距離です。
DetectionByAffiliationは敵味方の区別を付ける働きがあるのですが、どうにもこの区別は現状C++からしか設定できないようです。なのでそれが分からなかったら基本全部チェックで問題ないかと。一応設定の仕方はここに載っています。
AutoSuccessRangeFromLastSeenLocation(長い!)は対象を見失った地点から指定した距離内に対象が入った場合、強制的に知覚を成功させる、という機能なのですが…うーん。恐らくですが、コーン型の視界を持っていると補足した対象に頭を飛び越えられたりした場合、すぐに見失ったりしてしまうことに対する対策としてのパラメーターなんですよね…でも見失った位置を"いつまでも"覚えているので、普通に振り切った場合でも、見失った位置に近づくと成功になって全く関係ない位置から知覚されてしまったりして…ということで上手い使い方が見つからないです。知ってる人教えて下さい。
MaxAgeは知覚したものを覚えている時間です。これを過ぎると自動的に知覚情報のSuccessfullySensedがFalseで更新されます。
・AI Hearing config
続いて聴覚です。
これに関してはPawnSesingの聴覚とそれほど変わりません。DetectionByAffiliationとMaxAgeが付いたのと、LOSが選択制になったぐらいでしょうか?ただ"音"の立て方についてはPawnNoiseEmitterを用いた方法とは少し違います。それについては後で説明します。
・AI Damage sense config
Damageを受けた相手を知覚します。あえて言うなら痛覚でしょうか?
「普通にAnyDamageで情報を受け取れば?」などと思ってはいけません。
・AI Touch
触った相手を知覚します。触覚です。
「普通にCollision判定から受け取っては駄目なの?」とか言ってはいけません。(というか検証したら普通に動かない。情報求む)
・AI Prediction sense config
相手の位置を予測します。第六感です。(嘘)
パラメーターは少ないですが、情報発信の仕方は少し特殊です。後述します。
・AI Team sense config
一応ドキュメントによると仲間が近くにいる場合にその仲間を知覚する、らしいんですが…
設定方法が全然分からない…そもそも仲間の設定もC++通じてしかできないんだし…英語ですら資料が少ない…というわけで今後の誰かの研究に期待したいです。
・AIPerceptionStimuliSource
さてさて今度はいよいよ情報の発信側です。
まず発信側はAIPerceptionStimuliSourceというコンポーネントを追加しま…ん?ちょっと待って。
AIPerceptionStimuliSourceが無くても動く?ははは、そんな馬鹿な…
<検証中…>
動くよ!
じゃあこのAIPerceptionStimuliSourceってコンポーネントは一体何なんだよ!
はい、というわけでAIPerceptionStimuliSourceは必要ありません。でも一応付けといたほうがいいと思います。
・Report
気を取り直して次です。AI Sight config以外のSenseConfigでは特定タイミングで情報を"発生"させる必要があります。これは送信側でノードを実行することで行います。それがReportノードです。
まずはAI Hearing configです。"音"を発生させるのはReportNoiseEventというノードです。
次にAI Damage configです。これは普通のApplyDamageなどでは駄目で専用のReportDamageEventノードを実行することになります。
…そしてなぜかパラメーターに何も繋げられない謎のノードがありますが、もうあんまり気にしないことにします。なにか用途があるんだろうか?
・AI Prediction configの情報受け取り
AI Prediction configは他とは違って"予測"を扱うものなので、情報の受け取り方もちょっと変わっています。まずAIController側にRequestPawnPredictionEventまたはRequestControllerPredictionEventを用意します。
先程説明したAIStimulusからの情報の受け取りにこれらのノードを追加します。RequestorはAIControlerを持っているPawn、PredictedActorは知りたいActor、PredictionTimeは何秒後の位置を予測するか、です。
実装の一例です。これでSenseConfigの配列0番目にAI Prediction configを指定していた場合、BlackboardのPredictedLocationに対象の1秒後の予測位置が書き込まれます。(まあ普通はCastして対象を指定するとは思いますが…)上ではRequestPawnPredictionEventを使っていますが、RequestControllerPredictionEventの場合はGetControlledPawnをSelfノードにするだけです。Location以外の情報には意味がないことも注意しましょう。
さて、AI知覚の話はこれで終わりです。え?AI Team configやAI Touchはどうなってるんだって?
知らんよ。
そもそもExperimentalな機能過ぎて、現状正常に動くのかすらよく分からないし…というかAI知覚の大部分は普通にLineTraceやCollisionでやろうと思えば実装できるものばかりなんですよね(多分内部的にも同じだろうし)。そういう知識がどうしても足りない人のために用意されたものなんですが、AI知覚自体の設定が面倒なので、視野角の設定が難しい視覚やPredictionなど変則的なもの以外は利用価値が微妙なことになっています。
なのでTouchやらTeamやらそういう機能をつけたい場合は他のアプローチから探しましょう。ぶっちゃけそれで充分です。
それにしてもやっぱり一番長くなったなあ、でもあとはEQSとDebuggerだけだ。
何かご指摘、ご質問等ありましたらぜひお願いいたします。
AIで色々悩んでみた:NavMesh編
ではNavMeshです。
NavMeshはAIをもったPawnが動作と移動ができる範囲を定義します。なのでいくらBehaviorTreeでAIを組み込んでも、NavMeshがなくてはうんともすんとも言いません。
・NavMeshBoundsVolume
NavMeshの基本はNavMeshBoundsVolumeというVolumeです。Geometoryなどと同じくBrushSettingのパラメーターカテゴリで形状とXYZを決めます。あんまり複雑な形状にするメリットというのも思いつかないので基本的には初期値のBoxで十分だと思いますが…
NavMeshはNavMeshBoundsVolumeで指定された範囲に地面を這うようにして展開されます。Viewportで"P"を押すか"Show→Navigation"にチェックを入れることで、現在NavMeshの展開されている範囲を表示させることができます。ちなみに時々何かの加減でNavMeshの生成が止まってしまう場合がありますが、NavMeshBoundsVolumeを少し動かしてやることで再生成されます。
ThirdPersonTemplete全体にNavMeshBoundsVolumeを配置したところです。緑色の部分にNavMeshが生成されています。
ちなみにNavMeshは地面に沿って"繋がっている範囲"を表示してくれるので、AIとは関係無くても"ちゃんとレベルが全部繋がっているか?"の確認にも使ったりします。
NavMeshBoundsVolume(と後述のNavLinkProxyもですが)にはSupportedAgentsというパラメーターがあります。
これは通常allで固定されているのですのが、ProjectSettingのEngine->NavigationSystem->Agents->SupportedAgentsで追加することができます。
これはPawnの種類によってNavMeshを分けるという機能です。かなり設定できる項目も多いので、詳しくは
このあたりを参考にしてください。
・RecastNavMesh-Default
NavMeshBoundsVolumeを生成するとRecastNavMesh-DefaultというActorが自動的に配置されます。これはNavMeshをViewportに表示させる際、どのように表示させるかや、生成の細かい設定を司るActorです。様々な設定項目があるのですがそれはドキュメントにおまかせするとして、ここでは例として上の画像の問題を解決しましょう。
上の画像を見ると、NavMeshが階段部分で途切れていることが分かります。これは階段部分がNavMeshの生成する高さに対して急すぎて、ここだけ壁のように認識されているということです。実際問題としてこのマップでAIを設定してもPawnは階段の上には登らず、入口付近で引き返してしまいます。
ではどうするかというと、要はNavMeshの生成する高さをもう少し高くすればいいのです。RecastNavMesh-DefaultのGenerationカテゴリ->CellHeightを調整します。
初期値では10ですが、これを30に調整してやると…
このように綺麗に繋げることができます。
・NavLinkProxy
先程、初期設定においてThirdPersonTempleteの階段部分は"壁"のように認識されているからAIは通れない、という話をしましたが、では実際に壁になっていたらどうでしょう?
例えば人間が操るキャラクターであれば途中に崖があっても飛び降りることができます。しかしAIはNavMeshが途切れているので飛び降りることができません。そこは"通り抜けられない場所"と認識されているからです。では先程のようにCellHeightで高さをもの凄く上げてみたらどうでしょう?今度はNavMeshがPawnに引っかからなくなるためAI自体が動作しません。実はMaxStepHeightというRecastNavMesh-Defaultのパラメーターを上げると繋げること自体はできたりします。(物凄く汚いNavMeshになりますが…)そうすると崖から飛び降りることはできるかも知れません。しかし別の問題が発生します。
AIにとってNavMeshが繋がっている場所は"通り抜けられる場所"です。しかし"崖"というものは下方向から見れば"壁"であるため、飛び降りることはできても通り抜けることはできません。崖の下にいるAIは登れるはずもない壁に対して延々体当たりをし続けます。彼にとってそこは"通り抜けられるはずの場所"だからです。
こういった問題を解決するのがNavLinkProxyというActorです。これはふたつの地点を設定し、そこに限定して擬似的にNavMeshを繋げるという動作をします。
NavLinkProxyを貼ったところです。判定の大きさや位置、数も設定することができます。これでRightとLeftの二点間だけは擬似的にNavMeshが繋がっているので、AIはここを通り抜けることができます。しかし、これだけではLeft側からRight側へも繋がっていると認識されてしまうため、崖の下側からも登ろうとしてきてしまいます。
これを解決するにはDetailのSmartLinkカテゴリ->LinkDirectionを設定する事が必要です。
この例の場合はRightToLeftを選択することで、崖の下側からPawnが登ろうとしてきてしまう問題を解決することができます。
ちなみになのですが、例えば「崖の下に来たらジャンプで登りたい」などということをしたいなと思った場合にはEventReceiveSamartLinkReachedというNavLinkProxyBP内のEventから実装することができるようです。
・NavModifierVolumes
NavMeshは通常Collisionによって遮蔽されます。これで問題ない場合もあるのですが、マグマや毒沼などCollisionがなくても通り抜けられない設定にしたいことがあります。その場合出てくるのがNavModifierVolumesです。これはGeometryのsubtractive設定のようにNavMeshをくり抜いたり、その他特殊な設定をするためのVolumeです。
ということで取り敢えずNavMeshの中にこのVolumeを置いてみましょう。
はい。ということでこういう感じのものです。とても分かりやすいですね。
このVolumeにはいくつか種類があり、AreaClassというパラメーターで選択します。
デフォルトの設定はNavArea_Nullです。これはNavMeshを無効化してくり抜いたような形を作り出します。
NoneとNavArea_DefaultはどちらもNavMeshに対して何もしません。
NavArea_LowHeightは情報が少ないのですが…Nullと基本的には変わらないような…一応NavMesh自体は残るNullという感じですかね?必要な時あるのかな?NavLinkProxyの方にもこれ系のパラメーターがいくつかあるのでそれとの兼ね合いかも?違うかな?
NavArea_Obstacleはちょっと特殊でAIに「なるべくなら通りたくない場所」を設定します。要はAIがそこを通るための"コスト"が増えます。ふたつ重ねると更に通りにくくなるためある程度制御可能です。
以上ですが、うーむ…NavMeshはある程度短くなると思ったんですが、なんか今までで一番長いような…
これからやる知覚系が多分一番長くなるんですがね~。
AIで色々悩んでみた:Blackboard編
さてお次はBlackboardです。
BlackboardはAIの「記憶容量」を司るComponentであり、個々のAIがそれぞれ違ったBlackboardを持っています。人間がひとりひとり違った記憶を持っているのと同様です。
ただし人間とは違い、「何を記憶するか」は予め決めておくことが必要です。要は変数(記憶)の「型」を決める必要があります。BlackboardのBPの中身を見ていくと、Structureに若干似ていることが分かります。
左側のKeysカテゴリの下にいくつか変数が並んでいますが、これらが記憶できる領域となっています。
BlackboardはBlackboardAssetというBehaviorTree側のパラメーターによってBehaviorTreeに関連付けられています。RunBehaviorTree()を呼ぶと、基本的にはBlackboardAssetによって関連付けられたBlackboardが呼び出し側にComponentとして追加されます。
ただこれは絶対というわけではなくUseBlackboard()呼び出すことにより全く関連付けされていないBlackboardを使うことも可能です。…まあBehaviorTreeに関連付けられていないBlackboardを使いたい、なんて状況あまりないと思いますが。
Blackboard内のデータの書き換え、読み取りはBehaviorTree側とPawn、Controller側で別になっています。まずBehaviorTree側(つまりTask、Decolator、Serviceのいずれか)からはBP内からSetBlackboardValue()を使って書き換え、GetBlackboardValue()を使って読み取ります。ちなみにClearBlackboardValue()という関数を使ってBlackboard内のデータを初期化することも可能です。
さて、上の画像ではKeyという入力ピンが見えています。これはBlackboardKeySelector型というStructureのピンでこの型がBlackboard内の変数とこれらの関数をつなぐ橋渡しをします。この変数をBP内で用意し、EditableにしておくことでBehaviorTree内から関連付けられているBlackboardの変数を指定することができます。
お次はPawn及びController側からの操作です。こちらはSetValueAs???()とGetValueAs???()を使用します。
Blackboardの指定はGetBlackboard()やUseBlackboard()などからComponentを取得して行います。そしてKeyの指定はNameと呼ばれる文字列に変数名を入力します。つまり由緒正しい文字列指定です。変数化してもいいし、上の画像のようにMakeLiteralで直接書き込んでも問題ありません。
さて、Blackboardの操作には注意点があります。それはBlackboardEditor、BehaviorTreeEditor、その他のBPなど画面を行ったり来たりする機会が多い上に、文字列指定はもちろん、BlackboardKeySelectorも間違ったデータを指定していたとしてもエラーが基本的には出ない、ということです。
実際入力ミスの多発地帯で、なんか妙な動きをするなと思ったら入力し忘れだったり、指定ミスだったりすることがしょっちゅうあります。AI全般に言えることですが、入力ミスの可能性は常に頭に入れておきましょう。
ということでBlackboardについてはこんな感じです。次はNavMeshにするかぁ…画像を用意するのが一番キツい…orz
なにか間違い、ご指摘、ご質問等があれば是非お願いいたします。
AIで色々悩んでみた:BehaviorTree編
ここのところAIを中心に学習していたので復習と備忘録を兼ねて記事にしてみる。
まずはUE4のAIシステム全体の僕的な見取り図をどーん。
いい加減に書いた図なので細かいことは気にしないでください。
一見複雑にも見えますが、まずは一番上の方を見ていきましょう。PlayerPawnBPをPlayerControllerBPが操っています。更にそれを操作しているのはコントローラーから入力であり、その向こうには人間の脳があります。これがプレイヤーキャラクターの動く仕組みです。簡単ですね。
ControllerまではAIもそんなに変わりません。EnemyPawnBPをAIControllerBPが操っています。しかしその後にコントローラーからの入力はなく、人間の脳をシミュレートするためのシステムがあります。そのためAIControllerBPはPlayerControllerBPといくつか違いがあります。
・BehaviorTree
まずはRun_Behavior_TreeノードによるBehaviorTreeとの関連付けが追加されています。
これはAIの挙動を一種の樹形図にして示したもので人間の思考回路にあたります。詳しくは
ここなどを参考にしてください。
BehaviorTreeを組む時に気をつけるべきは必ず「優先度の高い行動を左側に設置する」ことです。後でDecoratorなどにより、このBehaviorTreeの流れをある程度無視するような処理が出てきますが、それらも優先度の高い行動が左側に設置されていることが大前提になっています。
条件式を弄って無理やり優先度の高い行動を右側に持ってくる、ということもできなくはないですが、フローは分かりにくくなるしちょっと行動を追加するだけで不安定になっていいことがありません。
・Composites
さてBehaviorTreeは基本的にRootノードから始まり、SequenceとSelectorというノードによってフローを制御しています。これらはCompositeと呼ばれます。Sequenceは左側から順番に子ノードを実行し、Successが返された場合次の子ノードへ移行し、Failureが返された場合、以降の子ノードの処理をキャンセルしてFailureを親ノードに返します。
というと複雑ですが、ざっくり言えば「一連の処理」を司るノードです。人間は机の上のものを取る際、「腕を伸ばし」「手を広げて」「物を持つ」という「一連の処理」をしています。Sequenceはそれらの動きをまとめて「机の上のものを取る」という処理を作っています。
次にSelectorですがこれは名前の通り子ノードのうちいずれかを選択するノードです。処理としては左側から順番に子ノードを実行し、Failureが返された場合次の子ノードへ移行して、Successが返された場合には、以降の子ノードの処理をキャンセルしてSuccessを親ノードに返します。
こちらもざっくりと言えば「意思選択」を司るノードといえるでしょう。残っている選択肢の中で常に優先度の高い行動を成功するまで続ける、それがこのノードの処理です。
さて、あくまで今紹介したこの基本機能において、という前提ですが実はSequenceとSelectorはSuccessとFailureが入れ替わっただけで全く同じ動作をしています。しかし、じゃあ入れ替えて組んでやろう、などという妙な考えは起こさないほうがいいです。Decoratorなど他のノードとの兼ね合いにおいて問題が発生します。
しかし人間にとっては全く思考を挟まない「一連の処理」と思考によって決定している(と思っている?)「意思決定」がプログラム上では殆ど同じ処理で表現されている、というのは大変興味深いですね。更に踏み込んで言えばBooleanにおいてはSequenceは「AND」、Failureは「OR」を表現しているものです。
・Task
それはともかくとして、これらCompositeは所詮プロセスに過ぎません。いくら繋げてもPawnやControllerは何も処理をしません。なので実際に行う処理を書くノードが必要になります。それがTaskです。
TaskはObjectの一種です。BehaviorTreeでは一番下位の”葉”ノードとして振る舞います。ControllerやPawnへのアクセスを持っており、それを通じて行う処理そのものと、処理が終わった際にSuccessとFailure、どちらを返すのかを記述します。
上の画像ではExecutionPinを上に接続すればSuccessを返し、下に接続すればFailureを返します。もちろん条件式で分岐させることも自由です。EventもTick処理やAbortされた時の処理など複数用意されています。
・DecoratorとService
DecoratorとServiceはいずれもCompositeにくっつける形で実装するノードです。これらのノードはCompositeやTaskと違いSuccessやFailureを返すことはありません。見た目が大分違うので分かりにくいですが、DecoratorもServiceも基本的にはTaskの亜種という認識の方が分かりやすいと思います。
ちなみに一番上の見取り図ではTaskとPawn、Controllerは繋がっていますが、DecoratorとServiceは繋がっていません。これは視認性が落ちるために省略したもので、実際にはDecoratorもServiceもPawn、Controllerにアクセス可能です。
Decoratorは基本的には条件式を伴ったTaskと同じで、条件分岐を司ります。ここまではTaskと一緒ですが、このノードは基本的にObserverAbortsというEnumパラメーターを持ちます。要素が上位ノードによって変化し、Sequenceの場合はNoneとSelf、Selectorの場合には更にLowerPriorityとBothが追加されます。
まずNoneの場合は実行タイミングに処理される普通の条件式として機能します。条件を満たしていれば下位ノードが実行され、満たしていなければ実行されません。他の場合についてですが、選択するとそれぞれSelfは自分の下位ノード、LowerPriorityは自分の下位ノードよりさらに優先順位の低いノード、Bothの場合はその両方が青く光ります。
この青く光った領域においてDecoratorは条件式の”監視”を行います。監視領域において監視している条件に変化が起こった場合、即座にDecoratorは処理を”奪って”条件式を判定しなおします。もし条件式を満たした場合にはやはり下位ノードが実行されます。
BehaviorTreeの場合、Taskのみだと条件式はSelectorが実行されたタイミングでしか判定できません。それを解決するためにこのような機能が備わっています。これにより優先順位の低い行動を行っている際にある条件が加わった場合、いつでも優先順位の高い行動に移行する、といった動作が可能になります。
Serviceは自らの下位ノード実行中に一定時間で定期的に呼び出されるノードです。ブラックボードやPawn、Controllerの各種変数の書き換えが主な役目です。Decoratorと比較すると簡単だと思いますが、独自の要素としてIntervalとRandomDeviationがあります。その名の通りIntervalは呼び出される定期の時間、RandomDeviationはランダムにそこに加算される数値を示しています。
はじめはAI関連全てを一括で書く予定だったのですが、長くなりすぎ…orz
流石にこのペースでは冗長すぎるので、何回かに分割して書くことにしました。
間違いなどご指摘等ありましたらぜひお願いいたします。