Letzte Woche ging es um Blueprint Interfaces und wie sie dich davon abhalten, auf alles hart zu casten. Interfaces lösen das "was rufe ich auf"-Problem. Aber es gibt ein zweites Problem, das sie nicht lösen: wem sage ich es überhaupt?

Dein Player nimmt Schaden. Die Health-Bar muss es wissen. Der Damage-Flash auch, der "Low Health"-Herzschlag-Sound, vielleicht ein Achievement-Tracker. Braucht dein BP_Player wirklich eine Referenz auf alle vier? Die heutige Antwort: nein.

Was ein Event Dispatcher wirklich ist

Ein Event Dispatcher ist ein Broadcast. Ein Blueprint verkündet "das ist passiert" und jeder, der zuhören wollte, wird benachrichtigt. Der Verkünder hat keine Ahnung, wer zuhört — können null sein, können zehn Systeme sein. In der Programmierung heißt das Observer-Pattern (oder Publish-Subscribe) und es ist der sauberste Weg für One-to-Many-Kommunikation.

Vergleich der drei Wege, wie Actors miteinander reden:

  • Direkter Aufruf — du hältst eine Referenz auf einen bestimmten Actor und rufst seine Funktion auf. One-to-One, eng gekoppelt.
  • Interface — du rufst eine Funktion auf irgendwas auf, ohne dich um die Klasse zu kümmern. Immer noch One-to-One (oder du loopst), brauchst immer noch eine Referenz.
  • Event Dispatcher — du broadcastest. One-to-Many. Der Sender braucht null Referenzen auf die Listener.

Die letzte Zeile ist der ganze Punkt.

Ein konkretes Beispiel

Health-Bar. Die naive Variante: BP_Player hält eine Referenz auf das WBP_HUD-Widget und ruft bei jeder Health-Änderung UpdateHealthBar(NewHealth) darauf auf. Funktioniert — bis du den Damage-Flash hinzufügst. Jetzt braucht der Player auch eine Referenz auf das Post-Process. Dann den Herzschlag-Sound. Dann... kennt dein Player-Blueprint jedes UI- und Audio-System im Spiel. Ändere eins und du riskierst, den Player zu brechen.

Die Dispatcher-Variante: BP_Player bekommt einen Event Dispatcher namens OnHealthChanged mit einem float-Parameter. Wenn sich Health ändert, Callt er OnHealthChanged(NewHealth) und macht weiter. Er weiß nicht und es ist ihm egal, wer zuhört.

Das HUD Bindet sich an OnHealthChanged und aktualisiert die Bar. Später fügst du einen Damage-Flash hinzu? Bindet sich auch. Einen Herzschlag-Sound? Bindet sich auch. Du fasst BP_Player nie wieder an. Das ist der Gewinn: neue Listener kosten null Änderungen am Sender.

Die vier Dinge, die du mit einem Dispatcher tun kannst

Zieh von einem Dispatcher weg und du siehst vier Verben. Lass die Namen nicht verschwimmen:

  • Call — feuern. Jeder, der gebunden ist, wird jetzt benachrichtigt. Das ist der Job des Senders.
  • Bind Event — anfangen zuzuhören. Du gibst ihm ein Custom Event, das läuft, wenn der Dispatcher feuert. Das ist der Job des Listeners.
  • Unbind / Unbind All — aufhören zuzuhören.
  • Assign — eine Blueprint-Abkürzung, die ein Custom Event erstellt und es in einem Node bindet. Es ist der rote Node, den du bekommst, wenn du einen Dispatcher in den Graph ziehst. Praktisch — aber er versteckt das Bind, also vergessen Leute das Unbind.

Ein Dispatcher kann Parameter tragen: gib OnHealthChanged einen float und jedes gebundene Event bekommt ihn. Definiere die Signatur einmal; alle Listener teilen sie.

Dispatcher, Interface oder direkter Aufruf?

Schnelle Entscheidung:

  • Ein bekanntes Ziel, einfacher Aufruf → direkte Referenz. Nicht over-engineeren.
  • Ein Ziel, aber es könnten verschiedene Klassen seinInterface.
  • Du verkündest etwas und weißt nicht (oder es ist dir egal), wer reagiert → Dispatcher.

Health, Tode, "Wave gestartet", "Tür geöffnet", "Item aufgehoben" — alles, wo die Anzahl der Reagierenden unbekannt oder wachsend ist, ist ein Dispatcher. UI- und Game-State-Benachrichtigungen sind der Lehrbuch-Fall.

Der Referenz-Haken, den keiner erwähnt

Jetzt der Teil, der die Leute stolpern lässt, die denken, Dispatcher bedeuten "gar keine Referenzen": zum Binden braucht der Listener eine Referenz auf den Sender. Das HUD muss den Player zu fassen kriegen, um zu sagen "bind mich an dein OnHealthChanged".

Die Entkopplung ist also einseitig. Der Sender bleibt selig ahnungslos über seine Listener — das ist die wertvolle Hälfte. Aber der Listener muss den Sender einmal erreichen, um sich zu subscriben. Meistens ist das ok (das HUD bekommt den Player über den Controller). Erwarte nur nicht, dass Dispatcher jede Referenz wegzaubern — sie entfernen die, die am meisten wehtun.


Plugin tip
Ein Dispatcher bringt das Signal zu deinem Listener — aber soll der Listener jetzt sofort reagieren? "Nur solange am Leben", "nur beim ersten Mal", "ist dieser Bool gerade umgesprungen?" — das ist Flow-Control-Logik, die du jedes Mal mit Bool-Variablen und Branch-Nodes neu baust. FoxBranch packt das in einzelne Nodes: Gates, die du öffnest und schließt, Do-Once-Guards, Edge Detection, Multi-Way-Routing. Optional, aber am Empfangsende eines vielbeschäftigten Dispatchers killt es eine Menge Spaghetti.

Typische Fehler

  • Unbind beim Zerstören vergessen. Das ist der große. Wenn ein Listener zerstört wird, aber gebunden bleibt, feuert der Dispatcher weiter auf ein totes Objekt — Accessed-None-Spam und ein langsames Anwachsen veralteter Bindings. Unbind in End Play.
  • Target ist standardmäßig Self. Wenn du Bindest, füllt sich der Target-Pin automatisch mit Self. Wolltest du an den Dispatcher eines anderen Actors binden, musst du diesen Actor einstecken — sonst hörst du nur dir selbst zu.
  • Doppelt binden. Bind dasselbe Event in BeginPlay und irgendwo, das nochmal läuft, und es feuert zweimal pro Call. Einmal binden, oder vorher Unbinden.
  • Callen, bevor jemand gebunden hat. Wenn der Sender während BeginPlay Callt, bevor das HUD gebunden hat, verpasst das HUD es. Reihenfolge zählt — früh binden.
  • Parameter-Mismatch. Ändere die Signatur des Dispatchers und jedes gebundene Event bricht still, bis du es neu erstellst. Entscheide deine Parameter, bevor du zehn Listener verdrahtest.

Wie du einen Dispatcher debuggst

Wenn ein Listener "nicht reagiert":

  • Setz ein Print String als ersten Node des gebundenen Events. Kein Print = das Bind ist nie passiert (falsches Target, oder zu spät gebunden).
  • Prüf, ob du wirklich Bind gerufen hast, nicht nur das Event erstellt.
  • Print String auch auf der Call-Seite. Wenn der Sender nie callt, feuert nichts dahinter.

Neun von zehn Mal ist es eine Self-vs-anderer-Target-Verwechslung oder ein Timing-Problem.

Die 30-Sekunden-Zusammenfassung

  • Event Dispatcher = Broadcast. Ein Sender, viele Listener, Sender braucht null Referenzen auf sie.
  • Call zum Feuern, Bind zum Zuhören, Unbind wenn fertig, Assign für den schnellen Kombi-Node.
  • Nutze ihn, wenn du nicht weißt (oder nicht wissen willst), wer reagiert — UI, Health, Game State.
  • Der Listener braucht trotzdem eine Referenz auf den Sender, um zu binden. Die Entkopplung ist einseitig.
  • Immer Unbind in End Play, achte auf das Self-Target und bind nicht doppelt.

Nächsten Sonntag, wenn die Votes halten: Actors spawnen — die Muster, die dein Spiel nicht crashen. Deferred Spawn, Transform-Fallen und warum dein gespawnter Actor manchmal sein eigenes BeginPlay ignoriert.

— Marco