Menu
CoddyTech

Beobachter-Muster

Teil des Abschnitts Objektorientierte Programmierung der GDScript-Journey von Coddy. Lektion 48 von 65.

Das Observer-Muster ermöglicht es einem Objekt, dem Subjekt, vielen anderen Objekten, den Beobachtern, mitzuteilen, dass etwas passiert ist. Das Subjekt weiß nicht, was die Beobachter mit dieser Nachricht machen; es gibt sie lediglich bekannt.

Manuell geschrieben, speichert das Subjekt ein Array von Beobachtern und ruft bei jedem dieselbe Methode auf:

# health.gd
class_name Health
extends RefCounted

var observers: Array = []
var hp := 100

func add_observer(observer) -> void:
	observers.append(observer)

func take_damage(amount: int) -> void:
	hp -= amount
	for observer in observers:
		observer.on_hp_changed(hp)

Godot hat dieses Muster integriert. Ein Signal ist die Bekanntmachung des Subjekts, und jedes connect() fügt einen Beobachter hinzu, sodass das Array und die Schleife nicht mehr benötigt werden:

# health.gd
class_name Health
extends RefCounted

signal hp_changed(new_hp: int)

var hp := 100

func take_damage(amount: int) -> void:
	hp -= amount
	hp_changed.emit(hp)

Ein Beobachter ist jedes Objekt mit einer Methode, deren Parameter mit dem Signal übereinstimmen:

# health_bar.gd
class_name HealthBar
extends RefCounted

func on_hp_changed(new_hp: int) -> void:
	print("Bar: ", new_hp, "/100")
# damage_log.gd
class_name DamageLog
extends RefCounted

func on_hp_changed(new_hp: int) -> void:
	if new_hp < 50:
		print("Log: low health!")
# main.gd
var health := Health.new()
var bar := HealthBar.new()
var damage_log := DamageLog.new()
health.hp_changed.connect(bar.on_hp_changed)
health.hp_changed.connect(damage_log.on_hp_changed)
health.take_damage(70)
Bar: 30/100
Log: low health!

Health weiß nicht, dass es eine Leiste oder ein Protokoll gibt. Auch ein Soundeffekt oder ein Erfolg kann darauf reagieren, ohne dass health.gd auch nur einmal geändert werden muss.

Drei Regeln sorgen dafür, dass Beobachter funktionieren:

  • Dieselbe Methode zweimal zu verbinden, ist ein Fehler. Wenn das passieren könnte, prüfe zuerst mit hp_changed.is_connected(bar.on_hp_changed). disconnect() entfernt einen Beobachter, und auch das Trennen eines nicht verbundenen Beobachters ist ein Fehler.
  • Eine Verbindung hält den Beobachter nicht am Leben. Bewahre den Beobachter in einer Variablen oder einem Array auf: Ein Objekt, das innerhalb des Aufrufs connect() erstellt wird, wird sofort freigegeben und empfängt das Signal nie.
  • Verbinde Methoden der Beobachter. Eine Lambda-Funktion, die das Subjekt verwendet und mit dem eigenen Signal des Subjekts verbunden ist, hält das Subjekt für immer am Leben, was Godot beim Schließen des Spiels als Speicherleck meldet.
challenge icon

Aufgabe

Mittel

Erstelle eine Wetterstation, die ihre Displays mit einem Signal benachrichtigt.

In weather_station.gd:

  • Deklariere das Signal temperature_changed(value: float).
  • attach(display) verbindet die Methode on_temperature_changed des Displays. Das Anfügen eines bereits angefügten Displays bewirkt nichts.
  • detach(display) trennt die Verbindung. Das Entfernen eines nicht angefügten Displays bewirkt nichts.
  • set_temperature(value) speichert den Wert in temperature und sendet das Signal mit diesem Wert aus.

In weather_display.gd gibt on_temperature_changed(value) den Namen des Displays und den Wert mit einer Nachkommastelle aus, etwa Phone: 21.5 C.

Der bereitgestellte Code liest die Displaynamen in einer Zeile, anschließend die Anzahl der folgenden Befehle und dann einen Befehl pro Zeile: attach NAME, detach NAME oder temp VALUE. Am Ende gibt er die letzte Temperatur aus. Für die Namen Phone Tablet und die Befehle attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19 lautet die Ausgabe:

Phone: 21.5 C
Tablet: 21.5 C
Tablet: 19.0 C
Final temperature: 19.0

Dein Code gehört in weather_station.gd und weather_display.gd. main.gd enthält den bereitgestellten Ein-/Ausgabecode und kann nicht bearbeitet werden.

Probier es selbst

extends Node

func _ready():
	# Vorgegebener Eingabe-/Ausgabecode: unverändert lassen
	var station := WeatherStation.new()
	var displays := {}
	for display_name in OS.read_string_from_stdin().split(" "):
		displays[display_name] = WeatherDisplay.new(display_name)
	var count := int(OS.read_string_from_stdin())
	for i in count:
		var parts := OS.read_string_from_stdin().split(" ")
		match parts[0]:
			"attach":
				station.attach(displays[parts[1]])
			"detach":
				station.detach(displays[parts[1]])
			"temp":
				station.set_temperature(float(parts[1]))
	print("Final temperature: %.1f" % station.temperature)
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Online-GDScript-Compiler