I’ve been enjoying tinkering with Godot and SwiftGodot lately. Following tutorials while translating all the GDScript parts into Swift has a nice side-effect of keeping my attention more focused, so I am not just zoning out and copying everything from the tutorial.

This week I released a code generation tool that lets me leverage Swift’s type safety when referencing objects in Godot that are usually referenced using raw strings.

Background

Godot game designers build scenes in the Godot editor. A scene is a tree of nodes with some properties that are configured in the editor. A node could be a sprite, a collision detector, or a path to follow. Nodes are built up into scenes that represent each game object, so maybe a spaceship for the player, a level map, or the heads-up-display interface.

Most scenes in a game need some behaviour; e.g. when a bullet scene collides with a rock scene, the bullet should disappear and the rock should explode. To add behaviour to a scene, the programmer must write some code.

Example

Lets say a game programmer wants to handle when the game ends; they want to make the player disappear.

func gameEnd() {
  guard let player = getNodeOrNull(
    path: "Node/PlayerSprite"
  ) as? Sprite2D else {
    print("didn't find a player node :(")
      return
  }
	
  player.hide() 
}

This code calls getNodeOrNull to get the node at path “Node/PlayerSprite” in the scene tree. It then attempts to cast it to a Sprite2D object. This is very brittle and will break if the programmer:

  1. Moves the PlayerSprite to have a different parent node in the tree
  2. Renames PlayerSprite.
  3. Mistypes the name.
  4. Changes the type of Sprite2D to something else.

It’s common to do all of these when making a game. These problems are particularly bad because they occur at run time. Since paths are just strings even our IDE can’t help us by suggesting the correct spelling of node names.

So I wrote a tool to make this better! SceneGen will generate strongly typed descriptions of all the nodes in each scene, so now, you can do:

func gameEnd() {
  node(.playerSprite).hide() 
}

Here .playerSprite is a reference to a generated struct that knows the path to "Node/PlayerSprite" and knows that it is a Sprite2D type object, so there’s no longer a need to supply these at the call-site.

The node() function is scoped tightly to only the nodes on the scene you’re working on so IDE suggestions will provide the exact names.

If you re-arrange, rename or change the types of any of these nodes in the editor, you’ll get compile-time errors exactly at the point in your code that needs to be fixed, instead of run time crashes / failures.

Conclusion

Having my IDE be directly aware of my scenes has been a huge improvement. I’m spending much less time debugging crashes and more time enjoying learning how to make games.