最終更新:2020 年 1 月(読み終わるまでの時間:10 分)

ScriptableObject を使用してゲームを構築する 3 つの方法

このページで学ぶ内容:ゲームコードの変更やデバッグを簡単に行う方法についてのヒントです。スクリプタブルオブジェクトを使用してゲームコードを作成します。

この一連のヒントは Schell Games の主席エンジニアである Ryan Hipple 氏から提供されたものです。Ryan Hipple 氏にはスクリプタブルオブジェクトを使用してゲームを構築してきた高度な経験があります。スクリプタブルオブジェクトに関する Hipple 氏の Unite でのセッションは、こちらでご覧いただけます。また、Unity エンジニアの Richard Fine がスクリプタブルオブジェクトについて紹介するセッションをご覧になることもお勧めします。Ryan さん、ありがとうございました!

 

 

ScriptableObject とは?

ScriptableObject はシリアライズ可能な Unity 独自のクラスで、スクリプトインスタンスからは独立して大量の共有データを保存できます。ScriptableObject を使用すると、変更やデバッグの管理が容易になります。ゲーム内の各種システム間において、一定の水準で柔軟なコミュニケーションが簡単に確立されるようになるため、プロジェクト全体にわたるさまざまな事由における変更や採用に加えて、コンポーネントの再利用管理も簡単になります。

ゲームエンジニアリングの 3 つの柱

モジュラーデザインの適用:

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

 

パーツ変更や編集の簡易化:

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

 

デバッグの平易化:

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

変数の設計

ScriptableObject を使用して構築できる最も簡単なものの 1 つは、自己完結型のアセットベースの変数です。ここでは FloatVariable の例を紹介しますが、この例は他のシリアライズ可能な型にも拡張できます。

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

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

FloatVariable.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}

例:プレイヤーの体力値

このユースケースの例として、プレイヤーの体力値(HP)があります。ローカルプレイヤーが 1 人のゲームで、PlayerHP という名前の FloatVariable をプレイヤーの HP として設定できます。プレイヤーがダメージを受けると PlayerHP は減り、プレイヤーが回復すると PlayerHP は増えます。

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

一度このように設定すれば、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() { }
}

イベントの設計

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

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

コード例:GameEvent ScriptableObject

GameEvent ScriptableObject: 

GameEvent ScriptableObject.cs (C#)
[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

GameEventListener:

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 を呼び出すことで、プレイヤーの死に対するレスポンスを容易に検証できます。

Schell Games で構築されたイベントシステムはさらに複雑で、データの受け渡しや型の自動生成を可能にする機能を持っています。現在使用されているイベントシステムは、基本的にこの例を開始点として構築されたものです。

その他システムの設計

スクリプタブルオブジェクトはデータである必要はありません。MonoBehaviour に実装しているシステムを例にとり、その実装を ScriptableObject に移すことができるか検討してみてください。InventoryManager を DontDestroyOnLoad MonoBehaviour に実装するのではなく、ScriptableObject に実装してみましょう。

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

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

このコンテンツにご満足いただけましたか?

はい!
いいえ。

弊社のウェブサイトは最善のユーザー体験をお届けするためにクッキーを使用しています。詳細については、クッキーポリシーのページをご覧ください。

OK