UnityにおけるC#イベント
CoddyのUnityジャーニー「Unityのゲームアーキテクチャ」セクションの一部。レッスン 12/64。
スコアが変わると、スコアのテキスト、サウンド、実績など、いくつかのものがその変化に関心を持ちます。ScoreKeeperがそれぞれを呼び出す場合、すべてに対してフィールドが必要になり、新機能を追加するたびにScoreKeeperを編集することになります。イベントを使うと、これが逆になります。ScoreKeeperが変更を通知し、関心のある側がそれを購読します。
これを実現するのが、C#の2つのキーワードです。Action<int>は、intを1つ受け取り、何も返さないメソッドの型です。この型のeventは、そのようなメソッドのリストです。他のコードは+=を使って自分のメソッドを追加し、所有者はInvokeですべてのメソッドを呼び出します。通常のC#では次のようになります。
using System;
class Alarm
{
public event Action<int> Rang;
public void Ring(int hour)
{
Rang?.Invoke(hour);
}
}
class Sleeper
{
public void WakeUp(int hour)
{
Console.WriteLine("Up at " + hour);
}
}
// 別の場所で
Alarm alarm = new Alarm();
Sleeper sleeper = new Sleeper();
alarm.Rang += sleeper.WakeUp;
alarm.Ring(7); // 7時に起床Unityでは、通知を送る側とリスナーはコンポーネントで、通常は別々のGameObjectにあります。
using System;
using UnityEngine;
public class ScoreKeeper : MonoBehaviour
{
public event Action<int> OnScoreChanged;
private int score;
public void AddPoints(int points)
{
score += points;
OnScoreChanged?.Invoke(score);
}
}// ScoreLabel.cs、HUDのテキストに設定
public ScoreKeeper keeper;
public TMP_Text label;
void Start()
{
keeper.OnScoreChanged += ShowScore;
}
void ShowScore(int score)
{
label.text = "Score: " + score;
}Action<int>は、各リスナーがintを1つ受け取るメソッドであることを意味します。ここでは、新しいスコアを受け取ります。Invokeは、購読した順に、購読しているすべてのメソッドを呼び出します。リスナーがいないとイベントはnullになるため、単純にOnScoreChanged(score)とするとNullReferenceExceptionが発生しますが、?.を使えば呼び出しをスキップします。eventキーワードによって、ほかのスクリプトで使える操作は+=と-=に制限されます。ScoreLabelでkeeper.OnScoreChanged(5)やkeeper.OnScoreChanged = nullを書くと、コンパイルできません。
ScoreKeeperにはラベル用のフィールドがなく、ラベルの存在も知りません。コンボカウンターやサウンドプレイヤーも同じ方法で購読でき、ScoreKeeperは変更せずに済みます。また、HUDのないテストシーンもエラーなく動作します。リスナーは、ここで示したようにInspectorの参照を使うか、GetComponentやシングルトンなど、知っている任意の方法で通知を送る側を見つけます。オブジェクトが破棄されるようになったら、-=でリスナーを解除することが重要です。これは次のレッスンで扱います。
チャレンジ
簡単ファイターがコンボを通知するようにします。
Fighter.csで、using System;とpublic event Action<int> OnComboChanged;を追加します。comboTextとflameを使っている行をOnComboChanged?.Invoke(combo);に置き換え、両方のフィールドを削除します。ComboTextとComboFlameのStart()内に、fighter.OnComboChanged += Show;と+= Grow;を書きます。
「プレイ」を押して、スペースキーを数回押します。コンボが増えたあと、リセットされます。その後、「確認」を押します。
自分で試してみよう
using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;
public class Fighter : MonoBehaviour
{
public float comboWindow = 1f;
public TMP_Text comboText;
public Transform flame;
private int combo;
private float sinceHit;
void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame)
{
combo++;
sinceHit = 0f;
comboText.text = "Combo x" + combo;
flame.localScale = Vector3.one * (0.5f + 0.25f * combo);
}
else if (combo > 0)
{
sinceHit += Time.deltaTime;
if (sinceHit > comboWindow)
{
combo = 0;
comboText.text = "";
flame.localScale = Vector3.one * 0.5f;
}
}
}
}
Unityのゲームアーキテクチャのすべてのレッスン
自分で練習してみよう: Unityプレイグラウンド