观察者模式
本课属于 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 会在游戏关闭时将其报告为内存泄漏。
挑战
中等构建一个天气站,通过信号通知各个显示器。
在 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)
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: GDScript 在线编译器