Menu
CoddyTech

C#-Ereignisse in Unity

Teil des Abschnitts Spielarchitektur in Unity der Unity-Journey von Coddy. Lektion 12 von 64.

Wenn sich der Punktestand ändert, sind mehrere Dinge davon betroffen: der Punktestandtext, ein Sound, ein Erfolg. Wenn der ScoreKeeper jedes davon aufruft, braucht er für jedes ein Feld, und jede neue Funktion bedeutet, dass er bearbeitet werden muss. Ein Ereignis kehrt das um: Der ScoreKeeper kündigt die Änderung an, und wer davon betroffen ist, abonniert sie.

Zwei C#-Wörter machen das möglich. Action<int> ist der Typ einer Methode, die einen int entgegennimmt und nichts zurückgibt. Ein event dieses Typs ist eine Liste solcher Methoden: Anderer Code fügt seine Methode mit += hinzu, und der Besitzer ruft sie alle mit Invoke auf. In einfachem 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);
    }
}

// an anderer Stelle
Alarm alarm = new Alarm();
Sleeper sleeper = new Sleeper();
alarm.Rang += sleeper.WakeUp;
alarm.Ring(7);   // Aufgestanden um 7

In Unity sind der Herausgeber und seine Listener Komponenten, üblicherweise auf unterschiedlichen GameObjects:

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, am Text des HUDs
public ScoreKeeper keeper;
public TMP_Text label;

void Start()
{
    keeper.OnScoreChanged += ShowScore;
}

void ShowScore(int score)
{
    label.text = "Score: " + score;
}

Action<int> bedeutet, dass jeder Listener eine Methode ist, die einen int entgegennimmt, hier den neuen Punktestand. Invoke ruft jede abonnierte Methode in der Reihenfolge auf, in der sie abonniert wurde. Gibt es keinen Listener, ist das Ereignis null. Ein einfaches OnScoreChanged(score) würde daher eine NullReferenceException auslösen, während ?. den Aufruf überspringt. Das Schlüsselwort event beschränkt andere Skripte auf += und -=: In ScoreLabel wird keeper.OnScoreChanged(5) oder keeper.OnScoreChanged = null nicht kompiliert.

Der ScoreKeeper hat kein Feld für das Label und weiß nicht, dass es existiert. Ein Combo-Zähler oder ein Sound-Player abonniert das Ereignis auf dieselbe Weise, und der ScoreKeeper bleibt unverändert. Eine Testszene ohne HUD läuft ohne Fehler. Ein Listener findet den Herausgeber über eine Inspector-Referenz, wie hier, oder auf jede andere dir bekannte Weise, etwa mit GetComponent oder einem Singleton. Einen Listener mit -= zu entfernen, ist wichtig, sobald Objekte zerstört werden; darum geht es in der nächsten Lektion.

challenge icon

Aufgabe

Einfach

Lass den Kämpfer seine Kombo anzeigen.

  1. Füge in Fighter.cs using System; und public event Action<int> OnComboChanged; hinzu.
  2. Ersetze die Zeilen, in denen comboText und flame verwendet werden, durch OnComboChanged?.Invoke(combo);, und lösche beide Felder.
  3. Schreibe in der Start()-Methode von ComboText und ComboFlame fighter.OnComboChanged += Show; beziehungsweise += Grow;.

Drücke „Spielen“ und drücke ein paar Mal die Leertaste: Die Kombo wächst und wird dann zurückgesetzt. Klicke anschließend auf „Prüfen“.

Probier es selbst

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;
            }
        }
    }
}

Alle Lektionen in Spielarchitektur in Unity

Übe selbstständig: Unity-Playground