オブザーバーパターン
CoddyのGDScriptジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 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に一切変更を加えることなく、監視できます。
オブザーバーが機能し続けるためのルールは3つあります。
- 同じメソッドを2回接続するのはエラーです。その可能性がある場合は、まず
hp_changed.is_connected(bar.on_hp_changed)で確認してください。disconnect()はオブザーバーを解除します。また、接続されていないオブザーバーを解除することもエラーです。 - 接続してもオブザーバーの存続は保証されません。オブザーバーは変数か配列に保持してください。
connect()の呼び出し内で作成されたオブジェクトはすぐに解放され、シグナルを受け取ることはありません。 - オブザーバーのメソッドを接続してください。サブジェクトを使用し、サブジェクト自身のシグナルに接続されたラムダは、サブジェクトを永遠に存続させます。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)がディスプレイ名と値を小数点以下1桁で出力します。例:Phone: 21.5 C
提供されたコードは、1行でディスプレイ名を読み込み、続いてコマンドの数を読み込んだあと、1行に1つずつコマンドを読み込みます: 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オンラインコンパイラ