Observer Pattern
Part of the Object Oriented Programming section of Coddy's GDScript journey. Lesson 48 of 65.
The Observer pattern lets one object, the subject, tell many other objects, the observers, that something happened. The subject does not know what the observers do with the news; it only announces it.
Written by hand, the subject keeps an array of observers and calls the same method on each one:
# 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 has this pattern built in. A signal is the subject's announcement, and every connect() adds an observer, so the array and the loop are no longer needed:
# 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)An observer is any object with a method whose parameters match the signal:
# 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 has no idea a bar or a log exists. A sound effect or an achievement can listen too, without a single change to health.gd.
Three rules keep observers working:
- Connecting the same method twice is an error. When it could happen, check first with
hp_changed.is_connected(bar.on_hp_changed).disconnect()removes an observer, and disconnecting one that is not connected is an error too. - A connection does not keep the observer alive. Keep the observer in a variable or an array: an object created inside the
connect()call is freed at once and never hears the signal. - Connect methods of the observers. A lambda that uses the subject and is connected to the subject's own signal keeps the subject alive forever, which Godot reports as a leak when the game closes.
Challenge
MediumBuild a weather station that notifies its displays with a signal.
In weather_station.gd:
- Declare the signal
temperature_changed(value: float). attach(display)connects the display'son_temperature_changedmethod. Attaching a display that is already attached does nothing.detach(display)disconnects it. Detaching a display that is not attached does nothing.set_temperature(value)stores the value intemperatureand emits the signal with it.
In weather_display.gd, on_temperature_changed(value) prints the display name and the value with one decimal, like Phone: 21.5 C.
The supplied code reads the display names on one line, how many commands follow, then one command per line: attach NAME, detach NAME or temp VALUE. At the end it prints the last temperature. For the names Phone Tablet and the commands attach Phone, attach Tablet, temp 21.5, detach Phone, temp 19 the output is:
Phone: 21.5 C
Tablet: 21.5 C
Tablet: 19.0 C
Final temperature: 19.0Your code goes in weather_station.gd and weather_display.gd. main.gd holds the supplied input/output code and cannot be edited.
Try it yourself
extends Node
func _ready():
# Supplied input/output code: keep it as it is
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)
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesIntroduction to OOPClasses vs ObjectsThe self KeywordMethodsPropertiesConstructor (_init)Recap - Simple Calculator4Polymorphism
Method Overriding RevisitedDuck TypingAbstract Classes and MethodsInterface DesignRecap - Shape Calculator2Class Properties
Instance vs Static VariablesStatic FunctionsProperty SettersProperty GettersPrivate by ConventionRecap - Bank Account Manager5Encapsulation
Public and Private MembersValidation with SettersInformation HidingRead-Only PropertiesRecap - Student Records System8Advanced OOP Concepts
Composition vs InheritanceInner ClassesNodes as ComponentsRefCounted vs NodeEnums in Classes11Project: Library Management
Project OverviewBook and User Classes3Inheritance
Basic InheritanceThe super KeywordMethod OverridingInheritance ChainsType Checks with isRecap - Employee Hierarchy6Special Methods
Virtual Methods IntroductionThe _to_string MethodCustom IteratorsRecap - Custom List9Design Patterns Part 1
Intro to Design PatternsSingleton PatternFactory PatternObserver PatternStrategy PatternPractice on your own: Online GDScript compiler