Ran into the same pattern constantly across three different projects: game state changes, UI needs to update, and I end up writing the same signal + callback boilerplate for every Label, ProgressBar, and inventory slot. At ten UI elements it's manageable. At forty it's noise.
So I wrote a small polling-based binding helper. Not glamorous, but it's eliminated a lot of glue code:
class_name DataBinding
extends RefCounted
var _getter: Callable
var _node: Node
var _property: StringName
var _last_value: Variant
func _init(node: Node, property: StringName, getter: Callable) -> void:
_node = node
_property = property
_getter = getter
_last_value = getter.call()
_node.set(_property, _last_value)
func poll() -> void:
if not is_instance_valid(_node):
return
var current: Variant = _getter.call()
if current != _last_value:
_last_value = current
_node.set(_property, current)
And a manager autoload that polls them and handles cleanup automatically:
# BindingManager.gd — register as autoload
extends Node
var _bindings: Array[DataBinding] = []
func bind(node: Node, property: StringName, getter: Callable) -> DataBinding:
var b := DataBinding.new(node, property, getter)
_bindings.append(b)
node.tree_exiting.connect(func(): _bindings.erase(b), CONNECT_ONE_SHOT)
return b
func _process(_delta: float) -> void:
for b in _bindings:
b.poll()
Usage is one line per binding:
BindingManager.bind($HealthBar, &"value", func(): return player.health)
BindingManager.bind($GoldLabel, &"text", func(): return "Gold: %d" % player.gold)
BindingManager.bind($StaminaBar, &"value", func(): return player.stamina)
The tree_exiting connection means cleanup is automatic. I don't manually track or unregister anything. The lambda captures whatever context it needs.
Main tradeoff: this polls every frame. For a few dozen UI bindings it's negligible, but I haven't profiled it past ~80 active bindings. The obvious alternative is signal-driven updates from property setters, but that requires every data class to emit signals, which is a different kind of coupling I find harder to maintain as the project grows.
Curious if anyone's taken this further: a proper reactive graph, some change-notification hook I haven't found, or a plugin that already does this cleanly. What's your actual approach for keeping UI in sync with game data?