My final GDScript rant

/home/blog/my-final-gdscript-rant


Some of you may know that I harbor rather strong feelings toward the GDScript language. After using Rust for a few projects, I've come to know the importance of strong static typing and to appreciate the compile-time guarantees it can bring.

It tends to make me kind of obnoxious to people who discuss the language casually, and to recommend beginners not to use the language and to choose C# instead.

Finally, before I vent about my pain points with the language, I would like to say that these are merely my personal frustrations and I am in no way blaming the GDScript team or any Godot maintainer for any of this.

Traits/interfaces

I think this feature is the most common and spread pain point with GDScript. Traits are a language construct that describes shared behavior between classes and provide a sort of multiple inheritance.

Here's an example in Rust:

struct Book {
    title: String,
    author: String,
    isbn: u64,
}

struct Person {
    name: String,
    last_name: String,
    age: u32,
}

trait Describe {
    fn describe(&self);
}

impl Describe for Book {
    fn describe(&self) {
        println!(
            "This book is called {} and was written by {}. Its ISBN is {}.",
            self.title, self.author, self.isbn
        );
    }
}

impl Describe for Person {
    fn describe(&self) {
        let year = if self.age < 2 { "year" } else { "years" };
        println!(
            "This person is called {} {} and is {} {year} old.",
            self.name, self.last_name, self.age
        );
    }
}

In GDScript, you have no clean way to ensure a class a method (except if your method is from a parent class, but in our example it wouldn't be possible).

GDScript's Object class has has_method, which seems like it would help, but you're checking a method's existence using a StringName, so things can and will break when refactoring your code, and it requires putting ugly guards everywhere:

var book: Book = ...
var john_doe: Person = ...

if book.has_method(&"describe"):
	book.describe()
if john_doe.has_method(&"describe"):
	john_doe.describe()

Or you can get rid of the guards because GDScript is duck-typed, and ensuring the object has the method falls back on the programmer, and static analysis won't help you.

# Maybe these objects don't actually have the method!
# This will create a runtime crash.
book.describe()
john_doe.describe()

This feature is currently listed on Godot's priorities page.

Generics

Generics (or type parameters) let classes or functions have members or arguments whose type is determined later. They can have upper bounds so you can ensure that they are children of a class or implement a trait.

This issue is not that critical because it's usually not an issue at all, since GDScript has inheritance. It is still a problem in some specific cases. Take for example this function from a class called Interactable, which is part of my implementation of components in GDScript for Godot Dash:

var components: Array[Component]

func query(component_type: Script) -> Component:
	var component_idx: int = components.find_custom(
		func(component: Component) -> bool:
			return component.get_script() == component_type
	)
	return components[component_idx] if component_idx >= 0 else null

This function checks if a component is present on an Interactable and returns it if it exists. Right now, all it knows about the component is that it inherits Component, even if I checked its script (so I know it inherits a child type).

If GDScript had generics, the function would look like this:

func query[T: Component]() -> T:
	var component_idx: int = components.find_custom(
		func(component: Component) -> bool:
			return component is T
	)
	return components[component_idx] if component_idx >= 0 else null

It would look for a component with a type specified at the call site (called T in the function), and the function guarantees that it returns something of type T.

Calling the function would look like this:

var easing := interactable.query[EasingComponent]()

It would also solve this common pain point I have where methods of a typed array like Array.filter returns an untyped array!

This missing feature looks hard to implement in Godot at the moment.

The billion-dollar mistake

This one is present in most programming languages with classes: variables can be null, and the language doesn't force the programmer to check if the variable is non-null before calling any methods on it.

Rust's Option<T> is one solution but I don't think it fits the syntax of GDScript.

Something like Swift would work better: types are not nullable by default, nullability is opt-in by adding a question mark after the type name, e.g. Node2D?.

Then, when trying to call methods or to index a collection type, you add a null guard with the ? operator, or you can create a scope where the variable is ensured to be non-null.

// Code taken from the Swift docs
if let starPath = imagePaths["star"] {
    print("The star image is at '\(starPath)'")
} else {
    print("Couldn't find the star image")
}
// Prints "The star image is at '/glyphs/star.png'"

if imagePaths["star"]?.hasSuffix(".png") == true {
    print("The star image is in PNG format")
}

Swift also has a "null-coalescing operator" (??) that can provide a fallback value in variable assignment expressions if the value of the first expression before the operator is null.

let defaultImagePath = "/images/default.png"
let heartPath = imagePaths["heart"] ?? defaultImagePath
print(heartPath)
// Prints "/images/default.png"

This would greatly help in GDScript, especially when working with Node references:

var timer: Timer = get_node_or_null(^"Timer")
timer.start() # Could be null.

I tried doing a workaround with match expression and variable binding but it didn't seem to work:

match may_return_null():
    null:
        fail_properly()
    var success:
        do_something_with(success)

Structs

GDScript lacks structs, so when the engine uses structs under the hood (in C++), they are either exposed as dictionaries or as methods with indices.

Exposing structs as dicts is very annoying. The analyzer doesn't check if a field exists and doesn't know its type.

Here's an example with Object.get_property_list, which returns an array of dictionaries in GDScript. The underlying C++ type is called PropertyInfo and it is exposed properly in C#!

Tuples are also on my wishlist but my point with them is the same as structs.

Final words

There are other smaller pet peeves I have with this language, but they aren't significant enough to need their own paragraphs.

I think these issues have degraded the way I view Godot, even if it's an amazing game engine. I became aware of them roughly less than a year ago, but at that point I had already been working on Godot Dash for a while, so it couldn't possibly be worth it. I wanted to avoid another situation like Raylib Dash, where I tried another engine, made little progress and got stuck for months, so I pushed through with it. I'm reconsidering this decision now, since I feel like GDScript is eroding my sanity.