シーン間のデータ受け渡し(Dictionaryを渡したい?)

2023年1月27日

結論ですが、今回使うUnityの機能(スクリプタブルオブジェクト)では、Dictionaryが対応していません
また、UnityのJsonUtilityでも対応していないので注意しましょう

Dictionaryを使いたいケースとして次のようなケースがあります

  • みかん→100円、りんご→150円のように価格を知ることができる一覧が欲しい
  • 山田→000-0000、山本→111-1111のように出羽町のようなものを作りたい

ペアでデータを管理できる便利なのが、Dictionaryになります。

Keyから値を取得する、Keyと値のペアとして情報を保存しておくのは効率がいいですね

ベースとなるサンプル

今回の説明は、ベースとなるサンプルがあります
このサンプルを作成、動作確認後に読み進めるとスムーズに実現ができます

スクリプタブルオブジェクトを使ったデータの受け渡し(Dictionaryの代わり)

スクリプタブルオブジェクトとリスト

このクラスは、シーンの名前とそのシーンをクリアしたかをメンバーとして持っています

[Serializable]
public class Status
{
    public string name;

    public bool isClear;
}

シーンごとの状態が欲しいので、このクラスをリストにして管理します

public List<Status> sceneStates;

スクリプタブルオブジェクトを作るコード

using System;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObject/SceneState")]
public class SceneStatus : ScriptableObject
{
    [Serializable]
    public class Status
    {
        public string name;

        public bool isClear;
    }

    public List<Status> scenesStates;
}

呼び出すサンプルコード

このデータを読み込むサンプルも作ってみます
sceneStatusには、インスペクターからスクリプタブルオブジェクトをドラッグ&ドロップしておきます

using UnityEngine;

public class SceneStateTest : MonoBehaviour
{
    [SerializeField]
    SceneStatus sceneStatus;

    void Start()
    {
        Debug.Log(sceneStatus.scenesStates[0].isClear);
    }
}

インスペクターで確認

プロジェクトウィンドウで作成されたスクリプタブルオブジェクトを選択し、インスペクターで確認するとこのようになります

2つほどシーンを代入してみましょう

実行結果

リストでDictionaryのような使い方をする

シーン名をキーにしてデータを読み取れるようにします
まだこのコードはエラーになります

using UnityEngine;

public class SceneStateTest : MonoBehaviour
{
    [SerializeField]
    SceneStatus sceneStatus;

    void Start()
    {
        Debug.Log(sceneStatus["Scene1"].isClear);
    }
}

スクリプタブルオブジェクトの変更

C#には、インスタンスをDictionaryのような振る舞いにできるインデクサという機能があります

アクセス方法をインデクサに変更したコード

この変更で出ていたエラーが解消されます

処理としては、次のようになります

  • リストの中で名前を探します
  • 見つかればクリアできたかのメンバー(クラスでペアにしたisClear)を呼び出し元に戻します

インデクサの書き方はプロパティに似ていて、Get・Setを使います(thisが入っているところは違いますね)

呼び出す時は、インスタンス名[キー]というようにDictionaryのように使うことができます

using System;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObject/SceneState")]
public class SceneStatus : ScriptableObject
{
    [Serializable]
    public class Status
    {
        public string name;

        public bool isClear;
    }

    public List<Status> scenesStates;

    public Status this[string key]
    {
        get
        {
            Status result = null;

            foreach (var state in scenesStates)
            {
                if (state.name == key)
                {
                    result = state;
                }
            }
            return result;
        }
    }
}

LINQに書き換えたコード(処理は同じ)

LINQの知識がある人は次のように書き換えてもいいでしょう

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObject/SceneState")]
public class SceneStatus : ScriptableObject
{
    [Serializable]
    public class Status
    {
        public string name;

        public bool isClear;
    }

    public List<Status> scenesStates;

    public Status this[string key]
    {
        get
        {
            return scenesStates.FirstOrDefault(state => state.name == key);
        }
    }
}

getをラムダ記号を使って省略したコード(処理は同じ)

さらにシンプルにできます
VisualStudioなどの開発環境ではインテリセンスのようなコード支援機能が使えますので自動変換が可能です
最初のサンプルから1行増えただけまで短くすることができましたね

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObject/SceneState")]
public class SceneStatus : ScriptableObject
{
    [Serializable]
    public class Status
    {
        public string name;

        public bool isClear;
    }

    public List<Status> scenesStates;

    public Status this[string key] => scenesStates.FirstOrDefault(state => state.name == key);
}

値のセットもできるように機能追加

読み出しだけの対応(get)から、機能の追加として値をセットできるコードも考えてみます
呼び出す方のサンプルを次のようにして、確認することができるようにします

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneStateTest : MonoBehaviour
{
    [SerializeField]
    SceneStatus sceneStatus;

    void Start()
    {
        sceneStatus["Scene1"].isClear = true;
        Debug.Log(sceneStatus["Scene1"].isClear);
    }
}

インスペクタで値が登録済みなら大丈夫ですが、Scene1のデータがない場合にはエラーになります

sceneStatus["Scene1"].isClear = true;

データがなければ作るようにする

データが存在しない場合は作ってくれる(インスタンスを作成してくれる)ように機能追加してみましょう

コード

インデクサに条件をつけることで対応します

スクリプタブルオブジェクト側のコードを変更します
もし名前が存在しない場合(if文)、新しくインスタンスを作って(new)、リストに追加(add)するコードです

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObject/SceneState")]
public class SceneStatus : ScriptableObject
{
    [Serializable]
    public class Status
    {
        public string name;

        public bool isClear;
    }

    public List<Status> scenesStates;

    public Status this[string nameKey]
    {
        get
        {
            if (scenesStates.Count(scene => scene.name == nameKey) == 0)
            {
                scenesStates.Add(new Status { name = nameKey, isClear = false });
            }

            return scenesStates.FirstOrDefault(scene => scene.name == nameKey);
        }
    }

キー自体が上書きされないようにする

今回はリストの1つのメンバーをキーとして使いますので、変更することはないはずですね
キーとしてのScene1を、"あり得ないシーン"に置き換えることができてしまいます

sceneStatus["Scene1"].name = "あり得ないシーン";

コード

性善説(そんなことする人いない)ではなくコードを堅牢にするのが賢明です

  • アクセスは、プロパティでできるようにしておきます(public)
  • nameへの書き込みはprivateにして、変更できないようにします(private set;)
  • インスペクタには表示したいので[serializeField]属性はつけておきます
  • データの作成は、コンストラクタ経由だけにします

もし、実行時にデータをクリアしたのであれば、コメントの OnEnable()イベントを有効にします

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObject/SceneState")]
public class SceneStatus : ScriptableObject
{
    [Serializable]
    public class Status
    {
        [SerializeField]
        string name;

        [SerializeField]
        bool isClear;

        public string Name { get => name; private set => name = value; }
        public bool IsClear { get => isClear; set => isClear = value; }

        public Status(string name, bool isClear)
        {
            Name = name;
            IsClear = isClear;
        }
    }

    public List<Status> scenesStates;

    public Status this[string nameKey]
    {
        get
        {
            if (scenesStates.Count(scene => scene.Name == nameKey) == 0)
            {
                scenesStates.Add(new Status(nameKey, false));
            }

            return scenesStates.FirstOrDefault(scene => scene.Name == nameKey);
        }
    }

    /*
    void OnEnable()
    {
        sceneStates.Clear();
    }
    */
}

Unity

Posted by hidepon