e.blog

主にUnity/UE周りのことについてまとめていきます

ゴール駆動型エージェントの実装(概念編)

概要

Unityだけに関わらず、ゲームやそれに類するコンテンツを制作する際に必要となるのが「AI」の存在です。
格闘ゲームやシミュレーションの対戦相手のような明確な相手ではなくとも、例えばちょっとした敵が出現するコンテンツなどでも、AIで操作されたキャラクターがいるとよりリアリティが増します。

特に、VRコンテンツの場合は没入感やリアリティがとても重要な要素になってきます。
敵が単調な動きだけをしていたらその時点で現実に引き戻されるし、コンテンツ自体が単調になりかねません。

ということで、今回はタイトルの通り、ゴール駆動型エージェントによるAI実装について書きたいと思います。

本記事は「実例で学ぶゲームAIプログラミング」を読んだ上で、それを参考に実装したものをベースに書いています。

※ ちなみに、Cygames Engineers' Blogの「ゲームAI – 基礎編(2) – 『はじめてのエージェントベースアーキテクチャ』」という記事もとてもよかったのでここで紹介しておきます。

※ 今回の記事を書くに当たって、ごく簡単なUnityのサンプルプロジェクトを作成しました。Githubで公開しているので、動作サンプルを見たい方はこちらを見てください。(ただし、学習目的で作っており、作り方がよくない箇所や、解釈が間違っている部分もあるかもしれません)

全部を一度に書くととても長くなってしまいそうなので、概念と実装を分けて書きたいと思います。
今回は「概念」を書きたいと思います。

動作イメージ


(AIらしさがないですが・・w 実際に実行するとアイテムの収集や敵への攻撃が自動で選択されていきます)

考え方

考え方の大事なポイントを列挙すると、

1. AIの行動ルールを「ゴール」として定義する
2. ゴールの「選択」を行う
3. ゴールの選択は「欲求」や「状況」を織り交ぜてファジーに判断する

この3点です。

ひとつずつ概要を書いていきましょう。

AIの行動ルールを「ゴール」として定義する

ゴール駆動型なので「ゴール」が単位になるのは自然な流れですね。
ちなみに「エージェント」はここでは「AI」です。

行動ルールを「ゴール」として定義するというのは、具体的に言うと以下のようなイメージです。

まず、シチュエーションとして、

徘徊しているゾンビがプレイヤーを見つけて攻撃を仕掛けてくる

というシーンを想像してみてください。

最初は当てもなく、(空腹を満たすために)色々なところをうろついていると思います。
そしてプレイヤーが視界(センサー)に入ると、プレイヤーを捕食しようと襲いかかります。(餌だ!)

さてここで、プレイヤーが視界に入った時点では当然、まだプレイヤーまで距離があります。
つまり、襲いかかるにはプレイヤーを攻撃できる場所まで移動する必要がある、ということです。
なので、これを行動として分解すると、

1. プレイヤーに攻撃できる場所まで移動する
2. プレイヤーを攻撃する
3. プレイヤーが死んだら捕食する

という「行動」が必要になります。
この3つの「行動」を満たした時、最終目的である「空腹を満たす」というゴールが達成できるという具合です。
つまりこれは、「空腹を満たす」というゴールを達成するのに3つのアクションが必要、と見ることもできます。

これがAIの行動ルールをゴールとして定義することのイメージです。
実際に読んでみるとなんだか当たり前のように感じませんか?

それは人がなにか物を考え、決定するときはこうした「ゴール」から逆算して行動を決定しているからに他なりません。
つまり、自分たちが行動を決定するやり方に近いから「当たり前に感じる」わけですね。

今回はこれを実際にコードに落とし、AIとしてキャラクターが動き出すまでを書いていきたいと思います。

ゴールの「選択」を行う

行動ルールをゴールとして定義するイメージはわいたでしょうか。

確かに考え方は自分たち人間が行うことに近いのでイメージしやすいと思いますが、状況に応じて無数にあるゴールからどれを選択したらいいのでしょうか。
答えは、それこそ無数にあるでしょう。この「ゴールの決定」自体がまさにAIの頭の良さにもつながります。
なので様々なアルゴリズムやロジックがあることと思います。

ですが、今回は比較的シンプルな方法でこれを実装しようと思います。
(というか、そもそもAIは冒頭で紹介した書籍を読んで学んだ範囲を書いているので、それ以上のことは書けませんw)

ゴールはサブゴールの集合体

今回実装した方法は、ゴールの決定を行うゴールを設定する、です。
なんのこっちゃと思うかもしれませんが、上で書いた通り、ゴールは複数の行動を集めたものと考えることができます。 そして、感の言い方なら気づいているかもしれませんが、ゴールは入れ子にすることができます。

どういうことかというと、一見、単純な行動に見えるものもよくよく見てみれば複数のゴールの集合なのです。

例えば、(AIではなく人が)目の前にあるバナナを食べる、というシーンを考えてみてください。
バナナを食べる、というゴールだとしても。
これを際限なく分解することが可能です。やってみましょう。

1. バナナを見る
2. バナナまで手を伸ばす
3. バナナを手に取る
4. バナナの皮をむく
5. バナナを口に運ぶ
6. バナナを咀嚼する
7. バナナを飲み込む
....

という具合です。
当然これはやりすぎなくらいに分解していますが、やろうと思えばもっと分解することも可能ですね。
このように、ひとつのゴールは無数のサブゴールから成り立っています。

今回の記事で扱うAIは、これを「ほどよく」分解し、現実的な範囲でゴールを決定する方法です。

ゴールの選択は「欲求」や「状況」を織り交ぜてファジーに判断する

ゴールの決定についてはイメージできましたでしょうか。

さて次は「ファジー」に決定する、という部分です。
ファジー理論は、ざっくり言うと「0か1かではない曖昧な決定」を下すこと。

仮に、ぱっとは決めづらい2つの事柄があった場合、人はそれぞれの事柄について色々考え、あの場合はこうだけど、この場合はこうだから・・と悩み、どちらかがいい、と断言できる例は稀でしょう。
AIの行動決定ロジックもこうしたファジーな状況を作り出せるとより「人間らしく」なります。

具体的に言うと、普通のアプリ開発のようにif文を連ねて、もし特定の値が一定以上だったらこうする、という分岐を行った場合、行動決定がロジカルすぎて途端に「機械らしく」見えてしまいます。

擬似コードで書くと以下のような感じです。

if (power > 1.0f) 攻撃
else if (energy < 0.5f) エネルギー補給
else if (hungry < 0.2f) なにか食べる

こうしてしまうと、攻撃力が一定以上ある場合はひたすら攻撃を繰り返すAIができてしまいます。
仮に他のパラメータの値が減少したりしていても、前方のif文の分岐に入ってしまって下はまったく評価されません。

これを防ぐために、上述の「ファジー理論」や、それに近い考え方を用いて行動を決定するロジックを組みます。

クラス構成

実際に動くサンプルを見てもらうのが早いかと思いますが、今回のサンプルのために用意したクラスは以下の通りです。

Goals

  • Goal
  • compositeGoal
  • GoalSeek
  • GoalWander
  • GoalPickup
  • GoalGetItem
  • GoalAttackTarget
  • GoalAttack
  • Brain

Plans

  • PlanBase
  • PlanWander
  • PlanGetPower
  • PlanGetEnergy
  • PlanAttackTarget
  • PlanObject
  • Reward

Planner

  • PlannerBase
  • CharaPlanner

Memory

  • Memory

AIBase

  • AIBase

クラスの連携

細かいクラスは具象化されたものなので、注目してもらいたい点としてはベースクラスの区分けです。
具体的には

  • Goal
  • PlanBase
  • Planner
  • Memory
  • AIBase

の5つ。

AIBase

順番は前後しますが、まずはAIBaseから。
AIBaseはAIのベースとなるクラスです。
主に、ゴール選択を行うBrainクラスを持っていたり、各種パラメータなど、全体を統括、使役するクラスです。

GoalクラスはこのAIBaseクラスをOwnerとして保持していて、オーナーから様々なデータや状況を得て動作を決定します。
UnityではMonoBehaviourを継承し、Prefabにアタッチするもの、と考えるとイメージしやすいかと思います。

Goal

ゴールクラスは今回の趣旨にもなっている「ゴール」を示すクラスです。
具体的な「行動」はこのクラスが担っています。
ゴールクラスのリストの中にBrainクラスがありますが、ベースはGoalクラスになっていて、全体のゴールを決定するロジックを持っているやや特殊なクラスとして存在しています。
ただ、動作のライフサイクル的にはゴールの仕組みそのままの実装になっています。

Plan

プランクラス。
プランクラスはその名の通り「計画」を表すクラスです。
イメージで言うと「旅行プラン」などを想像してもらうと分かりやすいかと思います。

例えば、旅行プランで検討しなければならない項目はいくつかありますよね。
金額はいくらなのか。今まで行ったことがある場所か。その場所でどんな体験ができるのか。

そうした様々な要件を検討して、「今まで行ったことがない、おいしいものが食べられる旅行プランにしよう」という感じで決定するかと思います。
プランクラスはまさにこうした「それぞれの計画を行った場合になにが得られるか」を表しています。

例えの粒度で表せば「沖縄旅行」「北海道旅行」などになります。

リストの中にRewardクラスがありますが、これは「報酬」を意味するクラスです。
各プランにはそれぞれ「報酬」が設定されていて、この報酬の状況を元に、「Planner」クラスがプランを決定します。

「報酬」と書くと金銭的なイメージが出ていますかもしれませんが、「なにを得られるか」が報酬です。
例えば沖縄旅行なら、今までに体験したことがないスクーバダイビングができる、は大きな報酬(体験)として認識されるでしょう。
一方で、北海道でおいしいものが食べたい、となればまた違った報酬(体験)になります。

キャラクターの性格や状況(金銭面とか)に応じて決定されるプランが変わるように、こうした「報酬」と「内容」をセットにして表しているのがこの「プラン」クラスとなります。

Planner

プランナー。プランを決定する役割を担っています。
前述のプランクラスを並べて、「現在の状況」から一番いいプランを選択する、というクラスです。

「現在の状況」というのは、キャラクターの状況のことです。
例えば、ライフが減っている、攻撃ができない、パワーが足りない、などなど。
ゲームのキャラクターの状況は刻一刻と変化していきます。

その状況に応じて最適な「プラン」を選択するのがこのプランナークラスです。
もう少し具体的なイメージを書くと例えば。

最初はパワーもたくさんあり、好戦的に敵に向かって行ったとします。
しかし途中でダメージを追い、ライフが減ってピンチに陥ると状況が変化します。
それまでは積極的に攻撃を仕掛けていたキャラクターが、敵から逃げるようになり、ライフを回復するアイテムを求めて動き回る、といった「プラン」が選択されることになります。

この「プランナー」クラスをいくつか用意して差し替えることで「性格」を表すこともできそうですね。
例えば、プランを選択する中で「ライフの減少」をまったく気にかけないプランナークラスを実装した場合。

向こう見ずでとにかく敵に突進していく、というキャラクターの出来上がりです。
逆に、ライフにばかり執着するようにすれば臆病者のキャラクターになりますね。
これはまさに「性格そのもの」と言えるでしょう。

なので(作っておいてなんですが)「プランナー」より「性格」を意味するクラス名のほうがよかったかもしれませんw

Brain

ゴールクラスの中にありますが、少しだけ特殊なので個別に解説。
Brainクラスはその名の通り、「脳」を司るクラスです。

「脳」の役割は「記憶」すること。
つまりキャラクターに「記憶」の概念を与えます。

一緒にMemoryクラスの説明もしてしまいますが、メモリクラスは記憶オブジェクト、と読んでも構いません。
キャラクターが記憶にとどめておくべきものを認識し、それを蓄えます。

そして必要があればそれを取り出して適切に利用します。
人がなにかを思い出して行動をするのに似たことを実現するために用いています。

PlanObject

プランオブジェクト。これは前述の「プラン」クラスから派生したものではなく、どちらかというと前述のMemoryクラスに近い存在のものです。

Cygames Engineers' Blogの「ゲームAI – 基礎編(2) – 『はじめてのエージェントベースアーキテクチャ』」で紹介されているものとほぼ同じです。(だと思う。実装が明かされていないので詳細は分かりませんがw)

役割としては、「プランを立てるにあたって必要な情報を格納したオブジェクト」です。
例えば、前述の例えを利用すると、ライフを回復したいキャラクターは「ライフ回復アイテム」の場所を探すことになります。
その場所はどこでしょうか? まだ一度も発見していなければ辺りを探すことになりますね。

そしてもし、過去にそれを「見て」いたら。
それを思い出してその場所まで戻ろうとするのが「人らしい」行動になります。

そしてプランオブジェクトはまさにこの挙動を取らせるためのクラスになります。
MonoBehaviourをアタッチして、キャラクターのセンサーに反応させる、と言えばピンとくる人もいるのではないでしょうか。

具体的に言えば、「ライフ回復アイテム」にこれをアタッチしておいて、キャラクターに「これは回復アイテムだ」と認識させます。
認識されたオブジェクトはすぐさま記憶として蓄えられます。

そして「思考サイクル」の中でライフが減った状況になり、「ライフ回復アイテム」が必要になったタイミングでこのことを思い出し、プランナーは「ライフ回復アイテムの回収」というプランを選択します。

こうすると、キャラクターが傷ついたときに自然とライフ回復を行うように仕向けることが可能になります。

概念編まとめ

どうでしょうか。なんとなく「ゴール駆動型」のイメージがついてきたでしょうか。
最初に自分が、参考にした本や記事を読んだときは「なんてよくできた仕組みなんだ」と思いました。

このあとは実際の実装について解説していきます。
が、だいぶ長くなってしまうので、続きは実装編として書こうと思います。

後編の「実装編」を書きました。
edom18.hateblo.jp

NavMeshAgentの挙動を手動でアップデートする

概要

NavMeshを使うことで、Unityでは簡単に目的までのパス(経路)を計算することができます。

docs.unity3d.com

経路探索して移動させたいオブジェクトに「NavMeshAgent」をアタッチし、「Window > Navigation」メニューで表示されるナビゲーション設定にて、経路構築を行うことですぐに「目的地への移動」を実現させることができます。

しかし、例えばアニメーションと組み合わせたり、なにかしらの自身のロジックと組み合わせて利用したいというケースもあると思います。
(例えばAIを実装して、AIに移動を行ってもらいたいが位置や回転のアップデートは自分のタイミングで行いたい場合など)

そういう場合には勝手に移動されてしまうと問題です。
そこで、手動で位置と回転をアップデートする方法を書いておこうと思います。
(ちなみにドキュメントにも記載があるので、そちらも合わせて見てみるといいかもしれません)

docs.unity3d.com

準備

さて、基本的にはNavMeshAgentをアタッチし、目的地を設定した段階で自動的に位置の移動が開始されます。
そのため、いくつかのスクリプトを記述して自動更新を止める必要があります。

自動更新を止めるスクリプトを書く

具体的には以下のようにプロパティを設定します。

void Awake ()
{
    _agent = GetComponent<NavMeshAgent>();

    // updateを自動で行わないように設定する
    _agent.updatePosition = false;
    _agent.updateRotation = false;

    // 目的地はインスペクタなどで事前に設定しておく
    _agent.SetDestination(_target.position);
}

これで、NavMeshAgentによるゲームオブジェクトの自動更新が停止します。
(ただし、見た目の更新が停止しているだけで、内部的な経路の探索と位置は常にアップデートされていきます)

更新を手動反映させる

自動反映をとめたので、今のままだとゲームオブジェクトはまったく動かなくなります。
今回の目的はこれを手動で更新する方法です。

位置に関してはnextPositionに、アップデート間隔で計算された次の位置が保持されているので、これをそのまま設定してやれば位置の更新ができます。
しかし回転についてはいくつか自分で計算しないとならないのでそれを行います。

最終的なコードは以下のようになります。
計算自体は軸と角度を求めるだけです。

void Update ()
{
    // 次の位置への方向を求める
    var dir = _agent.nextPosition - transform.position;

    // 方向と現在の前方との角度を計算(スムーズに回転するように係数を掛ける)
    float smooth = Mathf.Min(1.0f, Time.deltaTime / 0.15f);
    var angle = Mathf.Acos(Vector3.Dot(transform.forward, dir.normalized)) * Mathf.Rad2Deg * smooth;

    // 回転軸を計算
    var axis = Vector3.Cross(transform.forward, dir);

    // 回転の更新
    var rot = Quaternion.AngleAxis(angle, axis);
    transform.forward = rot * transform.forward;

    // 位置の更新
    transform.position = _agent.nextPosition;
}

ShaderLabについてメモ

もっぱらQiitaで記事を書いていましたが、そろそろUnityの話題に絞ったブログを作りたくなったので、過去に少しだけ使っていたこのブログをまっさらにしました。 今後のUnityの記事はここにまとめていこうと思います。

ということで、第一回目はメモとして残しておいたやつを投稿しておこうと思いますw

Propertiesに使える属性

Propertiesにはいくつかの属性を指定することができるようになっています。 指定できる属性は以下の通り。(詳細はドキュメントをご覧ください)

  • [HideInInspector] ... マテリアルインスペクタでプロパティを非表示にする
  • [NoScaleOffset] ... 属性のテクスチャに関して、マテリアルインスペクタの texture tiling / offset を非表示にする
  • [Normal] ... テクスチャが法線マップであることを示す
  • [HDR] ... テクスチャが High Dynamic Range (HDR) テクスチャであることを示す
  • [Gamma] ... Float / Vector プロパティがUIでsRGB値で指定されており、使用するカラースペースに応じて変換が必要な可能性があることを示す
  • [PerRenderData] ... テクスチャは MaterialPropertyBlock の形式の各レンダラーごとのデータであることを示す(マテリアルインスペクタでは、このプロパティのテクスチャ部分のUIが変わる)
  • [KeywordEnum(A,B,C)] ... Enumとして値を設定できるようにする

バリアントについて(#pragma multi_compile A B)

バリアントを定義し、マルチコンパイルすることで、複数の状態に応じたシェーダをコンパイルすることができるようです。 (ドキュメントはこちら

なお、詳細についてはこちらの記事(UnityのShader Variantについて調べてみた)が分かりやすいです。

バリアントについては以下のように #pragma を用いて宣言します。

#pragma multi_compile __ A B C

ここで、__ はどれも指定されていない場合の処理もコンパイルしてほしいことをコンパイラに伝えます。

使い方は以下のようになります。

fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;

#ifdef A
c.rgb += fixed3(0.5, 0.0, 0.0);
#elif B
c.rgb += fixed3(0.0, 0.5, 0.0);
#elif C
c.rgb += fixed3(0.0, 0.0, 0.5);
#endif

こうしておくことで、それぞれのバリアントが定義されているときように、すべての状態のシェーダがコンパイルされます。(なので マルチ なんですね)

[KeywordEnum()]を応用する

[KeywordEnum()] を応用することで、このバリアントをインスペクタから設定することも可能になります。

※ 注意点として、定義するバリアントはすべて大文字である必要があります。(小文字を含めるとうまく動きません)

各Pass共通の処理をまとめておく

以下のように CGINCLUDE を利用して、各Passで使う共通の処理などをまとめておくことができます。 (もし頂点シェーダの挙動はどれも共通で、フラグメントシェーダだけ違う、という場合も、ここに頂点シェーダを書いておいて使いまわすことができます)

Shader "Custom/AnyShader" {
    Properties {
        // ...
    }

    CGINCLUDE
    #include "UnityCG.cginc"
    
    sampler2D _MainTex;

    struct v2f {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
    };

    v2f vert(appdata_base v) {
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        o.uv = v.normal;
        return o;
    }
    ENDCG

    SubShader {
        Pass {
            // ...
            #pragma vertex vert
            // ...
        }
    }
}

セマンティクス

入力・出力に意味づけするもの。SV_POSITION などと指定しているのがそれ。

  • COLOR
  • SV_POSITION ... プロジェクション後の座標
  • POSITION ... 空間中の座標
  • TEXCOORD0
  • NORMAL ... 法線
  • TANGENT ... 接線
  • WPOS ... フラグメントシェーダで現在扱っているピクセルの座標