Menu
CoddyTech

オブザーバーパターン

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はゲーム終了時にこれをリークとして報告します。
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)がディスプレイ名と値を小数点以下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)
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: GDScriptオンラインコンパイラ