Open34

Unity学習録

brainvaderbrainvader

Scriptable Render Pipeline (SRP)

  1. 公式ドキュメント
  1. 一からSRPを作っていくチュートリアル

Rendererなども自作しながらビルトイン・パイプラインがどのような処理を実行しているか学べる.

  1. URPにCustom Scriptable Rendererを実装する

URPを前提にScriptableRendererDataScriptableRendererのカスタマイズの仕方を学べる.

  1. 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を書かなくてもある程度のことはできるよという例です.

ちなみにUniversal Render Pipelineライブラリにアクセスするには, プロジェクトのフォルダ以下にある, Library\PackageCache以下にあるcom.unity.render-pipelines.universal@12.1.4を覗くといいようです. URPのレンダリング方式の一つであるForwardRendererもForwardRender.csという形で実装を見ることができます.

brainvaderbrainvader

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何だと思う.

Universal 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

Texture2D.ReadPixels

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クラス

brainvaderbrainvader

Unityでのスクリプト・ディレクトリの構成

Assets直下に置かないほうが良いらしい.

  • パッケージとしての再配布容易性
  • サードパーティー製のパッケージ(アセット)と共存

とかそんな感じらしい. Unity Test Framework完全攻略ガイドの構成を踏襲した.

Unityプロジェクトのディレクトリ構成と .gitignore

プロダクション・コードとテストコードを分ける

フォルダを分けてAssembly Definitionというのを定義して参照してやる必要がある. Test Runnerウィンドウからボタンで作成することもできる.

が細かい制御が必要な場合はアセットとして追加できる. Asset -> Assembly Definitionでアクティブなフォルダ直下に作成されます. これをプロダクション・コードとテスト・コードにニコイチで作成する.

プロダクション・コードは以下のようになる.

テスト・コードではプロダクション・コードのアセンブリを参照するようにする.

細かいことは後で調べるます.

Assembly definitions
Workflow: Creating test assemblies

brainvaderbrainvader

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

brainvaderbrainvader

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.

C#のyield returnの内部挙動を理解する

コルーティン

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.

何か長い処理とかやるとそこのはコルーティンの実行時間としてスレッドを占有するために, ブロッキングが生じるようです.

本当に同時に実行するにはスレッドが必要になります.

小括

単純に上から下に実行する同期的な処理からコルーティンを使うことで制御に幅を持たせることができるようになることが分かった.

brainvaderbrainvader

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に置き換えて使えるようです.

brainvaderbrainvader

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?

内容的にはシステムコールとかを理解する必要があるような感じ.

【Unity開発者向け】「SynchronizationContext」と「Taskのawait」

brainvaderbrainvader

オブジェクト摘出

uint型で表現されるIDをRGB(A)に変換するアルゴリズム

Picking with an OpenGL hackにおけるオブジェクトのIDごとの塗分け方法を考える.

色のRGBAの4要素に分解できる. それぞれ8bitで表現されるとすると255種類ずつである. つまり8桁ごとにマスクをかけて色要素を取り出す. それを右シフトすれば8ビットの値が4種類得られる. RGBAに対応させれば色が指定できる.

実装

まずは頂点のIDを取得する必要がある. これはSV_VertexIDというシェーダー・セマンティクスで取得してくれる.

なおUnityでは色は正規化されているので255.0で割っている.

ObjectIDColor.hlsl
// 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;
}

後はこれを読み込めばよい.

ObjectID.shader
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

brainvaderbrainvader

プロジェクト管理 & パッケージ管理

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 対応してみた

Project manifest

Creating custom packages

OpenUPM

OpenUPMにパッケージを公開する&リリースの自動化をする

UPMパッケージをunitypackage形式で配布する

brainvaderbrainvader

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

シェーダ・グラフ

Custom Render Texture Shader Graph

brainvaderbrainvader

属性

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】[SerializeField]ってなに?

Unity serializeについてのまとめ

brainvaderbrainvader

Unityで線を引く

Line Renderer

一番手軽. 現状Pickerの可視化に使いたいのでこれで十分.

Mesh API

モノによってはもう少し拡張性が必要な場合があるかも.

Procedural Meshes for Lines in Unity

Asset

Linefyという良さそうなアセットもあります.

Linefy

brainvaderbrainvader

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を継承したクラス(コンポーネント)で行います.

GameLifetimeScope.cs
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    public override void Configure(IContainerBuilder builder)
    {
        builder.Register<YourService>(Lifetime.Scoped);
    }
}

これを空のオブジェクトに追加するというのが基本的な使い方です.

Resolving

同様に解決する場合も考えましょう. 解決する場所はPresenterとかControllerとにかく文脈で適切な名前を使います.

ServicePresenter.cs
class ServicePresenter
{
    public ServicePresenter(IObjectResolver container)
    {
        var yourService = container.Resolve<YourService>();
    }
}

といった感じで解決して依存性を取り出してくれます. ただしこのようにAPIを明示的に使う必要はないようで,

ServicePresenter.cs
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.

Constructor Injection

Reference

VContainer Docs

次回

Hello Worldを参考に, どういう役割分担すればよいか考える.

brainvaderbrainvader

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で記述します.

GamePresenter.cs
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とほぼ同じと考えて良いようです.

GamePresenter.cs
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

【Unity】PlayerLoopを使って毎フレーム実行される関数を追加する

VContainer入門 – MVPパターンを組んでみる

brainvaderbrainvader

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);
    }
}

ただこれだとコンポーネントがいっぱい使われる場合どうするんでしょうね?

brainvaderbrainvader

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クラスを継承して作るクラスを空のオブジェクトに追加することでスコープを設定します.

GameLifetimeScope.cs
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を作る.

RootLifetimeScope.cs
using VContainer;
using VContainer.Unity;

public class RootLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
    }
}
  1. 空のオブジェクトからPrefabを作っておきます.
  2. RootLifetimeScopコンポーネントをPrefabに追加する
  3. VContainerSettingsアセットを作る(Create -> VContainer -> VContainer Settings)
  4. 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.

brainvaderbrainvader

# エラー

'YourNameSpace.YourClass' is missing the class attribute 'ExtensionOfNativeClass'!

ネイティブ・スクリプトをゲーム・オブジェクトに追加した場合にでる

brainvaderbrainvader

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

  1. Asynchronous programming
brainvaderbrainvader

UniTaskへの非同期処理 (2) AwaitableとAwaiter

AwaitableとAwaiter

awaitは基本的にTaskかTask<TResult>を取ります. [1]

Async return types (C#)によると,

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

  1. How Async and Await Work
  2. await可能なクラスを作ってみよう
脚注
  1. ValueTaskとValueTask<TResult>という変種もあります. ↩︎

  2. C#8から導入されたIAsyncEnumerable<T>の場合は除きます. ↩︎

brainvaderbrainvader
brainvaderbrainvader

Visual Studioの設定

言語パックの追加

エラー・メッセージの検索が難しくなるので英語表記にする.

Windowsキーを押してVisual Studio Installerを検索する. 起動したら以下のような画面が表示される.

対象となるバージョンを選んで変更を押す. 言語パック・タブで表示を切り替えて, 追加したい言語パックにチェックを入れる. 右下の変更ボタンを押す.

インストールが始まるので起動中のVisual Studioがある場合は停止しておく. (場合によっては)気長に更新が終わるのを待つ. 起動ボタンから起動してみる. プロジェクトの読み込みを(再度)気長に待つ.

Visual Studioの言語設定を変更

日本語を削除すると勝手に英語になる. 手動で変更するにはツールバーのToolsメニューから, Tools -> Optionsダイアログを表示する. Environmentのドロップダウン・リストを表示して, International Settingsを選ぶ. LanguageをEnglish(あるいは好みの言語)に変更する.

##Reference

brainvaderbrainvader

VisualStudioでのTODO管理

描き方

// TODO: Write to do something!!

確認方法

View -> Task List

brainvaderbrainvader

VisualStudioでのTODO管理

描き方

// TODO: Write to do something!!

確認方法

View -> Task List

brainvaderbrainvader

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);

表示

Handles.DrawLine

MeshEditor

選択した対象か選択できる対象を描画していると思われる.

操作

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というズバリそれっぽいメソッドを見つけました.

brainvaderbrainvader

選択したオブジェクトの色を変える

マテリアル経由

とりあえずraycastでオブジェクトを選択する. Rendererコンポーネント経由でmaterialにアクセスできる.

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)

実際の例は以下が参考になる.

【Unity】スクリプトからマテリアルの色(Properties)を変更する

brainvaderbrainvader

Advanced Mesh API (1)

頂点レイアウト

VertexAttributeDescriptorで指定する.

VertexAttributeで頂点属性を指定できる. またデータ型はVertexAttributeFormatを使う.

頂点レイアウト

頂点データはNativeArray<T>で指定するのでGPU側でのデータの解釈を指示する必要があります.

Mesh.SetVertexBufferParamsで頂点数とレイアウトを指定する.

頂点データの作成

NativeArray<T>を使います.

頂点データの転送?

実際は転送しませんが, メッシュにデータを渡します.

Mesh.SetVertexBufferData