Menu
CoddyTech

Паттерн «Наблюдатель»

Часть раздела Объектно-ориентированное программирование путешествия по GDScript на Coddy. Урок 48 из 65.

Паттерн Observer позволяет одному объекту, субъекту, сообщать множеству других объектов, наблюдателям, что что-то произошло. Субъект не знает, что наблюдатели делают с этой информацией; он лишь сообщает о событии.

При написании вручную субъект хранит массив наблюдателей и вызывает один и тот же метод для каждого из них:

# 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(), сразу освобождается и никогда не получает сигнал.
  • Подключай методы наблюдателей. Лямбда-функция, которая использует субъект и подключена к собственному сигналу субъекта, удерживает субъект в памяти навсегда. 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