- #Godot
- #GDScript
- #Resources
Encapsulating numerical values into resources
Problem
In game development you always have to work with numbers: HP, XP, resource counters and many others. Also, game logic and UI elements must react to changes in these numbers. The problem is in creating duplicated code for common operations: setting min/max values, clamping, checking for zero, emitting signals and so on.
Let’s create a simple player class with HP for demonstration:
# player.gd
class_name Player extends Node
signal hp_changed
@export var hp: int:
set(new_value):
hp = new_value
hp = clampi(hp, 0, max_hp)
hp_changed.emit()
if hp == 0:
die()
@export var max_hp: int:
set(new_value):
max_hp = new_value
hp_changed.emit()
func take_hit(damage: int):
hp -= damage
func heal(amount: int):
hp += amount
func die():
# some death handling code
# hp_bar.gd
class_name PlayerHPBar extends ProgressBar
@export var player: Player
func _ready():
player.hp_changed.connect(_update)
func _update():
min_value = 0
max_value = player.max_hp
value = player.hp
This code is pretty straightforward. But the player can have XP, stamina and other stats to be handled, and the code becomes more and more complex and duplicated. Also, PlayerHPBar is bound to the player class and we cannot just create another progress bar (for enemies, for example) with the same simple behavior.
Solution
It’s a good idea just to encapsulate a number value in a separate resource and let other components communicate with it.
First of all, let’s create a simple resource class for a common integer value. Note that thanks to exported properties we can set up values in the inspector.
# int_value.gd
class_name IntValue extends Resource
signal depleted
@export var min_value: int = 0:
set(new_value):
assert(new_value < max_value, "Minimum value must be less than maximum")
min_value = new_value
value = clampi(value, min_value, max_value)
emit_changed()
@export var max_value: int = 10_000_000_000:
set(new_value):
assert(new_value > min_value, "Maximum value must be greater than minimum")
max_value = new_value
value = clampi(value, min_value, max_value)
emit_changed()
@export var value: int:
set(new_value):
value = clampi(new_value, min_value, max_value)
emit_changed()
if value <= min_value:
depleted.emit()
Now we can refactor our previous Player code and replace PlayerHPBar with a more generic IntProgressBar:
# player.gd
class_name Player extends Node
@export var hp: IntValue # note that we dont need separate export var for max value
func _ready():
hp.depleted.connect(die)
func take_hit(damage: int):
hp.value -= damage
func heal(amount: int):
hp.value += amount
func die():
# some death handling code
# int_progress_bar.gd
# Now we can use this node for any other IntValue. Just call setup wherever it's needed.
class_name IntProgressBar extends ProgressBar
var int_value: IntValue
func setup(wrapper: IntValue): # custom method
int_value = wrapper
int_value.changed.connect(_update)
func _update():
min_value = int_value.min_value
max_value = int_value.max_value
value = int_value.value
# level.gd
# This is the root node script which usually handles all the setup.
@export var player: Player
@export var player_hp_bar: IntProgressBar
func _ready():
player_hp_bar.setup(player.hp)
Now you can use this IntValue resource class to store any integer value without code duplication and make other components just subscribe to changes.
Note that you can create such resource classes for other types and use them the same way. The main idea is encapsulating common behavior and making life a bit easier.