Unity専用のアーキテクチャパターン SMCP

![smpc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/8f610b92-0d19-cd5f-b783-1b8558336e31.png)

# SMCPの目的
SMCP(エスエムシーピー)の目的はただ一つ、 **`MonoBehaviour`から大切なゲームロジックやデータモデルを分離する事です。** SMCPはUnityゲーム開発の一つの基盤です。特定のルールを守る事でストレスなく自由に開発できます。また、ゲームや規模に合わせた独自ルールを設ける事もできる柔軟なアーキテクチャパターンです。

# 3層のレイヤー

### ■ Logic/Model レイヤー
マスターデータやデータモデル、レベルアップのロジックやダメージ計算など、オニオンアーキテクチャでいうドメインやアプリケーションのようなレイヤーです。

**禁止**
- ScriptableObjectの設置は可能ですがMonoBehaviourを継承するスクリプトは置けません。
- transformなど再生中(ランタイム)のみ取得可能なUnity APIへのアクセスや参照は禁止です。

**別レイヤーの参照**
- Otherレイヤーにのみinterfaceを介してアクセスできます。

### ■ View レイヤー
キャラクターの操作や移動、アニメーションやUIなど目で見えるものを表現、操作するレイヤーです。オニオンアーキテクチャでいう、プレゼンテーションやアプリケーションのようなレイヤーです。

**別レイヤーの参照**
- Logic/Model レイヤーへのアクセスが可能です。
- Otherレイヤーにもinterfaceを介してアクセスできます。

### ■ Others レイヤー
このレイヤーで決まっているのはセーブシステム(ゲームデータを永続保存する機能)を置くという事のみです。後々入れ替えが発生しそうな外部プラグインのラッパークラスを置くのも良いかもしれません。どう使うかはプロジェクト次第です。

**別レイヤーの参照**
- Logic/Modelレイヤー、Viewレイヤー全てのレイヤーへアクセス可能です。

![smpc2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/a3b2f22d-4840-eeeb-1287-f94b6ff6b725.png)

# 名前の由来
SMCPの由来は、 **Pattern for separating MonoBehaviour and pure classes** の頭文字を取りました。日本語に訳すと **「MonoBehaviourとピュアクラスを分離するためのパターン」** です。

![SMCP2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/93e3a21f-b4a1-35b6-eb9d-9aeacd16c63b.png)

# サンプルプロジェクト

SMCPを使いパズルゲームのデモを作成しました。
完成プロジェクトなのでダウンロードして実際に動かす事が可能です。

https://github.com/IShix-g/SMCPxPuzzleGame

![screenShot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/f03afe34-19e0-9e84-ab69-7585cda87a42.png)

#### プロジェクトの詳細
- Unityバージョン : 2021.x
- 画面 : 16:9 Portrait スマートフォンの縦持ちを想定
- 使用プラグイン : [VContainer](https://vcontainer.hadashikick.jp/ja/)

#### 開発時の自分ルール
- ドメインを意識した書き方をする
- 必要最低限に実装する。必要な箇所以外の拡張性は考えない
- 基本パフォーマンスを優先する
- DIはLogic/Modelレイヤー以外での利用は最低限必要な箇所のみで利用する

※ サンプルプロジェクトの実装の流れや個人的に思ったポイントを記事にしました。この記事の最後に追記します。

# SMCPに必要なもの

### dllの分離
[Assembly Definition Files](https://docs.unity3d.com/ja/current/Manual/ScriptCompilationAssemblyDefinitionFiles.html)を使って分離します。
![スクリーンショット 2022-10-27 20.26.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389614/f51b3493-13c7-2663-8237-5d7d0a360b68.png)

### 推奨プラグイン
必須ではありませんが、DIの導入を推奨します。
おすすめのプラグインは[VContainer](https://vcontainer.hadashikick.jp/ja/)です。必要最低限の機能が揃った軽量、高速のDIです。

<details><summary>サンプルコード</summary>

[サンプルプロジェクトより] ゲームシーンに必要な依存の解決

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

namespace Others
{
    public class LifeTimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            builder.Register<IGameProgressRepository, PlayerPrefsGameProgressRepository>(Lifetime.Scoped);
            builder.Register<IBlockOrder, SimpleBlockOrder>(Lifetime.Scoped);
            builder.Register<Game>(Lifetime.Scoped);
        }
    }
}
```

</details>

# MonoBehaviourとピュアなクラスを分離する利点

#### エディタで再生しなくとも動作確認ができる
コードが思うように動かず、Debug.logで値を確認しながらエディタの再生停止を繰り返す。
この時間はほんとうに無駄です。こういう時はまずTestRunnerを使いロジックのみでテストします。例え原因が見つからなくとも、あとはエディタの再生中に起こる事だけに注視すれば良いので特定が早まります。

<details><summary>サンプルコード</summary>

[サンプルプロジェクトより] スクリプトのみでボードにブロックを置くテスト
```C#
[Test]
public void Board()
{
    var board = new Board();
    var factory = new BlockFactory();

    {
        var block = factory.Create(BlockType.Block1xX, 5, PieceColor.Black);
        if (board.CanISetBlock(block, 0, 0))
        {
            board.SetBlock(block, 0, 0);
        }
    }

    {
        var block = factory.Create(BlockType.Block2x2, 5, PieceColor.Black);
        if (board.CanISetBlock(block, 1, 0))
        {
            board.SetBlock(block, 1, 0);
        }
    }
    
    Debug.Log(board.Pieces);

    // |O|O|O|O|O| | | | | |
    // | |O| | | | | | | | |
    // |O|O| | | | | | | | |
    // | | | | | | | | | | |
    // | | | | | | | | | | |
    // | | | | | | | | | | |
    // | | | | | | | | | | |
    // | | | | | | | | | | |
    // | | | | | | | | | | |
    // | | | | | | | | | | |
}
```

</details>

#### 開発の分担が楽
例えば、攻撃する機能を作るとします。1人のエンジニアにプレイヤーの攻撃アニメーションの統合、攻撃の当たり判定などエディタを再生する事で開発できる部分を担当してもらいます。もう1人のエンジニアには攻撃力の管理、攻撃を当てた時のダメージ計算などを担当してもらいます。このように分担する事で同じシーンやスクリプトを触る事なくスムーズに開発する事が可能です。これは1人で開発する場合でも有効です。

#### テストが楽
コードは日々進化します。昨日までの条件で正しく動作したものが、今日も正しく動くとは限りません。MonoBehaviourに依存していないコードのテストは楽です。テストシーンを用意する必要がない。オブジェクトにアタッチする必要もない。特定の条件までゲームを進める必要もない。やる事は必要なクラスをnewするだけです。とても簡単にテストできます。

#### 大切な処理がピュアなクラスに集中する
レベルアップのロジック、獲得報酬のロジック、プレイヤーステータスの管理など大切な処理がMonoBehaviourを継承しないピュアなクラスに集中します。そうする事で外部からの影響を最低限に抑え安定性が高くなります。

#### レベルデザインがしやすい
例えば、「レベル5までには何人の敵を倒す必要があるか？」など、実際のロジックやマスターデータを使いながら数値だけで計算して調整したり、シーンを触らずにレベルアップのABテストをしたりなど、比較的楽に実施できます。

#### どこに処理があるか分かり易い
データの処理に問題があるならLogic/Modelレイヤー、動きに問題があるならViewレイヤーと問題が発生した際に確認する箇所が明確になります。

#### コードのレビュアーに優しい
処理を再現するコードと合わせていくつかのテストコードを書いてもらう事で、レビュアーは漏れなくテストできているかコードを確認できるので負担が減ります。

# [あとがき] SMCPの元ネタ
SMCPは[「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」](https://www.amazon.co.jp/%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88%E5%85%A5%E9%96%80-%E3%83%9C%E3%83%88%E3%83%A0%E3%82%A2%E3%83%83%E3%83%97%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B%EF%BC%81%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88%E3%81%AE%E5%9F%BA%E6%9C%AC-%E6%88%90%E7%80%AC-%E5%85%81%E5%AE%A3-ebook/dp/B082WXZVPC/ref=sr_1_1?qid=1666766913&s=books&sr=1-1&text=%E6%88%90%E7%80%AC+%E5%85%81%E5%AE%A3)の著者、[成瀬 允宣](https://twitter.com/nrslib?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor)さんが考案した[ADOP](https://nrslib.com/adop/)の理解しやすいレイヤー構造に強く影響を受けています。
ADOPの実態はヘキサゴナルアーキテクチャですがOthersというレイヤーがあるお陰で一気に理解が進みます。そして何よりADOPを表現するイメージ図が分かりやすい。今までアーキテクチャ系の図は理解するまでに時間がかかりましたが一目で理解できました。学ぶ側の視点に立って教えていただける成瀬さんにほんとに感謝です。
本もかなり分かり易いのでドメイン駆動設計を早く理解したいという方は是非読んでみてください。

※ SMCPのOthersレイヤーとADOPのOthersレイヤーは異なる役割です。ご注意ください。

# 追記 SMCP導入時の実装の流れや個人的に思ったポイント

https://qiita.com/IShix/items/0af9320bb4953560df63