411 Views
October 03, 17
スライド概要
ScriptableObjectとはどういった物か、ScriptableObjectを利用すると何が良いのかといった基本的な内容から、実際にScriptableObjectを活用した開発の効率化について紹介します。
このスライドは『ヴァルキリーコネコクト』のクオリティを支える開発技術最前線で講演した内容のスライドです。 http://www.a-tm.co.jp/recruit/news/event-6944/
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
Scriptable Object 入門と活用例 unity
Unityと オブジェクト unity
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class CallSO : MonoBehaviour { public BaseSO so{ get; set; } public void Call (Text label) { so.Push (label); } } ●シーンにGameObjectを配置する ●GameObjectにComponentを追加する
音を出す 移動する ゲーム進行管理 他のオブジェクト と接触 エンジンに モデルの 描画を依頼 物理演算 ●コンポーネントが、オブジェクトの振る舞いを決める ●オブジェクト同士が干渉してゲームを作る
●GameObjectに追加したコンポーネントの フィールドや参照関係を埋めて、振る舞いを調整
●(比較的静的な)世界が出来る Scene / Game Object / Component の関係
Scene / Game Object / Component の関係 Scene GameObject GameObject Component Component Component Component
Prefab GameObject Component Component ●PrefabでGame Objectをファイル(アセット)として書き出す
Instantiate ●Prefabをシーンに配置したり、動的にPrefabをInstantiate(複製)して ゲームを表現するのが、Unityの最も一般的な使い方 Prefab (GameObject & Monobehaviour) では面倒なケース
Prefab (GameObject & Monobehaviour) では面倒なケース ●使用する際にInstantiateする ●再生停止時に変更した情報が失われる ●GameObjectがいつも一緒に生成される ●沢山のフィールドを持つとInstantiateのコストが上がる
Scriptable Object ●UnityのObjectから派生したクラス
Scriptable Objectとは? ●UnityのObjectから派生したクラス ●アセットとして保存(シリアライズ)できる ●Objectへの参照やフィールドの情報を保持したまま、 シリアライズできる ●保存したデータはInspectorから簡単に確認できる Scriptable Objectは、Object派生クラスの一つ
Scriptable Objectは、Object派生クラスの一つ Object ScriptableObject Component Transform RectTransform Monobehaviour Renderer GameObject Texture Sprite アセットとしてプロジェクトに格納・使用できる
アセットとしてプロジェクトに格納・使用できる Objectへの参照を保持したままシリアライズ
Objectへの参照を保持したままシリアライズ
なるほど… Prefabの下位互換か…
ScriptableObjectは Prefab (GameObject & Monobehaviour) とは違うもの Monobehaviourを見直す
Monobehaviourを見直す ●Monobehaviourはスクリプト ●様々なコールバックをエンジン(Unity)から受け取れる ●GameObjectに追加してはじめて動作する ●SceneもしくはPrefabに保存(シリアライズ)できる ●保存したデータはInspectorから簡単に確認できる ●ゲーム再生終了時にリセットされる PrefabにはGameObjectとTransformが追加される
PrefabにはGameObjectとTransformが追加される Game Object Name Tag Layer Transform Vector3 Quaternion Component HP TYPE Scriptable Objectには余計なオブジェクトはつかない HP TYPE
HP TYPE HP TYPE HP TYPE コールバックの量が違う Monobehaviour Scriptable Object
Monobehaviour 64 コールバック Scriptable Object 4 コールバック ※スクリプトに定義したコールバックのみ呼ばれる 使用方法が違う 参照する Instantiateで
Monobehaviour Instantiateで Sceneに生成 再生終了時に リセット Scriptable Object 参照する 再生終了時に リセットされない ●Scriptable Objectはスクリプト ●殆どのコールバックを受け取らない
●Scriptable Objectはスクリプト ●殆どのコールバックを受け取らない ●Game Objectにアタッチする必要はない ●インスタンス毎に異なるアセットとして保存できる ●保存したデータはInspectorから簡単に確認できる ●ゲーム再生時にリセットされない Monobehaviour VS Scriptable Object スクリプト? コールバック Monobehaviour ●YES ●多い Scriptable Object ●YES ●少ない
Monobehaviour スクリプト? コールバック Game Objectに 追加する必要 余計なオブジェクト 保持するフィールドの値 ゲーム終了時の動作 ●YES ●多い ●ある ●GameObjectとTransformが付く ●簡単に確認できる ●リセットされる Scriptable Object ●YES ●少ない ●ない ●インスタンスのみで保存 ●簡単に確認できる ●リセットされない どんな感じで 使うの?
どんな感じで 使うの? Scriptable Objectを使ってみる Scriptable Objectのキーワード ●ScriptableObject ●CreateAssetMenu
●ScriptableObject ●CreateAssetMenu PrefabをScriptableObjectに変更 ・キャラクターのPrefabをScriptableObjectに変更してみる ・最大HPとATKとDEFを持つ ・HPの状況によって行う挙動
・キャラクターのPrefabをScriptableObjectに変更してみる ・最大HPとATKとDEFを持つ ・HPの状況によって行う挙動 public class CharacterAI : MonoBehaviour { [SerializeField, Range (0, 100)] int MaxHP = 100;
} [SerializeField, Range (0, 100)] int MaxHP = 100; [SerializeField, Range (0, 100)] int HPThreshold = 25; public int hp{ get; set; } public AIState goodhealth; public AIState badHealth; } [CreateAssetMenu] public class CharacterConfig : ScriptableObject { public int MaxHP = 100; public int HPThreshold = 25; public AIState goodHealth; public AIState badHealth; } public class CharacterAI : MonoBehaviour { [SerializeField] CharacterConfig config; public int hp{ get; set; } }
{ public int MaxHP = 100; public int HPThreshold = 25; public AIState goodhealth; public AIState badHealth; } CharacterConfig config; public int hp{ get; set; } } Scriptable Objectを継承する
Scriptable Objectを継承する [CreateAssetMenu] public class CharacterConfig : ScriptableObject { public int MaxHP = 100; public int HPThreshold = 25; public AIState goodhealth; public AIState badHealth; } ●ScriptableObjectからアセットを作成するメニューを追加
●ScriptableObjectからアセットを作成するメニューを追加 public class CharacterAI : MonoBehaviour { [SerializeField] CharacterConfig config; public int hp{ get; set; } } フィールドを公開
Scriptable Objectの持つコールバック ●Awake スクリプト開始時に呼ばれる ●OnEnable オブジェクトロード時に呼ばれる ●OnDisable アンロード時に呼ばれる
●OnEnable オブジェクトロード時に呼ばれる ●OnDisable アンロード時に呼ばれる ●OnDestroy インスタンス破棄時に呼ばれる Demo
Demo
unity
unity
で? Scriptable Objectは 何の役に立つの?
で? Scriptable Objectは 何の役に立つの? 例えばScriptable Object活用のアイディア ●シーン間・GameObject間で使いまわせるデータテーブル ●ロード・インスタンス化の高速化 ●動作の切り替え ●ステータスの共有
●動作の切り替え ●ステータスの共有 ●オブジェクトのバインド
ScriptableObject ●Scene間で使用するインスタンスを共有
●同じSOを参照するならば、同じインスタンスを参照
SPEED UP!! ●Scriptable Objectのパラメータを変更は、参照元Prefabにも反映
●ScriptableObjectのデータ共有は高速化にもメリット Serialize Deserialize
Serialize Deserialize ●UnityはシーンやPrefabを丸ごとシリアライズし、丸ごとデシリアライズする
●シリアライズは本当にそのままシリアライズする オブジェクト: 102個 コンポーネント: 510個 フィールド数: すごい デシリアライズのコスト プライスレス Sceme
●完全に同一のオブジェクトがあっても、丸ごとシリアライズ Scene
●共通項目はScriptableObjectを使い、デシリアライズの項目を減らす staticでは駄目なのか? ●シリアライズ出来ない為ホットリロード時にリセットされる ●Inspector等から調整するには特別なコードが必要 ●アクセスするにはスクリプトが必要 ●同一スクリプト異Prefabで少し面倒くさい ●スクリプトで全て管理するなら、デメリットは
●スクリプトで全て管理するなら、デメリットは 無視出来るかもしれない staticでは駄目なのか? ●シリアライズ出来ない為ホットリロード時にリセットされる ●Inspector等から調整するには特別なコードが必要 ●アクセスするにはスクリプトが必要 ●同一スクリプト異Prefabで少し面倒くさい ●プログラム実行中にプログラムを書き換えても 即座に動作に反映される挙動。 static等のシリアライズ出来ない項目は失われる
●同一スクリプト異Prefabで少し面倒くさい ●スクリプトで全て管理するなら、デメリットは 無視出来るかもしれない NPC Config 1 弱い NPC Config 2 強い
NPC Config 2 強い ●同一ScriptでもSOを切り替える事でパラメータを切替 勇者 行動 逃げる たたかう
たたかう ●挙動の差替もできる AI 追跡AI ・視認可能な距離 ・視認可能な角度 ・攻撃開始する距離 固定砲台AI ・視認可能な距離 ・回転速度 ・攻撃開始する距離 ・距離に応じて使用する弾頭
●パラメータと振る舞いを設定 追跡 固定砲台 ●ユニット毎にAIを設定
●ユニット毎にAIを設定 追跡AI ・視認可能な距離 ・追跡範囲 OR 砲台AI ・視認可能な距離 ・視認可能な角度 ・攻撃開始する距離 ●ユニット毎のパラメータはユニット自身が持つ
●ユニット毎のパラメータはユニット自身が持つ (SOを上書きすると、全てのユニットに影響する為) ゲーム進行 ●インスタンスを共有
●インスタンスを共有 マネージャー 参照 呼出 登録 Player NPC ●ScriptableObjectにGameObjectを登録、ScriptableObject経由で呼び出す
●ScriptableObjectにGameObjectを登録、ScriptableObject経由で呼び出す マネージャー Prefab/Scene Player Scene NPC ●シーンを跨いだオブジェクトも簡単に参照
●シーンを跨いだオブジェクトも簡単に参照 ●ScriptableObjectはゲーム停止時にリセットされない ●ゲームをプレイしながらパラメータを調整
●ScriptableObjectはゲーム停止時にリセットされない ●ゲームをプレイしながらパラメータを調整 Demo
ScriptableObject
[CreateAssetMenu] public class TankConfig : ScriptableObject { public float speed; public float rotate; }
public class TankController : MonoBehaviour
{
[SerializeField] TankConfig config;
public void Update ()
{
var v = Input.GetAxis ("Vertical");
var x = Input.GetAxis ("Horizontal");
if (v != 0)
GetComponent<NavMeshAgent> ().Move (transform.forward * v * config.speed);
if (x != 0)
transform.localRotation *= Quaternion.AngleAxis (x * config.rotate, Vector3.up);
}
}
unity
unity
unity
unity
NPC Config 1 弱い NPC Config 2 強い
unity
unity
unity
unity
AI 動作1 動作2
public class TankController : MonoBehaviour { [SerializeField] TankAIBase ai; public void Update () { ai.Do(gameObject); } } public abstract class TankAIBase : ScriptableObject { public virtual void Do (GameObject obj) { } }
[CreateAssetMenu] public class TanksStand : TankAIBase{} [CreateAssetMenu] public class TankRotate : TankAIBase { public float rotate = 2; public override void Do (GameObject obj) { obj.transform.localRotation *= Quaternion.AngleAxis (rotate, Vector3.up); } }
[CreateAssetMenu]
public class TankMove : TankAIBase
{
public float speed = 0.5f;
public override void Do (GameObject obj)
{
obj.GetComponent<NavMeshAgent>().Move(obj.transform.forward * speed);
}
}
unity
unity
Manager
Manager public class TankController : MonoBehaviour { public void Move(float accel)
public class TankController : MonoBehaviour
{
public void Move(float accel)
{
GetComponent<NavMeshAgent>().Move(transform.forward * (accel * 0.2f));
}
}
[CreateAssetMenu]
public class TankData : ScriptableObject
{
public TankController tankController{get; set;}
public void Up () { tankController.Move(5); }
public void Down () { tankController.Move(-5); }
}
[DefaultExecutionOrder (-100)]
public class EventTriggerBehaviour : MonoBehaviour
[DefaultExecutionOrder (-100)]
public class EventTriggerBehaviour : MonoBehaviour
{
[SerializeField]
UnityEvent onAwake = new UnityEvent (), onDestroy = new UnityEvent ();
void Awake ()
{
onAwake.Invoke ();
}
void OnDestroy ()
{
onDestroy.Invoke ();
}
unity
unity
unity
unity
unity
unity
応用 もっとScriptable Objectを 使いこなすTips
●Scriptable Objectのアイコンを変えて、一覧性を上げる
●ScriptableObjectのコードアイコンを変更すると、SOのアイコンが変わる ソースコード
ソースコード [クラス名] Icon Assets/Gizmos
●エディター拡張でScriptableObjectのUIを変更 拡張するクラスを登録 [CustomEditor (typeof(Config))] public class ConfigEditor : Editor { [CreateAssetMenu] public class Config : ScriptableObject {
[CustomEditor (typeof(Config))] public class ConfigEditor : Editor { public override void OnInspectorGUI () { base.OnInspectorGUI(); // レイアウト記述 } } [CreateAssetMenu] public class Config : ScriptableObject { public Item[] items = new Item[0]; [System.Serializable] public class Item { public Texture texture; public int param; public string name; } } PropertyDrawerを使う
[CreateAssetMenu] public class Config : ScriptableObject { public Item[] items = new Item[0]; [System.Serializable] public class Item { [PreviewTexture] public Texture texture; [Range(0, 100)] public int param; public string name; } } OnValidateを使う エディターで値を 変更時に呼ばれる
エディターで値を 変更時に呼ばれる [CreateAssetMenu] public class Data : ScriptableObject { [SerializeField] int count; void OnValidate () { count = Mathf.Clamp (count, 0, 99); } }
●アセットを外部ファイルから作る 何故EXCEL? ●数値編集・入力で最も優れたツール ●データテーブルを作るのに便利 ●バランスの良いゲームは、
●データテーブルを作るのに便利 ●バランスの良いゲームは、 ランダムではなく都合の良いテーブルを使う事がある ●Excelは実行時に読むようなデータ構造ではない
●エンジンが読みやすいScriptable Objectに変換する AssetPostprocessorで アセットのインポート処理に 割り込み 既にあればLoadAssetAtPath 無ければCreateAssetで ScriptableObjectを取得
既にあればLoadAssetAtPath
無ければCreateAssetで
ScriptableObjectを取得
EditorUtility.SetDirtyで
ScriptableObjectを生成
インポート処理に割込
public class param1_importer : AssetPostprocessor
{
static void OnPostprocessAllAssets (
string[] importedAssets, string[] deletedAssets, string[]
string[] movedFromAssetPaths)
{
ExcelItem data = (ExcelItem)AssetDatabase.LoadAssetAtPath (exportPath, typeof(ExcelItem));
if (data == null) {
data = ScriptableObject.CreateInstance<ExcelItem> ();
AssetDatabase.CreateAsset ((ScriptableObject)data, exportPath);
ExcelItem data = (ExcelItem)AssetDatabase.LoadAssetAtPath (exportPath, typeof(ExcelItem));
if (data == null) {
data = ScriptableObject.CreateInstance<ExcelItem> ();
AssetDatabase.CreateAsset ((ScriptableObject)data, exportPath);
}
// インポート処理
ScriptableObject obj = AssetDatabase.LoadAssetAtPath (
exportPath, typeof(ScriptableObject)) as ScriptableObject;
EditorUtility.SetDirty (obj);
}
}
ScriptableObject作成
変更を反映
unity
●ScriptedImporterでアセットをScriptableObjectとして使う ScriptedImporter
●アセットのように使える
{"index":100, "name":"layer test"}
unity
unity
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 64e7a24fcd53d4ec4b7372e57df90a95, type: 3}
m_Name: Data
m_EditorClassIdentifier:
index: 0
name:
●ScriptableObjectはエディターではYAMLフォーマット
開発
リリース
ScriptableObjectの中身を独自にデシリアライズ
コールバックを受ける
インターフェース
[CreateAssetMenu]
public class SampleDictionary : ScriptableObject, ISerializationCallbackReceiver
{
const string path = "item.json";
public Item item = new Item ();
public void OnBeforeSerialize ()
{
var json = JsonUtility.ToJson (item);
File.WriteAllText (path, json);
}
public void OnAfterDeserialize ()
public void OnAfterDeserialize () { if (File.Exists (path)) { var json = File.ReadAllText (path); JsonUtility.FromJsonOverwrite (json, item); } } } エディターで値を設定 動的に値を変更可能 再生前に巻き戻る
再生前に巻き戻る ●Scriptable Objectのリセット アイディア ●ゲーム開始時にScriptable Objectをリセットする処理を追加 ●シリアライズするデータとゲーム中に触るデータは分ける ●ゲーム再生終了時にScriptable Objectをアンロード
ゲーム開始時にScriptable Objectをリセット public abstract class ResettableScriptableObject : ScriptableObject { public abstract void Reset (); } [CreateAssetMenu] public class SaveData : ResettableScriptableObject { public int count = 0; public override void Reset ()
public class SaveData : ResettableScriptableObject { public int count = 0; public override void Reset () { //初期化コード count = 0; } } ゲーム開始時にScriptable Objectをリセット public class DataResetter : MonoBehaviour { public ResettableScriptableObject[] resettableScriptableObjects; private void Awake () { for (int i = 0; i < resettableScriptableObjects.Length; i++) { resettableScriptableObjects[i].Reset ();
for (int i = 0; i < resettableScriptableObjects.Length; i++) { resettableScriptableObjects[i].Reset (); } } } データを分ける public abstract class ResettableScriptableObject : ScriptableObject { [SerializeField] int _count = 0; public int count{get; set;} void OnEnable() { count = _count; }
count = _count; } } アンロードする public class ResettableScriptableObject : ScriptableObject { protected virtual void OnEnable() { #if UNITY_EDITOR if (EditorApplication.isPlayingOrWillChangePlaymode == true ){ UnityEditor.EditorApplication.playModeStateChanged += (state) => { if ( EditorApplication.isPlayingOrWillChangePlaymode == false ) { Resources.UnloadAsset(this); } }; } #endif }
if ( EditorApplication.isPlayingOrWillChangePlaymode == false ) { Resources.UnloadAsset(this); } } }; } #endif } } アンロードする [CreateAssetMenu] public class Config : ResettableScriptableObject { public float _count; }
Reset ●Resetメソッド呼び出し により初期化 ●起動するシーン内に リセットを呼び出す コンポーネントが必要 ●実行中の再初期化は 比較的容易 ●ゲーム再生時でも直接 値を操作できる データ分け ●アクセス時に初期化 ●実行中の再初期化は容易 ●実行中に値を編集したい 場合は、特別なコードが 必要になる Unload ●再生終了時に初期化 ●実行中の再初期化は システムのリソース管理 の理解が必要 ●再生前にプロジェクトの セーブが必要
生存戦略 Scriptable Objectの 寿命とアンロード
Scriptable Objectの 寿命とアンロード Unity ●ScriptableObjectをデシリアライズ ●インスタンスIDを付与 (参照関係の解決に使用する) C# ●デシリアライズしたデータを スクリプトに流し込む ●コールバックの実体を実行 ●動作を実装
●UnityエンジンはC++、スクリプトはC# 参照があった時に呼ばれる Awake OnEnable OnDisable OnDestroy Scriptable Objectのライフサイクル
OnDestroy Scriptable Objectのライフサイクル オブジェクトのデシリアライズ 参照関係の解決 コンポーネントの初期化(Awake) コンポーネントの初期化(OnEnable) コンポーネントの初期化(Start) コンポーネント実行(Update) ここにSOのOnEnable Scriptable Objectのライフサイクル
コンポーネント実行(Update) Scriptable Objectのライフサイクル Sprite Texture Image Material Shader Scriptable Object ●参照されると関係するオブジェクトをロード
●参照されると関係するオブジェクトをロード Sprite Texture Image Material Shader Scriptable Object ●テーブルが生きてるならば、同一のインスタンスにアクセスする
●テーブルが生きてるならば、同一のインスタンスにアクセスする C# Instance Scriptable Object 例) event
C# Instance Scriptable Object Scriptable Object ●再度0bjectを作ると、新しく参照関係を構築する
●再度0bjectを作ると、新しく参照関係を構築する Scene 1 Scene 2 ●移動先のシーンでSOが参照されている場合、破棄されない
●移動先のシーンでSOが参照されている場合、破棄されない Instance A Instance Bが 新しく作られる 破棄 Scene 1 Scene 2 Scene 3 ●参照されていないシーンに移動すると、アセットは破棄され、 参照されると別インスタンスIDを持って再びロードされる
●参照されていないシーンに移動すると、アセットは破棄され、 参照されると別インスタンスIDを持って再びロードされる 明確に破棄したい場合 ●Destroy CreateInstanceで生成したインスタンスを破棄 ●UnloadUnusedAsset 被参照のアセットを解放 ●Unload 指定したアセットを強制開放
どんな時に注意すべき?
●データテーブルとして使用してる時? NO、静的なデータでは問題は起こらない
●データテーブルとして使用してる時? NO、静的なデータでは問題は起こらない ●オブジェクトのバインド? Unloadで開放する場合は少し注意が必要。UnloadUnusedAssetsを使う場合はOK
●オブジェクトのバインド? Unloadで開放する場合は少し注意が必要。UnloadUnusedAssetsを使う場合はOK Scene 1 Scene 2 Scene 3 ●ゲーム進行等を保持する場合 非参照の状況でインスタンスが破棄される可能性があるので注意
●ゲーム進行等を保持する場合 非参照の状況でインスタンスが破棄される可能性があるので注意 Scene 1 Unload! Scene 3 ●逆に開放したい場合、シーン移行直前でSOをアンロードすると、 次のシーンでは新鮮なインスタンスにアクセスする
Pluggable AI With Scriptable Objects LIVE TRAINING ARCHIVE unity
unity
Thank you unity