Bite-Sized Godot: Setters and getters in GDScript using setget


In case you’re unfamiliar with them, setter and getter functions allow you to write custom logic to be ran when a variable is written to or read from. In GDScript, you do this by adding the setget command after declaring your variable, followed by the names of the setter and getter functions, in that order.

var health: float = 100.0 setget _set_health, _get_health

You’ll of course also want to make sure these functions are declared somewhere in your code, with the setter function setting the variable to a new value and the getter function returning a value.

var health: float = 100.0 setget _set_health, _get_health

func _set_health(new_value: float) -> void:
	health = new_value

func _get_health() -> float:
	return health

If you only want a setter or only a getter, simply use the same format but omit the undesired function name:

# Setter only
var health: float = 100.0 setget _set_health

# Getter only
var mana: float = 100.0 setget , _get_mana

So why care about this? As an example, let’s say we have a game where the player’s health should always be a float between 0 and 100. Using a setter, we can make sure the value is clamped before being applied to the variable, such as shown below.

const MIN_HEALTH: float = 0.0
const MAX_HEALTH: float = 100.0

var health: float = 100.0 setget _set_health

func _set_health(new_value: float) -> void:
	health = clamp(new_value, MIN_HEALTH, MAX_HEALTH)

Another common use includes emitting a signal every time a value is updated so that other objects that depend on that data can always be up to date.

signal health_updated(new_value)

func _set_health(new_value: float) -> void:
	health = clamp(new_value, MIN_HEALTH, MAX_HEALTH)
	emit_signal("health_updated", health)

You could also run logic to make sure data being passed into the variable is of the right type, throwing an error if it’s not and refusing to set the variable, and by similar logic, you could make a variable read-only by overriding the setter function to return without setting the variable to the new value.

var my_readonly_value: int = 100 setget _set_value

# Will prevent external objects from writing to my_readonly_value
func _set_value(new_value) -> void:
	return

Note that comment mentioning this only protects external objects from writing the value. Internally, an object is unprotected by default, but more on that in just a bit…

What are the uses for a getter function? Well, to be honest, I find myself not really ever needing them, but, as an example, you could use them to represent a generator of some sort, calculating a value in a large or changing sequence as needed rather than storing everything in memory just to access one part of it, such as the classic computing example of the fibonacci sequence. In the example below, we store just enough data to calculate the next number in the fibonacci sequence rather than storing the entire list in memory or having to recalculate all values in the sequence every time we just want the next value. It’s a bit of a stretch for a gamedev context, but the same idea could apply to always pull the latest entry from a logfile, caching data from expensive calculations, etc.

var next_fibonacci = [] setget _set_next_fib, _get_next_fib

# Make next_fibonacci read-only
func _set_next_fib(new_value) -> void:
	return

# Maintain only the two numbers needed to calculate the next fibonacci number
func _get_next_fib() -> int:
	var sum: int

	if next_fibonacci.size() == 0:
		sum = 0
	elif next_fibonacci.size() == 1:
		sum = 1
	else:
		sum = next_fibonacci.pop_front() + next_fibonacci[0]
	
	next_fibonacci.push_back(sum)
	return sum

As mentioned above, it’s also important to note that getters and setters only apply when a value is accessed by an external object. When accessed internally, it’s free reign unless you explicitly reference the variable using the format self.my_variable. This is how we can make a variable read-only, but still update it internally as needed, making it more flexible than declaring a CONST, which cannot be changed at runtime, while still (mostly) protecting the value.

With this in mind, using the fibonacci code example, we can run it and get varied outputs based on how we write our code inside of the script that defines it:

var next_fibonacci = [] setget _set_next_fib, _get_next_fib

# Make next_fibonacci read-only
func _set_next_fib(new_value) -> void:
	return

# Maintain only the two numbers needed to calculate the next fibonacci number
func _get_next_fib() -> int:
	var sum: int

	if next_fibonacci.size() == 0:
		sum = 0
	elif next_fibonacci.size() == 1:
		sum = 1
	else:
		sum = next_fibonacci.pop_front() + next_fibonacci[0]
	
	next_fibonacci.push_back(sum)
	return sum

func _ready():
	# Won't do anything as next_fibonacci is protected by the setter
	self.next_fibonacci = 'not valid, but discarded by setter'

	# Prints the expected output of [0, 1, 1, 2, 3]
	for i in range(5):
		print(self.next_fibonacci)
	
	# Overwrites the array
	next_fibonacci = 'not valid'

	# Will print 'not valid'
	print(next_fibonacci)

	# Will throw an error since next_fibonacci is no longer an array
	for i in range(5):
		print(self.next_fibonacci)

And that’s a quick introduction to getters and setters in GDScript. There’s not a whole lot to them, but they can come in handy when you want to maintain certain constraints within your codebase or just be aware of changes to your data. You can see what the official documentation has to say about them here.