テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】ScriptableObjectを使用して、Scene間・Object間のデータ共有を行う

今回はScriptableObjectを利用してデータの共有を行う方法を紹介します。

この方法は「Find系が不要」かつ「事前にデータを定義する事が可能」「Awakeが呼ばれる前にセットアップが完了」「オブジェクト間・シーン間でデータが共有できる」といった扱いが出来ます。

f:id:tsubaki_t1:20151028004401p:plain

目次

ScriptableObjectを使ったデータ共有

このやり方は単純です。

まずScriptableObjectのアセットを事前に作成して置き、このデータを用いてオブジェクト間のデータ共有を行います。

tsubakit1.hateblo.jp

呼び出すScriptableObjectを作成しインスタンスをアセットとして保存します。
アセットとして保存するアプローチはCreateAssetMenu属性を使用しても良いですし(基本機能で作れて手軽)、ScriptableObjectをAssetsファイルとして出力する汎用スクリプト · GitHubでも良いです(こっちの方がメニューがスッキリします)。

今回はCreateAssetMenuを選択します。CreateAssetMenuの属性があるScriptableObjectは、Assets/Createに同名のオブジェクトのインスタンスを保存するメニューが追加されます。

using UnityEngine;
using System.Collections;

[CreateAssetMenu()]
public class Data : ScriptableObject 
{
	public int count;
}

f:id:tsubaki_t1:20151027234109p:plain

 次にScriptableObjectをコンポーネントに設定します。

using UnityEngine;
using System.Collections;

public class DataLoader : MonoBehaviour {

	[SerializeField]
	Data data;
}

f:id:tsubaki_t1:20151027234641j:plain

これで「同じScriptableObjectを参照しているオブジェクトは値を共有」する事が出来ます。

tsubakit1.hateblo.jp

勿論、Resourcesから呼び出す事も出来ます。
例えば作成したDataオブジェクトをManagerに改名してResourcesフォルダ以下に置けば、以下のように呼び出す事が出来ます。

using UnityEngine;
using System.Collections;

public class DataLoader : MonoBehaviour {

	Data data;

	void Awake()
	{
		data = Resources.Load ("Manager");
	}
} 

このResources.Loadやシーンの定義で呼び出したScriptableObjectのインスタンスは、ScriptableObjectはResources.UnloadやUnloadUnusedAssetsでアンロードされない限り、同じインスタンスが使いまわされ、他のコンポーネントが設定した値をそのまま使いまわすことが出来ます

データの更新について

ScriptableObjectのインスタンスをResourcesもしくはエディタで接続している場合、参照元が同じオブジェクトであるため複数のオブジェクト間でのデータ共有が可能です。

データ共有にあたり一つ問題なのは、ScriptableObjectはLoadLevelやゲーム再生の停止では設定内容が破棄されない事です。
つまりゲーム再生終了時のパラメータがそのまま次の再生に持ち越されます。

tsubakit1.hateblo.jp

これを回避するには色々とアイディアがありますが、一番手っ取り早いのはプロパティをシリアライズしないようにする事のように思います。つまりprivateで[SerializeField]ではないフィールドや、実質メソッドでシリアライズ出来ないプロパティを使用する等です。

例えば上のDataクラスを以下のように記述します。

using UnityEngine;
using System.Collections;

[CreateAssetMenu()]
public class Data : ScriptableObject 
{
	private int m_Count;
	public int count{ get; set; }

	void OnEnable()
	{
		count = m_Count;
	}
}

m_Countはシリアライズされない項目なのでシリアライズされず、Inspectorで値を設定しつつデータの共有も可能…といった感じのことが出来るようになります。

実行順番について

ここで問題になるのがScriptableObjectのOnEnableの実行順番です。

この実行順番ですが「オブジェクトが参照された直後」となります。Unityではコンポーネントインスタンス化時は大雑把に言えば以下のタイミングで処理を行います。

  1. クラス(コンポーネント)のフィールドに定義している変数の初期化
  2. コンポーネントメタデータに定義されているデータの流し込み
  3. AwakeやOnEnable

基本的にScriptableObjectも流れは同じなのですが、ScriptableObjectの呼ばれるタイミングはどうやら2の「メタデータに定義されているデータの流し込み」時らしく、シリアライズしている場合はAwakeより前に呼ばれます

これはシーン内のコンポーネントから参照があった場合で、Resources等から呼び出した場合は呼び出した順番に依存します。

但しPlayerSettingsのOtherSettingsにあるPreloadAssetsに事前に設定して置くことで、ゲーム開始時に事前にロードしておく事が可能です。
まあインスタンスをロードした地点でOnEnableが呼ばれるのであまり意味は無いのですが、絶対にAwakeより前に呼びたい場合はこういったアプローチもあります。

f:id:tsubaki_t1:20151028000953p:plain

ちなみにOnEnable時にFind等のUnityAPIも使用することが出来ます。

ロードしたScriptableObjectのインスタンス
破棄されるタイミング

このScriptableObjectを使ったデータの共有ですが、永続ではありません。

何が問題かと言えば、このScriptableObjectはアセットとして呼び出しているため、Resources.UnloadやResources.UnloadUnusedAssetsで破棄されます

またLoadLevelの際次のシーンに該当のScriptableObjectのインスタンスを使用したオブジェクトが無い場合、これも破棄されます。

 

但しPreloadAssetsに設定している場合はシーンの読み込みやUnloadUnusedAssetsでは破棄されなくなるので、マネージャーとして使いたい場合はPreloadAssetsに設定しておくのが良いかもしれません。

この状態でもResources.Unloadで破棄(初期化)は出来るので、ゲームサイクルの開始にUnloadで一旦ロードしたインスタンスを初期化しておくと、色々と楽かもしれません。

 

ScriptableObjectはエクセルやらパラメータを格納するのに使えますが、実は値を設定することが可能でマネージャー的なものに使ったら案外便利かもなって感じで。

tsubakit1.hateblo.jp