Hero image

Last updated January 2020, 10 min. read

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

このウェブページは、お客様の便宜のために機械翻訳されたものです。翻訳されたコンテンツの正確性や信頼性は保証いたしかねます。翻訳されたコンテンツの正確性について疑問をお持ちの場合は、ウェブページの公式な英語版をご覧ください。

このページから得られるもの:ゲームコードを簡単に変更し、デバッグできるように、スクリプタブルオブジェクトを使って設計するためのヒント。

これらのヒントは、ライアン・ヒップルシェル・ゲームズの主任エンジニアからのもので、スクリプタブルオブジェクトを使用してゲームを設計する上級者向けの経験があります。ライアンのスクリプタブルオブジェクトに関するユナイトトークはこちらで視聴できます。また、スクリプタブルオブジェクトの素晴らしい入門セッションを見ていただくことをお勧めします。

ScriptableObject とは?

スクリプタブルオブジェクトは、スクリプトインスタンスとは独立して、大量の共有データを保存できるシリアライズ可能なUnityクラスです。ScriptableObject を使用すると、変更やデバッグの管理が容易になります。ゲーム内の異なるシステム間で柔軟なコミュニケーションを構築できるため、制作中にそれらを変更・適応しやすく、コンポーネントを再利用することができます。

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

モジュール設計を使用する:

  • 互いに直接依存するシステムを作成しないようにしましょう。例えば、インベントリシステムはゲーム内の他のシステムと通信できる必要がありますが、ハードリファレンスを作成したくないため、異なる構成や関係にシステムを再構成するのが難しくなります。
  • シーンをクリーンスレートとして作成します: シーン間に一時的なデータが存在しないようにします。シーンを切り替えるたびに、まったく新しくロードが実行されるようにしてください。これにより、他のシーンには存在しないユニークな動作を持つシーンを作成でき、ハックを行う必要がなくなります。
  • プレハブを独立して機能するように設定します。できる限りシーン内にドラッグする各プレハブには、ひとつひとつ、その内部に機能を持たせるようにします。こうすることで、シーンがプレハブのリストとなり、プレハブに個々の機能が含まれるようになるため、大規模なチームでソース管理を行いやすくなります。その結果、ほとんどのチェックインがプレハブレベルで行われ、シーン内の競合が少なくなります。
  • 各コンポーネントを単一の問題を解決することに集中させます。これにより、複数のコンポーネントを組み合わせて新しいものを構築しやすくなります。

パーツを簡単に変更・編集できるようにする:

  • ゲームの可能な限り多くをデータ駆動型にします。ゲームシステムをデータを指示として処理する機械のように設計すると、実行中でも効率的にゲームを変更できます。
  • システムができるだけモジュール式でコンポーネントベースになるように設定されていると、アーティストやデザイナーにとっても編集が容易になります。デザイナーが明示的な機能を求めることなくゲーム内で物事を組み合わせることができる場合(それぞれが一つのことだけを行う小さなコンポーネントを実装するおかげで)、彼らは新しいゲームプレイやメカニクスを見つけるために異なる方法でそのようなコンポーネントを組み合わせることができます。ライアンは、彼のチームがゲームで取り組んできた最もクールな機能のいくつかはこのプロセスから生まれたものであり、彼が「エマージェントデザイン」と呼ぶものです。
  • チームがランタイムでゲームを変更できることが重要です。ランタイムでゲームを変更できるほど、バランスや値を見つけることができ、もしランタイム状態をScriptable Objectsのように保存できるなら、素晴らしい状況にいます。

デバッグを簡単にする:

これは最初の二つのサブピラーです。ゲームをモジュール化するほど、ひとつひとつのモジュールを検証しやすくなります。ゲームがより編集しやすければ(つまり、独自のインスペクタービューを備えている機能が多ければ)、デバッグが楽になります。Inspectorでデバッグ状態を表示できることを確認し、デバッグ方法の計画がない限り、機能が完了したと考えないでください。

変数の設計

ScriptableObjectsを使って構築できる最もシンプルなものの一つは、自己完結型のアセットベースの変数です。以下はFloatVariableの例ですが、これは他のシリアライズ可能な型にも拡張されます。

チームの誰もが、どれだけ技術的であっても、新しいFloatVariableアセットを作成することで新しいゲーム変数を定義できます。任意のMonoBehaviourまたはScriptableObjectは、この新しい共有値を参照するためにpublic floatの代わりにpublic FloatVariableを使用できます。

さらに良いことに、あるMonoBehaviourがFloatVariableの値を変更すると、他のMonoBehaviourもその変更を見ることができます。これにより、互いに参照を必要としないシステム間のメッセージングレイヤーが作成されます。

FloatVariable.cs
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}
例:プレイヤーのヒットポイント

例:プレイヤーのヒットポイント

これの使用例は、プレイヤーのヒットポイント(HP)です。ローカルプレイヤーが 1 人のゲームで、PlayerHP という名前の FloatVariable をプレイヤーの HP として設定できます。プレイヤーがダメージを受けるとPlayerHPから減算され、プレイヤーが回復するとPlayerHPに加算されます。

シーン内に健康バーのPrefabを想像してみてください。HP バーは PlayerHP 変数を監視し、その表示を更新します。コードを変えることなく、簡単に別の変数(PlayerMP 変数など)を監視するよう設定することも可能です。健康バーはシーン内のプレイヤーについて何も知りません。ただ、プレイヤーが書き込むのと同じ変数から読み取るだけです。

このように設定されると、PlayerHPを監視するためにさらに多くのものを追加するのが簡単です。音楽システムは、PlayerHPが低下すると変化し、敵はプレイヤーが弱いと知ると攻撃パターンを変えることができ、画面空間効果は次の攻撃の危険性を強調することができます。ここで鍵となるのは、Player スクリプトはこれらのシステムにはメッセージを送信していないということで、言うなればこれらのシステムはプレイヤーゲームオブジェクトを認識している必要がない、ということになります。ゲームが実行中のときにInspectorに入ってPlayerHPの値を変更してテストすることもできます。

FloatVariableの値を編集する際には、ScriptableObjectに保存されている値を変更しないために、データをランタイム値にコピーするのが良いアイデアかもしれません。これを行うと、MonoBehaviourはディスクに保存されているInitialValueを編集しないようにRuntimeValueにアクセスする必要があります。

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

	[NonSerialized]
	public float RuntimeValue;

public void OnAfterDeserialize()
{
		RuntimeValue = InitialValue;
}

public void OnBeforeSerialize() { }
}

イベントの設計

Ryanのお気に入りの機能の1つは、ScriptableObjectの上に構築するイベントシステムです。イベントアーキテクチャを使うと、互いを直接認識しないシステム間でメッセージを送信させることで、コードをモジュール化しやすくなります。これにより、更新ループで常に監視することなく、状態の変化に応じて反応することができます。

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

コード例:GameEvent ScriptableObject

GameEvent ScriptableObject:

GameEvent ScriptableObject.cs
[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
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をリスニングし、挑発アニメーションをトリガーしたり、アイドル動作に戻るための状態変更を行うことができます。

このパターンにより、プレイヤーの死亡に対する新しい反応を追加するのが非常に簡単になります。さらに、InspectorのボタンやテストコードからイベントでRaiseを呼び出すことで、プレイヤーの死亡に対する反応をテストするのが簡単です。

Schell Gamesで構築されたイベントシステムは、はるかに複雑なものに成長し、データを渡したり、自動生成された型を許可する機能を持っています。この例は、彼らが今日使用しているものの出発点でした。

その他システムの設計

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

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

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