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
print(addone(2)) # 3
var plus_one = func(n): return n + 1
var x = plus_one
print(x(3)) # 4
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_counter
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_counter
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()
func _update_score_display():
pass # Do something to update the displayed score
Or more simply:
var score: int:
get:
return score
set(value: int):
score = value
update_score_display()
func update_score_display():
pass # Do something to update the displayed score
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.