Godot 4 GDScript Features - GDScript 2.0

The best new features are:

  • first-class functions
  • lambdas
  • new property syntax
  • await keyword
  • super keyword
  • typed arrays
  • built-in annotations
  • automatically generate documentation

Here, I will explain my initial understanding of the new features together with code examples.

First-class Functions and Lambdas

These are functions that may be assigned to variables and passed as arguments to other functions. They may also be returned from functions. They are defined in the same way as regular functions but in the place of where an expression would normally be.

	var test_var = func is_positive(x):
		return x >= 0

	print(test_var.call(8)) # true

	test_var = func is_negative(x):
		return x < 0

	print(test_var.call(-4)) # true

The function name is only needed when viewing stack traces to see when it was called. Otherwise, it is an anonymous function.

Note the use of the call method. call_deferred is another method that could be used to delay the call until the end of the frame.

	var is_negative = func(x):
		return x < 0

	var is_positive = func(x):
		return x >= 0

	var tests = { "pos": is_positive, "neg": is_negative }

	for n in range(-2, 3):
		prints(n, tests.pos.call(n), tests.neg.call(n))

Filter functions

These functions are useful for simple tasks such as for a map filter function where values of a collection are passed in to the function and a result is returned after applying the filter function.

	var arr = [2,1,4,5,3]
		
	arr.sort_custom(func(x, y): return x > y)

	print(arr) # [5, 4, 3, 2, 1]

	var dicts = [{a=1, b=6}, {a=5, b=5}, {a=3, b=2}, {a=4, b=1}]

	dicts.sort_custom(func(x, y): return x.a > y.a)

	print(dicts)
    # [{"a":5, "b":5}, {"a":4, "b":1}, {"a":3, "b":2}, {"a":1, "b":6}]

Lambdas

The expression is called a lambda. In GDScript, a lambda looks the same as an anonymous function. But in other languages (such as C# and JavaScript), it may be written to look more like a mathematical operation with arrow notation.

# GDScript
	var greet = func(): return "Hello, World!"
	var add_one = func(x): return x + 1
	print(greet.call())
	print(add_one.call(2))

# C#
Func<string> greet = () => "Hello, World!";
int add_one = (int x) => x + 1;

# Javascript
const greet = () => "Hello, World!";
const addOne = (x) => x + 1;

Functional programming

First-class functions play a big part in functional programming where the aim is to avoid affecting external state from within functions, and passing values in and out of deterministic (pure) functions.

# An unreliable function that causes side effects

var x = 5
var y = 1

func increment_x():
	x = x + y;

increment_x()

# Pure functions

func add_one(n):
	return n + 1

var plus_one = func(n): return n + 1

Closures

Can we implement a closure in GDScript? This is a function that encloses another function that has access to its parent scope.

	var get_counter = func():
		var count = 0
		return func():
			count += 1
			return count

	var counter = get_counter.call()
	print(counter.call()) # 1
	print(counter.call()) # 1 Didn't work :(
	print(counter.call()) # 1

When get_count is initialized, the value for count is set to zero, and the value of the return value is set to the inner anonymous function. Calling the get_count function causes the inner function to run to obtain the return value, and this has access to the outer functions scope. So it increments the count value and returns it. But unfortunately, the count value is reset on each call.

Currying

Currying is where we take a function that has multiple input arguments and create a sequence of functions that take each of the arguments in turn. First-class functions allow us to create these partial functions.

As an example we might have a function to display a message with a particular language: func(lang, msg_id). To curry it, we would create a function that is used like so: func(lang)(msg_id).

Where this is useful is to get a partial function where the lang value has been set (like applying configuration data).

var dictionary = func(lang)

var localized_message = dictionary.call("GREETING")

In a game, you could compose the partial functions for things such as weapons and characters. For example, use it like a factory pattern with custom options.

Here is an example of how to curry a simple math function with 3 inputs.

	var sum_all = func(x, y, z):
		return x + y + z
	
	var curry = func(f):
		return func(x):
			return func(y):
				return func(z):
					return f.call(x, y, z)
	
	var curried_sum = curry.call(sum_all)
	var partial_sum_x = curried_sum.call(1)
	var partial_sum_y = partial_sum_x.call(2)
	print(partial_sum_y.call(3)) # 6
	print(curried_sum.call(1).call(2).call(3)) # 6

Property syntax

In previous versions of Godot, exported variables together with their setter and getter functions involved a long line of cumbersome code and disjointed functions. The new properties feature allows for keeping together a variable declaration together with its setter and getter functions in a cohesive block.

There are now a couple of neater ways to do this. The first is similar to the old way where we call functions for our getter and setter.

var my_var: set = _setter, get = _getter

func _setter(new_value):
	my_var = new_value


func _getter():
	return my_var

The second way uses properties.

var _score: int
var score: int:
    get:
        return _score
    set(value: int):
        _score = value
        _update_score_display()

Or more simply:

var score: int:
    get:
        return score
    set(value: int):
        score = value
        update_score_display()

In the above examples, local vars and functions are implied by an underscore prefix, variable types are specified, and functions are no longer needed to set and get the var. Also, it is easy to leave out the get or set sections in a clean manner (I think).

Setters and Getters are useful for when we want to process input values to say restrict the data to a range, validate user input, or update UI etc. rather than simply setting a var or returning its value directly.

Now, the export keyword has annotations available with the simplest being @export. In this case it infers the variable type without needing to pass the type in as a parameter.

@export var my_number: int

There are various other annotations available too (read the docs: GDScript exports), such as for range (@export_range) allowing for code completion and hints for their arguments.

Await keyword

The yield keyword is removed in favor of await to have a more meaningful name as well as other features.

Simple syntax for waiting for a button press.

await $Button.pressed

Here we make use of a first-class signal function rather than before having to specify the object reference and the name of a signal with yield.

Documentation for await.

Super keyword

Instead of prefixing a function call with a dot (.) to call its parent (super class function), we now have the super keyword. Used on its own, it calls the function that it is extending i.e. the constructor of the class that it is extending. Or, we may access a method on the parent class by referencing the function name.

extends ClassA

class_name ClassB

func _ready():
    super() # call ClassA._ready() function
    super.funky("hello") # call ClassA.funky()

Typed arrays

Typed arrays provide type safety checks for data at run time and some optimization given that the engine knows the types of the array elements.

Also, when iterating over a typed array of basic types (Vector2 for example), the variable will have a known type so should invoke code completion in the editor for this variable rather than needing to type cast it to get this benefit (need to confirm).

var my_ints: Array[int] = range(1, 11)
for i in my_ints:
    prints(i, type_of(i))

And, we may infer the type.

var inferred_type_array := [1.0, 2.0, 3.0] # This is of type Array[float]

Conclusion

It looks like GDScript 2.0 brings us many useful improvements.

This is an overview of how I perceive many of the changes to GDScript with Godot 4. And hopefully it adds to your understanding. I will likely improve this page as my own knowledge and experience about the topic improves.

More Articles