Search Unity

ゲーム構築を劇的にスマートにする
Scriptable Objectの 3つの活用方法

  • ゲーム開発
  • 開発者向け

<このページで学べる内容>

様々な手段を駆使して膨大なデータを管理する必要はありません。Scriptable Objectという Unity独自のクラスを使えば、Unityがシリアライズしたデータをアセットとして格納できるので、よりシンプルに、より手軽に、ゲーム全体の管理や変更を行えるようになります。 これらのヒントは、Schell Games の主席エンジニアである Ryan Hipple 氏によって提供されました。Hipple 氏は Scriptable Object を使用してゲームを開発した経験を豊富に持っています。Scriptable Object に関する Hipple 氏の Unite でのセッションは、こちらでご覧いただけます。また、Unity エンジニアの Richard Fine による Scriptable Object をわかりやすく紹介したセッションをご覧になることもお勧めします。Ryan さん、ありがとうございました!

1)Scriptable Object の概要

ScriptableObject はシリアライズ可能な Unity 独自のクラスで、スクリプトインスタンスとは独立して大量の共有データを保存できます。Scriptable Object を使えば、変更とデバッグの管理が容易になります。ゲーム内の各種システムの間に一定レベルのフレキシブルなコミュニケーションを構築できるため、プロジェクトの進行に伴って発生するさまざまな事柄の変更やコンポーネントの再利用の管理が容易になります。

2)スマートなゲームエンジニアリングの 3 つの柱

常にモジュール性を保ち...

  • 互いに直接依存するシステムを構築することは避けましょう。例えば、インベントリシステムはゲーム内の他のシステムと通信できるようにするべきですが、システム間にハードリファレンスを作成することはお勧めできません。システムを別の設定や関係に組み立て直すことが困難になるからです。
  • シーンをまっさらな状態で作成し、シーンをまたぐ一時データを排除します。シーンを切り替えるたびに、まったく新しくロードが実行されるようにしてください。そうすることで、特別難しいことをしなくても、他のシーンにはない固有の動作を持つシーンを作成できます。
  • 独立して機能するようにプレハブを設定します。シーン内にドラッグする各プレハブには、できる限りその内部に機能を持たせます。こうすることで、シーンがプレハブのリストとなり、プレハブに個々の機能が含まれるようになるため、大規模なチームでソース管理を行いやすくなります。この方法を使えば、ほとんどのチェックインをプレハブレベルで行えるため、シーン内の競合を減らすことができます。
  • 各コンポーネントで解決する問題を 1 つに絞ります。そうすることで、複数のコンポーネントを組み合わせて新しいものを作成しやすくなります。

...編集可能にし

  • ゲームをできる限りデータ駆動型にしましょう。データを命令として処理する機械のようにゲームシステムを設計すれば、ゲーム実行時でも、より効率的にゲームに変更を加えられるようになります。
  • システムをできる限りモジュール方式つまりコンポーネントベースで構成すれば、アーティストやデザイナーにとっても編集が容易になります。1 つの機能だけを持つ小さなコンポーネントを実装することで、明確な機能について質問しなくても、デザイナーがゲーム内のコンポーネントを組み合わせることができるようになれば、コンポーネントをさまざまに組み合わせて新しいゲームプレイやメカニクスを発見できる可能性があります。Hipple 氏は、自分のチームが開発した最も優れた機能のいくつかはこのようなプロセスから生まれたといいます。彼はこのプロセスを「エマージェントデザイン」と呼んでいます。
  • 肝心なのは、実行時にゲームに変更を加えられるようにすることです。実行時にゲームを変更できれば、バランスや大事な点を見つけやすくなります。実行時の状態を保存し復元することができれば(Scriptable Object を使えば可能)、非常に便利です。

...デバッグ可能にする

この点はどちらかというと前述の重要な 2 点に付随する要素ですが、ゲームをモジュール化するほど、ひとつひとつのモジュールを検証しやすくなります。ゲームがより編集しやすければ、つまり、独自のインスペクタービューを備えている機能が多ければ、デバッグが楽になります。インスペクターでデバッグ状態を調べられるか確認すると共に、デバッグ手順を計画していないのに機能が完成したと思わないようにしてください。

3)Hipple 氏が勧める Scriptable Object で作成すべきものベスト 3

変数

Scriptable Object を使って開発する最もシンプルな方法は、自己完結型のアセットベースの変数を使用することです。ここでは FloatVariable の例を紹介しますが、この例は他のシリアライズ可能な型にも拡張できます。

FloatVariable.cs (C#)

[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}

このようにすれば、技術的な知識の量にかかわらず、チームメンバー全員が、FloatVariable アセットを新しく作成することで新しいゲーム変数を定義できるようになります。任意の MonoBehaviour または ScriptableObject で、public float の代わりに public な FloatVariable を使用してこの新しい共有値を参照できます。

さらに優れた点は、ある MonoBehaviour が FloatVariable の Value を変更すると、他の MonoBehaviour からその変更が見えることです。これによって、互いに参照する必要のないシステム間に一種のメッセージング層が作成されます。

この方法は、たとえばプレイヤーの HP に使用できます。1 人のローカルプレイヤーが存在するゲームで、PlayerHP と命名した FloatVariable をプレイヤーの HP として設定します。プレイヤーがダメージを受けると PlayerHP は減り、プレイヤーが回復すると PlayerHP は増えます。

シーン内に HP ゲージのプレハブがあるとします。HP バーは PlayerHP 変数を監視し、その表示を更新します。コードを変えることなく、たとえば PlayerMP 変数など、簡単に別の変数を監視させることも可能です。HP バーは、シーン内のプレイヤーのことはまったく認識せず、プレイヤーが書き込む変数をただ読み込んでいるだけです。

Unity Scriptable Object でのプレイヤーの死亡処理

一度このように設定すれば、PlayerHP を監視するものを簡単に追加できるようになります。PlayerHP が低下した時に音楽システムを変更したり、プレイヤーが弱っていることを敵が認識した時に敵の攻撃パターンを変更したり、次の攻撃の危険性をスクリーンスペースエフェクトで強調したりできます。ここで鍵となるのは、Player スクリプトはこれらのシステムにはメッセージを送信していないということで、言うなればこれらのシステムはプレイヤーゲームオブジェクトを認識している必要がない、ということになります。ちなみに、ゲーム実行時にインスペクターへ移動して PlayerHP の値を変更すれば、動作を検証することもできます。

FloatVariable の Value を編集するときは、ディスクに保存されている ScriptableObject の値を変更しないように、データを実行時値にコピーするとよいでしょう。こうすることで、MonoBehaviour が RuntimeValue にアクセスすることになり、ディスクに保存されている InitialValue を変更せずに済みます。

RuntimeValue.cs (C#)

[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
	public float InitialValue;

	[NonSerialized]
	public float RuntimeValue;

public void OnAfterDeserialize()
{
		RuntimeValue = InitialValue;
}

public void OnBeforeSerialize() { }
}

イベント

Scriptable Object を利用して作成できる機能は数多くありますが、その中でも私のお気に入りはイベントシステムです。イベントアーキテクチャを使うと、互いを直接認識しないシステム間でメッセージを送信させることで、コードをモジュール化しやすくなります。また、アップデートループで絶えず監視することなく、状態の変化に対して、何かに応答させることが可能になります。

イベントシステムは、GameEvent ScriptableObject と GameEventListener MonoBehaviour の 2 つの部分で構成されます。デザイナーは、重要な送信可能メッセージを表す GameEvent をプロジェクト内にいくつでも作成できます。GameEventListener は特定の GameEvent の発生に備えて待ち受け、UnityEvent(これはイベントというよりも、シリアライズされた関数呼出しです)を呼び出して応答します。

コード例:GameEvent ScriptableObject

[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
	private List<GameEventListener> listeners = 
		new List<GameEventListener>();

public void Raise()
{
	for(int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}

public void RegisterListener(GameEventListener listener)
{ listeners.Add(listener); }

public void UnregisterListener(GameEventListener listener)
{ listeners.Remove(listener); }
}

コード例:GameEventListener.cs (C#)

public class GameEventListener : MonoBehaviour
{
public GameEvent Event;
public UnityEvent Response;

private void OnEnable()
{ Event.RegisterListener(this); }

private void OnDisable()
{ Event.UnregisterListener(this); }

public void OnEventRaised()
{ Response.Invoke(); }
}

例として、ゲーム内でのプレイヤーの死亡処理を考えてみましょう。このような処理を行う際には、実行内容が大きく変わることがあり、各ロジックをどこでコーディングするかを決めるのが難しい場合があります。Player スクリプトでゲームオーバーの UI をトリガーして音楽を変更するか。プレイヤーがまだ生きているかどうかを敵に毎フレームチェックさせるか。イベントシステムを使えば、このような厄介な依存関係の問題を避けることができます。

プレイヤーが死ぬと、Player スクリプトが OnPlayerDied イベントの Raise を呼び出します。Player スクリプトは単純なブロードキャストであるため、どのシステムが Player スクリプトを待ち受けているかを認識する必要はありません。Game Over UI は OnPlayerDied イベントをリッスンし、アニメーション化を開始します。カメラスクリプトは OnPlayerDied をリッスンして黒へのフェードを開始し、音楽システムは音楽変更のレスポンスを返します。また、それぞれの敵に OnPlayerDied をリッスンさせ、嘲笑のアニメーションをトリガーしたり、状態の変更をトリガーして待機動作に戻したりできます。

このようなパターンを使うことで、プレイヤーの死に対する新しいレスポンスをきわめて簡単に追加できます。また、テストコードやインスペクターのボタンでイベントの Raise を呼び出すことで、プレイヤー死亡時のレスポンスを容易に検証できます。

Unity Scriptable Object でのプレイヤーの死亡処理

私が Schell Games で構築したイベントシステムは、はるかに複雑なものとなり、データの受け渡しや型の自動生成を可能にする機能を持っています。ここでは詳細をすべて説明することはできませんが、基本的に前述の例が、現在私たちが使用しているシステムの出発点にあります。

システム

Scriptable Object はデータである必要はありません。MonoBehaviour に実装しているシステムを例にとり、代わりに実装を ScriptableObject に移すことができるか検討してみてください。InventoryManager を DontDestroyOnLoad MonoBehaviour に実装するのではなく、ScriptableObject に実装してみましょう。

ScriptableObject はシーンに結び付けられないため、Transform を持たず、Update 関数を取得しませんが、特別な初期化を実行しなくても、シーンロード間でステートを維持します。インベントリへのアクセスにスクリプトが必要な場合は、シングルトンの代わりに、public な参照を使用してください。そうすることで、シングルトンを使用する場合よりも、テストインベントリやチュートリアルインベントリを簡単にスワップできます。

インベントリシステムへの参照を受け取る Player スクリプトを考えてみてください。プレイヤーがスポーンしたときに、Player スクリプトで所有しているすべてのオブジェクトを Inventory に要求し、装備をスポーンさせることができます。また、描画するアイテムを決定するために、装備 UI で Inventory を参照してアイテムをループ処理することもできます。

この記事はいかがでしたか?