> voidtick
Menu
  • #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.

More Articles