【Unity】アーキテクチャパターンSMCPをプロジェクトに導入する際の知見

先日、SMCPというUnity専用のアーキテクチャパターンを公開しました。
SMCPを実際にプロジェクトに導入する上での知見をこの記事で紹介したいと思います。

アーキテクチャパターンの詳細は下記記事をご覧下さい。

https://qiita.com/IShix/items/5e1450ad8fb7f4a39f22

# Logic/ModelレイヤーはToString()をオーバーライドすると便利
これは過去記事でも紹介しましたが、Logic/ModelレイヤーはTestRunnerで開発を進める為、状態が分かりません。
そこで`ToString()`を`override`すると`Debug.Log(instance);`という風に書けて便利です。

```C#
public override string ToString()
{
    var msg = $"- {GetType()} -\n";
    for (var column = 0; column < Size.Column; column++)
    {
        for (var row = 0; row < Size.Row; row++)
        {
            msg += $"|{(Has(column,row) ? "O" : " ")}";
        }
        msg += "|\n";
    }
    return msg;
}
```

[SMCPで紹介したサンプルゲームのMatrix.cs224行目](https://github.com/IShix-g/SMCPxPuzzleGame/blob/main/Assets/_Projects/Scripts/LogicAndModel/Matrix.cs#L227-L239)

### RiderのUnit Testsで上記`ToString()`を実行した結果

[テストコード](https://github.com/IShix-g/SMCPxPuzzleGame/blob/main/Assets/_Projects/Scripts/Others/Tests/LogicAndModelTest.cs#L12-L39)

![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/7bebccfe-d8c4-7c75-e42d-287e05bde5c3.png)


# 初期化が1度だけ走り、アプリ終了まで消えないDIコンテナ
DIに[VContainer](https://vcontainer.hadashikick.jp/ja/)を利用しています。
既存のお手製プラグインのコードを変更したく無いので、すべてのシーンに設置し初期化が1度しか走らないDIコンテナが必要になり作成しました。ちなみに全シーンに置く理由は全シーンに置けないとエディタで再生する際、タイトルシーン(ゲームで初めに表示されるシーン)から再生しないと正しく動作しなくなる為です。

### AppLifetimeScope.cs
※ 自身のプロジェクトで各シーンのCanvasが必要な為そのコードも含まれています。

```C#
using UnityEngine;
using VContainer;
using VContainer.Unity;

namespace Others
{
    /// <summary>
    /// 1番初めに初期化されるルートコンテナ
    /// ※ DontDestroyOnLoadなのでゲーム終了まで消えない
    /// ※ 全シーンに必ず設置する
    /// </summary>
    public sealed class AppLifetimeScope : LifetimeScope
    {
        [Header("※※ 全シーンに必ずこのPrefabを設置 ※※")]
        [SerializeField] Canvas _mainCanvas;
        
        static AppLifetimeScope s_instance;
        public Canvas MainCanvas => _mainCanvas;

        /// <summary>
        /// 初期化
        /// アプリ起動時に1度だけ呼ばれる
        /// </summary>
        void AppInitialize()
        {
            // FPSの設定
            Application.targetFrameRate = 60;
            // ※ ここで1度だけ初期化したいプラグインを初期化
            // (省略)
        }

        /// <summary>
        /// 依存の解決
        /// アプリ起動時に1度だけ呼ばれる
        /// </summary>
        protected override void Configure(IContainerBuilder builder)
        {
            // ※ 依存の解決
            // (省略)
        }

        /// <summary>
        /// シーンが遷移する度にAwakeで毎回実行
        /// </summary>
        void EverySceneAwake()
        {
            // canvasの設定
            s_instance._mainCanvas = _mainCanvas;
            // ※ ここでシーンが遷移する度に実行したい処理を書く
            // (省略)
        }

        protected override void Awake()
        {
            if (s_instance == default)
            {
                AppInitialize();
                base.Awake();
                s_instance = this;
                DontDestroyOnLoad(gameObject);
            }
            else if (s_instance != this)
            {
                Destroy(gameObject);
            }
            EverySceneAwake();
        }
    }
}
```

あとは、各シーンにAppLifetimeScopeを設置し、各シーンのLifetimeScopeのParentに設定するだけです。
![スクリーンショット 2022-12-03 12.21.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/681be036-056f-1110-6ba8-3ca9b2d5b7d0.png)


### LifetimeScopeの親になるルートLifetimeScope
VContainerには[Project root LifetimeScope](https://vcontainer.hadashikick.jp/ja/scoping/project-root-lifetimescope)というすべてのLifetimeScopeの親になるLifetimeScopeを設定できる機能があります。Hierarchyからの参照が必要無い場合、こちらも選択肢に入れても良いと思います。

# プラグインの対応

SMCPはAssembly Definitionで分割する必要があるので、Asset StoreからダウンロードしたプラグインにAssembly Definitionが含まれていない場合、設定する必要があります。その中で特筆すべきプラグインが2つあったので紹介します。

### SR Debuggerは無理
SR Debuggerのメニューにカスタムボタンを表示する事がよくあるのですが、その時に使う`SROptions`というクラスが`partial class`の為、dllの分離ができませんでした。どうしようもないので例外として対応しました。

### DotTweenは要設定
初期設定では、Assembly Definitionで分割されておりません。下記手順で生成する必要があります。

Tools > Demigiant > DOTween Utility Panelを開く
![スクリーンショット 2022-12-02 12.55.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/4369e113-55d0-5477-2bdb-6004b5b7412e.png)

「Create ASMDEF」をクリック
![スクリーンショット 2022-12-02 12.55.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/9a2602b7-a90d-5ef1-a305-8240981b17b0.png)


# [パフォーマンス向上] 可能なものはsealedにする
これはIL2CPPでビルドするプロジェクト向けです。可能なものは`sealed class`にすると仮想メソッドの呼び出し分パフォーマンスが向上します。ただ、残念ながらUnity2018移行この機能は削除されてしまいました。いつか再実装される可能性もあるので習慣として`sealed class`にしています。

```C#
public sealed class TestScript
{
}
```

# [パフォーマンス向上] メソッドのインライン化
外部アセンブリ(Assembly Definitionで分割したdll)からコールされるメソッドは積極的に[インライン化](https://learn.microsoft.com/ja-jp/visualstudio/ide/reference/inline-method?view=vs-2022)してほしいので`MethodImpl`属性を付与する事で多少パフォーマンスが向上する可能性があります。

```C#
using System.Runtime.CompilerServices;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Method()
{
}
```

# [追記] Managed Stripping LevelとDI問題

普段、Managed Stripping LevelをMediumでビルドしているのですが、その場合、[VContainer](https://vcontainer.hadashikick.jp/ja/)で渡しているほとんどのクラスのコンストラクタやメソッドが未使用としてマークされてしまいビルド時に削除されてしまいます。

Project Settings > Player > Managed Stripping Level
![スクリーンショット 2023-06-07 17.42.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/77feb749-1ac3-1d42-49bc-d42b940b2ce9.png)

その問題を解決するには、2つのアプローチがありますが今回は、`LogicAndModel`配下がDIの対象且つ消されては困るコードなので**Link XML**で対応しました。詳しくはリファレンスの[マネージコードストリッピング](https://docs.unity3d.com/ja/2018.4/Manual/ManagedCodeStripping.html)で確認できます。

下記コードをAssets配下の適当な場所に設置しました。
```xml:link.xml
<linker>
       <assembly fullname="LogicAndModel" preserve="all"/>
</linker>
```