Part 4: Godot with SwiftUI


At my talk in November, I talked about my work on the Swift bindings for Godot - allowing Swift developers to write Godot games using Swift and accessing the API that Godot exposes to developers. I also proposed rewriting chunks of Godot's own Game Editor using Swift. I have a branch tracking this work, but that will be the subject of another post.

My current effort is slightly different: how to build a native iPadOS (and hopefully VisionOS) experience for Godot. So rather than rewriting the existing Editor codebase with Swift, this effort is about making a SwiftUI on top of the existing Editor.

A simple chart shows the difference. On the left is the current way Godot works; in the middle is my proposal for the long-term evolution of Godot, as proposed in my talk in November; and on the right is what I am currently working on.

My Swift code drives Godot entirely using the SwiftGodot API bridge.

While I would love to deliver a complete SwiftUI-based experience for Godot, this would take too long to roll out. So, I decided to go for a gradual approach, porting high-traffic components of Godot to SwiftUI while keeping some parts using the Godot user experience.

This way, I can ship something that people can use early and hopefully get into a virtuous cycle of collecting feedback and improving those parts of the experience.

Today, I want to discuss how I am approaching this problem.

Xcode provides a smooth platform to iterate quickly on your user interface with the SwiftUI Previews. You code your user interface on a text editor, and a live preview shows your changes on the right - and you can even interact with it. It is just lovely.

Xcode's SwiftUI preview will update the UI live as you type the code on the left.

One problem I am facing with Godot on iPad is that Godot's build and launch times can take a long time. These broke my flow, prevented me from using the SwiftUI Canvas, and did not bring joy into my day.

To solve this problem, I built a standalone SwiftUI project with the user interface components and a data model that the Godot code base can provide. I call this "XogotUI."

XogotUI is a pure Swift/SwiftUI library containing only user interface elements and the required data models. This library has zero external dependencies to ensure that my build times remain short and that I can quickly prototype ideas using the Xcode Canvas.

The following diagram shows how the code is thus structured:

XogotUI is pure SwiftUI

Idioms

The Godot Editor relies on extensive introspection of your Godot project. Godot has a rich object model paired with an API to look inside and modify those objects entirely. Luckily, almost all the capabilities I need to build an editor have surfaced in Swift via my SwiftGodot bindings. Luckily for the community of SwiftGodot users, the more I work on Godot for iPad, the better the bindings get. And also luckily for me, SwiftGodot has attracted a community of lovely contributors that also make my job a lot easier.

While all this neat data about Godot exists in SwiftGodot, XogotUI can not consume it, as it is a significant dependency. Those of you who know me know that I detest gratuitous abstractions. I avoid them like the plague. But given the above constraints, I made an exception in this case.

The Data Model

For every component I port to SwiftUI, I first need to understand what Godot is doing behind the scenes and what the user experience needs to be. The first step is to define the data model for what will be rendered; it works like this:

  • I define the data model that the SwiftUI views can be built with. It exposes all the information required to render a particular control. Many of these objects are Swift's `@Observable,` allowing me to update the data and have SwiftUI update the contents dynamically.
  • In XogotUI: Fill out the data model with fake data. I use this to iterate on the user interface quickly.
  • In Xogot: Fill the data model with data extracted from the Godot Objects.

Calling Back

I needed a callback system to act on user actions or gather additional information from the user.   

Two idioms have emerged: one where I post notifications to the higher level, and that higher level responds by altering the data model. I got a lot of mileage from providing a single entry point that takes an enumeration with associated values with the operation. It looks like this:

It is a "fire and forget" approach, as my code did not need to wait for any replies. I wrote a lot of code this way, which was great.   

The Godot Object Inspector required a different idiom. I needed to query information dynamically and get results back immediately. I am using the Delegate pattern to define a public contract for the callbacks. It is slightly more cumbersome to use but allows me to get responses back:

Property Pad

The property pad was an interesting problem. To work with this model, I needed a data model for all possible data properties in Godot. Godot defines a PropertyInfo that describes the name and the type of a property.

But the type is peculiar. Half of it is made out of the actual storage for the object (a string, a vector, an object, a dictionary - Godot calls this the Variant type), and the other half is semantic information about how to represent the data type in the UI. You could have a "Background Image" property stored as a "string," with a semantic payload that describes it as "This should be edited as a file picker that tends to have PNG or JPG extensions."

My data model is just an enum with the semantic representations for these items. This is what they look like:

The properties are rendered using SwiftUI with a switch statement that looks like this:

It turns every semantic value into a SwiftUI view.

As you can see, each view gets limited information: the label to display the information and the property name to modify. To work with the rest of the system, they need to access and alter the object's contents. Once again, I used a public API contract to achieve this. Still, the critical bit is that they grab both the data storage and their callbacks using the SwiftUI environment, and this helps me avoid passing too much data around. Instead, I inject those values into the environment a few layers up, and the values become accessible to each property editor.

The following code snippet shows the entire Boolean property editor:

import Foundation
import SwiftUI

public struct BoolEditor: View {
    @Environment(ValueStore.self) private var store
    let label: String
    let propName: String
    
    public var body: some View {
        Toggle(label,
               isOn: Binding<Bool> (
                get: { store.fetch (propName) },
                set: { newV in store.set (propName, newV) }))
    }
}

Short and simple - and that is all there is to it.

Here is where things get fascinating. In Godot, objects can be modified externally (the user can change the object's position on the screen, for example, which would change the value of the "position" property). Or the user might have pressed the undo key.

I am using Swift's new '@Observable' macro on the ValueStore. When SwiftUI renders the view, it records the usage of `store`. So, when an external event modifies the object's contents, the view is automatically updated with the changes.

In Conclusion

This post described the high-level overview of how I am extending Godot to get a native iPadOS UI using SwiftUI, and what I am doing to speed up my development. At first, the separation and duplication of work seemed like a lot, but the quick iteration times have paid off. I can tweak the UI with gusto and iterate quickly on polishing the edges and pay a minimal integration price.

Subscribe to La Terminal Blog

Sign up now to be the first to know.
Guenter Gibbon
Subscribe