Unity学習録
Scriptable Render Pipeline (SRP)
- 公式ドキュメント
- 一からSRPを作っていくチュートリアル
Rendererなども自作しながらビルトイン・パイプラインがどのような処理を実行しているか学べる.
- URPにCustom Scriptable Rendererを実装する
URPを前提にScriptableRendererDataやScriptableRendererのカスタマイズの仕方を学べる.
- ScriptableRendererFeature とScriptableRenderPass実装
実際にSRPを丸ごと実装し直したり, ScriptableRendererを継承するカスタマイズのやり方はしないと思う. ScriptableRendererFeatureとScriptableRenderPassによってレンダラの描画方法をカスタマイズできるからです.
一つ目は,
A barebones sample implementation of a scriptable renderer feature for Unity's scriptable render pipeline.
とあるようにScriptable Renderer Featureの実装を基礎から学べる.
仕組みの概要をつかみたい場合は【超基本編】URPに独自のパスを追加する方法が参考になります.
いくつかの具体例です.
独自にScriptableRendererFeatureを書かなくてもある程度のことはできるよという例です.
- 【Unity】_CameraOpaqueTexture(SceneColor)で半透明を描画できるようにして半透明オブジェクトを含めた背景を歪ませる
- 【Unity】ShaderGraphで不透明オブジェクトの描画結果を取得する(_CameraOpaqueTexture)
ちなみにUniversal Render Pipelineライブラリにアクセスするには, プロジェクトのフォルダ以下にある, Library\PackageCache以下にあるcom.unity.render-pipelines.universal@12.1.4を覗くといいようです. URPのレンダリング方式の一つであるForwardRendererもForwardRender.csという形で実装を見ることができます.
URPの用語
Universal Render Pipeline Asset
URPのテンプレートから作るとなんとなく存在しProject settings -> GraphicsのScriptable Render Pipeline Settingsに設定されています.
手を動かして学ぶUnity Universal Render Pipeline - part 1
手動で作ることもできます.
Configuring the Universal Render Pipeline
【Unity】Universal Render Pipeline(URP)とは? - 概念の説明~セットアップまで
Universal Renderer
Universal Render Pipeline AssetのRenderer Listに設定されている. 名前通りRenderer何だと思う.
Renderer Feature
Rendererの特徴? 良く分からないがRenderer Objectsってのが作られるらしい.
How to add a Renderer Feature to a Renderer
使い方は公式にも例がある. Render Objects Renderer Featureという長い名前になっているが.
Example: How to create a custom rendering effect using the Render Objects Renderer Feature
Universal Rendering Examplesも見ようねってことらしいです.
RenderPipelineManagerクラス
レンダー・パイプラインを管理するクラスです.
Using the beginCameraRendering event
beginCameraRenderingとかendCameraRenderingは何かというとURPのレンダリング・ループのイベントに対するフックを提供します. RenderPipelineManagerを通じて各フックにイベント・ハンドラを渡します.
それぞれが呼ばれるタイミングは以下で説明されています.
Rendering in the Universal Render Pipeline
Custom Render Pipeline
Creating a Render Pipeline Asset and Render Pipeline Instance in a custom render pipeline
Custom Pipeline Taking Control of Rendering
【Unity】SRPを自作して独自の描画フローを構築する
ScriptableRendererFeatureクラス
- AddRenderPasses
- Create
ScriptableRendererクラス
ゲームデータの保存
- PlayerPrefs
- System.Serializable
- JSON
- Binary
などがある. 1か2で当面は足りると思う. ネットワーク介するなら3か4が必要.
Unityでのスクリプト・ディレクトリの構成
Assets直下に置かないほうが良いらしい.
- パッケージとしての再配布容易性
- サードパーティー製のパッケージ(アセット)と共存
とかそんな感じらしい. Unity Test Framework完全攻略ガイドの構成を踏襲した.
Unityプロジェクトのディレクトリ構成と .gitignore
プロダクション・コードとテストコードを分ける
フォルダを分けてAssembly Definitionというのを定義して参照してやる必要がある. Test Runnerウィンドウからボタンで作成することもできる.
が細かい制御が必要な場合はアセットとして追加できる. Asset -> Assembly Definitionでアクティブなフォルダ直下に作成されます. これをプロダクション・コードとテスト・コードにニコイチで作成する.
プロダクション・コードは以下のようになる.
テスト・コードではプロダクション・コードのアセンブリを参照するようにする.
細かいことは後で調べるます.
Assembly DefinitionとAssembly Referenceの違い
Assembly Definition: 定義したフォルダ以下すべてを単一のアセンブリとしてコンパイルする.
Assembly Reference: サブフォルダではない場所で定義したアセンブリを参照する.
アセンブリの分類
名前 | 説明 |
---|---|
Predefined Assembly | 標準で生成されるアセンブリ (Assembly-CSharp.dllなど) |
Precompiled Assembly (Plugins) | 事前にコンパイル済みのアセンブリ |
Assembly Definition Asset | ユーザー定義のアセンブリ |
Test Assembly | テスト用のユーザー定義アセンブリ |
何もしなければAssembly-CSharp.dllのような事前に定義されたアセンブリが生成されたり参照されたりするようです.
マニュアルではPrecompiled Assemblyのことをプラグインと呼んでいるようです. ネイティブ・プラグインとか言いますしね.
例えばNUnitはTest AssemblyからPrecompiled Assemblyとして参照します.
これはdll拡張子を持っているのでわかりやすいです.
Plugins or Packages
例えばモバイル用(Plugins/AndroidやPlugins/iOS)を含むネイティブ・プラグインなんかが配置されるらしい. ただUniRxのようなC#ファイルも配置されたりする. またそもそもNUnitのdllはPackagesフォルダ以下にCustom Nunitとしてインストールされたりします.
CRIWARE Unity Plug-inが変わりますを見ると, UniRxと同じようにPluginsフォルダ以下に配置していたようですが, Assets直下に切り替えたそうです.
なおOpenUPMを使うとパッケージとなりPackagesフォルダ以下にインストールされるようです.
OpenUPM(Unity Package Manager)を使ってパッケージをインストールする方法 【Unity】これが一番すっきりしてよさそうです.
Plugin直下に置くのはネイティブ・ブラグインに限定して, 後はパッケージでもいいのかもしれません. あるいはプロジェクトの外部からとってきた場合はPackagesで, プロジェクトの一部の場合は管理都合からPluginsとかも使ってよいのかもしれません.
【Unity】Assets/Plugins はもう不要?→必要(な場合もある)など見ても良く分かりません.
当面は動いたらなんでもいいですが.
Is "Plugins" folder still a thing?
Special folders and script compilation order
UniTaskで値を監視できるか? (Ⅰ)
IEnumeratorインタフェースとyield-return文
本家(.NET)ではコルーティンという言葉は明示的には使っていないようです. UnityではズバリCoroutinesというものが出てきます.
コルーティンを定義するにはIEnumeratorインターフェースに準拠しているオブジェクトが必要です. IEnumeratorインタフェースはドキュメントでは以下のように定義されています.
Supports a simple iteration over a non-generic collection.
実装すると繰り返し処理のためのメソッドを実装することになります. この繰り返しを呼び出し側から制御できるようにreturnの代わりにyield-return文を使います.
You use a yield return statement to return each element one at a time.
コルーティン
Unityはこの機能を使って独自にコルーティンを実現しているというわけです. コルーティンの特徴は「待て!」ができることと状態を持てることでしょうか. 処理を待たせておいて別の処理をし, 復帰するというような制御が可能になる.
It’s best to use coroutines if you need to deal with long asynchronous operations, such as waiting for HTTP transfers, asset loads, or file I/O to complete.
こうした制御はNode.jsはイベント・ループで処理しています(Pythonも多分). Unityはゲームエンジンなのでゲーム・ループで処理するようです.
Script lifecycle flowchartを見ると, Update関数の最初の方にコルーティンに関する記載があります.
更にこの辺は用語の混乱もありますややこしいようです.
parallel(並列)には「平行」という意味もあり, concurrentが「並行」と訳されてしまったため、どちらも「へいこう」となってしまって, とても紛らわしくなってしまいました. …(中略)…, concurrentを「並行」ではなく「協調」とでも訳してくれていれば, このようなコラムを書く苦労もなかったことでしょう.
『C#によるマルチコアのための非同期/並列処理プログラミング』
というようにコルーティンも協調的な動作と考えると腑に落ちます. 和を以て貴しとなすという感じでしょうか. マニュアルにも以下のような記載がありました.
If you want to reduce the amount of CPU time spent on the main thread, it’s just as important to avoid blocking operations in coroutines as in any other script code.
何か長い処理とかやるとそこのはコルーティンの実行時間としてスレッドを占有するために, ブロッキングが生じるようです.
本当に同時に実行するにはスレッドが必要になります.
小括
単純に上から下に実行する同期的な処理からコルーティンを使うことで制御に幅を持たせることができるようになることが分かった.
UniTaskで値を監視できるか? (Ⅱ)
"ハードな"非同期処理
主にI/Oが念頭にあると思います. I/Oは周辺機器(モニターや補助記憶装置, NICカードなど)との通信が絡んでくることが前提です. 例えば補助記憶装置からの読み込みを最後まで待っているとかネットワークインタフェースカードに対する読み書き, あるいはUIの更新待ちのような待ちが各所で発生します. 根本はCPUの処理能力に比べて周辺機器へのアクセスが遅いという事情があります. ここが同期的でない(つまり非同期)なポイントとなります.
この"待つ"という状態をうまく表現したいというのが非同期処理の一つの課題のようです. 単一のシステムを構成する個々のコンポーネントをいかに協調させるかという結構"ハード"な仕組みなのです.
多くの場合ユーザーはUIがロックされることを待ちと考えるでしょう. 単純なコマンドライン・アプリケーションでも長い計算などを実行すると別の処理ができません. 現代では別窓を開いて処理すると思いますが, カオス理論の先駆けであるEdward Norton Lorenzは計算機を走らせている間コーヒー・ブレイクを取っていたという時代もあります.
当然待たせておけという対応ではUXは改善しません(システム・アップデートなんかがこれですね)ので, いかに待ち時間を有効に使うかということも考える必要があります.
こうやってみるとawaitというキーワードはなかなか説明的ですばらいいなぁと思えてきます. 多くの言語(python, JavaScript, Rust, Swiftなど)で採用されるだけのことはあります.
まとめると以下のようなことが言えると思います.
- システム・コンポーネント間の協調
- ユーザーとシステム間の協調
並列処理との違い
こうした課題から見直すと並列処理とは見ているものがそもそも違うことが分かります.
並列処理は個々の処理を並列に実行する手法にすぎません. 普通のプログラムである同期プログラムでも処理を速くしたければ採用できます(いわゆるボトルネックの解消). マルチコア時代では非同期の待ちをうまく処理するために複数のスレッドを使って並列に処理を分散することもできます. これは非同期処理と並列処理がC#なんかでは同一視できる理由です(Node.jsやPythonは違うはず). しかしコアが複数なければできないので, 実質ハードウェア実装とも考えられます.
- 待ち処理を並列化して非同期に実行する
重い処理
よくある重い処理を非同期に解決という説明は話がごっちゃになっています. 様々な原因(I/Oバウンド, CPUバウンド)で処理が長期化することで待ちが発生する. 非同期処理の目的はこの待ちを解決することです.
シングル・コアなら時分割処理やイベント・ループなんかでマルチタスクを実現することになるでしょう(バーチャル・マルチタスクとでも呼び分けた方がいいかもしれません). そしてマルチ・コア時代の手法としてThreadとかTaskという並列処理の手法を採用しているというわけです(こちらは真のマルチタスクですね).
Unityでは従来Taskを使うかCoroutineを使うかで非同期処理が可能でした. UniTaskもこのTaskをUnity用に改良したというところから来ているようです. ほぼTaskをUniTaskに置き換えて使えるようです.
UniTaskで値を監視できるか? (Ⅲ)
「3分間待つのだぞ」
作業中の待つ行為を抽象化したのがasync/awaitです. ハード的な問題に限定されずソフトウェア上で生じる待ち作業全般に利用できます.
C#本家ではTask/Task<T>とasync-awaitを組み合わせることで実行できます.
公式のAsynchronous programming with async and awaitが分かりやすいです. 料理の例ですが, スケジューリングが大事なのが分かりますね.
もう少し具体的な説明はWhat happens in an async methodが分かりやすかったです.
Asyncという接尾辞をメソッド名につけるのが慣例のようです.
でUnity用のTaskがUniTaskというわけですね. 内部的なことはまた調べる必要がありますが, Task以外でも独自に"待てるクラス"を作れるらしいのでそういう機能を使っているのでしょうか.
await可能なクラスを作ってみよう
UnityWebRequestでasync awaitする メモ
UniTask
UnityでOpenUPMを簡単に利用できるようになったを参考にUniTaskをインストールする.
UniTaskのアセンブリを追加しておく.
UniTask.Linqを忘れずに.
さてUniRxにあったEveryValueChangedと同名のメソッドがUniTaskにもある. 非同期ストリームと呼ばれるasync-foreachを使って記述できる. カメラにアタッチしたコンポーネントなのでt.rotationはカメラの回転を監視しています. 回転した場合平行移動のための移動量を計算する平面を変更する必要があるからです.
async UniTaskVoid Start() {
// ....setup
var valueChangeEnumerable = UniTaskAsyncEnumerable.EveryValueChanged(this.trasform, t => t.rotation, PlayerLoopTiming.LastUpdate);
await foreach (var _ in valueChangeEnumerable.WithCancellation(this.GetCancellationTokenOnDestroy())) {
this.dragArea = this.dragArea.Update();
}
}
疑問点
What happens in an async methodの以下の部分が謎でした. 結局スレッドは使うのかいつ生成するのか?
The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.
良く分からないがStackoverflowに似た質問があった.
If async-await doesn't create any additional threads, then how does it make applications responsive?
内容的にはシステムコールとかを理解する必要があるような感じ.
Experimental Packageを有効にする
Unity Test Frameworkの2系を追加しようとしたら表示されなくて困っていたら, PreviewじゃなくExperimentalになっていたらしい.
Unity 2021におけるパッケージ関連の変更について
Unity Test Framework 2.0 ready for feedback
オブジェクト摘出
uint型で表現されるIDをRGB(A)に変換するアルゴリズム
Picking with an OpenGL hackにおけるオブジェクトのIDごとの塗分け方法を考える.
色のRGBAの4要素に分解できる. それぞれ8bitで表現されるとすると255種類ずつである. つまり8桁ごとにマスクをかけて色要素を取り出す. それを右シフトすれば8ビットの値が4種類得られる. RGBAに対応させれば色が指定できる.
実装
まずは頂点のIDを取得する必要がある. これはSV_VertexIDというシェーダー・セマンティクスで取得してくれる.
なおUnityでは色は正規化されているので255.0で割っている.
// Input to the vertex shader
struct Attributes {
uint vertexID: SV_VertexID;
float4 vertex : POSITION;
};
// Output from the vertex shader
// Input to the fragment shader
struct Varyings
{
half4 color : COLOR;
float4 pos : SV_POSITION;
};
// vertex shader function
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.pos = TransformObjectToHClip(IN.vertex.xyz);
// convert from uint ids to RGBA colors
float a = (IN.vertexID & 0xff) >> 24;
float r = (IN.vertexID & 0xff) >> 16;
float g = (IN.vertexID & 0xff) >> 8;
// float b = (IN.vertexID & 0xff);
OUT.color = float4(r/255.0, g/255.0, b/255.0, 1.0f);
return OUT;
}
// fragment shader function
half4 frag(Varyings IN) : SV_Target
{
return IN.color;
}
後はこれを読み込めばよい.
Shader "ObjectID"
{
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalRenderPipeline"
}
Pass
{
Name "Update"
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "./ObjectIDColor.hlsl.hlsl"
#pragma vertex vert
#pragma fragment frag
#pragma target 5.0
ENDHLSL
}
}
}
結果
Reference
windowsアプリ、RGBの色を取り出す時のbitシフトの数字の意味が知りたい。
Color - Scripting API
プロジェクト管理 & パッケージ管理
Git管理
A Brief Anatomy of A Unity Project Folder
【Unity】【Package Manager】自作PackageをGitリポジトリのサブフォルダに置けるようになってた(Unity2019.3.4から)
Unity Custom PackageをGithub経由で配布
既存ライブラリを Unity Package Manager で導入できるようにする
Is it safe to ignore the 'Packages' folder in Unity 2018?
It seems in Unity 2020.1 (and probably earlier) that the actual package contents are held in Library/PackageCache, and only the manifest file lives in the root Packages folder.
とあり実際エクスプローラーから開くとPackagesフォルダはmanifest.jsonとpackages-lock.jsonしか入っていません. 実態はLibrary/PackageCacheに入っています.
.gitignore
Unity.gitignoreを使うのがいいかも.
パッケージ管理
Unity で .unitypackage で配布していたアセットを Package Manager 対応してみた
OpenUPM
CustomTexture.hlsl
Custom Textureをシェーダーでアクセスする.
ビルトインではUnityCustomRenderTexture.cgincというのが使われていました.
Unity-Built-in-Shaders/CGIncludes/UnityCustomRenderTexture.cginc
なおアセットからCustom Render Textureを作成した場合UnityCustomRenderTexture.cgincが読み込まれたシェーダが作成されます.
References
シェーダの基本
Custom Shaders HLSL and Core Library
シェーダ・グラフ
属性
DefaultExecutionOrder
相対的な実行の順番を指定できる.
DisallowMultipleComponent
コンポーネントが重複しないようにする.
AddComponentMenu
メニュー・リストのComponentに追加する.
RequireComponent
必要なコンポーネントを自動的に付加してくれる.
SerializeField
Scripting APIには以下のような説明があります.
Force Unity to serialize a private field.
とあります. Script serializationによるとシリアライゼーションとは,
Serialization is the automatic process of transforming data structures or GameObject states into a format that Unity can store and reconstruct later.
またクラスにSerializable属性を付けることでクラス全体をシリアライズできるようです.
基本的にはpublicなコンポーネントはインスペクター上に表示されます. この状態では他のクラスから変更可能なため知らないうちに値が変わってしまうということを防ぐためにもprivateにしておく必要があるようです.
Unityの[SerializeField]について色々な疑問に答えてみる
Unityで線を引く
Line Renderer
一番手軽. 現状Pickerの可視化に使いたいのでこれで十分.
Mesh API
モノによってはもう少し拡張性が必要な場合があるかも.
Procedural Meshes for Lines in Unity
Asset
Linefyという良さそうなアセットもあります.
VContainer (1) 概要
VContainerとは?
まずなぜUではなくVなのか.
"V" means making Unity's initial "U" more thinner and solid..!
薄くかつ中身の詰まっているという感じでしょうか. そんな願望を表しているようです. 普通にUではダメだったのでしょうか.
Containerというのは依存性を詰め込んだ箱のようなイメージでしょうか. DI(Dependency Injection)コンテナのコンテナです.
IContainerBuilder & IObjectResolver
コンテナはビルダーから作ります. 脈絡は無視すると,
builder.Register<YourService>(Lifetime.Scoped);
のようになります. これで依存性を詰め込んだコンテナが作られます. コンテナの作成はVContainerの仕事です.
次は依存性の注入ですが, 必要な依存性はVContainerが解決してくれます. 再び脈絡を無視すると,
var yourService = container.Resolve<yourService >();
となります. 依存性をコンテナに登録し必要な場所で解決して取り出すというのがVContainerの流れです.
Registering
無視した脈絡を考えましょう. コンテナの作成はLifetimeScopeを継承したクラス(コンポーネント)で行います.
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
public override void Configure(IContainerBuilder builder)
{
builder.Register<YourService>(Lifetime.Scoped);
}
}
これを空のオブジェクトに追加するというのが基本的な使い方です.
Resolving
同様に解決する場合も考えましょう. 解決する場所はPresenterとかControllerとにかく文脈で適切な名前を使います.
class ServicePresenter
{
public ServicePresenter(IObjectResolver container)
{
var yourService = container.Resolve<YourService>();
}
}
といった感じで解決して依存性を取り出してくれます. ただしこのようにAPIを明示的に使う必要はないようで,
class ServicePresenter
{
readonly YourService yourService ;
public ServicePresenter(YourService yourService)
{
this.yourService = yourService;
}
}
とすると勝手に解決されるようです.
When registering a class that needs dependencies, provide a constructor that takes them as arguments. VContainer will take care of the rest.
Reference
次回
Hello Worldを参考に, どういう役割分担すればよいか考える.
VContainer (2) 役割分担
Hello World
Hello Worldの例に出てくる
名前 | 役割 | 例 |
---|---|---|
View | 画面の表示 | HelloScreen |
service | 依存性として注入されるクラス | HelloWorldService |
presenter | 依存性を注入されるクラス | GamePresenter |
lifecycle marker interfaces | presenterに実装するインターフェース | IStartable |
lifetime scope | 依存性の生成されるタイミング | GameLifetimeScope |
builder | 依存性の登録先であるコンテナのビルダー | IContainerBuilder |
View
表示画面です. 今回はuGUIのbuttonです. Monobehavior継承クラスの場合が多いでしょう.
Service
ボタンが実行するロジックを提供するクラスです.
Presenter
分かりにくいですがMVCアーキテクチャ・パターンのControllerのような役割をするようです.
今回はViewにServiceを提供します. 普通ならView(ここではボタン)の依存性を直接渡しますが両者の関係をPresenterで記述します.
class GamePresenter {
readonly HelloWorldService helloWorldService;
readonly HelloScreen helloScreen;
public GamePresenter(
HelloWorldService helloWorldService,
HelloScreen helloScreen)
{
this.helloWorldService = helloWorldService;
this.helloScreen = helloScreen;
}
}
内部的にはIObjectResolverがコンストラクタの引数として渡されて依存性を解決してくれているはずです. ただしこのままでは依存性とは呼べません. 両者の間になんの依存関係もないからです.
Lifecycle marker interfaces
Lifecycle marker interfacesを使うとVContainerで定義された独自のPlayerLoopSystemを介してエントリー・ポイントにロジックを挿入できます.
IStartableというインターフェースを使うとStartというエントリー・ポイントでボタンにリスナーを追加することができます. これはMonobehaviour.Updateとほぼ同じと考えて良いようです.
class GamePresenter: IStartable {
readonly HelloWorldService helloWorldService;
readonly HelloScreen helloScreen;
public GamePresenter(
HelloWorldService helloWorldService,
HelloScreen helloScreen)
{
this.helloWorldService = helloWorldService;
this.helloScreen = helloScreen;
}
void IStartable.Start()
{
helloScreen.HelloButton.onClick.AddListener(() => helloWorldService.Hello());
}
}
Available interfaces - Plain C# Entry pointに利用可能なインターフェースが掲載されています.
Builder
IContainerBuilderのインスタンスです. 前回紹介しましたがコンテナを作成し, 依存性を登録しておきます.
登録対象
登録対象に応じていくつかのAPIを提供しています.
GamePresentorはエントリー・ポイントでした. よって登録にはRegisterEntryPointを使います.
builder.RegisterEntryPoint<GamePresenter>();
HelloScreenコンポーネントの場合はRegisterComponentメソッドを使います.
builder.RegisterComponent<HelloScreen>();
またHelloWorldServiceは生のC#としてRegisterで登録します.
builder.Register<HelloWorldService>(Lifetime.Singleton);
他にも様々な登録メソッドが提供されています.
Lifetime
Lifetime.Singletonは依存性をキャッシュしていつも同じインスタンスを注入するということです. Lifetime.Transientを指定すると依存性が解決されるたびに新しいインスタンスを生成して注入します.
もう一つLifetime.Scopedというのがあるのですが良く分かりません.
まとめ
普通ならHelloSceneとHelloWorldServiceは密結合になるところでしたが, VContainerのおかげで疎結合に保つことができました. 特にコンポーネントは密結合になりがちです. Viewがシンプルでステートレスに定義されているのもいいですね.
またLifetimeの指定することで, サービスのような常駐のインスタンスをキャッシュすることも簡単にできるようになりました.
Reference
VContainer (2) 依存性の登録
IContainerBuilderのRegisterXメソッド
Registerという名前が付くメソッドが複数あります. 依存性となる対象によっていろいろです.
Register
普通のC#クラスの場合に使います.
builder.Register<ServiceA>(Lifetime.Singleton);
インターフェースの場合は具象クラスとセットで登録するようです.
builder.Register<IServiceA, ServiceA>();
As
2つ以上のインターフェースがあって個別に指定したい場合
builder.Register<ServiceA>(Lifetime.Singleton)
.As<IServiceA, IInputPort>();
AsImplementedInterfaces
全部のインターフェースを登録する場合
builder.Register<ServiceA>(Lifetime.Singleton)
.AsImplementedInterfaces();
AsSelf
ここまでだとインターフェースで依存性を指定した場合しか解決されません. 別の言い方をすると具象クラスであるServiceAを注入することはできません. この場合AsSelfを追加で呼び出します.
builder.Register<ServiceA>(Lifetime.Singleton)
.AsImplementedInterfaces()
.AsSelf();
RegisterEntryPoint
lifecycle marker interfacesの場合はエントリー・ポイントととして登録します.
builder.RegisterEntryPoint<GamePresenter>();
### RegisterEntryPointExceptionHandler
例外処理を拡張したい場合はRegisterEntryPointExceptionHandlerを使います.
builder.RegisterEntryPointExceptionHandler(ex =>
{
UnityEngine.Debug.LogException(ex);
// Additional process ...
});
UseEntryPoints
複数のエントリー・ポイントを登録したい場合は
builder.RegisterEntryPoint<ScopedEntryPointA>();
builder.RegisterEntryPoint<ScopedEntryPointB>();
builder.RegisterEntryPoint<ScopedEntryPointC>().AsSelf();
builder.RegisterEntryPointExceptionHandler(ex => ...);
としてもいいですがUseEntryPointsを使います.
builder.UseEntryPoints(entryPoints =>
{
entryPoints.Add<ScopedEntryPointA>();
entryPoints.Add<ScopedEntryPointB>();
entryPoints.Add<ScopedEntryPointC>().AsSelf();
entryPoints.OnException(ex => ...)
});
RegisterInstance
クラスとかインターフェースではなく実行時にインスタンスを登録したい場合はRegisterInstanceを使います.
// ...
var obj = new ServiceA();
// ...
builder.RegisterInstance(obj);
意味的には以下と同じです. 明示的にインスタンスを登録するか, DIコンテナ側に任せるかの違いです.
builder.Register<ServiceA>(Lifetime.Singleton)
インスタンスを登録するとDIコンテナではリソースの開放(dispose)が自動的に実行されないです. 特定の引数リストを取る場合なんかに使うのでしょうようです.
Register with Delegate
インスタンスの生成をデリゲート内で行うと, 特定のインスタンスを生成しつつDIコンテナによって寿命の管理ができます.
builder.Register<IFoo>(container =>
{
var serviceA = container.Resolve<ServiceA>();
return serviceA.ProvideFoo(); // fooインスタンスの生成?
}, Lifetime.Scoped);
これは以下のように簡潔に書いても良いらしい.
builder.Register<IFoo>(_ =>
{
var foo = new Foo();
// Do something;
return foo;
}, Lifetime.Scoped);
IObjectResolver.Instantiate
C#のインスタンスではなく, ゲーム・オブジェクトの場合はIObjectResolverのInstantiateを使います.
builder.Register(container =>
{
return container.Instantiate(prefab);
}, Lifetime.Scoped);
WithParameter
WithParameterを使うとクラスとパラメーター値のセットを依存性として登録できます.
builder.Register<SomeService>(Lifetime.Singleton)
.WithParameter<string>("http://example.com");
SomeServiceコンストラクタがstring型の引数を取る場合URL文字列として解決されます. ややあいまいなのでより明確にパラメーターの名前も指定できます.
builder.Register<SomeService>(Lifetime.Singleton)
.WithParameter("url", "http://example.com");
RegisterComponent
コンポーネントを登録したい場合は以下のようになります.
public class GameLifetimeScope : LifetimeScope
{
[SerializeField]
HelloScreen helloScreen;
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterEntryPoint<GamePresenter>();
builder.Register<HelloWorldService>(Lifetime.Singleton);
builder.RegisterComponent(helloScreen);
}
}
ただこれだとコンポーネントがいっぱい使われる場合どうするんでしょうね?
VContainer (3) LifetimeScope
LifetimeScopeとは?
依存性となるインスタンスの寿命を管理するためのスコープということのようです. 階層構造を持ち, その範囲内で指定された手法で依存性を解決します.
class ClassA
{
public ClassA(LifetimeScope currentScope)
{
// You can inject LifetimeScope if you need to, but
// in this case it would be enough to just inject ServiceA.
var foo = currentScope.Container.Resolve<ServiceA>();
}
}
内部的には以下のようにコンテナへアクセスして依存性を解決しているようです.
依存性の生成と再利用
ストラテジー | 説明 |
---|---|
Lifetime.Singleton | 単一のインスタンスを寿命が続く限り再利用して注入する |
LifeTime.Transient | 常に新しいインスタンスを生成して注入する |
Lifetime.Scoped | Singletonと同じようにふるまうが子には新しい別のインスタンスを生成し注入する |
LifetimeScopeコンポーネント
LifetimeScopeクラスを継承して作るクラスを空のオブジェクトに追加することでスコープを設定します.
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
public override void Configure(IContainerBuilder builder)
{
builder.Register<YourService>(Lifetime.Scoped);
}
}
インスペクタ上では以下のようになります.
Parentフィールドから親のスコープを設定できます.
Root Lifetime Scope
適当にRootLifetimeScop.csを作る.
using VContainer;
using VContainer.Unity;
public class RootLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
}
}
- 空のオブジェクトからPrefabを作っておきます.
- RootLifetimeScopコンポーネントをPrefabに追加する
- VContainerSettingsアセットを作る(Create -> VContainer -> VContainer Settings)
- 2のPrefabをVContainerSettingsのRoot Lifetime Scopeに設定する.
EnqueueParent
シーンのロードなど動的なタイミングで構築する場合EnqueueParentメソッドで親を指定できます.
using (LifetimeScope.EnqueueParent(parent))
{
// parent a generated lifetime scope to the parent instance
}
Auto Inject Game Objects
LifetimeScopeコンポーネントのフィールドで指定したオブジェクトで[Inject]属性を持つメソッドを定義したコンポーネント(MonoBehaviour継承クラス)が追加されている場合, 自動的に依存性が解決される.
The MonoBehaviours of all specified GameObject (and their children) will be automatically Injected when the LifetimeScope is initialized.
# エラー
'YourNameSpace.YourClass' is missing the class attribute 'ExtensionOfNativeClass'!
ネイティブ・スクリプトをゲーム・オブジェクトに追加した場合にでる
UniTaskへの非同期処理 (1) C#における非同期処理
Task-based Asynchronous Pattern (TAP)
Taskクラスを使った非同期処理が現在のC#の標準となっている.
The core of async programming is the Task and Task<T> objects, which model asynchronous operations.
async/awaitなしでやる場合は以下を参照する.
Task-based asynchronous pattern (TAP) in .NET: Introduction and overview
非同期処理の種類
大別するとI/O-boundなコードとCPU-boundなコードに分けられる. 後者は比較的わかりやすいです.
var result = await Task.Run( _ => CPUIntensiveMethod());
Task.Runはスレッド・プールで処理されます.
Queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work.
よってこれはマルチコアCPUの場合並列処理となるはずです. I/O-boundの場合はTask<T>型を返すようなメソッドを後に置きます. この場合Task Parallel Libraryは使ってはいけないという注意書きがあります.
If the work you have is I/O-bound, use async and await without Task.Run. You should not use the Task Parallel Library.
ステート・マシン
Async/awaitが絡むコードはコンパイラによってステート・マシンに変換されるらしいです.
On the C# side of things, the compiler transforms your code into a state machine that keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished.
awaitを忘れるな.
忘れると当然処理は呼び出し側に戻らないです.
非同期メソッドの名前習慣
末尾にAsyncをつけろとのこと.
public async Task<string> DoSthAsync(int id) { /* ... */ }
ノン・ブロッキングにTaskを待とう
必ずしもawaitが適していない場面もあるようです.
処理 | 代替 |
---|---|
await | Task.Wait/Task.Result |
await Task.WhenAny | Task.WaitAny |
Task.WhenAll | Task.WaitAll |
Task.Delay | Thread.Sleep |
その他
- async voidはイベント・ハンドラーで使え
- ValueTask
- ConfigureAwait
Reference
UniTaskへの非同期処理 (2) AwaitableとAwaiter
AwaitableとAwaiter
awaitは基本的にTaskかTask<TResult>を取ります. [1]
Starting with C# 7.0, any type that has an accessible GetAwaiter method.
とあり, この説明文からはGetAwaiterというメソッドがあれば十分なように見えます. [2]
Awaiterを返すのがAwaitableです. IEnumaratorとIEnumerableの関係に似ていますね.
The task of an await_expression is required to be awaitable.
await式では正確にはawaitableである必要があります. Taskクラスはそれを満たしています. ほかにもいろいろ条件が書いてありますが, 非同期メソッドの内部実装の例が非常に分かりやすいです.
// 同名のメソッドを持っていれば型は問わない。
class Awatable
{
public Awaiter GetAwaiter() { }
}
// 同上、同名のメソッドを持っていれば型は問わない。
struct Awaiter
{
public bool IsCompleted { get; }
public void OnCompleted(Action continuation) { }
public T GetResult() { }
}
UniTaskもこんな感じで定義されているんだと思いますが, 整理すると以下のようになります.
名前 | 説明 |
---|---|
GetAwaiter | Awaiter型のインスタンスを返すメソッド |
IsCompleted | タスクが終了しているかを表すフラグ |
INotifyCompletion.OnCompleted | タスク終了時に実行される継続処理を表すメソッド |
GetResult | タスクの結果を取得するメソッド |
ただしこれを動きそうな感じにしてもコンパイルできません.
using System;
using System.Threading;
using System.Runtime.CompilerServices;
using UnityEngine;
public class AsyncLogic : MonoBehaviour
{
public class Awaitable
{
public HelloWorldAwaiter GetAwaiter() { return new HelloWorldAwaiter(); }
}
// 同上、同名のメソッドを持っていれば型は問わない。
public struct HelloWorldAwaiter : INotifyCompletion
{
public bool IsCompleted { get { return false; } }
public void OnCompleted(Action continuation)
{
continuation();
}
public string GetResult() { return "Hello world"; }
}
public async HelloWorldAwaiter DoAsync() {
await new Awaitable();
}
}
エラーメッセージには,
とあります. 何が何だか分かりませんが, とりあえず実装しようとしていたのはtask-like型ということが分かりました.
## 追記
GetAwaiterメソッドが返す方をawaiter型と呼んだりします.
- System.Runtime.INotifyCompletionを実装している
- void OnCompleted(Action continuation)
- bool IsCompleted
- GetResult()
上の条件を満たしている必要があります. Exploring the async/await State Machine – The Awaitable Patternを参考にMyAwaitableクラスを定義してみる.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using UnityEngine;
public class AsyncLogic : MonoBehaviour
{
private static bool _returnCompletedTask;
public class MyAwaitable
{
public MyAwaiter GetAwaiter() => new MyAwaiter();
}
// 同上、同名のメソッドを持っていれば型は問わない。
public struct MyAwaiter : INotifyCompletion
{
public bool IsCompleted {
get
{
Debug.Log("IsCompleted called");
return _returnCompletedTask;
}
}
public void OnCompleted(Action continuation)
{
Debug.Log("OnCompleted called");
continuation();
}
public int GetResult()
{
Debug.Log("GetResult called");
return 5;
}
}
// Start is called before the first frame update
async void Start()
{
_returnCompletedTask = true;
int result1 = await new MyAwaitable();
Debug.Log($"result1: {result1}");
_returnCompletedTask = false;
int result2 = await new MyAwaitable();
Debug.Log($"result2: {result2}");
_returnCompletedTask = true;
int result3 = await DoAsync();
Debug.Log($"result3: {result3}");
}
public async Task<int> DoAsync() => await new MyAwaitable();
}
Task<int>にすることで一応動くようです.
Reference
UniTaskへの非同期処理 (3) 状態マシン
非同期の正体見たり状態マシン
Exploring the async/await State Machine – Concrete Implementationを参考にする.
IAsyncStateMachine
状態マシンはIAsyncStateMachineを実装している必要があります.
AsyncStateMachine属性
AsyncStateMachineAttribute Class
Working Exampleを考える
Reference
UniTaskへの非同期処理 (4) Task-like型
カスタムAwaiter
Reference
Visual Studioの設定
言語パックの追加
エラー・メッセージの検索が難しくなるので英語表記にする.
Windowsキーを押してVisual Studio Installerを検索する. 起動したら以下のような画面が表示される.
対象となるバージョンを選んで変更を押す. 言語パック・タブで表示を切り替えて, 追加したい言語パックにチェックを入れる. 右下の変更ボタンを押す.
インストールが始まるので起動中のVisual Studioがある場合は停止しておく. (場合によっては)気長に更新が終わるのを待つ. 起動ボタンから起動してみる. プロジェクトの読み込みを(再度)気長に待つ.
Visual Studioの言語設定を変更
日本語を削除すると勝手に英語になる. 手動で変更するにはツールバーのToolsメニューから, Tools -> Optionsダイアログを表示する. Environmentのドロップダウン・リストを表示して, International Settingsを選ぶ. LanguageをEnglish(あるいは好みの言語)に変更する.
##Reference
VisualStudioでのTODO管理
描き方
// TODO: Write to do something!!
確認方法
View -> Task List
VisualStudioでのTODO管理
描き方
// TODO: Write to do something!!
確認方法
View -> Task List
ProBuilderメモ
選択対象のハイライト
EditorHandleDrawingScopes.LineDrawingScope にApplyWireMaterialとある.
これはUnityCsReferenceというエンジンのC#実装部分にあるHandleUtility. ApplyWireMaterialを呼んでいる. この先はC++のエンジンがうまいことやるのかもしれない.
さてEditorHandleDrawingにはこれ以外にも,
- lineMaterial
- edgeUnselectedColor
- edgeSelectedColor
EditorHandleDrawing.DrawSceneHandlesでEdgeモードの際に描画されることから, これが選択時のハイライトの部分と思われる.
case SelectMode.Edge:
case SelectMode.TextureEdge:
{
// When in Edge mode, use the same material for wireframe
Render(wireHandles, m_ForceEdgeLinesGL ? glWireMaterial : edgeMaterial, edgeUnselectedColor, CompareFunction.LessEqual, false);
if (xRay) Render(s_SelectedEdgeHandles, m_ForceEdgeLinesGL ? s_GlWireMaterial : s_EdgeMaterial, edgeSelectedColor * k_OccludedTint, CompareFunction.Greater);
Render(s_SelectedEdgeHandles, m_ForceEdgeLinesGL ? s_GlWireMaterial : s_EdgeMaterial, edgeSelectedColor, CompareFunction.LessEqual);
break;
}
ProBuilderEditor.OnSceneGUIから呼び出されている.
EditorHandleDrawing.DrawSceneHandles(SceneDragAndDropListener.isDragging ? SelectMode.None : selectMode);
表示
選択した対象か選択できる対象を描画していると思われる.
操作
Utilityには
- PickObject
- PickFace
という二つの重要そうなメソッドがある.
MeshEditorのUpdateで呼び出されているけど何か関係があるのか?
if(!m_DragState.active)
m_Selection = Utility.PickFace(m_SceneCamera, Input.mousePosition);
またAwakeでは選択対象を描画すると思われる処理もある.
void Awake()
{
m_SceneCamera = Camera.main;
m_CameraMotion = m_SceneCamera.GetComponent<CameraMotion>();
Camera.onPostRender += DrawSelection;
}
ただこのデリゲートは
Handles.Draw(m_Selection.mesh, m_Selection.face, Color.cyan);
とColor.cyanで表示される. こんな色は見たことないです.
Gizmo
Gizmoに関してはEditorHandleDrawingScopes.DrawGizmoというズバリそれっぽいメソッドを見つけました.
選択したオブジェクトの色を変える
マテリアル経由
とりあえずraycastでオブジェクトを選択する. Rendererコンポーネント経由でmaterialにアクセスできる.
colorあるいはSetColorメソッドで変更できる.
GetComponent<MeshRenderer>.material.color = yourColor;
GetComponent<MeshRenderer>.material.SetColor(yourColor);
ただこれは遅いらしくPropertyToIDを使うと良いらしい.
シェーダープロパティアクセスが2.5倍早くなるPropertyToID関数
【Unity】ShaderプロパティへのアクセスはShader.PropertyToID を使用した方が早い
単にidを指定するだけで速くなるらしい.
id = Shader.PropertyToID("_Color")l
GetComponent<MeshRenderer>.material.SetColor(id, Color.pink)
実際の例は以下が参考になる.
Advanced Mesh API (1)
頂点レイアウト
VertexAttributeDescriptorで指定する.
VertexAttributeで頂点属性を指定できる. またデータ型はVertexAttributeFormatを使う.
頂点レイアウト
頂点データはNativeArray<T>で指定するのでGPU側でのデータの解釈を指示する必要があります.
Mesh.SetVertexBufferParamsで頂点数とレイアウトを指定する.
頂点データの作成
NativeArray<T>を使います.
頂点データの転送?
実際は転送しませんが, メッシュにデータを渡します.