Unityで寿命はあるけど広くアクセスさせたい変数の置き場を考える

UE4にはGameModeやGameInstanceという便利なクラスがあります。

Unityにはない概念かなと思うので、すごく簡単に説明すると
・GameInstance：ゲーム全体が寿命のインスタンス
・GameMode：シーン寿命のインスタンス
で、各々がGetGameInstanceやGetGameModeなどの関数で簡単にアクセスできるものです。
厳密にはネットワーク挟んだりするとサーバ側ではインスタンスがサーバ上のみになったりするので厳密にはこれではないのですが、今回の目的とはちょっとそれてしまうので割愛します。
良い名前が思いつかなかったのでUE4さんから借りた程度に思ってください。

public staticなものをあちこちからアクセスしたいって需要は普通にあって、でも初期化と破棄のタイミングを間違えてバグらせてしまうというのは大いにあるとは思うのですが、シーン読み込み時に初期化、シーン破棄時に破棄され、なおかつAdditiveなシーンからGetGameModeをした場合でもCurrentSceneのGameModeが取れたらいいのではないかと思い、軽く作ってみました。

簡単なテストで動いてはいるのですが、実際バグマミレだと思うので、私が実環境で使い始めるまではこんな感じで。

https://github.com/yKimisaki/UniGameInstance

ちなみに2019.3の練習で作ったものなので、SerializeReference使っています。
ので2019.2以前では使えません。あしからず。

# やりたいこと

```Player.cs
using Tonari.Unity;
using UnityEngine;

namespace Assets.Scripts
{
    public class Player : MonoBehaviour
    {
        public void Start()
        {
            // このgameObjectがいるシーンのGameInstanceをもらう
            MyGameInstance gameInstance = gameObject.GetGameInstance<MyGameInstance>();
            // GameInstanceが持ってるアイテムマスタから装備情報をもらう
            var equipment = gameInstance.GetItemMaster(equipments[0].ItemId);

            // このgameObjectがいるシーンのGameModeをもらう
            MyBattleGameMode gameMode = gameObject.GetGameMode<MyBattleGameMode>();
            // 敵グループ管理のモデルに、自分を狙うようにお願いする
            // 敵グループ管理のモデルはバトル用ゲームモードが持っている
            gameMode.GetEnemyGroupModel().SetTargetPlayer(this);
        }
    }
}
```

# 使い方

## GameInstanceのつくり方

```SampleGameInstance.cs
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Assets.Scripts
{
    /* SerializeReferenceのためにSerializableつけてください */
    [Serializable]
    public class SampleGameInstance : Tonari.Unity.GameInstance
    {
        [SerializeReference]
        private List<int> numbers = new List<int>();

        public override void Initialize()
        {
            base.Initialize();

            /* 初期化ロジック */

            numbers.Add(5);
        }

        public override void Dispose()
        {
            /* 破棄ロジック */

            numbers.Clear();
            numbers = null;

            base.Dispose();
        }
    }
}
```

## GameModeのつくり方

GameInstanceとだいたい同じです。

```SampleGameMode.cs
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Assets.Scripts
{
    /* SerializeReferenceのためにSerializableつけてください */
    [Serializable]
    public class SampleGameMode : Tonari.Unity.GameMode
    {
        [SerializeReference]
        private List<int> numbers = new List<int>();

        public override void Initialize()
        {
            base.Initialize();

            /* 初期化ロジック */

            numbers.Add(5);
        }

        public override void Dispose()
        {
            /* 破棄ロジック */

            numbers.Clear();
            numbers = null;

            base.Dispose();
        }
    }
}
```

## 設定の仕方

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330809/b8040ffd-d3a9-3823-8b65-1b4d72b0ad3c.png)

Windowメニューにこんなのを追加しているので開くと、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330809/a62524ff-6516-26d3-c3d5-96d267c363e8.png)

という質素な画面が出てきます。
この質素な画面でGameInstanceと、各シーンに紐づくGameModeを設定します。

プルダウンを押すと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330809/1e85b49d-cd38-ddfd-8178-f56d81899793.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/330809/999253d0-376e-9926-0567-81de5b2edbbb.png)

さっき作ったGameInstanceやGameModeが出てくるので、選択して設定は終了です。

### 注意

GameModeを設定するには、設定したいシーンを開いてからGameModeSettingWindowを使用してください。
いったんは雑で、すみません。

## 寿命と初期化の方法について

GameInstanceとGameModeは、ScriptableObjectに初期値の入ったインスタンスを格納しています。
これはDefaultではResources直下にGameModeSettings.assetとして生成されます。
GameInstanceは起動時や、任意のタイミングで`GameInstance.ResetInstance()`を呼ぶことで、1つ前のインスタンスをDisposeし、初期インスタンスをディープコピーしてキャッシュします。
GameModeはシーンの読み込み時に初期インスタンスをディープコピーしてキャッシュし、シーンが破棄されるタイミングでDisponseします。

つまり、シーンA→B→Aと移動した場合、最初のAのGameModeインスタンスと最後のAのGameModeインスタンスは別物になります。
ディープコピーされたインスタンスの寿命は完全にシーンの寿命と一致します。

また、ゲーム内でエラーが起こりタイトルに戻った際にGameInstance.ResetInstance()をすることで、エラーの起きている時代のGameInstanceを破棄し、新しいGameInstanceを使用することができます。
マスタの再取得などはこのパターンでできるかと思います。