17
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unityでシングルトンを使わずにシーン間でデータを保持する。

Last updated at Posted at 2022-10-27

1.はじめに

cocone株式会社さんの『cocone tech talk』のLT記事を参考にさせていただきました。
ありがとうございます。

本記事のUnityのバージョンは『Unity 2021.3.1f1』を使用しています。

2. なぜシングルトンを使わないのか

 結論からいうと、シーン間で情報を保持するだけならシングルトンを利用する必要がないからです。
そもそもシングルトンが採用される理由として、

  • インスタンスが一つであることを保証したい
  • 様々なクラスから参照を持たせたい

などが挙げられると思います。

 ゲーム開発においてシングルトンを採用した方が都合がよい場面にも遭遇しますが、

「この"〇〇Manager"ってクラスよく使うしシングルトンにしとこ」

のように安易な考えでシングルトン化すると無数の参照を持つ密結合クラスが出来上がります。

 むやみに乱用することで、可読性が悪くクラス同士が依存しあう設計になってしまいます。
また、実は複数のインスタンスを持つ方が都合がよいと分かった際の影響範囲が非常に大きいと言えます。

3. 実装の詳細

 各シーンのルートオブジェクトにAbstructSceneを継承したコンポーネントを配置します。
b9285f5ac48425a9266cf9fda59932d3.png

 基底クラスとなるAbstructSceneの詳細。
こいつが各シーンに配置されて、受け取った変数を使用します。

AbstructScene.cs
using UnityEngine;
using Cysharp.Threading.Tasks;

public abstract class AbstructScene : MonoBehaviour
{
    protected SceneOperator _sceneOperator;

    private void Awake()
    {
        OnAwake();
    }
    protected virtual void OnAwake() { }

    /// <summary>
    /// SceneOperatorのLoadSceneを非同期で呼ぶ
    /// </summary>
    public async void LoadScene(string sceneName,string message)
    {
        //最初に一度インスタンスを初期化
        if (_sceneOperator == null)
        {
            _sceneOperator = new SceneOperator(message);
        }
        await _sceneOperator.LoadScene(sceneName);
    }

    /// <summary>
    /// 前のシーンからSceneOperatorを引き継ぐための関数
    /// </summary>
    public void SetOperator(SceneOperator appOperator)
    {
        _sceneOperator = appOperator;
    }
    
    /// <summary>
    /// ロード時に呼ばれる
    /// </summary>
    public abstract UniTask OnLoad(string message);
    /// <summary>
    /// ロードされた後に呼ばれる
    /// </summary>
    public abstract void OnOpen();
    /// <summary>
    /// シーンの破棄時に呼ばれる
    /// </summary>
    public abstract UniTask OnUnLoad();
}

 非同期でシーンを読み込んで、次のシーンのAbstructSceneに値を渡します。

SceneOperator.cs
using UnityEngine;
using System.Threading;
using UnityEngine.SceneManagement;
using Cysharp.Threading.Tasks;

public class SceneOperator
{
    /// <summary>
    /// シーン間で保持する文字列
    /// </summary>
    string _message;

    public SceneOperator(string message)
    {
        SetUp(message);
    }

    public void SetUp(string message)
    {
        _message = message;
    }

    public async UniTask LoadScene(string sceneName)
    {
        //シーンを破棄
        await GetActiveAbstructScene(SceneManager.GetActiveScene()).UnLoad();

        //シーンをロード
        await SceneManager.LoadSceneAsync(sceneName);

        //ロード先のAbstructSceneを取得
        var absScene = GetActiveAbstructScene(SceneManager.GetSceneByName(sceneName));
        absScene.SetOperator(this);

        //ロード時の処理を呼ぶ
        await absScene.Load(_message);
        absScene.Open();
    }

    /// <summary>
    /// シーン内のRootObjectsからAbstructSceneを返す
    /// </summary>
    AbstructScene GetActiveAbstructScene(Scene scene)
    {
        AbstructScene abstructScene = null;
        foreach (var obj in scene.GetRootGameObjects())
        {
            if (obj.TryGetComponent(out AbstructScene getAbstructScene))
            {
                abstructScene = getAbstructScene;
                break;
            }
        }
        if (abstructScene)
        {
            return abstructScene;
        }
        else
        {
            throw new System.ArgumentNullException($"{scene.name}のRootObjectsにAbstructSceneがアタッチされたオブジェクトが含まれていません。");
        }
    }
}

AbstructSceneの実装例。

InGameManager.cs
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;

public class InGameManager : AbstructScene
{
    /// <summary>シーン内で使用する文字列</summary>
    protected string _message = "";

    [Header("コンポーネント")]
    [SerializeField,Tooltip("押下時にロードが呼ばれるボタン")]
    Button _loadButton;

    [SerializeField,Tooltip("押下時に次のシーンに渡したい文字列を決定するボタン")]
    Button _inputButton;

    [SerializeField,Tooltip("次のシーンに渡したい文字列を入力するフィールド")]
    protected InputField _inputField = null;

    [SerializeField,Tooltip("現在保持されている変数を表示するText")]
    protected Text _text;

    [Header("ロード関連")]
    [SerializeField,Tooltip("ロード先のシーン名")]
    string _sceneName;

    protected override void OnAwake()
    {
        //押下時に指定のシーンにロードする。
        _loadButton.onClick.AddListener(() => { LoadScene(_sceneName, _inputField.text); });

        //押下時に次のシーンに渡す文字列を変更する
        _inputButton.onClick.AddListener(() => {
            if (_sceneOperator == null)
            {
                _sceneOperator = new SceneOperator(_inputField.text);
            }
            else
            {
                _sceneOperator.SetUp(_inputField.text);
            }         
        });
    }
    public override async UniTask OnLoad(string message)
    {
        _message = message;
        await UniTask.Yield();
    }

    /// <summary>
    /// シーンがロードされた後に呼ばれる
    /// </summary>
    public override void OnOpen()
    {
        _text.text = _message;
    }

    public override async UniTask OnUnLoad()
    {
        await UniTask.Yield();
    }
}

 実行するとこんな感じです。
真ん中のボタンを押すとシーンが切り替わり、上に表示されているテキストがシーンをまたいで保持されています。
 InputFieldで値を変更できます。
KickHero-InGame-Windows_-Mac_-Linux-Unity-2021.3.1f1-Personal-DX11-2022-10-27-06-40-53_Trim.gif

注意点としてAbstructSceneに依存しては元も子もないので値を使用する側のクラスで少し工夫が必要です。、

4. まとめ

 ちなみに、自分は決してアンチシングルトンではありませんw(Inputなどはシングルトンにしがち)
シングルトンはGoFを始めとしたさまざまな場所で取り上げられ使用されているデザインパターンですが、使用には少し注意が必要だったりします。

 今回は設計の引き出しを増やす目的で実装してみました。
これでシングルトンを使うことで起きがちな密結合を防ぐことができそうです。

 初めてのQiitaへの記事投稿になるので、間違っている点やミス等があればぜひご教示ください。

17
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?