- 投稿日:2020-11-22T23:59:39+09:00
Unityから考えるDI
はじめに
Zenject/Extenjectを使った方がなんか良いらしい。じゃぁ使い方を調べよう。そんな具合でそこから入ったものの、だいぶ飲み込むのに苦労した経験があります。特にZenject/Extenjectは多機能ですので、余計に要点を把握しにくいのではないかと思う所があります。
ですんで、そういったものを使う前の具体的な話からしていって、意義を書き留めておきたいなと思います。用語整理
DI
Dependency Injection。依存性の注入。詳細は後述。
DIコンテナ
DIをしてくれるフレームワーク。Zenject/Extenject、VContainer等。
Zenject/Extenject
DIをしてくれるフレームワークの一種。基本的にZenjectもExtenjectも同じものだが、政治的要因でZenjectからExtenjectが分離した歴史を持つ。現在はExtenjectが本流だが、そういう経緯からExtenjectのことも含めてZenjectと呼称することが多い。本稿ではZenject/Extenjectと表記する。
VContainer
DIをしてくれるフレームワークの一種。最近リリースされた。Zenject/Extenjectよりもシンプルで軽いらしい。
今実装すること、将来実装すること
ゲーム制作も順調です。次のことを実装しようと思います。
- ハイスコアの記録
- 将来的にはサーバーに保存してスコアランキングを表示する
サーバーはまだ選定すら済んでいません。とりあえず、PlayerPrefsを使って保存することにしましょう。1ゲーム終了時にはGameControllerクラスのOnFinishedが実行されるとします。まずはこんなコードになるでしょう。
GameController.csusing UnityEngine; public class GameController { ・・・ private void OnFinished(int score, int currentHighScore) { if (score < currentHighScore) return; PlayerPrefs.SetInt("Score", score); PlayerPrefs.Save(); } }慣れた方ならGameControllerというネーミングに警戒心を覚えるでしょう。しかもそこに保存するコードを直接書くなんて!実際にここは今後書き換える可能性が非常に高いです。将来的にサーバーに保存したい所ですから。ハイスコアの保存に関することは一つのクラスにまとめておきましょう(単一責任の原則)。
HighScoreRepository.csusing UnityEngine; public class HighScoreRepository { public bool Save(int score, int currentHighScore) { if (score < currentHighScore) return false; PlayerPrefs.SetInt("Score", score); PlayerPrefs.Save(); return true; } }GameController.cspublic class GameController { private readonly HighScoreRepository _highScore = new HighScoreRepository(); ・・・ public void OnFinished(int score, int currentHighScore) { _highScore.Save(score, currentHighScore); } }Repositoryというのは永久保存する場所という意味です。HDDやサーバーなどです。これで少しは安眠できるでしょう。
依存とその問題
サーバーが決まり、契約も済ませました。いつでも使える状態です。じゃぁHighScoreRepositoryを書き換えましょう。ただ、開発中はPlayerPrefsの方が便利なので、前のコードも残しておきたい所です。コメントアウトしておきましょうかね。コメントアウトする場所をちょっと弄れば、切り替えられます。
HighScoreRepository.csusing UnityEngine; ///* public class HighScoreRepository { public bool Save(int score, int currentHighScore) { if (score < currentHighScore) return false; //サーバー保存処理 return true; } } //*/ /* public class HighScoreRepository { public bool Save(int score, int currentHighScore) { if (score < currentHighScore) return false; PlayerPrefs.SetInt("Score", score); PlayerPrefs.Save(); return true; } } //*/ちょっと不細工ですね。GameControllerを弄って、if文で切り替えられるようにしましょうか?それもまたリスクがあります。今はOnFinishedだけですが、今後他の場所でHighScoreRepository.Saveを実行する可能性はないでしょうか?そこでちゃんと忘れずにifで切り替えられるでしょうか?怖いですね。
GameControllerがHighScoreRepositoryに依存している状態です。HighScoreRepositoryやその周辺をいじるときは、GameControllerもうまく歩調を合わせてやらないといけません。面倒ですね。DI:依存性の注入
ここで出てきます。依存性の注入。GameControllerでは依存するHighScoreRepositoryを次のように書いていました。
private readonly HighScoreRepository _highScore = new HighScoreRepository();これをGameControllerの外でやります。外で作るだけではあまり状況は変化しないので、HighScoreRepositoryをInterfaceにして、外で切り替えられるようにします。例えばこうします。
IHighScoreRepository.cspublic interface IHighScoreRepository { bool Save(int score, int currentHighScore); }HighScoreRepositoryPlayerPrefs.csusing UnityEngine; public class HighScoreRepositoryPlayerPrefs : IHighScoreRepository { public bool Save(int score, int currentHighScore) { if (score < currentHighScore) return false; PlayerPrefs.SetInt("Score", score); PlayerPrefs.Save(); return true; } }HighScoreRepositoryServer.cspublic class HighScoreRepositoryServer : IHighScoreRepository { public bool Save(int score, int currentHighScore) { if (score < currentHighScore) return false; //サーバー保存処理 return true; } }GameController.cspublic class GameController { private readonly IHighScoreRepository _highScore; public GameController(IHighScoreRepository highScoreRepository) { _highScore = highScoreRepository; } public void OnFinished(int score, int currentHighScore) { _highScore.Save(score, currentHighScore); } }HighScoreRepositoryはInterfaceにしました。これは上述通り。で、注目するところはGameControllerのコンストラクタ。ここでIHighScoreRepositoryをもらうようにします。HighScoreRepositoryに依存していたわけですが、これを外から注入、つまり入れてやります。これが依存性の注入です。GameControllerにとってはPlayerPrefsかサーバーかどこかよく分からんけど、とりあえず良い所に保存される、という認識になります。
したがってGameControllerを作るときはこんな感じになります。GameControllerLoader.csusing UnityEngine; public class GameControllerLoader : MonoBehaviour { private GameController _gameController; void Start() { IHighScoreRepository highScore; bool debugMode = true; if (debugMode) { highScore = new HighScoreRepositoryPlayerPrefs(); } else { highScore = new HighScoreRepositoryServer(); } _gameController = new GameController(highScore); } }サーバーのみに必要なパラメータ
そういえば、せっかくハイスコアランキングに登録するのに名前が表示されないなんてちょっと寂しいですね。PlayerPrefsには必要ない要素でしたが。どうしましょうか?
IHighScoreRepository.Saveの引数に追加するのも一つです。しかし、PlayerPrefsには不要で、サーバー保存時のみに必要です。GameControllerも書き直さなきゃいけません。面倒ですね。じゃぁこうしましょう。INameGetter.cspublic interface INameGetter { string Get(); }NameGetterConst.cspublic class NameGetterConst : INameGetter { public string Get() { return "仕様書無しさん"; } }HighScoreRepositoryServer.cspublic class HighScoreRepositoryServer : IHighScoreRepository { private readonly INameGetter _name; public HighScoreRepositoryServer(INameGetter name) { _name = name; } public bool Save(int score, int currentHighScore) { if (score < currentHighScore) return false; //サーバー保存処理 //ここで _name.Get() を使う。 return true; } }GameControllerLoader.csusing UnityEngine; public class GameControllerLoader : MonoBehaviour { private GameController _gameController; void Start() { IHighScoreRepository highScore; bool debugMode = true; if (debugMode) { highScore = new HighScoreRepositoryPlayerPrefs(); } else { var name = new NameGetterConst(); highScore = new HighScoreRepositoryServer(name); } _gameController = new GameController(highScore); } }GameControllerを書き換えなくても済みました。HighScoreRepositoryServerの生成時に直接文字列を入れても良いのですが、将来はサーバー保存時に名前入力ダイアログを出したいので、またInterfaceにしました。今はサーバー保存の実装に注力して後で切り替えます。
Loaderの肥大化
割とシンプルなプログラムですが、GameControllerLoaderは割と大きくなってきます。これからもどんどん大きくなるでしょう。単一責任の原則にしたがってクラスを作っていけば、それなりの数になります。この生成を管理するとなると面倒です。
はい、お待たせしました。ここで出てくるのがDIコンテナです。Zenject/Extenjectの場合
Zenject/Extenjectをインポートした後、SceneContextを作ります。で、MonoInstallerのスクリプトを作成、それを空のGameObjectにアタッチして、SceneContextのMonoInstallerにアタッチします。※この辺りの詳しい利用方法は検索すれば出てくると思います。
スクリプトは以下のようになります。Installer.csusing Zenject; public class Installer : MonoInstaller { public override void InstallBindings() { bool debugMode = true; if (debugMode) { Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryPlayerPrefs>().AsSingle(); } else { Container.Bind<INameGetter>().To<NameGetterConst>().AsSingle(); Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryServer>().AsSingle(); } Container.Bind<GameController>().AsSingle(); } }GameControllerLoader.csusing UnityEngine; using Zenject; public class GameControllerLoader : MonoBehaviour { [Inject] private GameController _gameController; }先ほどと同じように動くはずです。GameControllerLoader.StartがInstaller.InstallBindingsに移ったような感じです。
詳しく見ていきましょう。まず_gameControllerに[Inject]という属性がついています。Zenject/Extenjectはこれを探してきます。見つかったら、ここにインスタンスを放り込みます。
この放り込まれるインスタンスの型は予めZenject/Extenjectに伝えておかねばなりません。それがInstaller.InstallBindingsのContainer.Bind<GameController>().AsSingle();です。AsSingle()はインスタンスを1個だけ作るという意味です。今回はGameControllerのInjectが1カ所しかありませんが、複数書かれる場合もあります。そのとき、常に同じ一つのインスタンスが挿入される、という意味です。
このようにZenject/ExtenjectはGameControllerのインスタンスを作ってくれる訳ですが、GameControllerのコンストラクタには引数がありました。IHighScoreRepositoryです。これについても何を挿入したら良いか、予めZenject/Extenjectに伝えておかねばなりません。それがbool debugMode = true; if (debugMode) { Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryPlayerPrefs>().AsSingle(); } else { Container.Bind<INameGetter>().To<NameGetterConst>().AsSingle(); Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryServer>().AsSingle(); }です。
debugMode==trueの時、IHighScoreRepositoryを挿入しなければいけない時はHighScoreRepositoryPlayerPrefsを作ってそれを入れて、という意味になります。
debugMode==falseだとHighScoreRepositoryServerになりますが、このコンストラクタでさらにINameGetterが要りますので、INameGetterをNameGetterConstに指定します。VContainerの場合
まずVContainerをインポートします(manifest.jsonに「"nuget.mono-cecil": "0.1.6-preview"」を追加するのを忘れずに)。で、下記のスクリプトを作って、空のGameObjectにアタッチします。InspectorにGameControllerLoader欄ができてるので、そこに上述のGameControllerLoaderのGameObjectを放り込んでおきます。
GameLifetimeScope.csusing UnityEngine; using VContainer; using VContainer.Unity; public class GameLifetimeScope : LifetimeScope { [SerializeField] private GameControllerLoader gameControllerLoader; protected override void Configure(IContainerBuilder builder) { bool debugMode = true; if (debugMode) { builder.Register<IHighScoreRepository,HighScoreRepositoryPlayerPrefs>(Lifetime.Singleton); } else { builder.Register<INameGetter,NameGetterConst>(Lifetime.Singleton); builder.Register<IHighScoreRepository, HighScoreRepositoryServer>(Lifetime.Singleton); } builder.Register<GameController>(Lifetime.Singleton); builder.RegisterComponent(gameControllerLoader); } }やってることはZenject/Extenjectと同じです。唯一違うのがこいつです。
builder.RegisterComponent(gameControllerLoader);これはHierarchyにあるGameObjectについて、インジェクションをして欲しい対象を指定しなければいけません。Zenject/Extenjectはこれを自動でやってくれてたんですね。
インジェクションの種類
[Inject]属性のあるフィールドやコンストラクタで必要なインスタンスを放り込んでくれることをそれぞれ、フィールドインジェクションやコンストラクタインジェクションと言います。メソッドインジェクションというのもあります。
[Inject] public void Construct(GameController gameController) { //コンストラクタ代わりに実行される }Zenject/Extenject、VContainer共通です。
原則的にはコンストラクタによるインジェクションを基本とします。そもそも何かに依存するのを避けるために、DIコンテナを使わない方法から出発しました。なのにDIコンテナがないと動かないというのはちょっと矛盾します。
とはいえ、MonoBehaviourはコンストラクタを持てないので、代わりにフィールドインジェクションやメソッドインジェクションを使います。DIコンテナは重い?
まぁ重いとは言われます。リフレクションと言って、ソースコードの文字列を解析して、インジェクションが必要な所があれば、都度そこにインスタンスを放り込むという処理を行っているためです。そのためゲームを立ち上げるときなんかにちょっと時間がかかるようになるかもしれません。支障が出るほど重くなるのはちょっと考えにくいとは思います。
DIの意義
おおよそのやり方は上述の通りです。これで何がしやすくなるのか。例えばテストがしやすくなります。サーバーが無くてもハイスコア関係の仮実装が可能で、それによってGameControllerも動かせるようになったのは見ての通りです。
また複数人で作るのにも有効ではないでしょうか。HighScoreRepositoryServerができていないから、GameControllerのOnFinishedが完成しない、といった事態も避けられます。
設計にお悩みの方は是非試してみてください。
- 投稿日:2020-11-22T18:03:29+09:00
【Unity】ランダムで沢山の武器の中から三つを選べるシステムを実装してみた
この記事は?
先日stgゲームを作っている際に、タイトル通りのことを作ってみようと思ったのですが、
自分にとって難しかったので実装までの道のりを備忘録的に書いてみました。実装までの道のり
まず、問題の切り分けをしました。ランダムで沢山の武器の中から三つを選ぶシステムを作るためには、
1 沢山の武器を作り、配列の中に入れる。
2 乱数を使って三つの「被らない」乱数を添え字として使う。
3 プレイヤーが選んだことを検知して選んだ武器を渡す。
この3つがあれば実装できそうなので、順に実装していきました。1 沢山の武器を作り、配列の中に入れる。
あとから武器を追加する可能性があるので、静的配列ではなく動的配列を作り、順次武器を作ってその中に入れていきます。
2 乱数を使って三つの「被らない」乱数を添え字として使う。
これが正直一番大変でした。ただwhileを使って被らないような内容を作るだけなのですが、
for文やらif文やらが混雑してなんの文がなにをしているかがわからなくなってしまい、
解読に時間がかかったり、Unityがフリーズしたりと。
切り分けて一つずつ問題を解決していけば難しくないことに気が付きました。3 プレイヤーが選んだことを検知して選んだ武器を渡す。
これはゲームを作ったことのある人なら誰もが覚えのあるOnButtonClickメソッドで、
選んだ武器の情報を取得すればよさそうです。最後に
Listとかを使うときはAddやらRemoveやらを使わなければならないみたいなので使うのを尻込みしていましたが、
色々検索してみたら意外にもなんとかなったのでGoogle先生に感謝しながらコード書いていきたいと思います。
- 投稿日:2020-11-22T17:11:39+09:00
MagicLeapハッカソンでARゲームを作ったの呼吸
はじめに
はじめまして。ごだと申します。
2020年9月に「Magic Leap Challenge」というハッカソンに参加させていただきました。
デバイスを1週間貸していただけて、家での開発ができるという素晴らしいイベントでした。
その中で気づいたことを今回まとめてみましたので、お役に立てれば幸いです。ゲームデザイン
「触覚」をコンセプトにした音楽ゲームを作りました。
プレイヤーはテーブルを指で叩くことで移動してくるノーツを処理することができます。
(譜面を作り込む時間の余裕がなく、BGMとSEを合わせられてないのですが雰囲気を感じていただければ幸いです)
動画:https://youtu.be/ckV3ReiJ7og実装にあたり、空間メッシュとハンドトラッキング機能を活用しました。
単純ではありますが、テーブルより僅か上にノーツを処理するためのボタンを配置することでARでは得づらい触覚という体験を加えることができました。触覚
近年AR技術はどんどん進化を続けています。
多くのデバイスや技術が開発され、今では周囲の環境を映像として取り込むだけでなく、空間の特徴点・メッシュ構造・深度・輝度など現実世界の多くの事柄をパラメータとして取得できるようになりました。
クリエイターはそれらを活用して、ホログラムをより自然に現実に溶け込ませることができるようになりました。
自分もその流れに則り、ARを考える際に現実とホログラムをどう重ねるかを大事にするようにしています。
今回は触覚という感覚を中心にして、テーブルを叩く音や手に伝わる振動とホログラムを連動できればと考えました。視野角
現状のARグラス開発で難しいのは視野角の問題かと思います。
視野角がVRほどは広くなく、MagicLeapでは50度となっています。
また、MagicLeapは手前37cmは非表示領域となり、ホログラムを表示することはできません。ホログラムが見切れてしまうとその瞬間ではどうしても没入感が途切れてしまうので、視野を意識してデザインを行うことはとても大事だと思います。
自分が今回作ったゲームはそれを意識したデザインができていないのですが、振り返ってみてこうすればよかったというアイデアはあるので列挙したいと思います。1. 手前に表示されるホログラムは小さくして、大きいものを出さない。 2. プレイ前にキャリブレーションを入れて、視野に収まるように事前に全体のオフセット、スケールを調整する。 3. ビネットのようなエフェクトを入れて、視野の境界をぼかす。3に関しては、Kazuya Hiruma様の ARグラスで 魅力的な絵作り という資料を拝見させていただき、素晴らしいアイデアだと感じております。
明度
ARでホログラムの色合いをどのようにするかという話題になります。
絵作りの話になるので正解はないのですが、自分はARでは明度の高い色を中心にした方がいいと思っています。明度が高いと以下の2つのメリットがあると考えているためです。1. 見やすい。 2. 現実からホログラムが浮き出て見えて、近未来感を演出できる。100%主観ではあるのですが、自分が仕事でARコンテンツを作る際もこちらの方が評判がいいのであながち間違ってはないんじゃないかなと思っています。
Unityでこれに対応するのは難しくありません。
シーンのアンビエントライトを明るめにすれば影の表現は薄まりますが、画面全体を明るく仕上げることができます。以下はアンビエントカラーをデフォルトのRGB(54, 58, 66)
からRGB(212, 212, 212)
に変化させたときの違いを表しています。3DオブジェクトのシェーダーにはMobile/Diffuse
を使用しています。影
影は物体がどのような形状なのか、複数の物体がどのような位置関係にあるのかなど空間に多くの情報を補足してくれます。今回自分が作ったゲームにおいて、ノーツは2種類の軌道を描きます。
(1)ドアから豚やユニコーンが出現して突進してくる
(2)空中のドラゴンが地を這う炎を射出してくる
このように1は平面的な軌道なのですが、2は立体的な軌道を描くようになっています。
今回はこの立体的な軌道をプレイヤーに分かりやすく伝えたかったので、影の実装を行いました。ARKitPluginのMobileARShadowを参考にしました。
MLShadow.shader
https://github.com/HippoAR/Unity-Technologies-unity-arkit-plugin/blob/master/Assets/UnityARKitPlugin/Examples/Common/Shaders/MobileARShadow.shaderMLShadow.shaderShader "Custom/MLShadow" { SubShader { Pass { Tags { "LightMode" = "ForwardBase" "RenderType"="Opaque" "Queue"="Geometry+1" "ForceNoShadowCasting"="True" } LOD 150 CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc" #pragma multi_compile_fwdbase #include "AutoLight.cginc" struct appdata { float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 pos : SV_POSITION; LIGHTING_COORDS(0,1) UNITY_VERTEX_OUTPUT_STEREO }; v2f vert(appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_OUTPUT(v2f, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.pos = UnityObjectToClipPos (v.vertex); TRANSFER_VERTEX_TO_FRAGMENT(o); return o; } fixed4 frag(v2f i) : COLOR { UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); fixed attenuation = LIGHT_ATTENUATION(i); fixed v = (1 - attenuation) * .2; return fixed4(v, v, v, 1); } ENDCG } } }
大きめのPlaneをすべての3Dモデルの下に配置し、上のシェーダーを設定して影を落とします。ARグラスは黒の範囲を描画しないため、LIGHT_ATTENUATION
の値を反転して影の部分が白くなるように使っています。また「シングルパスインスタンシングレンダリング」へ対応させるためのマクロも要所に挟んでいます。こちらを対応しないとホログラムが片目にしか表示されません。音
Unityは3Dサウンドに対応しており、音の発生源とプレイヤーの距離によって音量を変えることができます。視覚だけでなく聴覚を利用して空間を表現することで、より説得力のある体験を届けることができます。
以下の手順で3Dサウンドに対応させることができます。
- 音の発生源に
AudioSource
をアタッチする。SpatialBlend
の値を1にする。MinDistance
、MaxDistance
を設定する。(この間で音が減衰する)デバッグ
ARは周囲の環境に作用する技術であり、エディタだけで変更の確認を行うことは難しいです。
それを解決するために各種デバイスがエミュレータを提供していたり、UnityMARSのようなツールが開発されはじめています。MagicLeapにはそれに対する回答として 「Zero Iteration」 という仕組みが用意されています。
既に用意されている様々な部屋モデルの上にホログラムを表示し、入力をエミュレートすることができます。
Unityエディタと同期しているところが素晴らしく、エディタで動的に状態を確認したり、操作を加えたりできます。
今回のハッカソンではこのツールのおかげで確認作業を効率的に行うことができました。まとめ
MagicLeapはやれることが多い素晴らしいデバイスで、1週間楽しくAR開発を行うことができました。
今後もARで色々なアプリを作っていけたらと思います。
ありがとうございました。
- 投稿日:2020-11-22T15:01:33+09:00
Unityメモ 移動する床とプレイヤー 親子関係
プレイヤーのスクリプト
レイでの判定分け レイヤー使用void OnCollisionStay(Collision col) { if (Physics.Linecast(m_charaRay.position, m_charaRay.position + Vector3.down, LayerMask.GetMask("Ground"))) { m_isGroundCollider = true; } else if(Physics.Linecast(m_charaRay.position, m_charaRay.position + Vector3.down,LayerMask.GetMask("MoveGround"))) { m_isGroundCollider = true; gameObject.transform.SetParent(col.transform);//親子関係を設定 } else { m_isGroundCollider = false; gameObject.transform.SetParent(null);//親子関係を外す } }注意点
プレイヤーでの移動方法で挙動が変わってくるので現在は
transform.positionを操作しているVector3 moveDir = Vector3.zero; if (Input.GetKey(KeyCode.W)) { moveDir += forwardDir; } if (Input.GetKey(KeyCode.S)) { moveDir -= forwardDir; } if (Input.GetKey(KeyCode.D)) { moveDir.z += m_moveSpeed; } if (Input.GetKey(KeyCode.A)) { moveDir.z -= m_moveSpeed; } //トランスフォームで移動させたら移動する床でも子オブジェクト状態でも移動できる if (moveDir.sqrMagnitude > Mathf.Epsilon) { transform.position += moveDir; }