Menu
CoddyTech

观察者模式

本课属于 Coddy GDScript 学习旅程的“面向对象编程”单元。第 48 课,共 65 课。

观察者模式可以让一个对象(主题)告诉许多其他对象(观察者)某件事已经发生。主题不知道观察者会如何处理这个消息,它只负责通知。

手动编写时,主题会保存一个观察者数组,并对每个观察者调用同一个方法:

# 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 内置了这种模式。信号是主题对象发出的通知,每次调用 connect() 都会添加一个观察者,因此不再需要数组和循环:

# 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)

观察者是任何拥有一个方法的对象,该方法的参数与信号匹配:

# 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 完全不知道进度条或日志的存在。音效或成就也可以监听,而无需对 health.gd 做任何修改。

遵循三条规则,观察者才能正常工作:

  • 同一个方法连接两次会导致错误。如果可能发生这种情况,请先用 hp_changed.is_connected(bar.on_hp_changed) 检查。disconnect() 会移除观察者,断开一个尚未连接的观察者也会导致错误。
  • 连接不会让观察者一直存活。请将观察者保存在变量或数组中:在 connect() 调用内部创建的对象会立即被释放,因此永远收不到信号。
  • 连接观察者的方法。使用主体并连接到主体自身信号的 lambda 会让主体永远存活,Godot 会在游戏关闭时将其报告为内存泄漏。
challenge icon

挑战

中等

构建一个天气站,通过信号通知各个显示器。

在 weather_station.gd 中:

  • 声明信号 temperature_changed(value: float)。
  • attach(display) 会连接显示器的 on_temperature_changed 方法。连接已经连接的显示器时,不执行任何操作。
  • detach(display) 会断开连接。断开尚未连接的显示器时,不执行任何操作。
  • set_temperature(value) 会将值存入 temperature,并使用该值发出信号。

在 weather_display.gd 中,on_temperature_changed(value) 会打印显示器名称和保留一位小数的值,格式如 Phone: 21.5 C。

提供的代码会读取一行显示器名称、后续命令的数量,然后逐行读取命令:attach NAME、detach NAME 或 temp VALUE。最后会打印最终温度。对于名称 Phone Tablet 和命令 attach Phone、attach Tablet、temp 21.5、detach Phone、temp 19,输出为:

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

你的代码写在 weather_station.gd 和 weather_display.gd 中。main.gd 包含提供的输入/输出代码,不能编辑。

亲手试试

extends Node

func _ready():
	# 提供的输入/输出代码:保持原样
	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 icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: GDScript 在线编译器