YAMADA TAISHI’s diary

ゲームについてとか私の日記とか。このブログのあらゆるコードは好きにどうぞ。利用規約があるものは記事内のGitHubのRepositoryのリンクで貼られていると思うので、そちらを参照ください。

UnityでFMODを使おう!【前編】

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
FMODの使い方を探した時に日本語記事がなかったので本記事を作成しました。
他の人の取っ掛かりになればなと思います。

後編はコチラ
orotiyamatano.hatenablog.com

関連
orotiyamatano.hatenablog.com

目次


そもそもミドルウェア・オーディオエンジンが使えると何が嬉しいのか


  • サウンドミキサー機能がUnity標準以外も使えるようになる!

バーブとか色々

ゼルダやオクトパストラベラーとかにも使われてましたよね note.com

BGMが再生されているときにSEやキャラクターの音声が埋もれてしまったり、音割れしてしまうことはありませんか?
BGM側の音をリアルタイムに小さくなるように音量調整する機能をダッキングと言います。

studio-sunny-side.hatenablog.com

  • 負荷が小さい

サウンドレイテンシー、メモリ消費、ストリーム負荷、圧縮音声のデコード負荷等、ミドルウェア側でやってくれる。

  • ランダム再生などが楽

ランダム再生させるときはUnityの標準機能で行う場合自分でロジックをプログラミングすることになるが、ミドルウェア側で設定すればロジックを組むことなく再生できる。

他にも使う理由はあると思いますが、このあたりが主に皆さんが使う理由の中心だと思います。

FMODとは


ミドルウェア・オーディオエンジンです。
ゲームなどで使われ、音声の管理や再生をやりやすくするするミドルウェアです。
類似のサウンドエンジンとしてCRI(ADX等),Wwiseなどが存在します。

ちなみにUnityのオーディオ機能の内部ではFMODが使われてるらしい。
(なので相性は良いかも?)

サウンドエンジンと比較してみる


CRI Wwise FMOD
小規模開発環境の価格 個人/小規模向け(前年度年商が1,000万円以下)ADX2 LEなら無料。
その売上以上はADX2に移行。
開発予算$150k以下なら無料。
現在$150kは日本円だと1600万くらい?
開発予算$500k規模まで無料、
1ゲームあたり$2000(年間売上$200k以下なら無料。
現在$200kは日本円だと2100万くらい?)
商用の価格 ADX2なら初期費用Androidなら35万、月間250万以上超えると更に増えていく?
詳しくは知らない
プロジェクトごとの料金。$1500k以下なら$7200 ベーシックなら開発予算$500k~1500kだったら1ゲームあたり$5000
ロゴ表記 ADX2 LE は必要なし。ADX2は25万払えばロゴ表記破棄できる 必要なし? インディー版なら必須、ベーシックなら$5000払えばロゴ表記を破棄できる
機能制限 LEはブラウザや家庭ゲーム機未対応。暗号化機能もなし。 無料版だとサウンドファイル、メディアアセット数が500まで インディー版でも、機能には制限なし
対応プラットフォーム
(エンジン側が対応していないのもあるので注意)
Swich,PSVita,PS4/PS5,
iOS,Android,Webブラウザ(WebGL),Xbox Series,Stadia,
アーケード,macOS,Windows,Xbox One(ADX2の一覧。他自動車向けのソフトウェアなどもあるみたい?)
Android,Linux,iOS,maxOS,Windows,
PS4/PS5,Swich,Xbox One,Xbox Series, Stadia,tvOS,Universal Windows Platform,android tv
Windows,macOS,Linux,iOS,Android,
HTML5(WebGL),PS5/PS4,Xbox,Xbox Series,Switch
未対応プラットフォームについて
(公式ではVR機器対応と書いてないものもあったが基本的にはどれもOculusは動きそう)
ADX2LEの場合はWebGLや家庭ゲーム機未対応 WebGL未対応 Stadia未対応
対応エンジン Unity,UE4,Cocos2d-x Unity,UE4(推してるのはUnityとUE4だが、Cocosなどでも使えるっぽい?) Unity,UE4,CryEngine
使っている作品(画像を右クリックして別タブで開けば大きな画像がみれます)
スマホ以外にもタイトルはあるが、スマホ推し?

他にもいっぱいタイトルがあった。表示してるものは現在最新

価格の面からかインディータイトルが多め?
特徴 日本企業。リップシンクなどCRI独自の多彩な機能がある。
日本語資料が多いので、日本でよく使われる。
カナダ企業。世界で最も利用されているっぽい。日本語の説明もある。 オーストラリア企業。競合と比べ安い。少し機能は少なめ。
UXが良い。日本語対応は今は無し

詳細に関してはコチラ。

https://www.fmod.com/licensing#premium

  • FMODの採用タイトル

https://www.fmod.com/games

どうしてFMODを使うのか


価格が良心的で、UXが良くて、WebGLプラットフォーム対応無料で使えるものがFMODしか無いから!

今までFMODが日本で話題にならなかった理由


  • 日本語に一切対応していない
  • インディー版が無料になったのが2020年12月頃に発表と、割と最近

jp.gamesindustry.biz

FMODの使い方


英語が使える方ならFMODの使い方を覚えるのは容易だと思います。
ラーニング用の教材も豊富でドキュメントもしっかりある。

https://www.fmod.com/learn

とはいえ、私を含め英語が達者ではない人も多いと思います。
なので今回はクイックチュートリアルとUnityに組み込むまでを簡単にではありますが、日本語訳してみようと思います。
本記事ではクイックチュートリアルのみ翻訳します。Unityへの組み込みは後編にて取り扱います。

FMOD Studio - クイックスタートチュートリアル


始める前に


ダウンロードはコチラから。
https://www.fmod.com/download

(FMODのサイトへの登録が必須)

本編


補足しながら、ざっくり翻訳(というか意訳)していきます。
↓翻訳元ページ
https://www.fmod.com/resources/documentation-studio?version=2.1&page=quick-start-tutorial.html

本記事はWindowsベースで記述しますが、多分Macでも、ほぼ同じだと思います。

チュートリアルは概念を学んでから本チュートリアルを読むのがオススメします。
(概念についてはこちら。今回は飛ばします https://www.fmod.com/resources/documentation-studio?version=2.1&page=fmod-studio-concepts.html )

1.FMOD Studioの起動

ダイアログが表示されます。

2.チュートリアルでは新規プロジェクトが必要なので、右下の[New Project]をクリック

最初のダイアログが消え、「EventEditerWindow(イベントエディタウィンドウ)」に[Untitled]という名前で新規のプロジェクトが表示されます。

(EventEditerWindowとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#event-editor-window )

このまま作業開始することも出来ますが、最初にプロジェクトを保存しておくのをオススメします。

3.左上の[File]を開き、[Save]を選択

4.場所を選択し、覚えやすいプロジェクト名を命名して[保存(S)]ボタンをクリック

(私はNewProjectという名前で今回は保存します)
これでプロジェクトをいつでも保存、ロード可能になりました。

早速、コンテンツの作成をしていきます。
FMOD Studioのコンテンツの基本単位は、「Event(イベント)」と呼ばれます。
(Eventとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#event )
EventはEventEditerWindowの左側にある、「EventBrowser(イベントブラウザ)」で作成、編集します。
(EventBrowserとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#events-browser )

5.EventBrowserを右クリックし、「ContextMenu(コンテキストメニュー)」から[New Event]>[New 3D Event Timeline]を選択、[Enter]キーを押して名前を確定

(ContextMenuとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#context-menu )

すると、[New Event]という名前のEventが作成されます。
[New Event]の「Timeline(タイムライン)」と「ParameterSheet(パラメーターシート)」のプロパティが
EventBrowserの右側の「Editor(エディタ)」と「OverView(オーバービュー)」と「Deck(デッキ)」に表示されます。

(Timelineとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#timeline )
(ParameterSheetとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#parameter-sheet )
(Editorとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#editor )
(Deckとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#deck )
(OverViewとは一番右側の概要説明ウィンドウのこと。プレビューとかが表示される)

EventEditorWindowの詳細について知りたい場合は コチラ を参照ください。

[New Event]は作成したばかりのため、再生ボタンを押しても何も音が鳴りません。
音を鳴らすためにはオーディオファイルをインポートする必要があります。

6.EventBrowserのAssetsタブをクリック

「AssetsBrowser(アセットブラウザ)」が表示されます。

(AssetsBrowserとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#assets-browser )

AssetsBrowserにはプロジェクトにインポートされた全てのオーディオファイルが表示されます。
今はまだインポートしていないため空です。

7.[File]>[Import Assets...]をクリック

インポートするファイルが選択出来るようになります。

8.今回はサンプルとして コチラ からQuick Start Tutorial Assets(「one.ogg」「two.ogg」「three.ogg」)をダウンロードし、解凍。その3つを選択して[開く(O)]をクリック すると、アセットがインポートされAssetsBrowserに3つのアセットが表示されます。

AudioAssetsの詳細について知りたい場合は コチラ を参照ください。

9.Editerで[Audio 1]のオーディオトラックを右クリック、ContextMenuから[Add Multi instrument]をクリック

オーディオトラックに青い(?)ボックスが表示されます。
コレがMulti Instrumentです。

10.Multi Instrumentをクリックして選択

するとMulti InstrumentのプロパティがDeckに表示されます

11.AssetsBrowserで一番上のアセットをクリックし、Shiftキーを押しながら一番下のアセットをクリックして全てのアセットを選択

12.アセットをドラックしてMulti Instrumentのプレイリストにドラッグします

Multi Instrumentは実行(トリガー)されるとプレイリストの内容に応じて音声を出力します。
再生ボタンを押してEventを再生し試聴することでテストできます。

13.TransportBar(トランスポートバー)の再生ボタンをクリック

TimeLineの再生位置がInstrumentまで行くと、instrumentが実行(トリガー)されると(今回の設定の場合)3つのアセットの中からランダムに再生されます。

14.TransportBarの停止ボタンをクリックしてオーディオを停止します。もう一度聴きたい場合は再生ボタンを再度クリックしてください。

Instrumentの詳細が知りたい場合は コチラ を参照ください。

現在の設定のままだと、Eventをゲーム中からパラメーターを変更する方法がありません。それを修正しましょう。
パラメーターを使用すると、パラメーターの値を変更することによってEventの動作を変更させることが出来ます。

パラメーターを元に動作するイベントを追加するには、まず最初にプリセットパラメーターをプロジェクトに追加する必要があります。

15.ツールバーの[Window]>[Preset Browser]を選択

Preset Browserが表示されます。
ここでは、プリセットパラメーターとプリセットエフェクトを作成、編集できます。

16.ParametersBrowserの空の部分を右クリックし、ContextMenuに表示される[New Parameter]をクリック

[Add Parameter]ダイアログが表示されます。

17.デフォルト設定で問題ないので、[OK]ボタンをクリック

パラメータの追加ウィンドウが消え、「Parameter 1」という名前のパラメータがParametersBrowserに表示されます。

パラメータとプリセットパラメータの詳細については、 パラメータパラメータリファレンス の章を参照してください。

Preset Browserはもう必要ないので閉じます。

18.Preset Browserウィンドウの☓ボタンをクリックして閉じます

このチュートリアルでは、Eventのボリューム(何故かチュートリアルとUIが違うので今回はボリュームでいきます)をパラメーターの値に依存させるようにします。

19.Event Master Trackを選択するには、Master Trackのヘッドをクリックします。

トラックの シグナルチェーン がDeckに表示されます。さらに、EventのMacros DrawerがDeckの右端に表示されます。

20.ピッチナンバーボックスを右クリックし、ContextMenuから[Add Automation]を選択します

「自動化および変調ドロワー」が表示され、マクロのドロワーの右側のセクションラベルは「Automation: Volume」が表示されます。

21.Volume Automationセクションの[Add Curve]ボタンをクリックし、ContextMenuから[Browse]を選択してから、[Select Parameter]ポップアップから[Parameter 1]パラメーターを選択します

Volume Automationセクションに赤い破線が表示されます。
これはオートメーションカーブであり、パラメーターのすべての可能な値での自動化されたプロパティの値を示す折れ線グラフです。
現在、オートメーションカーブは完全に水平です。つまり、プロパティの値は、どんなパラメーターが渡されても同じ値です。

22.赤い線をクリック

オートメーションカーブが実線になり、クリックしたポイントにオートメーションポイントが表示されます。

オートメーションポイントは、オートメーションパラメーターに打った地点でのオートメーションプロパティの値を決定し、オートメーションカーブの形状を定義します。

23.オートメーションカーブの別の部分をクリックして、2番目のオートメーションポイントを追加します

24.新しいオートメーションポイントをクリックして上下にドラッグし、値を変更します

このオートメーションカーブにより、パラメーターの値に応じてイベントのボリュームが変化します。

25.TransportBarの再生ボタンをクリック

Parameter 1のデフォルト値0のオートメーションによって設定されたボリュームで再生されます。

26.Eventの再生が終了したら、TransportBarの[Parameter 1]パラメーターのナンバーボックスでマウスボタンをクリックして押したままにし、パラメーターが最大値の10db(大きすぎると思うので程々に調整してください)に達するまでマウスカーソルを上にドラッグします

27.TransportBarの再生ボタンをクリック

Eventのボリュームは、タイムラインの再生位置の速度とイベントの出力のピッチに影響を与えるため、パラメーターの値が変更されたため、イベントのサウンドが異なることに気付くでしょう。

オートメーションの詳細については、オーサリングイベントの章の オートメーション セクションを参照してください。

ゲームでこのイベントを聞くには、それを Bank に割り当てて、そのBankを構築する必要があります。
ビルドは、プロジェクトをFMOD StudioAPIが使用できる形式に変換します。

28.「New Event」イベントを右クリックし、ContextMenuから「Assign to Bank>Master」を選択します

EventがそのBankに割り当てられたので、Eventを作成するたびにそのBankに含まれます。

29.「File>Build」を選択します

FMOD Studioは、プロジェクトのBankファイルを作成します。

30.フォルダーまたはエクスプローラーを開き、このFMODStudioプロジェクトを保存した場所に移動します

内部FMOD Studioプロジェクトフォルダには、「Build」という名前のサブフォルダを見つけることができます。

このフォルダーには、「Desktop」という名前の別のサブフォルダーが含まれています。
このサブフォルダーには、「Master.bank」および「Master.strings.bank」というファイルが含まれています。
これらのファイルはプロジェクトで構築されたバンクであり、作成したイベントをプレイするためにゲームで必要になります。

ゲームでビルドされたバンクファイルのコンテンツを再生する方法については、 FMODAPIユーザーマニュアル を参照してください。

以上!


これで一通りのクイックチュートリアルは終了です。
長かった……。 分かりづらい所は極力スクリーンショットを貼り付けましたので詰まることなくクイックチュートリアルが出来ると思います。
それでもわからない方は、やはり原文を読みに行ってください。
それが結局早いです。

まとめ


FMODは良いかも!
まだ私も記事を書きながら試しているため、実際に鳴らす所まで試していませんが、
実際に使って見るのはありなんじゃないかと思い始めています。
近いうちに後編を書こうと思うので2週間以内に記事を作成したいと思っていますので、ゆっくりお待ちいただければと思います。
以上です。

後編はコチラ
orotiyamatano.hatenablog.com

Unityでルール画像を使ってフェードイン、フェードアウトさせる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
前に社内勉強会があり、その際にShaderのプレゼンをしたのですが、業務にも使えそうなShaderの使い方を教えて欲しいと言われたので
今回は簡単なフェードイン、フェードアウトのリポジトリを作ったのでソレの解説です。

github.com

目次


前提


利用プラグイン

UniRxとDOTweenの無料のものを使っています。
どちらともライセンス表記が必須です。

Unityバージョン

Unity2019.4.16fで動作確認。多分DOTweenとUniRxが動けば、ほぼどのバージョンでも動くと思います。

リポジトリ


github.com

サンプル


ボタンを押すとフェードイン、フェードアウトをトグルするものを作りました。

f:id:OrotiYamatano:20210124215702g:plain

Shaderの解説


f:id:OrotiYamatano:20210124235235p:plain

Disolve(RGB)にルール画像を設定するとその設定されたルール画像の輝度に応じて、フェードされます。

edoさんのディゾルブShaderを参考にしました。

qiita.com

コードは以下の通り。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Dissolve" {
    Properties {
        _Color ("Main Color", Color) = (.5,.5,.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _DissolveTex ("Desolve (RGB)", 2D) = "white" {}
        _CutOff("Cut off", Range(0.0, 1.0)) = 0.0
    }

    SubShader {
        Tags { "RenderType"="Opaque" "Queue"="Transparent" }
        Pass {
            Name "BASE"
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _DissolveTex;
            float4 _DissolveTex_ST;

            float4 _Color;
            float _CutOff;
            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                float2 dissolvecoord : TEXCOORD1;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Color * tex2D(_MainTex, i.texcoord);
                fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
                if (_CutOff > a) {
                    discard;
                }

                return col;
            }
            ENDCG           
        }

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            Name "Add"
            Cull Off
            Blend One One

            CGPROGRAM
            #pragma vertex vert

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _DissolveTex;
            float4 _DissolveTex_ST;

            float4 _Color;
            float _CutOff;
            float _Width;

            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 dissolvecoord : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
                return o;
            }
            ENDCG           
        }

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            Name "Add"
            Cull Off
            Blend One One

            CGPROGRAM
            #pragma vertex vert

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _DissolveTex;
            float4 _DissolveTex_ST;

            float4 _Color;
            float _CutOff;
            float _Width;

            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 dissolvecoord : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
                return o;
            }

            ENDCG           
        }
    } 

    Fallback "VertexLit"
}

fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);

の部分で_DissolveTexの輝度を計算しています。

_CutOffより小さい場合、描画せずに処理を中断しています。

トランジションの処理の解説


コード

        public class Transition : MonoBehaviour,IUtilTransition{
            [SerializeField] private Material transitionMaterial;
            public delegate void Callback();
            private Callback callbackComplete;
            private static readonly int Property = Shader.PropertyToID("_CutOff");
            private float TransitionRate { get; set; }
            private bool IsFade { get; set; }

            private void Start() {
               if (transitionMaterial == null) {
                   Debug.LogError("Transition.cs:マテリアルが設定されてない");
               }
               this.ObserveEveryValueChanged(x => TransitionRate).Subscribe(_ => {
                       transitionMaterial.SetFloat(Property, TransitionRate);
                   }
               ).AddTo(this);

            }
            
            private void OnApplicationQuit(){
                transitionMaterial.SetFloat(Property, 1.0f);
            }

            public bool IsActiveFade() {
               return IsFade;
            }

            /// <summary>
            /// カットアウトさせる
            /// </summary>
            /// <param name="startVal"></param>
            /// <param name="endVal"></param>
            /// <param name="duration"></param>
            public IUtilTransition Fade(float startVal,float endVal,float duration) {
                IsFade = true;
                TransitionRate = startVal;
                DOTween
                    .To(() => TransitionRate, (x) => TransitionRate = x, endVal, duration)
                    .SetEase(Ease.InOutSine)
                    .OnComplete(() => {
                        IsFade = false;
                        callbackComplete?.Invoke();
                    });
                return this;
            }

            /// <summary>
            /// 完了
            /// </summary>
            /// <param name="action"></param>
            /// <returns></returns>
            public IUtilTransition Complete(Callback action) {
                this.callbackComplete = action;
                return this;
            }

            /// <summary>
            /// フェードアウト
            /// </summary>
            public IUtilTransition FadeOut() {
                return Fade( 0, 1,0.2f);
            }


            /// <summary>
            /// フェードイン
            /// </summary>
            public IUtilTransition FadeIn() {
                return Fade(1, 0,0.2f);
            }
        }

TransitionRateの値が変わったらマテリアルに対して
カットオフの値を設定することで段々と透過するようにしています。

DoTweenのOnCompleteが呼ばれた際に続けてデリゲートでcallbackCompleteを呼ぶことでフェードの終了後にCompleteで処理を行うことが出来ます。

トランジション処理を呼び出す側


コード

public class PressButton : MonoBehaviour {
    [SerializeField] 
    private TransitionFade.Transition transition;
    private IUtilTransition fadeObj;   //Unityは早くインターフェースでSerializeField使えるようにして欲しい……
    private bool isDoneFade;

    private void Start() {
        Button btn = GetComponent<Button>();
        fadeObj = transition;
        
        //ボタンが押されなおかつ、フェード中ではない?
        btn.OnClickAsObservable().Where(_ =>!fadeObj.IsActiveFade()).Subscribe(_ => {
            if (isDoneFade) {   
                //フェードが完了してたら、フェードインする
                fadeObj.FadeIn().Complete(() => {
                    isDoneFade = false;
                    Debug.Log("フェードイン終了");
                });
                return;
            }

            //フェードが行われてなかったら、フェードアウトする
            fadeObj.FadeOut().Complete(() => {
                Debug.Log("フェードアウト終了");
                isDoneFade = true;
            });
        });
    }

}

別にインターフェースを使わなくても良いんですが、一応定義しておいたのでインターフェースの変数で色々処理をしています。

処理は注釈通りです。
UniRxの処理が重なって分かりづらい方も居ると思いますので念の為解説すると
ボタンが押された時にフェード処理の場合tureを返すfadeObj.IsActiveFade()という関数を見た上で
フェード中でなければフェードインもしくはフェードアウトを行う用にする処理しているというのが上記のスクリプトの内容になっています。

フェードするスピードや始まりの値を変えたい場合、FadeInやFadeOut関数を呼ばす直接Fade関数を呼んで、各値を設定してもFadeが出来ます。

まとめ


簡単なShaderのスクリプトでフェードイン・フェードアウトを実現しました。
Scene遷移時などパッと画面が変わってしまうと味気ないので、黒い画像へのフェードを挟むだけでもある程度見た目が良くなると思います。
参考になればと思います。以上です。

初心者による初心者のためのシェーダー芸解説

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。

本記事はUnityゲーム開発者ギルドアドベントカレンダー2の19日目の記事です。

adventar.org

今回は初心者による初心者のためのシェーダー芸解説です。

目次


どうしてシェーダー芸?


Unityゲーム開発者ギルドアドベントカレンダーですが、今回はUnityではなく。シェーダー芸にについて書こうと思います。

Unityじゃなく?なんでシェーダー芸なのか?
Unityゲーム開発者ギルドで、10月末ぐらいからシェーダー芸の勉強を初めたからです!

では、なぜ勉強を初めたのか?

なんとなく!
楽しそう
だったから!

というのもありますが。
主に3つの理由があります。

メイン目的: シェーダー をツイッターでつぶやいて見栄えを良くしたい(ゲーム画面のツイートと比べてGLSLだけで済むので楽そうだと思った)
サブ目的:ゲームの表現の幅を広げたい。マテリアル周りに詳しくなりたい、レンダリングまわり詳しくなりたい
サブ目的:VJみたいなの憧れる

という理由です。
最近会社のSlackワークスペースに #shader勉強部屋 というのを設立して
シェーダー芸を勉強中

11月から
約1ヶ月で12のシェーダーを作成しました
※最近は1週間に1つくらいのペース

シェーダー芸とは


そもそもシェーダー芸とは?
簡単に言うとシェーダーだけでリアルタイムの演出動画や作品をつくること!

フラグメントシェーダー、バーテックスシェーダーなど種別は問わないですが
基本的にシェーダー芸というとGLSLのフラグメントシェーダーを表すことが多い気がします。

今回私が作ったのもGLSLのフラグメントシェーダーです。

いろんな方の作品は以下サイトで見ることが出来ます。
GLSL SandBox
glslsandbox.com

shadertoy
www.shadertoy.com

もしくはTwitterで #shader #つぶやきGLSLなどを検索すると出ます。

何か役に立つのか?
ぶっちゃけ、表現力は上がるが、ゲーム制作にあんまり意味は無い!
楽しい!

役に立たなくてもいい!
なぜならシェーダー「芸」だから
元々「芸」って役立つものじゃないしね!

シェーダー基礎解説

※私が今回紹介するコードは Twigl で使えるコードです

次がコード
(因みにレギュレーションはClassic)

コード

precision highp float;
uniform vec2 resolution;

float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    float d = step(pct,0.2);
    vec3 color=vec3(d);
    gl_FragColor=vec4(color,1.0);
}

普通のプログラムと全然違いますね!

何をやってるのか?というと

の表示です。

座標
をもとに模様を描いている

どういう感じで処理されるのか?
フラグメントシェーダーは、同時にすべてのピクセルが処理されます。

CPUは同時に処理するのに向いてないのでGPUを使って処理されます。

小ネタ:
最近PCを自作したのですが、CPU、メモリ、電源、マザボだけ指して、
動作確認をしたらグラフィック処理がされず画面が表示されませんでした。
なんでかな~?と思っていましたが、それもそのはずでIntelのCPUならオンボード(グラフィック機能あり)で処理されますが
RyzenのCPUはオンボードではないCPUもあり、別途グラフィックボードを購入する必要があったりします。
そのためVGA(グラフィック表示システム)がエラーを引き起こし表示出来たかったのですよね。

組み込み変数・定義の説明

GLSLには組み込み変数や定義があったりします。
今回はその一部を紹介しておこうと思います。
ついでに通常に変数についても少し触れておきます。

precision highp float; //floatの精度指定です
uniform vec2 resolution; //スクリーンのサイズの取得(変数名は別で定義可能)
gl_FragCoord→ピクセルの位置を表します
gl_FragColor→最終的なピクセルカラーです
(gl_FragColorは新しいGLSLではもう非推奨らしいけど,twgl準拠)
他にもマウスの位置を取得とか、時間の取得とか、前フレームのテクスチャの情報を保存しておくとがあります

プログラマーなら分かると思うけど……
vec2→ x,y が格納できる変数定義
vec3 → x,y,z が格納できる変数定義
vec4→ x,y,z,wが格納できる変数定義

などがあります。今回はとりあえず
これらがわかっていれば大丈夫です。

とはいえ同時にピクセル処理されると言われても
ピンと来ない方が多いと思います、

(理解しやすいように)
時間に応じて
塗りつぶしをしてみるプログラムを書いてみようと思います。

precision highp float;
uniform float time; //1secで取得 floatで時間を取得する
void main(){
    float d = 0.;
    if(gl_FragCoord.x>time*60.){
        d = 1.;
    }

    vec3 color=vec3(d);
    gl_FragColor = vec4(color,1.0);
}

bit.ly

見ての通りx軸を1秒1分単位で順番に塗り潰していく処理です。
y軸の場合はこんな感じ。 bit.ly

なんとなく
分かってきましたでしょうか?
xは左が0で右端が解像度ごとに変わり、
yは下が0で上が解像度ごとに変わります。

組み込み関数


シェーダーには組み込み関数というものがあります。
ifで処理をすると処理負荷が高いので代わりになるものが用意されています。

今回使う簡単なものを紹介いたします。

step(a, b)→
a>bだったら0.0に、それ以上なら1.0になる

length(x)→
xの長さをfloatで返す

本格的に解説に入る前に

本格的に解説に入る前に↓プログラムの解説を行います。

vec2 st = ( gl_FragCoord.xy * 2. - resolution) / min( resolution.x, resolution.y );

解像度xyを取得しmin関数でで小さい方の数値を取得し割ります。
数値を表示すると画面では以下のような感じになります。

まずlengthの説明

軽くlengthの説明をしましたがもう少し深いlengthの話をしようと思います。
length(x)は
xの長さをfloatで返すとのことでしたが、一番最初のスクリプトを短くしてlengthにだけ焦点をあてて見るとこういった↓スクリプトが出来上がります。

precision highp float;
uniform vec2 resolution;
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = length(st); //関数内にあったlengthをそのまま利用
    vec3 color=vec3(pct);
    gl_FragColor=vec4(color,1.0);
}

分かりやすいですね。
stの中心位置はxとyが共に0に近いので0、それぞれ中心を離れるほど1に近づきます。

では、最初のスクリプトに近づけて、circle関数を作って-0.5をしてみましょう。

precision highp float;
uniform vec2 resolution;

float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    vec3 color=vec3(pct);
    gl_FragColor=vec4(color,1.0);
}

するとこうなりました。
何が変わったのかというと、-0.5された文だけ円の黒い所が広がりました。
ソレだけですね。

今回のlengthはこれだけにのみ使っています。

stepの説明

これでスクリプトの半分は何をしているのか分かりました。
ではstepは何をやっているのでしょうか?

step(a, b)は
a>bだったら0.0に、それ以上なら1.0になる

でした。
つまり……。

precision highp float;
uniform vec2 resolution;

float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    float d = step(pct,0.2);    //ココを追加、a>bだったら0.0に、それ以上なら1.0になる
    vec3 color=vec3(d);
    gl_FragColor=vec4(color,1.0);
}

これで元の画像に!

これからシェーダーを勉強する場合

ネットの色んなシェーダーをパクって改変して覚えていくことを
私は推奨します!
覚えたコードは保存しておくと便利!

例えばさっきの
シェーダーを改変する
ならこうです

precision highp float;
uniform vec2 resolution;
uniform float time;
float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    float d = step(sin(time*4.0)*0.5+0.7,pct);  //a>bだったら0.0に、それ以上なら1.0になる
    vec3 color=vec3(d);
    gl_FragColor=vec4(color,1.0);
}

↓時間とsinを使うことで円を拡大縮小することが出来ます。 bit.ly

サンプルコードを2つ解説


参考までに私が書いたコードを紹介します。
口頭で説明したほうが早いのですが、ブログなので長文で解説させて頂きます。

precision highp float;
uniform vec2 resolution;
uniform float time;
mat2 rotate2d(float _angle){
    return mat2(cos(_angle),-sin(_angle),
        sin(_angle),cos(_angle));
}
float square(vec2 p) {
    return abs(p.x) + abs(p.y);
}
void main(){
  vec2 r = resolution,p = (2.*gl_FragCoord.xy - r) / min(r.x,r.y);
  float a = sin(time * 5.0) * 0.5 + 0.5;
  vec2 d = mix(vec2(length(p),0.7), vec2(square(p* rotate2d(radians(time*180.))),0.5), a);  //線形補間
  vec3 color = vec3(step(0.5,d.x));
  gl_FragColor = vec4(color,1.0);
}

bit.ly

まず、rotate2d関数は回転を表しています。2次元行列を使ってxとyの値を入れ替えています。
_angleと書いてあるとおり角度によってどのぐらい入れ替えるかが決定します。
今回はradians(time*180.)の値を入れているので1秒間に180度回転するってことですね。
mix(a,b,c)というのがありますね。
こちらはaとbどちらをどのぐらい表示するかをcの値で線形補間で遷移させるものです。
左のlengthは先程もでた円ですので右側が資格になります。
つまりのこるsquare関数は回転を止めていただくと分かりやすいですが、ダイヤ型の正方形を表示させる命令式です。

どうでしょうか?いっけん難しそうに見えますが、パーツに別ければ理解できそうな気がしてきませんか?

因みに回転に使われているSinCosはこんな動きになります。
三角関数で使われるものですが、この程度の話であれば深く考える必要はないと思います。
徐々に慣れていきましょう。

もう一つ解説


もう一つ解説してみようと思います。

precision highp float;
uniform vec2 resolution;
uniform float time;
void main(){
 vec2 r=resolution,p=(gl_FragCoord.xy*2.-r)/min(r.x,r.y);
 float d = length(p);
 d = (10.* d)+time*-10.;
 d = abs(sin(d));
 d = step(0.5,d);
 gl_FragColor = vec4(vec3(d,0.0,0.0),1.0);
}

bit.ly

まず一番簡単な一番最後のvec4(vec3(d,0.0,0.0),1.0)の部分。
こちらですがRGBAとなっており、色を表しています。目がチカチカしますね。
time-10.部分がtimeの速さになります。
10.
dの部分は円の太さです。
ここはlengthから値を取得してきて×10した値の絶対値(abs)をsin関数に入れて-1~1の値で遷移させ、
最終的にstepできっぱりと別れるように色分けしています。
簡単ですね。

と言った感じで色々シェーダーを見ると複雑なシェーダーも簡単なものなら読み解くことが出来ます。

まとめ


今回は2Dシェーダーの解説しかしなかったけれど、
シェーダーは奥が深いです。
現在私は3Dの表示(レイマーチング)の勉強をしているけれど、またまだやることが多いです。

最近はUnityや3Dソフト内でノードベースのシェーダーの表示処理がありますが、
やっていることはプログラムと同じな上、ノードベースであるぶんやれる表現に限りがあります。

色々表現手段を増やすためや処理負荷を下げるためにプログラマはシェーダーの勉強が欠かせません。

また、表現手段が増えなくても自分が書いたコードが動くのは見ていて楽しいです。
皆さんもこれをきっかけにシェーダーの勉強をやってみてはいかがでしょうか?
シェーダー芸は楽しいぞ!
以上!やまだたいしでした。

参考資料


www.iquilezles.org

qiita.com

qiita.com

docs.google.com

その他のシェーダー芸


orotiyamatano.hatenablog.com

TextMeshProの使い方【後編】

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
TextMeshProの使い方後編です!

前編はこちら orotiyamatano.hatenablog.com


目次


設定


早速前回の続きからです。

Atlas & Material についてです。
こちらは生成されたFontアトラスとそのマテリアルの参照を表しています。
変更は出来ません。

Font Weights は
太字と斜体がフォントの外観をどのように変更するかを制御できるものです。
文字のスタイルによって表示するフォントアセットを指定することが出来るようです。
設定すれば、italic(斜体)のときはAのフォント、太字(Bold)の時はBのフォントみたいな感じで使い分けができそう。
また下の方の項目でテキストの太さと間隔を調整することもできるようです。
今回は触らないので詳細は割愛。

Fallback Font Assets は
フォントを複数のアセットに分散し管理できる機能のようです。
今回は常用漢字と英数字とひらがなだけなので8192*8192でギリギリ納まるので問題ないですが、
納まりきらない時に分散出来るらしいです。
また、1つのフォントにすべての文字が含まれていない場合に別のフォントを読み込ませることで代替表示させるために用いるようです。
別のTMP_FontAssetを指定するだけのようです。

Character Table は
その名の通りフォントアセットに含まれる文字をUnicode値で検索するために使用される辞書のようです。
打ち込んだ文字と表示される文字が別(例えば「あ」と打ち込んだのに「い」と表示される)の場合に値を編集してなおすことが出来るようです。
下記のGlyph TableのIDと紐付いているようです。

Glyph Table は
登録した文字とその設定が一覧表示されます。
特定の文字が表示されない、文字が崩れているという場合にココで検索するとどんな状態で文字が保存されているか調べることが出来ると思います。
値が変更することが出来るので文字の位置が合わないといったことがある場合ココを編集すれば良さそうです。

Glyph Adjustment Table は
特定の文字2つを選んで文字間を調整するものっぽい?
その組み合わせの場合のみ文字間が調整されるようです。
上手く動かないという話もあるようなので、あまり機能としては信用しないほうが良いかも?

いざベイク!


各ベイク設定について


では、テクスチャのアトラス化をするためにベイクをやっていこうと思います。
フォントアセットのUpdate Atlas Textureをクリックします。

すると、ウィンドウで開くと思います。
前半で設定した値と違っても反映されると思うので大丈夫だと思いますが、怖い人は合わせて置くと良いでしょう。
因みに私の設定はこんな感じにしました。

Sampling Point Size は大元となるフォントのポイントの大きさですが、コレはベイクする速度を考えてAuto Sizingにしておくことにしました。
Paddingはベイクされるときの文字間です。小さすぎると文字を表示する際に隣の文字が表示されてしまうことがあるのでデフォルトの9のままを適用しました。
Packing Method は ベイクするスピードで遅いほど精度が高くなるのですが、特にこだわりは無いのでFastにしておきます。
Atlas Resolutionはアトラステクスチャのサイズです。今回は常用漢字ひらがな・カタカナ、英数字、ちょっとした記号なのでいっぱい入る8192*8192のサイズにしておきます。
Character Setには焼き込む文字の指定方法を指定します。後で変更するので詳細は割愛。
Select Font Asset は実際に焼き込む設定のアセットです。特に変更することはありません。
Character SequenceはCharacter Setに指定する方法によって入れる設定が変わります。詳細は割愛。
Render Modeはベイクするときの設定です。これは前編で解説しているので割愛します。SDFAAで今回は焼き込みます。
Get Kerning Pairs は カーニング情報をフォントデータからコピーするか?です。フォントによってはそもそも含まれない場合もあるのでfalseでいいです。
あるやつはtrueの方がキレイに表示できると思いますがソコまで気にする必要も無いかと……。

ベイクする文字の指定


ベイクする文字の指定方法はいくつかあります。

  1. ASCII→ASCIIコードでの指定です。ASCIIコード表にある全ての文字がベイクされます。
  2. Extended ASCII→拡張ASCIIコード指定です。拡張ASCIIのすべての文字がベイクされます。
  3. ASCII Lowercase→ ASCII Lowercaseコード指定です。
  4. ASCII Uppercase→ ASCII Uppercaseコード指定です。
  5. Numbers+Symbols→番号記号またはナンバーサインと呼ばれる文字列がベイクされます。
  6. Custom Range→Decimal(10進数)で指定された文字(文字コード)をベイクします。今回これを使います。
  7. Unicode Range(Hex)→ Hex(16進数)Unicodeで指定された文字コードをベイクします。
  8. Custom Characters→指定した文字がベイクされます。(文字をそのまま入力する)
  9. Character from File→文字が入力されたテキストを指定します。そのテキストがベイクされます。(多分)

ASCII系↓ theasciicode.com.ar

Numbers+Symbols↓ coolsymbol.com


今回ベイクするのは常用漢字などです。
↓ここの文字列を使わせていただくことにしました。
gist.github.com

準備が出来たらGenerate Font Atlasをクリックします。

するとベイクが始まります。

始まらない場合、エラーが表示されるので、そのエラーを解決してベイクしましょう。
性能の低いPCでは固まってしまうこともあるので触らずに気長に待ちましょう。

エラーがなければすべてベイクされるはずです。

Saveをクリックして保存しておきましょう。

ここまで来て言うのは何なんですが、画像を見ると included 2434/2635 とすべてベイク出来ていないようです。
いらない文字が入ってしまっていただけだとは思いますが気になる方は
↓こちらのほうを利用してCustom Charactersでベイクし直したほうが良いかも知れません。
gist.github.com


ベイクされたことでアセットのマテリアルとテクスチャが保存されたはずです。
びっしりと白い文字が割り当てられてるか確認してみましょう。

これでもう使えます。
マテリアルとテクスチャの設定が出来るのですが、要望があれば別途記事を書きます。
ココに関しては他の方も記事を書かれているので私が書く必要はないと思いますが……。

TextMeshProの表示


TextMeshProの表示は普通のテキストとほとんど似ています。
解説する必要もほとんど無いと思うのでUIでの表示のみ解説します。

挿入したいところに右クリック> UI>Text -TxteMeshProを選択

生成されたTxteMeshProのゲームオブジェクトのTxteMeshPro - Textコンポーネントを開き、MainSettings> Font Assetを生成したFont Assetに変更します。

すると表示されるはずです。
後はTextMeshProのコンポーネントの値をいじって楽しんでみてください。

縁取りの方法などは他の方の書かれた記事にも記載があるので、そちらの方が分かりやすいと思います。

アセットを更新しない限り、もうフォントは必要ないのでプロジェクトから消してしまっても構わないです。
注意事項としてはGitでこのアセットを管理するときにバイナリで管理されるため読み込みに失敗して、テクスチャアトラスの参照が剥がれてしまうことがあるようです。
あまりにも高頻度で参照が剥がれてしまうようでしたら今回解説したFallback Font Assets機能を使って、2048*2048のテクスチャ複数枚で管理するなどをおすすめします。

一旦これで以上になります。
閲覧ありがとうございました。

まとめ


以上でTextMeshProの使い方を終わりにします。
間違っていたら連絡ください手直しいたします。
今回はベイクするまでの設定を中心に教えましたが、TextMeshProはベイクする作業より色々装飾をつける作業のほうが楽しいので、ここで終わりにせず色々いじってみて欲しいです。
私としてはベイクについて中心的に述べてある記事が少ないことが気がかりだったために、この記事を書きました、装飾の仕方などは別の方の記事を参考にしていただければと思います。
(それはそれとして要望があれば書きます。Twitterなりでメンションくれれば即反応すると思います)
これからTextMeshProを始める人の手助けになれば幸いです。
以上、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )でした。

TextMeshProの使い方【前編】

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
以前、TextMeshProをStatic SDFであればフォントを使わなくて済むのでFonts66コンプリートパックを使うことができるという記事を書かせていただきました。

orotiyamatano.hatenablog.com

しかし、結構散見されるのは、「Static SDFの使い方が分からない!」、「TextMeshProの設定が分からない」という意見です。
確かにStatic SDFのやり方は一見分かりづらい&忘れがちのため、今回調べてメモをしておこうと思ったためこの記事を書くことにしました。

目次


TextMeshProとは?


通称TMPとも略されるTextMeshPro。
そもそもコレが何なのかというのを解説させてもらうと、
Textが使えるアセットでUnity 2018.2からUnity公式で使えるようになったアセットです。
2018.2以降であればデフォルトでUnityに組み込まれています。

テキスト表示であればコレまでも使えていたではないか!と思われる方もいると思いますが、
Text Mesh Pro の方が優秀だと断言しておきます。

何が嬉しいのか


何が嬉しいのか1つめキレイ。

普通のUI Textだとテキストがぼやけるデメリットがあります。
Text Mesh ProはSDF(Signed Distance Field)(日本語訳すると符号付き距離函数とかいうやつ)を使う。
「距離情報によるテクスチャ画像のベクトル化」を行いキレイにフォントを表示するのだ。
要するにフォントデータをテクスチャに焼き込んでおいてロジック側でにじみが無いように拡大させてあげることができるのが、このSDF形式という物らしいです。
だからキレイに出力されます。

じゃあ、なぜUI Textがぼやけるのかというと、
ココに関しては設定などによっては、ぼやけなくも出来るのだが、基本的にフォントはキャッシュ(一部情報として保存)されており、
テクスチャフォントアトラス(複数の画像を1つの画像データとして保存したデータのこと)化されソレを表示している。
そのためスケールを変えるなどを行うと元のテクスチャの大きさは変わらないため、ボヤケてしまうのです。

何が嬉しいのか2つめ容量が優しい。
先程もいったようにテクスチャフォントアトラスを作る必要があるのでUI Textはフォントをバイナリ(ココでは最終的なアプリの内部情報のことを指す)に含める必要があります。

今やフォントといえばベクターフォントが多いですが、このベクターフォントはドット情報を保存するのではなく、
このベクターフォントは線の長さや向きなどを記憶させておいて、どんなにフォントを大きくしても滲まないようにしてあげるものです。

しかし、いかんせんこれは容量が多いです。
また、フォントサイズを異なる大きさで保存する場合、その大きさごとにフォントアトラスが生成されます。

(↓こんな感じ)

TextMeshProはテクスチャを焼いて(焼く→生成すること。IT用語。語源は英語のBake.)おいてから、SDFで処理するため、フォントデータをバイナリに組み込む必要がアリません。
容量的に優しいですし、フォント規約にはバイナリにフォントデータを組み込んでは行けないというものもあるため、それを回避することが出来ます。

ちなみに焼き込んだデータはフォントサイズごとに用意する必要がないのでさほど重くないです。

(↓こんな感じ)

何が嬉しいのか3つめ色々装飾が出来るのに軽い。

UI Textでは出来ることが限られています。
アウトラインくらいならつけることが出来ますが、アウトラインをつける処理の特性上、Text Mesh Proの方が軽いです。

ざっと述べるとこんな感じです。
単に表示させるだけならUI Textでも十分ですが、細かい動きをつけたり、見た目をよくしようとすると断然Text Mesh Proにしておいたほうが良いです。

TextMeshProの利用方法


初期設定


TMP(TextMeshPro。以下TMP)の実行方法。

まずはフォントを使えるようにしてあげないといけないのでツールバー>Component>Mesh>TextMeshPro - Textを選択します。

すると、TMP Importerというウィンドウが表示されるのでImport TMP Essentialsをクリックします。

するとTMPのシェーダーなどが一通りインポートされ準備完了です。

フォント


上記の設定で使うことも出来るのですが、
先程もいったように一旦テクスチャを生成しなければいけません。
またデフォルト設定でいくつか既に出来上がっているのですが、英語文字しか含まれていません。
まぁ、生成しなくても日本語文字は動的に生成されるのですが、動的にテクスチャアトラスを生成する必要があるため、
フォントデータをバイナリに組み込む必要があります。TMPの利点を最大限に活かせません。もったいないですね。

それに貴方が使いたいであろうデフォルト以外のフォントはまだ使えません。
ココからは新しくフォントを設定する方法についてです。

まず、使用したいフォントをドロック&ドロップでUnityに取り込みます。(バイナリデータに含めたくないので後で消します)
(フォント名に日本語が含まれる場合は上手く使えないことがあるようなので、フォント名を日本語から英字に変えておいた方が良いかも?)
(ちなみに私が今回使うのは株式会社ネットユーコムさんのAFSまるご風書体)

右クリックをして
Create > TextMeshPro > FontAssetを指定

すると、テクスチャアトラスの設定されてないSDFアセットが生成されます。

生成されたSDFアセットを選択し、Inspecter Windowの設定を変更します。

何が設定できるのか項目を見ていきましょう。
まずはFace Infoから。

Face Infoにはフォント内に設定されている基本情報が格納されます。
基本的に値を変えた場合、最悪文字が崩れる場合があるため変更はしないほうが良いと思います。

各項目について解説しようと思って連番をつけましたが、
フォントをTextMeshProの機能でアトラス化するときに参照する情報みたいで変更すると上手く切り取られなくなるようです。
購入フォントでは設定情報が誤っているということは、ほとんど無いと思われるので、変えないほうが良いと思われます。
(切り抜くために↓こういう情報が入ってるらしい)

次はGeneration Settingについてです。

(多分)描画する上で参照情報です。

  1. SourceFontFile →フォントの参照データです。ない場合はエラーになります。
  2. Atlas Population Mode → 出力設定です。Dynamicになっている場合、アトラス化済み以外のデータは随時動的に生成されます。今回はアプリにフォントデータを含めたくないのでStaticにします。(Bake済みデータしか表示されなくなるので注意)
  3. Atlas Render Mode → アトラス画像を生成するときのレンダリングロジックの選択です。選択する物によって生成スピード、文字の綺麗さなどが変わります。
    英語のネット記事を読む限りAtlas Render ModeはSDFAAが高速で安定しているようです。
    1. SMOOTH_HINTED→スムージング表現(フォントヒンディング)グリフのアウトラインの8bitまたはアンチエイリアス処理された画像から、ヒントを使用してグリフのビットマップ表現をレンダリング
    2. SMOOTH→スムージング表現 グリフのアウトラインの8ビットまたはアンチエイリアス処理された画像から、ヒントなしでグリフのビットマップ表現をレンダリング
    3. RASTER_HINTED→ラスター表現(フォントヒンディング) グリフのアウトラインのバイナリ(1ビットモノクロ)画像からグリフのビットマップ表現をヒント付きでレンダリング
    4. RASTER→ラスター表現 グリフのアウトラインのバイナリ(1ビットモノクロ)画像から、ヒントなしでグリフのビットマップ表現をレンダリング
    5. SDF→ SDFシェーダを使用するために必要なテクスチャを作成する。 グリフのアウトラインのバイナリ(1ビットモノクロ)画像から、ヒントなしでグリフの符号付き距離フィールド(SDF)表現をレンダリング。 1.SDF8→SDFのグリフのサンプリングが8倍スケールアップ版 1.SDF16→SDFのグリフのサンプリングが16倍スケールアップ版 1.SDF32→SDFのグリフのサンプリングが32倍スケールアップ版 1.SDFAA_HINTED→グリフの8ビットまたはアンチエイリアス処理された画像からグリフの符号付き距離フィールド(SDF)表現を、ヒント付きでレンダリングします。このレンダリングモードは非常に高速だが、精度が少し低下します。
    6. SDFAA → グリフの8bitまたはアンチエイリアス処理された画像からグリフの符号付き距離フィールド(SDF)表現を、ヒントなしでレンダリングします。レンダリングモードは非常に高速だが、精度が少し低下 SDFは数が大きいほど正確になります。
  4. Sampling Point Size → どうやらサンプリング元になるデータのフォントサイズを指定するようです。大きければ大きいほどキレイ?まぁ焼くスピードも遅くなると思うのでデフォにしておくことにします。私の設定は90になっているけど90は少し大きいかも?
  5. Padding → 文字間ですね。
  6. Atlas Width → アトラス画像の横幅です。今回は常用漢字と英数字を入れたいので1024では収まらないと思うので、もっと大きい数値に8192にしておきます。
  7. Atlas Height → アトラス画像の縦幅です。今回は常用漢字と英数字を入れたいので1024では収まらないと思うので、もっと大きい数値に8192にしておきます。

前半はとりあえずここまで


意外と長くなったので、前編、後編で区切らせていただこうと思います。

一旦ココまでで失礼します!
次回は実際に書き出しと描画をします。後、アセットを管理する上で気をつけることなどを書けたら良いなと思います。
何か誤っているなどアリましたら教えて下さい!
↓続き
orotiyamatano.hatenablog.com

もうソレ何番煎じ?Unity、UEの比較

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
これまでUnity、UEの比較記事が大量がありました。
しかし、どちらも(平等ではなく)公平に語られてないような気がします。
なので、私が出来るだけ公平に答えたいと思います。
本当はTwitterに載せようと思ったのですが、思いの外、長くなったのでブログにしました。
思いついた順で書いてるので読みにくいかも知れませんが、ご承知ください。

目次


一番最初に言いたいこと


意見が私が偏ってないと言い張る理由は、業務で半年以上UEを使っていること、
Unityもゲームをリリースする程度には触ってきたことです。
強いて言うなら、UEよりUnityの方が触っている期間が長いので肩を持ってしまっています。
(後、Unityゲーム開発者ギルドに入ってしまっているので……)

しかしながら、UEも半年は触ってきたので色々述べられるところがあると思います。

後、これだけは言っておこうと思います。

そもそも、それぞれ利点欠点があるので比較するものではない!

では、早速つらつらと比較していこうと思います。

本文


まずUnity、マルチスレッド、イベントドリブンの考え方はUniRxなど外部ライブラリの力を使わないと使えないので、そういった実装がやりづらいのが難点ですね。
UnityではモノビヘイビアとC#の力で色々制御されているのでユーザー(エンジンの使用者。以下ユーザー)が色々違反したように書けないのが良いです。
(DOTS?しらんな……)

UEは自由に書けすぎてしまうのが難点ですね、イベントドリブン、マルチスレッドは出来ますが、全てをユーザー側で厳密に管理しないといけないです。
設計思考がチームごとに厳密にないプロジェクトも多いと思うのでカオスなコードが出来上がりやすいです。

UEは命名規則もあることから、ルールでなんとかしようという取り組みがあるのが伺えます。


グラフィックはUEに軍配。様々なツールとのやりとりが出来たり、確認がしやすいのが良いですね。
Unityも頑張ってきてるのでココはなんとも言えないです。
Unityは前まではパイプラインまわりがいじれなかったのにイジれるようになったりもしたので良いですね。
後はポストプロセスをもう少し頑張って。


システムについて
Unityはユーザー側で色々考えて実装したり、ライブラリを買ったりして実装することが多いですが、
UEはデバッグコマンドやベースシステムが組み込まれてるのが良いですね。
Unityだとデバッグコマンドなどは自前実装するか、ライブラリを買うしかありません。
UEは日本語資料を増やせとは言わないので、総量として英語だけでもドキュメントを頑張って欲しいです。

Unityのシステムは基礎しか無いので自分で考える必要がありますが、
逆にその基礎は色んな会社で触られるからか、ユーザーのWEB記事が充実してます。
UEはこんなの誰が使うんだ?という機能が多すぎます。
使えない機能はなくして……。


Unityはある程度のPCスペックでエンジンが動くのが良いです。
どの環境でもほぼ同じように動く!
UEは思い通りに動かないことが多いのでイライラします。
しかしながら、UEはエンジンからサワれるので色々カスタマイズしやすいのは利点ですね。
Unityのように色々組み込み方を覚えずに済む。


コードビルドはUnityが軍配ですかね。
IL2CPPだとUnityも時間がかかりますが、UEは通常のコードビルドも結構かかってしまうのが難点です。
ただ、UEはノードベースのBlueprintはビルドは爆速で確認のイテレーションが回しやすいので
BlueprintだけのプロトタイプならUEが早く作れて良いかも?
UEのパッケージング周りは私は、あまり触ったことがないのでソコに関しては割愛。


2D処理はUnityが軍配ですね。
3DはUE。
UEはアニメーション周りが充実していて使いやすいです。
Unityは結構ネイティブ的な使い方っぽい感じがしますが、私があまり機能を知らないだけの可能性もあるので、深くは語りません。
Unityは外部のアセットになるのですが、色々2Dのアセットが充実していて動きを作りやすいのがいいですね。


個人的には初心者にはUnityをおすすめします。
ポインタ周りは初心者には辛すぎる。
UEのBlueprintだけで実装する手もありですが、スケールアップしていく中で
UEは参考資料も少なかったり、色々弄りたくなると思います。
ゲームプログラマを目指すなら、そのぐらい頑張れ!」と言いたい方もいると思いますが、
本当の初心者例えば小学生や中学生がポインタの概念を理解するのは辛いと思います。
最初作ったそのままの感覚で進められるUnityの方が強い。

しかしながら、大規模開発はUEで行われることが今は多いので、大規模コンシューマーに関わりたいなら勉強していて損はなさそう。


後、最初から大規模予定ならUEでも良いかも。
大規模開発に向いてる。
Unityは小規模チーム開発に向いてるかも。
どちらも少人数、大規模が出来ないわけじゃないですが……
どちらも同程度に使えるならという前提なら私はそう思います。

まとめというか、感想というか一言


公平に語ったつもりですが、賛否両論あると思います。
どちらが優れているとかは無いと思います。

作るのは開発者であってエンジンは手段でしか無いです。
状況に応じて判断すべきで、どちらが良いかとは一概にいえないと思います。

Unityエンジニアだから、UEエンジニアだからって現状を嘆いたり、相手を悪く言う必要はないと思います。
というか、どちらも触っている私に精神衛生上に優しくないので、なかよくしてください。

以上です。

MVRP4Uリポジトリの解説

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
以前 UnityのMVP、MV(R)Pを調べたけど、どれが正しいんだ?という記事を書きましたが、
結構閲覧されている割に一番見て欲しいリポジトリの中身を見られていないようだったので、
今回はそのリポジトリの中身の解説をしていきます。
(合わせて2019verにバージョンアップをしました)

github.com

目次


中身について


中身はただボタンを押すと、カウントアップするものです。

f:id:OrotiYamatano:20200908020112g:plain
中身

使用ライブラリ


Zenject(Extenject)


ZenjectとはDIフレームワークです。
DI(Dependency Injection)は依存性の注入という意味ですが、かんたんに言うと、
半自動的にインスタンスを生成してくれる機能です。(ちょっと語弊があるケド……)

シングルトン(Singleton)とかは世界に一つだけで、グローバルにどこからでもアクセスできるという利点がありますが、
逆に言うと世界のどこからでもアクセスできてしまいます。

ゲーム業界では音声の管理やキー入力の管理など好んで使われますが、業界によってはグローバル変数と同じぐらい嫌われ者です。

そこで出てきたのがサービスロケーター(Service Locator)パターンと呼ばれるもので、
コレクションとして登録しておいて必要に応じてインスタンスを生成し取り出す形です。

もう少し分かりやすく言うと、「シングルトンなファクトリークラス(サービスロケーター)にインスタンスをキャッシュしておいて必要に応じて取り出す」って感じです。

手元のインスタンスに対して代入してから使うので、グローバル変数みたいに直接にアクセスするわけではないので、多少マシです。

しかしながら、サービスロケーターでは結局ずっとキャッシュを保持し続けることになり優しくありませんし、
消す仕組みをいれたとしても「シングルトンなファクトリークラス」を参照する必要があり密結合です。

そこで出てくるのがDIパターンです。
DIパターンは先にインスタンスを生成しておいて、生成しておいたクラスをインスタンスに代入してくれます。
使用側はあまり意識せずに利用できるような仕組みです。

詳細は割愛しますが、DIパターンを書くとコード量が多くなるので、DIのコードを書かず
いい感じにDIの良さを体験できるのがDIフレームワークです。

まぁかんたんに言えば、「シングルトンを寿命管理したりスコープ管理したり出来るいい感じのヤツ」がIDフレームワークと考えてくれれば良いです。

UnityのDIフレームワークで今回利用するのがZenject(Extenject)ってわけです。
ExtenjectとはZenjectの派生リポジトリなんですが、(権利周りで)なんやかんやあって
普通最新であるはずの本家リポジトリのZenjectより最新で使いやすいのでExtenjectの方を使いましょう。

assetstore.unity.com

UniRx


UniRxとは
Uni(Unity)Rx(Reactive Extensions)と名前の通り
Unityでリアクティブプログラミングをするためのライブラリです。

作者さんのオレオレライブラリって訳ではなく、ちゃんとした考えに則って作られてます。(本家Rx系から機能を移植されるかは作者の気分次第かも?)

Rxの強みは非同期処理とObservableパターンのプログラミングがしやすくなることだと私は考えます。
Observableパターンとは例えばボタンが押した時に処理が反応するという物を作りたい時、
普通ならボタンを押下という処理からボタンの処理という関数を呼び出すのが普通です。

ですが、Observableパターンは逆で、ボタンを押されたかを監視して、押されたら処理を実行するという処理になります。
ん?イベント駆動型とどう違うんだ?と思った貴方!鋭い!

実際にはイベント駆動型の場合別スレッドで受け取る側を用意していてボタンを押した次のフレームなどに処理されますが、
Observableパターンの場合は同一フレーム内で即実行される。とだけ、ここでは言っておきます。

詳しくは調べてください。
というかそんなことに気がつく貴方はここの記事読まなくても良いのでは?

とにかく、Observableパターンはそのようなイベント駆動型と似ていて、ライブラリを使えば、コードを短く書ける利点があるだけでなく、感覚的に処理が書けます。
もちろん学習コストはかかりますが、慣れれば手放したくないという人が、ほとんどです。

これまでゲームはシングルスレッドで、Update(もしくはTick)処理にて毎フレームゲームオブジェクトを更新したり更新しなかったりするのが普通でした。
しかし、ゲームのオブジェクトの巨大化やシミュレーションするオブジェクト数の増加により単純にUpdateの呼び出しにさえコストを計算する必要が出てきました。

そこで、マルチスレッド化をしたり、Updateを必要なときだけ呼び出して、他の場合は呼び出さないと言ったような処理をしたいとき、
UniRxのようなイベントベース、メッセージベースの機能は便利に働きます。
(Unityは早くシングルスレッドやめろ)

UniRxは必要なときにしかUpdateを呼び出さない処理が組み込まれています。(UpdateAsObservable)
また、頻繁に呼ばないと行けない時に呼び出す関数(Observable.EveryUpdate)などもあり、
使い分けをすることで普通にUnityでUpdate関数を呼び出すより高速化が測れます。

後、MonoBehaviour を継承していないクラスでUpdate出来たり……とにかく便利

qiita.com

作者の一言

前回紹介したMicroCoroutineを改良して、配列をお掃除しながら走査する(かつ配列走査速度は極力最高速を維持する)ようになった

neue.cc

どうやって早くしてるのかとかはUnity触ったことのある人なら一度はお世話になる
皆おなじみテラシュールブログの解説が分かりやすいらしいです。

tsubakit1.hateblo.jp

まぁ、早くなるし、短く書けるのでオススメです。

assetstore.unity.com

MV(R)Pとは


まず、リポジトリの中身を説明する前にレイヤ別けの考え方を説明しておきます。

というか、MVP(MVC)については私より説明がうまいページがあるので、ここを読んできてください。

developers.cyberagent.co.jp

MVPをリアクティブプログラミングでやるのがMV(R)Pです。
この上のリンクのページもキレイにプログラムを書けているのですが、ライブラリを用いていないためコード数が多く複雑です。

ライブラリを使うともっとスッキリ書くことが出来ます。

後、UnityはMonoBehaviourによって支えられていますが、MonoBehaviourを継承して使っているということはMonoBehaviourに依存しているということでもあります。
きれいなソースコードを目指す人はできる限りMonoBehaviourを継承しないマッサラなクラスがきれいなので出来るだけ依存しないようにしましょう。
MonoBehaviourに依存していないコード、イコールUnityにアタッチしていないコードとなるのでUnity上でのコンポーネントのアタッチの考慮をしなくていいのでキレイです。

ソースコード


では、コードの中身を解説していきたいと思います。

Model


まずは分かりやすいModel(TestModel.cs)から。

まずメンバ変数として2つ定義します。

        private readonly IntReactiveProperty num = new IntReactiveProperty();
        public IReadOnlyReactiveProperty<int> Num => num;

いきなり、マッサラなC#しか触ってこなかった人だと分からない型が、出てきましたね?
これはUniRxで定義されている変数の型で、IntReactiveProperty は値が変更した際にイベントのようなもの(OnNext)を発行する型です。
intの派生形だと思ってもらえばいいです。
IReadOnlyReactivePropertypablic で定義されていますが見ての通りReadOnlyで変数 num を参照しています。
つまり Num を外部から使えば num の変更したタイミングと値を取得することが出来ます。

次にコンストラクタの解説に進みます。

        private TestModel(){
            num.Value = 0; //値のリセット
        }

はい。ただnumを初期化してるだけです。

次はビジネスロジックです。
といってもただ加算するものですので、ただ足すだけです。

        public void CountUp()
        {
            num.Value++;
        }

はい。
これで終わりです。

以下がModelの全文。

using UniRx;

namespace Sample.Models
{
    /// <summary>
    /// Model
    /// ビジネスロジックはmodelに書く
    /// </summary>
    public class TestModel
    {
        private readonly IntReactiveProperty num = new IntReactiveProperty();
        public IReadOnlyReactiveProperty<int> Num => num;


        private TestModel(){
            num.Value = 0; //値のリセット
        }

        // カウントアップの処理(ビジネスロジック)
        public void CountUp()
        {
            num.Value++;
        }

    }
}

簡単ですね。
UniRxを使わなくてもModelはほぼ同じような中身になると思います。

View


次はView(TestView.cs)の解説です。
ViewはUnityのオブジェクトにアタッチします。

メンバ変数の解説をします。

        [SerializeField]
        private Button countButton = null;
        [SerializeField]
        private Text text = null;

        //ボタンがタッチされたらPresenterに通知
        public IObservable<Unit> PushButtonObservable => countButton.onClick.AsObservable();

Unityお馴染みの SerializeFieldButtonText を定義します。
もちろん、Unityエディタ側で対応するオブジェクトをアタッチしておきます。

f:id:OrotiYamatano:20200912015247p:plain
スクリプトに該当オブジェクトをアタッチ

次に IObservable<Unit> で定義してあるものですが、こちらもUniRxの機能を使っています。
IObservableと書いている通り監視者です。
ボタン( countButton )が押された時( onClick )に IObservable型にキャスト( AsObservable ) して返す処理を監視します。
publicになっていますので、外部からコレを参照すると、クリックしたタイミングを取得できます。

次に見た目への反映部分の処理です。

        //見た目へ変更を加える(Presenterから呼ばれる)
        public void TextMeshUguiSet(string str)
        {
            text.text = str;
        }

はい。
ただ、pubicメソッドでテキストを代入してるだけです。
これで終わりです。

以下がViewの全文。

using System;
using UniRx;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;

namespace Sample.Views
{
    /// <summary>
    /// Viewの設定
    /// </summary>
    public class TestView : MonoBehaviour
    {
        [SerializeField]
        private Button countButton = null;
        [SerializeField]
        private Text text = null;

        //ボタンがタッチされたらPresenterに通知
        public IObservable<Unit> PushButtonObservable => countButton.onClick.AsObservable();

        //見た目へ変更を加える(Presenterから呼ばれる)
        public void TextMeshUguiSet(string str)
        {
            text.text = str;
        }
        
    }
}

簡単ですね。
Unityにアタッチしてる部分なのでそんなに難しい処理は無いです。

Presenter


次に仲介者であるPresenter(TestPresenter.cs)を解説していきます。
まずは、メンバ変数の解説。

        //読み取りしかしない
        private readonly TestView testView;
        private readonly TestModel upButtonModel;

メンバ変数は、先程解説したTestViewとTestModelを変数として保持しています。
privateでreadonlyなので、ViewとModelは互いに存在を知りません。

コンストラクタの解説です。

        //Presenterの処理
        public TestPresenter(TestModel model, TestView view)
        {
            upButtonModel =  model ?? throw new ArgumentNullException(nameof(model));
            testView = view ? view : throw new ArgumentNullException(nameof(view));
            //ModelとViewが増えたら追記していく
            
            upButtonModel.Num.Subscribe(ViewNumUpdate); //Modelに変更があったらViewへ更新
            testView.PushButtonObservable.Subscribe(_=> CountUp());  //Viewからカウントアップ通知があったらModelを更新

        }

はい、ここでModelとViewの中身を入れてます。
こちらでZenjectの コンストラクタ/メソッドインジェクション を利用しています。
後でもう少し詳しく解説しますが、「先のModelとViewの中身を入れてるんだな~」ぐらいの認識でOKです。
ArgumentNullException は中身なければエラー吐くぐらいな感じです。
定義忘れとかのチェックのために念の為書いておきます。

で、 upButtonModel.Num.Subscribe(ViewNumUpdate); ですが、コメント文のとおりです。
Numの解説に「 Num を外部から使えば num の変更したタイミングと値を取得することが出来ます。」と書きましたが、ここで使用してます。
ViewNumUpdate はメソッドです。メソッド呼び出しをするということです。

要約するとSubscribeでupButtonModelのNumつまり、Modelのnumに変更があったら変更通知を取得でき、ViewNumUpdateメソッド呼び出しをしているということです。

testView.PushButtonObservable.Subscribe(_=> CountUp()); ですが、コチラもコメント文のとおりです。
PushButtonObservableの説明で「外部からコレを参照すると、クリックしたタイミングを取得できます。」と書きましたが、ここで使用してます。(なんせ仲介者ですからね)
こちらはCountUpメソッドを呼んでいます。

では、ViewNumUpdateメソッドとCountUpメソッドの解説です。
と言っても解説するほどの内容は無いです。

        /// <summary>
        /// Modelのカウントアップ処理を呼ぶ
        /// </summary>
        private void CountUp()
        {
            upButtonModel.CountUp();
        }

        /// <summary>
        /// ViewのTextMeshUguiSetを呼ぶ
        /// </summary>
        /// <param name="num"></param>
        private void ViewNumUpdate(int num)
        {
            testView.TextMeshUguiSet(num.ToString());
        }

はい、各Model,Viewの処理を引き継いで渡してる(仲介してる)だけです。
これで終わりです。

以下がPresenterの全文。

using System;
using Sample.Models;
using Sample.Views;
using UniRx;

namespace Sample.Presenter {
    /// <summary>
    /// Presenter
    /// Modelの変更をViewに反映し、ViewのアクションをModelへ反映
    /// </summary>
    public class TestPresenter
    {
        //読み取りしかしない
        private readonly TestView testView;
        private readonly TestModel upButtonModel;

        //Presenterの処理
        public TestPresenter(TestModel model, TestView view)
        {
            upButtonModel =  model ?? throw new ArgumentNullException(nameof(model));
            testView = view ? view : throw new ArgumentNullException(nameof(view));
            //ModelとViewが増えたら追記していく
            
            upButtonModel.Num.Subscribe(ViewNumUpdate); //Modelに変更があったらViewへ更新
            testView.PushButtonObservable.Subscribe(_=> CountUp());  //Viewからカウントアップ通知があったらModelを更新

        }
        
        /// <summary>
        /// Modelのカウントアップ処理を呼ぶ
        /// </summary>
        private void CountUp()
        {
            upButtonModel.CountUp();
        }

        /// <summary>
        /// ViewのTextMeshUguiSetを呼ぶ
        /// </summary>
        /// <param name="num"></param>
        private void ViewNumUpdate(int num)
        {
            testView.TextMeshUguiSet(num.ToString());
        }
    }
}

簡単ですね。
ViewとModelの仲介をしてるだけなので難しい処理は無いです。
見ての通りZenjectを使うとPresenterがMonoBehaviourを継承せずに済み、依存性が薄いクラスが出来上がります。

Zenjectinstaller


一気に説明します。
zenjectInstallerと名前にある通り、zenjectのインストール関連を管理しています。
Containerへのインストールですね。
今回はMonoInstallerを使用します。
定義したものはMonoBehaviourのように振る舞うようになります。
インストールの定義を書かれたものがInstallerのスクリプト

TestModelをAsCached( ContractTypeが要求されるたびにResultTypeの同じインスタンスを再利用。これは最初の使用時に遅れて生成)。
TestPresenterをAsCached( ContractTypeが要求されるたびにResultTypeの同じインスタンスを再利用。これは最初の使用時に遅れて生成)。
した上で、NonLazy(これを指定すると最初にインスタンスが生成される)

using Sample.Models;
using Sample.Presenter;
using Zenject;

namespace Sample.ZenjectInstaller
{
    public class SampleButtonInstaller : MonoInstaller
    {
        //zenjectでModelとPresenterのインストールする
        public override void InstallBindings()
        {
            Container.Bind<TestModel>().AsCached();
            Container.Bind<TestPresenter>().AsCached().NonLazy();
        }
    }
}

各オブジェクトの紐付け


TestModelとTestPresenterがMonoBehabiorのように振る舞うようになりましたが、肝心な部分を解説していません。
それぞれのオブジェクトをどうやってPresenterにつなぎ込んでいるのかです。

それはズバリ、SceneContextやZenjectBindingで解消されます!
後、マッサラなクラスやUnityにアタッチしているオブジェクト()をZenjectの SceneContextZenjectBinding に設定することで依存関係を直してれます。

f:id:OrotiYamatano:20200912022613p:plain

まとめ


ぶっちゃけ、勢いで書いてみたものの、Zenject周りの解説があってるのかとか、
サービスロケーターの説明がちゃんと正しいのかはちょっと不安です。

でも、アウトゲーム部分を作る分にはキレイなコードだと自負しています。
(インゲームに適用するのはおすすめしません)

こんな雑な説明をしていますが、一応私も業務でUnityを使ったことがある身です。
説明は下手ですが、コードは問題ないと思います。ある程度なら肥大化しても耐えうるでしょう。
Zenjectは生成時がちょっと処理負荷が重いですが、そこはちゃんと使いこなして生成タイミングをずらしたりすれば良いと思います。
疎結合のコードが出来る良いものです。
みなさんも色々設計考えながらコーディングしてみてはいかがでしょうか?
以上で解説を終わります。

おすすめの記事とか


ryo620.org

light11.hatenadiary.com

booth.pm

booth.pm

qiita.com

おまけ。


Editorフォルダ配下に途中まで、スクリプト自動生成スクリプトを書きました。
指定先のフォルダのtextを参照してクラスの雛形を作るだけです。

この辺を参考にすると作れます。

light11.hatenadiary.com

(Scriptが小文字になってるのが気に食わなくてソコだけはgifと変わってます)
(あ、削除する方は小文字で変え忘れてますね……ちゃんと消えない……まぁ、いいや)

f:id:OrotiYamatano:20200912025138p:plain

using UnityEngine;

using System.IO;
using System.Text;
using UnityEditor;

// コードの自動生成
public class MVPRU : EditorWindow
{

        private string _baseClassName = string.Empty;
        private string _sceneName = string.Empty;

        
        [MenuItem("Window/MVPRU")]
        private static void Open()
        {
            GetWindow<MVPRU>("MVPRU");
        }


        private void OnGUI()
        {
            EditorGUILayout.LabelField("SceneName");
            _sceneName = GUILayout.TextField(_sceneName);
            EditorGUILayout.LabelField("Create Base Class Name");
            _baseClassName = EditorGUILayout.TextField(_baseClassName);

            if (GUILayout.Button("CreateScript"))
            {
                string path = Application.dataPath;
                string namePath = "Scripts/" + _sceneName + "/";
                path += "/"+namePath;

                
                CreateScriptAsset(_sceneName+".Models", _baseClassName, "Model", path + "/Models",_sceneName);
                //CreateScriptAsset("Script."+_sceneName+".Presenters", _baseClassName, "Presenter", path+ "/Presenters",_sceneName);
                CreateScriptAsset(_sceneName+".Views", _baseClassName, "View", path + "/Views",_sceneName);
                
                Debug.Log($"Create Script Path : {path}");
            }


            if (GUILayout.Button("ClearScript"))
            {
                string path = Application.dataPath;
                string namePath = "script/" + _sceneName + "/";
                path += "/"+namePath;
                
                RemoveScriptAsset(_baseClassName, "Model", path + "/Models");
                RemoveScriptAsset(_baseClassName, "Presenter", path+ "/Presenters");
                RemoveScriptAsset(_baseClassName, "View", path + "/Views");
                SafeCreateDirectory(path + "/ZenjectInstaller/");
                Debug.Log($"Remove Script Path : {path}");
            }

        }

        private const string TemplateScriptFilePath = "ScriptTemplate/";

        private static void CreateScriptAsset(string nameSpace, string baseClassName, string domainName, string filePath,string sceneName)
        {
            string templateRawText = Resources.Load($"{TemplateScriptFilePath}{domainName}.cs").ToString();
            string replacedText = templateRawText.Replace("#SCRIPTNAME#", baseClassName).Replace("#NAMESPACE", nameSpace).Replace("#SCRIPTSCENENAME", sceneName);
            var encoding = new UTF8Encoding(true, false);

            if (Path.GetExtension(filePath) != "")
            {
                // If you select Non directory, then get parent directory.
                filePath = Directory.GetParent(filePath).FullName + "/";
            }

            SafeCreateDirectory(filePath);
            filePath += "/";

            string fileName = $"{baseClassName}{domainName}.cs";
            File.WriteAllText(filePath + fileName, replacedText, encoding);

            var createdScript = AssetDatabase.LoadAssetAtPath<MonoScript>(filePath + fileName);
            ProjectWindowUtil.ShowCreatedAsset(createdScript);
            AssetDatabase.Refresh();
        }

        private static void RemoveScriptAsset( string baseClassName, string domainName, string filePath)
        {
            if (Path.GetExtension(filePath) != "")
            {
                // If you select Non directory, then get parent directory.
                filePath = Directory.GetParent(filePath).FullName + "/";
            }
            filePath += "/";

            string fileName = $"{baseClassName}{domainName}.cs";

            File.Delete(filePath + fileName);

            AssetDatabase.Refresh();
            
        }
        
        
        private static DirectoryInfo SafeCreateDirectory( string path )
        {
            return Directory.Exists( path ) ? null : Directory.CreateDirectory( path );
        }
}