(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

ScriptableObjectとは【Unity】【ScriptableObject】


このエントリーをはてなブックマークに追加

この記事でのバージョン
Unity 2017.3.1f1


はじめに

今回はUnityの便利&重要機能の一つ、ScriptableObjectについて

用途や利点、作り方から使い方に至るまで、出来る限りまとめてみました!


ScriptableObjectとは

そもそもScriptableObjectとはなんぞやという所からですが、

公式のマニュアル等を見てみると以下のように書かれています。

ScriptableObject はスクリプトインスタンスから独立した大量の共有データを格納できるクラスです。


データを格納するオブジェクトです。もう少し言えば、Unityがデータをシリアライズ・デシリアライズしパラメータを格納するための仕組みです。


ScriptableObject は独自のアセットを作成するための仕組みです。また、Unity のシリアライズ機構が扱う形式とも言えます。


ScriptableObjectは、Unityによってシリアライズされたデータ群をアセットとして保持する為の仕組みです。



色々な書き方がされていますが、自分なりに分かりやすさ重視で簡潔にまとめてみると、

ScriptableObjectとは

「ゲーム中に変化しないデータをアセットとして作成する仕組み」

と言った感じでしょうか。


用途

上記の通り、ScriptableObjectはゲーム中に変化しないデータを扱うのに向いています。

言い方を変えると静的なデータ、固定データ、定数などの事で、

データ量は多くても少なくても有用です。(詳しくは次項の利点にて)


一口に変化しないデータと言っても、様々な用途が考えられます。

例えば、敵キャラの初期ステータスと言った数値データや、UIに表示するテキストデータ

イベントが発生する条件の条件データなどの事です。


また、一応ゲーム中に変化するデータを扱う事も出来ますが、

実機上だとゲーム終了時に全データが初期値に戻ってしまうので、

永続的なセーブデータのような使い方には向いてません。


利点

用途はなんとなく分かってもらえたと思いますが、

なんでわざわざScriptableObjectを使うの?という話になるかと思います。


実際、ScriptableObjectを使わずとも

プログラムで直接数値を設定したり、定数クラスを作ってそこにまとめたり、

変数を作ってInspectorで設定したり、JSONにまとめたり等、他のやり方はいくらでもあります。


なので、今度はScriptableObjectの利点についてです。


メモリ節約

例えば以下のように、HPというステータスをもった敵クラスがあったとします。

ただし、簡略化のため敵の種類は1種類として考えます。

//初期状態のHP
private int _initialHP = 10;

//現在のHP
private int _currentHP = 0;

/// <summary>
/// 初期化を行う
/// </summary>
public void Init() {

  //HPを初期状態の数値にする
  _currentHP = _initialHP;

}


この場合、_initialHPと_currentHPでメモリを消費しているのですが、

今回は分かりやすいように、それぞれ1メモリ、計2メモリ消費してると言い換えてみます。


この_initialHPと_currentHPはそれぞれの敵固有の値になっているので、

敵(のインスタンス)を大量に作るほど、消費するメモリも増えていきます。

10体作れば20メモリ、100体作れば200メモリです。


各々で値が異なってくる_currentHPはしょうがないですが、

全員同じはずの_initialHPが複数作られ、メモリを消費するのは勿体ないですよね?


なので_initialHPをstatic(この場合はconstの方が適切ですが)にしてみます。

すると_initialHPは共通の値となり、敵をどれだけ作っても1メモリしか消費しなくなります。

10体作っても11メモリ、100体作っても101メモリです。

//初期状態のHP
private static int _initialHP = 10;


ただし、staticにしてしまうと、常にメモリを使う事になるので、

敵がいないタイトル画面やリザルト画面等でも1メモリは常に使われてる事になります。


これをScriptableObjectにしてしまえば、

共通の値でメモリを無駄遣いせず(ScriptableObjectが共通の値を持つ)

必要ない時はメモリを使わない(ScriptableObjectをロードせず)、という具合にメモリ節約が行えます。


一応、以下のような直書き(ハードコーディング)という方法もありますが、

/// <summary>
/// 初期化を行う
/// </summary>
public void Init() {

  //HPを初期状態の数値にする
  _currentHP = 10;

}


これだと初期状態のHPが必要な場所で、毎回10を書かなくてはいけないため、

変更時の漏れや値のズレによるバグの原因となります。

また、敵の種類やステータスが増えると、それだけ直書きする手間やバグの可能性も増えるため

基本的にはやらないほうが得策です。


パラメータの比較や管理が容易

ScriptableObjectは一つのアセット(ファイル)なので、コピーや差し替えが簡単に行えます。

という事は、ファイルを複数用意する事で、パラメータの比較が容易に行えるという事でもあります。


例えば敵キャラに複数のパラメータ(HPや攻撃力、防御力など)があった場合、

それをプログラムで直接指定してしまうと、比較する際に毎回書き換える必要がありますが、


f:id:kan_kikuchi:20180319140600j:plain


このパラメータをScriptableObjectにしてしまえば、ファイルを差し替えるだけになります。


f:id:kan_kikuchi:20180319140615j:plain


また、敵ごとにパラメータのファイルを用意すれば、管理も容易です。


Unity上で確認、調整がしやすい。他のアセットを設定出来る。

今までの利点はJSON等の他のファイルを使っても同じ話だったりします。

なので次は他のファイル形式と比較した場合のScriptableObjectの利点で、

Unity上で確認、調整がしやすいという話です。


JSON等のファイルはUnity用のファイル形式ではないため、Unity上では確認しづらいですが、


f:id:kan_kikuchi:20180319151411j:plain


ScriptableObjectはUnityが提供しているファイル形式なので、見やすいですし、

Inspector上でそのまま値を変更する事も可能です。


f:id:kan_kikuchi:20180319144850j:plain


しかも、boolならチェックボックス、enumならプルダウン等、

Componentと同じ感覚で設定が出来る上に、

Sprite等をドラック&ドロップして他のアセットを設定する(参照を持つ)事も可能です。


余談ですが、Odinというアセットを使うと、

さらに確認や調整がしやすく(というよりInspectorが使いやすく)なりますが、これはまた別のお話。


f:id:kan_kikuchi:20180319152045j:plain



作成や使用、拡張が簡単

詳しくは後述しますが、ScriptableObjectは簡単に作成したり、使ったり出来ます。

JSON等の他のファイルだとそのままでは使えないので、ひと手間必要になりますが、

ScriptableObjectにはそれがありません。


また、エディタ拡張属性簡単に拡張する事も可能です

入力値の最小最大を制限したり、行間を調整したり、説明を表示したり、

一部の値を変更不可能にしたりと、ちょっとのプログラムで好きなように機能や表示を変えられます。

f:id:kan_kikuchi:20180324070254j:plain


ロード時間が短く、メモリ使用量も軽減

これはJSONと比較してなんですが、

ロード時間を短くし、メモリ使用量も軽減する事が出来ます。


f:id:kan_kikuchi:20180321173048j:plain



作成方法

さて、利点が分かった所で次はScriptableobjectの作成方法です。


クラスの作成

まずはScriptableobjectを継承したクラスを作成し、そのクラスに設定したいデータの変数を記述します。

using UnityEngine;

//ScriptableObjectを継承したクラス
public class EnemyStatus : ScriptableObject {

  //設定したいデータの変数
  public int    HP     = 100, SP = 50, Atk = 5, Def = 15, Spd = 99, Exp = 58;
  public string Name   = "なまえ";
  public bool   IsBoss = false;

  /*簡略化のために全てpublicにしてますが、Scriptableobjectは基本的に変更しないデータを扱うので、
  以下のようにprivateな変数にSerializeFieldを付けて、getterとsetterを別途用意する方が安全です。
  setterは後述する「プログラムから作成」の時に使います。

  [SerializeField]
  private bool _isBoss = false;
  public  bool  IsBoss {
    get { return _isBoss; }
    #if UNITY_EDITOR
    set { _isBoss = value; }
    #endif
  }

  */

}

f:id:kan_kikuchi:20180325202525j:plain


ただし、敵のステータスのように同じ項目で違う数値を設定したい場合は、

各敵のステータスをまとめた構造体またはクラスを作ってListで設定した方が何かと便利です。

using System.Collections.Generic;
using UnityEngine;

//ScriptableObjectを継承したクラス
public class EnemyStatusData : ScriptableObject {

  //ListステータスのList
  public List<EnemyStatus> EnemyStatusList = new List<EnemyStatus>();

}

//System.Serializableを設定しないと、データを保持できない(シリアライズできない)ので注意
[System.Serializable]
public class EnemyStatus{

  //設定したいデータの変数
  public string Name   = "なまえ";
  public int    HP     = 100, SP = 50, Atk = 5, Def = 15, Spd = 99, Exp = 58;
  public bool   IsBoss = false;

}

f:id:kan_kikuchi:20180325203424j:plain


また、データの変数の先頭を名前などのstringにすると、

各項目の名前が、そのstringに設定したものになって分かりやすいのでオススメです。

//設定したいデータの変数
public string Name   = "なまえ"; //先頭をstring
public int    HP     = 100, SP = 50, Atk = 5, Def = 15, Spd = 99, Exp = 58;
public bool   IsBoss = false;

f:id:kan_kikuchi:20180325203541j:plain


次はScriptableobjectの実体、つまりファイルの作成方法ですが、

これには大きく分けて二つの方法があります。


メニューから作成

1つ目がScriptableobjectを継承したクラスにCreateAssetMenuという属性を付ける方法です。

これだけでProjectのCreateに作成用のメニューが追加されます。

//これを付けるだけでProjectのCreateに作成用のメニューが追加
[CreateAssetMenu]
public class EnemyStatusData : ScriptableObject {
/*略*/
}

f:id:kan_kikuchi:20180326073901j:plain


また、CreateAssetMenuはfileNameで作成時のファイル名を、menuNameでメニューの表示名を、

orderで表示順(数字が低い方が上)を指定する事が出来ます。

//ファイル名、メニュー表示名、表示順を指定
[CreateAssetMenu(
  fileName = "EnemyStatusData", 
  menuName = "ScriptableObject/EnemyStatusData", 
  order    = 0)
]
public class EnemyStatusData : ScriptableObject {
/*略*/
}

f:id:kan_kikuchi:20180326073908j:plain


なお、メニューから作成した場合、データは手動で設定する感じになります。


プログラム(Excel)から作成

2つ目がプログラムから作成する方法で、データ自体もプログラムで設定したい場合に使います。

やり方はMenuItemを付けたメソッド内で、データを設定したScriptableObjectを書き出すだけです。

using UnityEngine;
using UnityEditor;

public static class EnemyStatusDataCreator {

  //MenuItemを付ける事で上部メニューに項目を追加
  [MenuItem("Create/EnemyStatusData")]
  private static void Create() {
    //ScriptableObjectのインスタンスを作成
    EnemyStatusData enemyStatusData = ScriptableObject.CreateInstance<EnemyStatusData>();

    //データを設定
    EnemyStatus status1 = new EnemyStatus(), status2 = new EnemyStatus();
    status1.Name = "リオレウス";
    status2.Name = "リオレイア";

    enemyStatusData.EnemyStatusList.Add(status1);
    enemyStatusData.EnemyStatusList.Add(status2);

    //ファイル書き出し
    AssetDatabase.CreateAsset(enemyStatusData, "Assets/EnemyStatusData.asset");
  }

}

f:id:kan_kikuchi:20180326081416g:plain


上記の例ではデータは直接記述していますが、何かしらの計算をして設定したり、

csv等のファイルを読み込んでその値を設定するなんて事も、もちろん可能です。


また、Excelのデータを変換する事も比較的に楽に行えますが、

説明が長くなるので今回は割愛します。(あとで別記事でするかも)





使い方

作り方も分かったので後は使い方ですが、これは至ってシンプルです。

他のアセットと同様にResources.Load等でロードして、データを参照(取得)するだけ。

//EnemyStatusDataをResourcesからロードして、EnemyStatusを参照(取得)
EnemyStatusData enemyStatusData = Resources.Load<EnemyStatusData>("EnemyStatusData");
EnemyStatus     enemyStatus     = enemyStatusData.EnemyStatusList[0];


余談ですが、他のクラスからのアクセスがいつでも簡単にできる方法も以前紹介してたりします。





欠点、注意点

最後にScriptableobjectの欠点や、使用の際の注意点やハマり所についての話です。


変更した値が戻る場合と戻らない場合

用途の項目でも説明した通り、Scriptableobjectは

実機上だとゲーム終了時に全データが初期値に戻ってしまいます。

なので、セーブデータのような可変するデータには向いていません。


逆にエディタ上では値を変更しても戻りません。

なので、エディタ上のセーブデータ(エディタ拡張の設定ファイルとか)には使えますし、

実際のゲームでは変更しないが、エディタ上で調整したい場合にも使いやすいです。


ただし、エディタ上でもプログラムから値を変更した際は、

変更があった事を記録しないと元に戻るので注意が必要です。

(Inspectorから変更した場合は問題ない。)





クラス名とファイル名は同じにする必要がある

Scriptableobjectを継承したクラス名と、スクリプトのファイル名が異なっている場合は、

警告が表示され、作成したファイルのScriptの欄がNoneになってしまいます。


f:id:kan_kikuchi:20180326090837j:plain
f:id:kan_kikuchi:20180326090844j:plain

No script asset for EnemyStatusData. Check that the definition is in a file of the same name.

f:id:kan_kikuchi:20180326091325j:plain


この状態で使えるかどうかは試していないので分かりませんが、

警告が出ているという事は推奨されていない使い方なので、

素直にクラス名とファイル名は同じにしましょう。


データ量が多いとInspectorの表示が重くなる

設定されているデータが数千〜数万件を越えてくると、

Inspectorでの表示が重くなり、最悪エディタ自体を強制終了しないといけなくなります。


そういうデータはエディタ上で選択しない、JSON等の違う形式を使うという回避方法もありますが、

そもそもそれだけのデータを一つにまとめてしまうと、

ロードに時間がかかったり、大量にメモリを消費してしまったりするので、

データを小分けにする事をオススメします。


f:id:kan_kikuchi:20180326092245j:plain


データサイズは圧縮したJSONの方がちょっと軽い

ロード時間やメモリ使用量的にはJSONよりScriptableobjectの方が有利という話をしましたが、

データサイズ的にはは圧縮したJSONの方が軽くなるっぽいです。


f:id:kan_kikuchi:20180321173048j:plain


なので、どうしてもデータサイズをもっと小さくしたいという場合には、

JSONを使うという選択肢も考えてみましょう。


おわりに

以上で説明は終わりですが、結構便利かつ簡単に使えるという事が分かってもらえたでしょうか。


ScriptableObjectは名前からだと用途が分かりにくい上に、代替手段が色々とあるため、

あまり目立った存在ではありませんが、使ったことがない方は試してみる事をオススメします。