Zenjectの導入
UnityのためのDependency Injection フレームワークにZenjectというものがある。
つい最近知ったものだけれども、
使えるシーンが多いと思うので現状把握している範囲でまとめておこうと思う。
Github
GitHub - modesttree/Zenject: Dependency Injection Framework for Unity3D
Asset store
https://www.assetstore.unity3d.com/jp/#!/content/17758
知ったきっかけはこちらのAmingさんの開発者ブログ
Unity3DのDIフレームワーク、Zenjectの紹介 | Aiming 開発者ブログ
via Unityまとめ
Unityを日頃使っていて、
なんとなく感じている痒いところの解決の助けになりそうに思っていて、
例えば、どうしてもシングルトンを使ってしまっているところであるとか、
管理用のゲームオブジェクトがシーンに配置されていて、
SerializeFieldに必要なオブジェクトを配置していて、それがいつの間にか外れていて
実行時にExceptionで発覚してギャフン、みたいなのを
軽減させる手段に成りえるかと感じた。
Introduction
GithubのIntroductionのおおまかな訳。
ZenjectはUnityのための、軽量なDependency Injectionフレームワークです。
(ただUnity以外で使うこともできます)
これは貴方のアプリケーションを、
とても分断されたレスポンシビリティをもつ疎結合なパーツの集合にすることができます。
Zenjectは、スケーラブルで最高にフレキシブルな方法で、
容易に書けて、再利用でき、リファクタ、テストできる多くのコンフィグレーションにより、
各パーツをのりづけ(関連付け)できます。
すごそうな雰囲気だが、
まだこれだけだと良く分からないかも。
Dependency Injection
そもそもの話として、
僕はDIコンテナというものは使ったことがなく、今回初めて触れた。
なので、Zenjectの機能の範囲でDIの領域からはみ出ている部分に触れていることもあるかもしれない。
また、DIとしてのベストプラクティスを踏み外している部分や、
やや的を外しているかも。
DIについて調べた確認したページなども書いておく。
DIコンテナの本当の使いどころ | 技術トピックス | ウルシステムズ株式会社
2005年なので随分前の記事。DIの適さない使い方などにも触れられている。
分野によっては前から普通に使われていたのだなと、
自分は不勉強だったなと思った。
c# - DIコンテナを使うメリットが分からない - スタック・オーバーフロー
StackOverflowで、回答がわかりやすいなと。
依存性の注入 - Wikipedia
Wikipedia。
上記のStackOverflowにもリンクがあった。
DIという用語を作りだしたのはMartin Fowler、そうだったのか。
Inversion of Control Containers and the Dependency Injection pattern
Martin Fowlerのブログ。
検索したらちゃんと出てきた。
2004年の記事。
ある程度見ていくと、既存のDIについてはJavaやphp、
C#もあるけど、
Unityというゲームエンジンで動作するフレームワークとは
少し前提条件が違うようにも思うが、
パターンとして考え方を整えるために色々読んでおくのは良いかと。
また、ZenjectのReadmeにも、
DIを使う理由については行数を割いて書かれていて、
https://github.com/modesttree/Zenject#theory
https://github.com/modesttree/Zenject#misconceptions
のあたりで書かれている。
サービスを使うにあたって、
直に生成するよりかはコンストラクタなどでインターフェースを受けるほうが柔軟で、
それをさらに推し進めると、その依存性の解決は最初にやっちゃえばいいよね、
という感じだろうか。ざっくり言うと。
Pokémon Goでの事例
ZenjectはポケモンGoでも採用されている。
Unite LA 2016でのトークの動画が上がっていて、
全般的にDIを採用したアーキテクチャについて話している。
www.youtube.com
- Unityを長く使っていて、いつもとっ散らかっちゃうけど今回はすごくきれいにできた
- 既存のゲームデザインとは異なるため多くのイテレーションを必要とし、柔軟なアーキテクチャが必要だった
- Testについて。
- ゲームステートごとにInstallerがある。
- InstallPrefabというAttributeを別途作成・使用している
- Q&Aにて、パフォーマンスに関してはキャッシングが効いてるから大丈夫と思う、みたいなことを言っている
英語なので、少し怪しいが。
サンプル
まず Hello World Exampleを試してみる。
https://github.com/modesttree/Zenject#hello-world-example
ヒエラルキービューで右クリック
Zenject -> Scene Context
でSceneContextオブジェクトを生成、配置する。
Projectビューの右クリックからCreate -> Zenject -> MonoInstaller
でInstallerとなるスクリプトを作成する。
ファイルダイアログが開くので、TestInstaller.csという名前で保存する。
この作ったTestInstaller.csに、
ZenjectのReadmeに載っているコードをコピペか、同じように記述する。
TestInstallerをシーンに配置する。(ここではSceneContextにAddComponentしちゃう)
Scene ContextのInstallerプロパティに、
TestInstallerを追加する。
エディタ実行をすると、ログにHello Worldと出力される。
また、Ctrl + Shift + Vで、バリデーションがされて、
問題があればエラーを出してくれる。
オブジェクトの生成
いくつかシンプルなケースをいくつか試してみる。
プレハブをもとにオブジェクトを生成したい場合、
従来であればInstantiateを呼び出すことになるが、
ZenjectではFactoryをBindすることができる。
ここではSceneContextにSerializeFieldでプレハブを受けるInstallerを用意する。
サンプル同様にInstallerをSceneContetのプロパティに設定しておく。
Installerのコードは以下のようになっている。
using UnityEngine; using Zenject; public class InstancingInstaller : MonoInstaller<InstancingInstaller> { [SerializeField] GameObject cubePrefab; public override void InstallBindings() { Container.BindFactory<Cube, Cube.Factory>().FromPrefab(cubePrefab); Container.Bind<ITickable>().To<InstancingManager>().AsSingle(); Container.Bind<InstancingManager>().AsSingle(); } }
FromPrefabからFactoryをBindしている。
cubePrefabはCubeコンポーネントを持っていて、そのなかでFactoryを定義している
using System.Collections; using System.Collections.Generic; using UnityEngine; using Zenject; public class Cube : MonoBehaviour { public class Factory : Factory<Cube> { } void Start () { //... } void Update () { //... } }
ITickableを実装したInstancingManagerというクラスからFactoryを使う。
Instancingmanagerは以下。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Zenject; public class InstancingManager : ITickable { Cube.Factory factory; public InstancingManager(Cube.Factory factory) { this.factory = factory; } void ITickable.Tick() { //if(someCondition) //{ //factory.Create(); //} } }
factory.Create()を呼べば、オブジェクトが生成される。
ScriptableObjectInstaller
Readmeに書いてあるものからスクリプタブルオブジェクトの利用について。
https://github.com/modesttree/Zenject#scriptable-object-installer
Scriptable Object InstallerでScriptable Objectを利用できる。
右クリックからCreate -> Zenject -> Scriptable Object Installer
ファイル保存ダイアログがでるので、任意の名前をつける。(この例だとGameSettingsInstaller)
このScriptableObjectInstallerはScriptableObjectを継承しているので、
エディタで設定したいプロパティはここに持たせる。
エディタスクリプトにより、このScriptableObjectが生成できるようになっており、
Create -> Installers -> ScriptableObjectInstaller
で作る。
これをSceneContextに追加れば使用できる。
BindはContainer.BindInstance()で行う。
Scene Bindings
これもReadmeにあるものから。
https://github.com/modesttree/Zenject#scene-bindings
すでにシーンに配置してあるオブジェクト/MonoBehaviourを、Bindingさせる方法。
Unityだとシーンに配置させるケースは多々あるので、
それをBindingして受けれるようにする方法。
オブジェクトにZenject Bindingコンポーネントをアタッチし、
Componentsに、そのオブジェクトが持っているBindしたいコンポーネントを追加。
これだけで、[Inject]であったり,ITickableのコンストラクタなりで受けることができる。
[Inject]はListで受けることもできるので、
Amingさんの開発ブログの例もそうだが、
シーンに配置したいくつかのオブジェクトにZenjectBindingを設定しておくと、
オブジェクトのリストが実行時に作られている状態になる。
これは便利。
同じようなことをやるときにオブジェクトのStartなりでマネージャに渡したりしていたのが手間が省けるし、
場合によってはGameObject.FindObjectsOfTypeなどで取得をしたりしていたものが、
このリストでとれるようになる。
まあ、動的にも生成されるようなものはPrefabから生成したさいに別途登録するような使い方にはなると思う。
いくつかのケースを書いてみた。
ITickableなどを用いていると、
Unityの標準のUpdateと別にタスクが回るようなものなので、
従来の使いかたに慣れ切っていると、
少し導入に障害を感じるかもともおもう。
ただ、色々とスッキリかけるので利点は感じる。
もっと使っていきたいと思う。
利用者が増えて日本語でも情報が増えるとうれしい。
Transform.positionの代入とCS1612エラーで色々考えた
UnityでC#のプログラムを書いていて、
ひと月に一回くらいウッカリ遭遇するエラーがあって、
transform.position.x = 10.0f;
とか書いたときに、
error CS1612: Cannot modify a value type return value of `CubeBehaviour.AAA.position'. Consider storing the value in a temporary variable
という。
正しく書くのは単にpositionのx,y,zは直接代入しないで、
new Vector3したものをpositionに入れればよい。
たまに忘れたころにエラー出てきて、おっとっと。みたいな。
僕がボケてるだけかもしれないけど、
ちょいちょい引っかかるので、
これなにがどうしてこうなってんの、どうにかなんないのっていうのを考えてみた話(たいしたオチはない)
エラー自体の意味するところは以下のものとなる。
Compiler Error CS1612
プロパティを介して代入しようとしてもダメだよ、プロパティは実体はメソッドだから。
ということである。
なにがプロパティなのかというとpositionなわけで、
定義を見てみると、
namespace UnityEngine
{
public class Transform : Component, IEnumerable
{
...
public Vector3 position { get; set; }
となっている。
プロパティでエラーになるっていうのがどういうことか、
もうちょっと分かりやすくpublic変数と比べてみると、
class AAA
{
public Vector3 position { get; set; }
}
class BBB
{
public Vector3 position;
}
void Start()
{
AAA a = new AAA();
a.position.x = 1.0f; // error: CS1612
BBB b = new BBB();
b.position.x = 1.0f;
}
AAA,BBBという2つのクラスに、
外から見て同じように扱えるpositionというものがあるが、
これは上のAAAのpositionへの代入だけエラーが出る。
プロパティだから。
ここで、じゃあTransformのpositionもpublicな変数にしちゃえば良いじゃない、
とか一瞬思ってしまったわけだが、
まあ、そうはなかなかいかないというものだと思う。
内部の詳細は当然分からないけど、
Transformの内部処理、エンジン側としてはオブジェクトの情報に変更があったということは知っておかなきゃいけない。
単純に座標の更新があったというだけすればいいわけじゃなくて
dirtyフラグを立てるであるとかして、
可視判定の更新をかけるかであるとか、
最適化で動いてないものは処理をスキップするとか
物理のほうでも位置の参照をしているであるとか、もしくは衝突判定とか、
親のTransformのほうでもAABBの更新するとかどうとか。とかとか。
妄想だけど、まあ色々やることはありそう。
なので実体はメソッドであるプロパティでないと都合は悪いのだろう。
なので解決方法としては、
"自分がしっかりする"、しかない。
まあ、それでもなにか少しできることであれば
拡張メソッドを用意しておいて、
そちらを使うことを習慣にしておく、
とかがひとまず打てる手かな、、と。
using UnityEngine; using System.Collections; public static class TransformExtension { public static void SetPosition(this Transform self, float x, float y, float z) { self.position = new Vector3(x, y, z); } public static void SetPositionX(this Transform self, float x) { self.position = new Vector3(x, self.position.y, self.position.z); } public static void SetPositionY(this Transform self, float y) { self.position = new Vector3(self.position.x, y, self.position.z); } public static void SetPositionZ(this Transform self, float z) { self.position = new Vector3(self.position.x, self.position.y, z); } }
例えばこんな風に。
座標のxだけ更新したい場合とかもちょっとスッキリ書けるようになるし。
transform.SetPositionX(10.0f);
とか。
メソッド名は微妙だが。
ゲームメカニクス おもしろくするためのゲームデザイン
ゲームメカニクス おもしろくするためのゲームデザイン (Professional Game Developerシリーズ)
- 作者: アーネスト・アダムス,ヨリス・ドーマンズ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2014/02/08
- メディア: Kindle版
- この商品を含むブログを見る
買ったのはずいぶん前で、一度目を通していたと思うけど、
読み直した。
前回は途中でツールの説明になったあたりから後、飛ばし読んでしまったんだと思う。
サブタイトルで割とゲームデザイン全般に対する本のような、
アクションゲームはこんなのがあってー、STGはこうでー、
のような印象を受けるんじゃないかなって思うけど、
実際には、ゲーム内のリソースと内部経済・フィードバックループでの循環について
踏み込んだ内容になっていて、
著者のフレームワーク「Machinations」の紹介、ツールの紹介と、
それをつかったパターンについて書かれてる本、かな。
内部経済を表現できる適応範囲は広いので、
実際、パックマンのようなものから、RTSのようなものまで話題に上がってて、
ゲームデザインを考えるのに、
Machinationsを使うかどうかは置いておいても、
リソース・フィードバックループを分かっておくと良さそう。
ツールはFlash製なので、あんまり使い良くはない。
公式のwikiとかで動いてなかったりするし。
Android上で動作するものがあると、合間時間でもいじれてよいかも。
公式のページはこちらにあるので、とりあえず触るのにはここから。
wikiなんかへのリンクもあるし、
ツールのソースはgithubにあってそこへのリンクもある。
Machinations: Game Feedback Diagrams
フィードバックについての話題はMarc LeBlancさんのトークと合わせて紹介されていて、
スライドはここからアクセスできる。
The collected game design rants of Marc LeBlanc
GDCのサイトに、Machinationsに関してのセッションの動画があって動画がここから見れる。
www.gdcvault.com
日本語のスライドとしてはこちらを最初に見ると分かりやすいです。
Machinationの紹介
絵でわかる人工知能
絵でわかる人工知能 明日使いたくなるキーワード68 (サイエンス・アイ新書)
- 作者: 三宅陽一郎,森川幸人
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2016/09/16
- メディア: 新書
- この商品を含むブログ (1件) を見る
CEDECで売られていたので購入。
ちょっとずつ読んだり、気になる章から読んだりしていたが、
人口知能に関わる各トピックごとに書かれているので、
どこから読んでも大丈夫。
森川さんのイラストも可愛くて、
何回も読み返せるし、
一回読んで終わりという本ではなくて、
気になったときに何回も読み直すような本かなって思う。
人口知能に関する広大なトピックを
俯瞰でみて広く全体をカバーしており、
この膨大な知識に基づいてかかれているのは素晴らしい。
著者である三宅さんがゲームAIのスペシャリストなので、
ゲームAIに関わるトピックも多く含まれており、
ゲーム開発者であれば読んでいて損はないはず。
カラーパレットを使ったSprite
スプライトを用いたアイテムやキャラクターなど、
パレットを変えるだけで色替え出来れば、
カラーバリエーション分のカラーテクスチャを持つより、
容量節約できていいよねってことで
試してみた実装。
(ただし、ちょっと筋が悪いって思っている方法)
GitHub - kuriharaan/ColorPalette: Color palette experiment for sprites
やり方
パレットを作りたいテクスチャを選択し、
エディタスクリプトを実行すると、
テクスチャの各ピクセルを順に調べ、
カラーのリストを作り、
新しくソーステクスチャと同じサイズのアルファテクスチャ(Alpha8)を作って、
インデクスを格納していく。
描画には、アルファテクスチャをスプライトとして設定し、
パレットを参照するためのシェーダを持ったマテリアルを割り当てる。
Unity5.4からシェーダの配列パラメータの設定がスクリプトで出来るようになったらしい。
Unity - Scripting API: Material.SetColorArray
など。
スクリプトでは、このSetColorArrayを使ってテーブルを設定するようにしている。
シェーダではテクスチャのPixelからアルファ値をとり、
color 配列のインデクスとして使用するようにしている。
一応、これで望む挙動はしているが、
いくらか微妙な点がある。
・カラー情報を別途シリアライズしないといけない(今は初期値を決めうち)
・エディタでプレビューできない
逆に、よいところはスクリプトからカラーを設定すれば、
なんでもカラーを指定できるところ。
少し違ったやり方でパレットはテクスチャで持って、
スプライトのテクスチャに加えて
もう一段マテリアルに、パレットテクスチャを
指定する方法がある。
アセットストアにあるアセットのひとつに
OldSkool Palette Sprites
https://www.assetstore.unity3d.com/jp/#!/content/8649
というものが実はあって、
紹介動画を見ると、
パレットテクスチャを作る方法をとっている。
OldSkool Palette Sprites Tutorial - YouTube
こちらのほうが現実的な、運用に良い方法に思う。
追記。
仕組み上、アルファテクスチャはポイントサンプリングにする必要があるので、
そういうビジュアルで大丈夫な用途に限ったほうが良さそう。
これは紹介しているアセットも同じで。
Utility Based AI
Game AI Proを読んだので、いくつかの手法を実際に実装してみたくなり、
まず utility based AIを試してみる。
これは意思決定のための方法で、
いろんな場面に適用できる。
シムズみたいなゲームであれば、
AIエージェントが次になにをするかの判断、
コマンド選択式のRPGであればエネミーのアクションの選択に、
といった具合。
Game AI Pro では
www.gameaipro.com
Section 2
9. An Introduction to Utility Theory
に詳しく書かれている。
ゲーム世界から情報を統計・取得し、
0~1に正規化されたユーティリティ値にする。
それらを変換、組み合わせを行い選択肢となるアクションのスコアを決め、
一番高いものを選択する。
(もしくはウェイトにした行動を抽選したり、同種のアクションをバケツにいれて、とか。詳しくはGame AI proに)
階段 or エスカレータ どっちで行く?エージェント
Unity上で簡単な例を作ってみた。
カプセル型のAIエージェントが階段に向かって歩いていき、
階段に近づいたときに、階段でいくか、エスカレータに乗るかの判断を行う。
階段、エスカレータそれぞれでエリア判定用のコリジョンを設定しており、
エリア内にすでにどれだけのエージェントが居るのかが判断のためのファクターとなる。
階段、エスカレータともにキャパシティの値を持っており、
現在のエージェント数をキャパシティで除算したのもが混雑度となり、
ユーティリティ値となる。
選択肢は階段かエスカレータどちらかということになるわけだが、
スコアとしては混雑していないほうがよいので
1 - [混雑度]
という変換をする。
正規化された値なのでこういう変換が簡単。
あと、このままだとリニア曲線になるので、値を二乗して二次関数になるように。
こんな感じ。
float crowdedness = (float)statictics.escalaterArea.NumberOfAgents / (float)statictics.escalaterCapacity; float score = 1.0f - Mathf.Min(crowdedness, 1.0f); return score * score;
まずはこれで階段、エスカレータでスコアを出し、
比較して意思決定を行う。
これだけだと単純すぎるので、別のユーティリティを加味するようにしてみる。
階段は長くなるほど、上るのは疲れてしんどいもの。
なので、階段の長さをファクターとし、
階段をアクションとする場合のスコアに掛け合わせる。
float crowdedness = (float)statictics.stairsArea.NumberOfAgents / (float)statictics.stairsCapacity; float hardships = Mathf.Min(1.0f, statictics.stairsLength / statictics.stairsLengthMax ); float score = 1.0f - Mathf.Min(crowdedness, 1.0f); return ((score * score) + (1.0f - hardships)) * 0.5f;
階段の最大長として想定している値で、階段の長さを除算し、
ネガティブな値に変換(1から引く)、
もともとのスコアと、平均を取るようにする。
これで、階段が長いほどスコアが下がるようになります。
階段が長いと判定されるような値に設定しておくと次のようになる。
ほとんどのケースで、エージェントがエスカレータを選ぶようになる。
実際には、階段の長さに応じて判定のための値を設定するようにし、
エージェントに疲労パラメータを追加してもよさそう。
こんなふうにパラメータを後から足せるのが非常に良い。
Game AI Proにも記述があるが、
ユーティリティ計算のためにはエディタを作るとよいようで、
実際、すべて正規化された値なので、計算方法の組み合わせで表現ができ
データドリブンに出来るので良さそうである。
プロジェクトを進めるにあたって、
ある程度Utility basedに基づいてAIを構築していくのであれば、
ノードエディタもセットで作ると良さそう。
今回試したプロジェクト
GitHub - kuriharaan/UtilityBasedAI: Experimental project for utility based AI