- 投稿日:2020-07-11T22:34:12+09:00
「ゲームAI技術入門」の要点をまとめる
目的
全くゲーム業界で仕事をしてもいないし、そもそも最新のゲームに疎いのですが、ゲームAI技術入門を読んで面白かったのでメモとして要約してみます。
ゲームの中の人口知能
環境があるから知性があり、知性があるから環境がある。ゲームという環境の中で状況を認識し判断する人工的な知性をゲームAIとする。大きく以下の3つがある。
キャラクターAI
ゲーム世界から情報を集めて意志決定するAI。ナビゲーションAI
地形やゲーム内のオブジェクトなど、ゲームのしかけや環境を認識するAI。
マップの特徴を解析し、経路や位置取りを検索する。メタAI
ゲームをするユーザとゲームコンテンツの境界に立ってゲームをコントロールするAI。
時間軸に沿ってゲーム全体の進行を管理する。それぞれがゲーム内で違う階層を担当し、ゲームのユーザを楽しませるため別々の問題を解いている。
その中で特ユーザを楽しませるためにキャラクターAIに求められるのはリアリティである。以下の2点。
- 合理的である
- ある地点からある地点にキャラクターが移動するときに、わざわざ遠回りするのは理に適わない。行き過ぎた正確な行動もまた合理的ではないが、自然な判断を行わなければリアリティがない。
- 人間的である
- 戦闘をするゲームで瀕死の仲間をかばったり、一見不合理に見えるが人間的な行動を取るとリアリティが出る。
まとめ
ゲームAI自体が全人口知能分野の縮図のような分野であり、あらゆる人口知能技術が現れる。現実と似たゲーム世界を作り上げ、その中で人口知能を作る。現実で応用される前のあらゆる人口知能技術がゲームの中で試され鍛えられていく。
知能のしくみ
知能を理解するために、エージェントアーキテクチャという世界と知能を結ぶモデルを用いる。
エージェントは「役割と機能を持った」AIであり、以下の特徴を持つ。
- 世界と知能を分離して考える。
- 知能は世界からセンサ(知覚)を通じて情報を得る。
- 知能は世界にエフェクタ(身体、機能)を通じて影響を及ぼす。
知能はセンサから得た情報とこれまでの記憶を合わせて、知識を形成する。知識を基に意志決定を行い、行動を組み立てる。
知識を得る難しさ
人間は世界をある型にはめて理解しようとする。視野は130度あっても、集中して見ているのは2度にすぎない。人間であればゲーム世界にスイカがあるとしたら、その重さや動かすことが出来るか、食べることが出来るかを無意識に分類することが出来るが、現在の人口知能にはそのようなバックグラウンドがないため、スイカ側に「食べられる」「持ち上げられる」という許容された情報(アフォーダンス)を埋め込んでいく必要がある。人口知能はこうしてスイカがどのようなものかを理解する。
ブラックボードアーキテクチャ
ブラックボード(共有読み書きメモリ)と、その周りに専門的な処理機能を持つKS(ナレッジソース)があり、KSがセンサやアクションを分業で担当してブラックボード上のデータを解釈するアーキテクチャ。2000年以降のバーチャルゲームにおけるキャラクターAIとして試行錯誤の上で発表された。その後のAI設計者の基本的な概念として活用された。
意識の理論
キャラクターが何かを見ている、聞き耳を立てているとき、ユーザーは「そのキャラクターの意識がそちらに向いている」と感じる。思考モジュールが注意を払っている対象に対して操作を行うというこの意識モデルを
CERE-CRANIUM認識アーキテクチャ
と呼ぶ。知識表現
キャラクターをゲーム世界に配置しただけではキャラクターは何も認識できない。ゲームのグラフィックデータは基本的にユーザがゲーム世界を認識するためのデータであり、キャラクターAIに環境を認識させるには別のデータが必要となる。
人口知能は人間に与えられた問題に対して思考する「問題特化型人工知能」が大半だが、ゲームAIにはゲームという閉じた世界で自分で問いを設定出来る「汎用人口知能」を目指して開発されているが、まだそのように粋には達していない。センサの設計
五感の中でも視覚と聴覚以外のセンサはゲーム内で実装される技術がまだ発達していない。この2つのセンサが厳密に必要とされるステルスゲームにて実装技術が発達した。
視野領域
ゲーム内のキャラクターは扇形の「視野領域」を持ち、フィールド上の障害物などに衝突せずに対象のキャラクターが視野領域に入った時を「見えた」状態、衝突しなければ「見えなかった状態」として見做す。この一連の過程を「レイキャスト」(raycast)と呼ぶ。
存在確率マップ
フィールド上をマス目に分割し、兵士キャラクターがフィールド内を見回りのため徘徊してユーザのキャラクターがいないことを確認したマップ上のマスを存在確率ゼロとみなし、マップ内のすべてのマスをゼロにするために徘徊し続けるシステムを存在確率マップと呼ぶ。
後半
後半は別途まとめます。
- 投稿日:2020-07-11T21:02:53+09:00
.NET 系の DI コンテナ
やっとコードよりの話になれる!!過去の 2 記事は言語ごとの事情や、その人の経験などで色々ちょっとずつ異なることがあるので「〇〇の場合は違う」とか「こういう側面もある」とか色々コメントしやすい感じだったのですが、そのおかげで初めての Qiita のデイリーで No1 取れました。やったね!
ということで、自分の主戦場の C# での DI コンテナ事情について書いてみたいと思います。
Microsoft.Extensions.DependencyInjection
ASP.NET Core などで何も考えないと使うことになる、事実上の標準の DI コンテナです。
https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection
非常にシンプルで DI コンテナとして最低限これくらいは持ってるだろうと思われる機能だけ持ってます。
例えば、以下のようなクラスがあったとします。
interface IMyService { void Greet(); } class MyService : IMyService { private readonly IMessagePrinter _messagePrinter; private readonly IMessageGenerator _messageGenerator; public MyService(IMessagePrinter messagePrinter, IMessageGenerator messageGenerator) { _messagePrinter = messagePrinter; _messageGenerator = messageGenerator; } public void Greet() => _messagePrinter.Print(_messageGenerator.Generate()); } interface IMessagePrinter { void Print(string message); } class ConsoleMessagePrinter : IMessagePrinter { public void Print(string message) => Console.WriteLine(message); } interface IMessageGenerator { string Generate(); } class MyMessageGenerator : IMessageGenerator { public string Generate() => "Hello world"; }Microsoft.Extensions.DependencyInjection を使うと上記のクラスを組み立て可能なコンテナを作って IMyService を取得して Greet を呼び出すコードは以下のような感じになります。
class Program { static void Main(string[] args) { // 型の登録 var services = new ServiceCollection(); services.AddTransient<IMyService, MyService>(); services.AddTransient<IMessagePrinter, ConsoleMessagePrinter>(); services.AddTransient<IMessageGenerator, MyMessageGenerator>(); // インスタンスを提供してくれる人を作る using var provider = services.BuildServiceProvider(); var myService = provider.GetService<IMyService>(); myService.Greet(); } }実行結果は
Hello world
と表示されるだけです。インスタンス管理
AddTransient で登録するとコンテナから取得するたびに別のインスタンスを返します。AddSingleton で登録すると毎回同じインスタンスになります。AddScoped で登録すると同じスコープ内だと同じインスタンスになります。
スコープを作るには ServiceCollection に BuildServiceProvider をした結果の ServiceProvider の CreateScope メソッドを使います。各クラスのコンストラクタが呼ばれたときにわかりやすいように標準出力にメッセージを出すように手を加えた後に以下のようにコードを書き替えてみました。class Program { static void Main(string[] args) { // 型の登録 var services = new ServiceCollection(); services.AddScoped<IMyService, MyService>(); services.AddSingleton<IMessagePrinter, ConsoleMessagePrinter>(); services.AddSingleton<IMessageGenerator, MyMessageGenerator>(); // インスタンスを提供してくれる人を作る using var provider = services.BuildServiceProvider(); Console.WriteLine("Scope1"); using (var scope = provider.CreateScope()) { var s = scope.ServiceProvider.GetService<IMyService>(); s.Greet(); } Console.WriteLine("Scope2"); using (var scope = provider.CreateScope()) { var s = scope.ServiceProvider.GetService<IMyService>(); s.Greet(); } } }MyService が AddScoped で残りは AddSingleton にしてみました。
実行すると以下のようになります。Scope1 ConsoleMessagePrinter のコンストラクタ MyMessageGenerator のコンストラクタ MyService のコンストラクタ Hello world Scope2 MyService のコンストラクタ Hello worldSingleton のものはスコープが変わってもインスタンスは新たに作られなくて、AddScoped で登録したものはスコープが変わると再生成されてることがわかります。
生成処理をカスタマイズしたい
AddScoped や AddTransient や AddSingleton はラムダ式を受け取るオーバーライドがあって、それを使うとオブジェクトの生成処理をカスタマイズできるようになっています。
例えば MyService の生成ロジックを自前のものに置き換えたコードを以下に示します。ちなみに、このコードの場合は別に生成処理を変えたところで意味はありません。単純に new してるだけなので。
class Program { static void Main(string[] args) { // 型の登録 var services = new ServiceCollection(); services.AddScoped<IMyService, MyService>(provider => { // ここで任意の生成ロジックを入れることが出来る var printer = provider.GetRequiredService<IMessagePrinter>(); var generator = provider.GetRequiredService<IMessageGenerator>(); return new MyService(printer, generator); }); services.AddSingleton<IMessagePrinter, ConsoleMessagePrinter>(); services.AddSingleton<IMessageGenerator, MyMessageGenerator>(); // インスタンスを提供してくれる人を作る using var provider = services.BuildServiceProvider(); Console.WriteLine("Scope1"); using (var scope = provider.CreateScope()) { var s = scope.ServiceProvider.GetService<IMyService>(); s.Greet(); } Console.WriteLine("Scope2"); using (var scope = provider.CreateScope()) { var s = scope.ServiceProvider.GetService<IMyService>(); s.Greet(); } } }実行結果は同じです。
Microsoft.Extensions.DependencyInjection について深く知りたい人は、Microsoft.Extensions.DependencyInjection Deep Dive を見てみるといいと思います。
他の DI コンテナと使いたい
とまぁ、こんな感じで必要最低限の機能セット(登録と取得とシンプルなライフサイクル管理とインスタンス生成のカスタマイズ)がある程度なのですが、もうちょっと高度な機能を持った DI コンテナを使いたいという要望に応えられるようになっています。
以下にリストがあります。
https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.DependencyInjection
試しに Unity を使ってみましょう。Unity は昔は Microsoft がメンテナンスしてた OSS の DI コンテナで、今は完全に Microsoft から離れてメンテナンスされています。
Unity.Microsoft.DependencyInjection
パッケージを追加することで Unity が使えるようになります。ただの DI コンテナとして使うだけなら別に Unity をあえて使う必要はないので、追加でUnity.Interception
パッケージも追加してみようと思います。ということでこんな感じで IMyService はログを出すような追加処理が入るようにしてみました。
using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using Unity; using Unity.Interception; using Unity.Interception.ContainerIntegration; using Unity.Interception.InterceptionBehaviors; using Unity.Interception.Interceptors.InstanceInterceptors.InterfaceInterception; using Unity.Interception.PolicyInjection.Pipeline; using Unity.Lifetime; using Unity.Microsoft.DependencyInjection; namespace UnityLab { public interface IMyService { void Greet(); } public class MyService : IMyService { private readonly IMessagePrinter _messagePrinter; private readonly IMessageGenerator _messageGenerator; public MyService(IMessagePrinter messagePrinter, IMessageGenerator messageGenerator) { _messagePrinter = messagePrinter; _messageGenerator = messageGenerator; } public void Greet() => _messagePrinter.Print(_messageGenerator.Generate()); } public interface IMessagePrinter { void Print(string message); } public class ConsoleMessagePrinter : IMessagePrinter { public void Print(string message) => Console.WriteLine(message); } public interface IMessageGenerator { string Generate(); } public class MyMessageGenerator : IMessageGenerator { public string Generate() => "Hello world"; } public class LogBehavior : IInterceptionBehavior { private readonly IMessagePrinter _messagePrinter; public bool WillExecute => true; public LogBehavior(IMessagePrinter messagePrinter) { _messagePrinter = messagePrinter; } public IEnumerable<Type> GetRequiredInterfaces() => Type.EmptyTypes; public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { _messagePrinter.Print($"Begin: {input.MethodBase.Name}"); try { var result = getNext()(input, getNext); _messagePrinter.Print($"End: {input.MethodBase.Name}"); return result; } catch (Exception ex) { _messagePrinter.Print($"Exception: {input.MethodBase.Name}, {ex}"); throw; } } } class Program { static void Main(string[] args) { // 型の登録 var services = new ServiceCollection(); services.AddSingleton<IMessagePrinter, ConsoleMessagePrinter>(); services.AddSingleton<IMessageGenerator, MyMessageGenerator>(); // Unity のコンテナに登録してログ機能も追加 var container = new UnityContainer() .AddNewExtension<Interception>(); container.RegisterType<IMyService, MyService>( new SingletonLifetimeManager(), new Interceptor<InterfaceInterceptor>(), new InterceptionBehavior<LogBehavior>()); // インスタンスを提供してくれる人を作る var provider = services.BuildServiceProvider(container); var s = provider.GetService<IMyService>(); s.Greet(); } } }実行すると以下のような感じになります。
Begin: Greet Hello world End: Greet内部的には ServiceCollection に登録されている情報を見て UnityContainer に登録処理をして、UnityContainer をラップする IServiceProvider が作られてる感じです。
なので、ServiceCollection で登録したやつも UnityContainer で登録したやつも同じコンテナにあるように(実際同じコンテナにあるので)インジェクション出来ます。まとめ
ここら辺まで出来たら、あとは ASP.NET Core あたりのドキュメントを見ながらぽちぽちやってみるのがいいと思います。
- 投稿日:2020-07-11T10:45:41+09:00
Riderがサポートするシェーダー周りの機能メモ
Riderの導入
Rider202.5958.478以降をインストールした後に、PackageManagerのJetBrains Rider Editor(2019系だとRider Editor)のパッケージを2.0.5にアップデートする。
プロジェクト内にC#スクリプトがないとシェーダーの補完とかも効かないようなのでC#スクリプトを作成しておく。
ここまでの作業で導入終了。シェーダーのサポートが効くはず。ダメなときは、Preferences>External ToolsのRegenerate Project filesボタンを押すと良かったりするのかもしれない。New Shader Support - How To?
機能紹介
サッと触ってみて見つけた機能を列挙。VSCodeのUnityシェーダー用のプラグインをこれまで使っていたのでそれとの比較も併せて書きます。
入力補完
HLSLの組み込み関数もUnityの関数もユーザー定義の関数やら変数やらも全部出てきてくれます。前述のVSCodeのプラグインより網羅している感じ。ShaderLab部分の記述についてはサポートされていないみたい。(Blend構文とかPropertiesの型とか)
ShaderLab部分の補完に関してはVSCodeのプラグインの方が良い。
Rename
Shift+F6でリネームできる。VSCodeではCtrl+Hで文字列置換でやっていたけど、Riderはちゃんと識別子に対応してリネームしてくれる。cgincファイルで定義されている関数をリネームすると関数を呼び出してるファイル側でもリネームされる。
Find Usage
Ctrl+Alt+F7でUsageを見つけてくれてそこに飛べる。
Go to Declaration
Ctrl+Bで宣言に飛べる。
include周り
#include "."
と入力すると候補を出してくれる。誤字しないし、フォルダを潜ってcgincを探すのも楽。
cgincの位置を変えたり、includeしてない状態で関数を呼び出そうとするとエラーで適切なインクルードを指摘してくれて、おすと
#include
文を自動挿入してくれる。
キーワード
#ifdef
系は対応する#endif
が分かりやすく表示される。入力時に既存のキーワードが表示されるので誤字、重複が防げる。
コード整形
Riderは通常はCtrl+Alt+Enterで自動フォーマットしてくれるみたいだけど、シェーダーで打つと改行されちゃうので、
1.Ctrl+A
で全選択
2.Alt+Enter
でメニュー表示
3.Enter
でFormat Selection実行
でやれば自動整形してくれる。
ShaderLab部分も整形してくれて助かるんだけど、
#pragma
とか#include
とか#
始まりの行はインデントされてない状態になってしまう。設定でどうにかできそうな気もするけどCode Styleの設定にcg/HLSLがないんですよね...
VSCodeのプラグインだとここもインデントされる。けどVSCodeの方は特定の書き方するとインデントがずれるバグがあるのでどっこいどっこい。ShaderLab補完への対応
RiderのLive Templateなる機能を使って予めShaderLabの定型文を用意しておけばCtrl+Jで簡単に入力できる。っていうのがぱっと見つけられたShaderLab補完への対応策。
add
と入力すればBlend One One
を定型文として入力できるテンプレートを設定しておいたもの。
ただ、ShaderLab部分ではadd
と入力しても自動でテンプレートが出てこないので、Ctrl+Jを押してテンプレート一覧を表示して選んだり、add
と入力した後にCtrl+Jを押してテンプレート候補を表示させて選んだりする手間はある。備考
既存の複雑目なシェーダー(cginc)を開いてみたら、入力補完は表示されるけどシンタックスハイライトが効いてない表示になっていたけど、再度確認しようとして開いてたら直ってたから、導入前にRiderを開いてたりしてなんかおかしいときは一度RiderかUnityかを閉じたりするといいのかもしれない。