
前回: Unity ScriptableObject のデザインパターン「データコンテナ」を確認する - Self-Taught CODE Tokushima Tech Blog
前回の記事で ScriptableObject の公式のサンプルである Paddle Ball プロジェクトのデザインパターンの紹介部分における「データコンテナ」の内容を確認しました。全体として以下の 5 つがあり、今回は 2 つ目のデリゲートです。
- ✅
データコンテナ (DATA CONTAINERS)
- デリゲートオブジェクト (DELEGATE OBJECTS)
- イベントチャンネル (EVENT CHANNELS)
- ランタイムセット (RUNTIME SETS)
- 拡張可能な Enum (ENUMS)
デリゲートオブジェクト
Use ScriptableObjects as Delegate Objects | Unity
ストラテジーパターン
まず、デリゲートの使い方として「ストラテジーパターン (Strategy Pattern) が紹介されています。

ストラテジーパターンに関する詳細は省きますが、例として、EnemyUnit という MonoBehaviour で実装された敵ユニットがあった場合に、移動処理を直接記述するのではなく、ScriptableObject に移譲して、入れ替え可能にするケースが紹介されています。
当然ながらストラテジーパターンは ScriptableObject である必要はまったくなく、POCO でも実装は可能です。
しかし、デザイナーが入れ替えをしやすくするのに ScriptableObject で実装するのは都合が良さそうです。
プラガブル (Pluggable) な振る舞い
Context は StrategyBase という ScriptableObject に依存しています。
ScriptableObject は単体でもカスタムアセットを作ることで、同じ型のアセットを作ることができますが、Inspector からパラメータの値を変えることはできても、 振る舞い(つまり内部のメソッドの動作) までは変えられません。
よって、先程の図のように StrategyBase という基底クラス ScriptableObject(これに動作は不要なので抽象クラス) に対して Strategy1, Strategy2 という子の ScriptableObject を作成しています。
ドキュメントにある通り、抽象基底クラスではなく Interface でも問題ありません。
「プラガブル (Pluggable)」という用語は、このようなコードやアセット単位だけではなく、多くのソフトウェア開発の文脈で使われます。「入れ替え可能にしておく」ということ意識しておくと制作の助けになると思います。
使用例: AudioDelegate / パターンデモ
ここではパターンの方のデモの方を詳細に見ます。Ball がただ、壁やブロックに跳ね返って音が出るというだけのものです。
ただ音が出るのではなく、ボールがいるエリアによって音が変わります。
壁やブロックには以下のように BallCollided_SO (ボールが衝突したことを通知) と SoundPlayed_SO (音を鳴らすように通知) という ScriptableObject のアセットが Broadcast 側 つまり「通知する側」としてつけられています。
このデモで重要なのは音を出すように通知する SoundPlayed_SO ですね。
そしてこれを Listen つまり「受信する側」なのは、DemoSounds というオブジェクトです。
| Inspector |
Hierarchy |
 |
 |
DemoSounds は各エリアの音を保持する AudioSource**Pitch のオブジェクト群を子に持っています。
それらは AudioModifier を通して、SimpleAudioDelegateSO のアセットを保持しています。これが、それぞれの音源を出し分ける設定になっています。以下の 4 種類ですね。
SimpleAudioDelegateSO とその親の AudioDelegateSO は以下のような実装になっており、それぞれの設定値に応じて Play を実行することが分かります。
今回は、どれも AudioClip は一つですが、複数設定してランダムな音源を鳴らすこともできたようです。
public abstract class AudioDelegateSO : ScriptableObject
{
public abstract void Play(AudioSource source);
}
[CreateAssetMenu(fileName ="SimpleAudioDelegate",menuName ="PaddleBall/Simple Audio Delegate")]
public class SimpleAudioDelegateSO : AudioDelegateSO
{
[SerializeField] private AudioClip[] clips;
[SerializeField] private RangedFloat volume;
[SerializeField] private RangedFloat pitch;
public override void Play(AudioSource source)
{
source.clip = clips[Random.Range(0, clips.Length)];
source.volume = Random.Range(volume.minValue, volume.maxValue);
source.pitch = Random.Range(pitch.minValue, pitch.maxValue);
source.Play();
}
}
そして、この Play を実行するのが AudioModifier です。
[RequireComponent(typeof(AudioSource))]
public class AudioModifier : MonoBehaviour
{
[SerializeField] private AudioDelegateSO m_AudioDelegate;
private AudioSource m_AudioSource;
private void Awake()
{
m_AudioSource = GetComponent<AudioSource>();
}
public void Play()
{
m_AudioDelegate.Play(m_AudioSource);
}
}
ここの構造を見て気付いたかもしれませんが、実は AudioModifier と同じオブジェクトについている AudioSource に設定されている以下ような Audio Resource (音源) は実際には利用されません。
SimpleAudioDelegateSO の方で、そこについている Clips オブジェクトの方に置き換えられるためです。
| こちらの AudioSource は使われない |
こちらの Clips が使われる |
 |
 |
そして、最後に、DemoSounds に戻ってきます。
前述の通り、DemoSounds は SoundPlayed_SO を Listen しています。
つまり、音を鳴らす通知を受けるのは DemoSounds なのです。

上記のように DemoSounds は Vector2EventChannelListner を経由して、SoundPlayed_SO から受け取った Vector2 の情報(つまり音を鳴らしたい場所) の情報を受取り、AudioSourceController.PlayClosestSource を呼び出しています。
AudioSourceController は、自分の子に AudioModifier が複数配置されていることを分かっているため、そこから受け取った Vector2 の座標情報に最も近い position を持つものを選び、その Play を実行しています。
これにより、Ball が衝突した場所に応じて、鳴る音が変化するという処理が実現されています。
イベントもボールは関係なく単に Vector2 を受け取り、それに対して音を鳴らすだけなので、ボールへの依存もありません。
ストラテジーパターンとして
この部分のストラテジーパターンは少し理解が難しく感じます。確かに AudioModifier と SimpleAudioDelegateSO の関係は再生するクリップやそのオーディオ処理を入れ替えられる点でストラテジーパターンなのですが、実は AudioModifier 自体も SimpleAudioDelegateSO のアセットの数だけ作成されています。
つまり、切り替えに使われているのは実質 AudioModifier のオブジェクト群です。
例:目的システムにおけるストラテジーパターンの実装
続いて、Paddle Game の内容における「目的システム」の紹介です。以前の記事では「イベントチャンネル」の一部で私が紹介しましたが、ここでは「ストラテジーパターン」としての紹介になります。
まず、Paddle Game はどちらかのプレイヤーが「あるスコアに到達」すると勝敗が決まりゲームが終了します。この「ゲームの目的」を管理しているのが ObjectiveSO と ObjectiveManager です。
ObjectiveSO: 個別の目的を管理する
ObjectiveManager: 全ての目的を管理する
以下を見てもらうと分かる通り、ObjectvieManager に Objectives として ScoreObjectiveSO のアセットがリストとして紐づけられているのが分かり、「個別と全体」の関係が明らかです。
ただ、この Paddle Ball はスコアのみが勝利条件なので「目的は一つだけ」というサンプルです。
| ObjectiveManager |
ScoreObjectiveSO |
 |
 |
そのままコードを見て確認します。
ObjectiveSO
ベースとなる ObjectiveSO 、つまり「個別の目的」は、シンプルに「完了イベントの通知 (CompleteObjective)」のみを担当しています。自身が管理する目的が完了したら通知ということですね。
public class ObjectiveSO : DescriptionSO
{
[Space]
[Tooltip("On-screen name")]
[SerializeField] private string m_Title;
[Header("Broadcast on Event Channels")]
[Tooltip("Event sent that objective is complete")]
[SerializeField] private VoidEventChannelSO m_ObjectiveCompleted;
private bool m_IsCompleted;
public bool IsCompleted => m_IsCompleted;
public VoidEventChannelSO ObjectiveComplete => m_ObjectiveCompleted;
protected virtual void CompleteObjective()
{
m_IsCompleted = true;
m_ObjectiveCompleted.RaiseEvent();
}
public void ResetObjective()
{
m_IsCompleted = false;
}
}
ScoreObjectiveSO
ObjectiveSO を継承した ScoreObjectiveSO はこの Paddle Game のスコア用に拡張されています。
ScoreListEventChannelSO のイベント通知を受け取って「スコアが目標に到達したか」「していれば完了通知」を行います。
この Paddle Game は二人プレイなので、「二人どちらかのスコアが目標に到達したか」ですが、ScoreListEventChannelSO は List<PlayerScore> を渡してくるので、おそらくプレイヤーが増えても対応できるようになっています。
[CreateAssetMenu(menuName = "Objectives/Score Objective", fileName = "ScoreObjective")]
public class ScoreObjectiveSO : ObjectiveSO
{
[Header("Score Goal")]
[Tooltip("Score to win")]
[SerializeField] private int m_TargetScore = 1;
[Header("Broadcast on Event Channels")]
[Tooltip("Notify listeners that a Player has reached winning score")]
[SerializeField] private PlayerScoreEventChannelSO m_TargetScoreReached;
[Header("Listen to Event Channels")]
[Tooltip("Signal when ScoreManager updates")]
[SerializeField] private ScoreListEventChannelSO m_ScoreManagerUpdated;
public int TargetScore => m_TargetScore;
private void OnEnable()
{
m_ScoreManagerUpdated.OnEventRaised += UpdateScoreManager;
}
private void OnDisable()
{
m_ScoreManagerUpdated.OnEventRaised -= UpdateScoreManager;
}
private void UpdateScoreManager(List<PlayerScore> playerScores)
{
if (HasReachedTargetScore(playerScores))
CompleteObjective();
}
private bool HasReachedTargetScore(List<PlayerScore> playerScores)
{
foreach (PlayerScore playerScore in playerScores)
{
if (playerScore.score.Value >= m_TargetScore)
{
m_TargetScoreReached.RaiseEvent(playerScore);
return true;
}
}
return false;
}
}
ObjectiveManager
ObjectiveManager は上述の通り、複数の ObjectiveSO を管理しています。
m_ObjectiveCompleted (これは ScoreObjectiveSO が Broadcast しています) を受取り、管理する全ての ObjectiveSO が完了しているかを確認します。
完了していれば m_AllObjectivesCompleted で「全ての目標の完了を通知」します。(ちなみに、このイベントはゲーム自体を管理する GameManager が受信しています)
public class ObjectiveManager : MonoBehaviour
{
[Tooltip("List of Objectives needed for win condition.")]
[SerializeField] private List<ObjectiveSO> m_Objectives = new List<ObjectiveSO>();
[Header("Broadcast on Event Channels")]
[Tooltip("Signal that all objectives are complete.")]
[SerializeField] private VoidEventChannelSO m_AllObjectivesCompleted;
[Header("Listen to Event Channels")]
[Tooltip("Gameplay has begun.")]
[SerializeField] private VoidEventChannelSO m_GameStarted;
[Tooltip("Signal to update every time a single objective completes.")]
[SerializeField] private VoidEventChannelSO m_ObjectiveCompleted;
private void OnEnable()
{
m_GameStarted.OnEventRaised += OnGameStarted;
m_ObjectiveCompleted.OnEventRaised += OnCompleteObjective;
}
private void OnDisable()
{
m_GameStarted.OnEventRaised -= OnGameStarted;
m_ObjectiveCompleted.OnEventRaised -= OnCompleteObjective;
}
public bool IsObjectiveListComplete()
{
foreach (ObjectiveSO objective in m_Objectives)
{
if (!objective.IsCompleted)
{
return false;
}
}
return true;
}
private void OnGameStarted()
{
foreach (ObjectiveSO objective in m_Objectives)
{
objective.ResetObjective();
}
}
private void OnCompleteObjective()
{
if (IsObjectiveListComplete())
{
if (m_AllObjectivesCompleted != null)
m_AllObjectivesCompleted.RaiseEvent();
}
}
}
ストラテジーパターンとして
Audio の例では AudioDelegateSOと SimpleAudioDelegateSO の関係がありましたが、こちらはそれが ObjectvieSO と ScoreObjectiveSO になっています。
こちらの例は、他に TimeIsUpObjectiveSO (時間切れ) など、他のゲームの完了条件も作れそうです。
いずれにしろ、利用するクラスである AudioModifier や ObjectiveManager が「何が設定されているかを意識せずに使える」「それを交換でき、それで振る舞いが変わる」という点で、どちらもストラテジーパターンとして利用できそうです。
最後に
今回は Paddle Ball プロジェクトに含まれているデザインパターンの説明から「デリゲート」を確認しました。
ほとんどストラテジーパターンの説明でしたが、単に ScriptableObject の「カスタムアセットが入れ替え可能」というだけではなく、「振る舞いを変えた ScriptableObject の入れ替え」という手法の紹介という点でも面白かったです。