Menu
CoddyTech

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やシングルトンなど、知っている任意の方法で通知を送る側を見つけます。オブジェクトが破棄されるようになったら、-=でリスナーを解除することが重要です。これは次のレッスンで扱います。

challenge icon

チャレンジ

簡単

ファイターがコンボを通知するようにします。

  1. Fighter.csで、using System;とpublic event Action<int> OnComboChanged;を追加します。
  2. comboTextとflameを使っている行をOnComboChanged?.Invoke(combo);に置き換え、両方のフィールドを削除します。
  3. 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プレイグラウンド