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