I had been making a lot of progress with my new game, Project Kuiper. I have been so much more productive with Godot than I have ever managed in any previous game development attempt. I made the unusual decision to use Kotlin, rather than GDScript or even C#, because I am so familiar and comfortable within the Kotlin ecosystem. The Godot Kotlin/JVM ecosystem is probably the most mature of all the 'alternative' language projects for Godot.

But in the last week everything has come to a crashing halt. I've discovered that I've been using one feature/programming pattern that is just not safe in Godot/Kotlin, which has lead to consistent and hard game crashes.

A Godot project comprises one or more scene files (.tscn), and each scene will be made of many Nodes. Each node may have a script attached to it, in my case written in Kotlin. I have three such scenes - Main Menu, Game Setup, and the Game itself. In the Game proper, there's a little menu containing an option to return to the Main Menu, e.g. to restart the game. When I choose that and then start a new game, the game crashes. And a hard crash, an ACCESS_VIOLATION_EXCEPTION, killing the JVM, killing the game completely.

It turns out I had been using a common godot game development pattern called an event bus singleton, a common class containing all the signals that my various Nodes use to communicate with each other. That singleton is created when the game starts, and is shared across all scenes. Individual Nodes within the main Game scene connect to those signals, for instance, to respond to a card being dropped on a location within my game map. Many of my game nodes are created dynamically (using instanced PackedScenes), so I cannot use the Godot editor's signal connection tool.

In my Kotlin SignalBus class, I create signals using the following Godot/Kotlin syntax:

@RegisterSignal("hex", "action")
val confirmAction by signal2<Hex, ActionWrapper>()

And then, say, in the ActiveActionsFan class's _onReady() method, I connect to that signal:

signalBus.confirmAction.connect { hex, actionWrapper ->
    if (actionWrapper.action == null) {
        logError("ConfirmAction received a null action $actionWrapper")
        return@connect
    }
    actionWrapper.action?.let {
        gameState.company.activateAction(hex, it)
        addOngoingAction(it)
        // update the resource panel. Company cannot do this as it doesn't have access to the signal bus
        gameState.company.resources.forEach { resource ->
            signalBus.updateResource.emit(resource.key.name, resource.value.toFloat())
        }
    }
}

This is a really neat way for the ConfirmAction Node (really, a complex packed scene in itself) to communicate to the ActiveActionsFan Node (another complex packed scene that displays a list of going actions) that an action has been confirmed. That in turns emits another signal (signalBus.updateResource) to update another Node about changes to the game state.

So what's the problem?

The problem is that when I return to the Main Menu, I am not disconnecting from the signals. When I start a new game, the ActiveActionsFan Node has been destroyed and a new one created, but the signal connection to the original is still there. When the signal is emitted (actually, when the autoloaded SignalBus class is added to the scene tree), it tries to call a method on a Node that Godot has long since deleted and freed up. But the SignalBus does not know this. This causes a crash.

I've no idea if this pattern causes a similar problem in GDScript or C#. But it's pretty fundamental to my approach to the design of my game. The lack of signal disconnection is an acknowledged problem in the Godot/JVM software system. An alternative approach has been proposed, I have tried it, but I'll need extensive refactoring of my code just to test it out. (There are 20 signals in my SignalBus so far.)

So I am feeling frustrated and disappointed. I've made so much progress, but done so with a fundamental flaw in my design based on an assumption that had been working very well, and did reflect a common practice within the Godot gamedev community. I'm worried that if I spend the time to refactor all those 20 signals (using a different mechanism to connect, and explicitly disconnecting them whenever a Node is destroyed in the _exitTree() method (not on the _onDestroy() method!)) I may not actually solve the problem. I may just be moving the problem around. I am not sure how to test this out, and I don't want to waste time on a refactor that doesn't work.

Kotlin... or GDScript, or C#?

At the back of my mind there has always been this question of "why Kotlin?". Certainly, whenever I describe my project to anyone else, they all find it bizarre that I am choosing to use Kotlin with Godot - nobody else does! Surely I'm just making it hard for myself? Yet whenever I look at the GDScript language, I cringe... and some of the concise logic I have written in Kotlin must be a pain to implement in GDScript. Maybe C# is a good half-way house? But I want to write a game, not learn a new language and a new ecosystem for software development.

Update: New approach to signals

Over the weekend I worked through different approaches to solve this problem. I decided to remove the autoload global SignalBus class, and instead create a signal Node for each scene file. I realised that all my signals were specific to particular scenes. There are now several SignalBuses, or perhaps better, SignalNodes. There are one or two packed scenes which are shared between different scenes, and so need access to different SignalNodes. I decided the primary game SignalNode would be the default, and if a packed scene needs access to another SignalNode, it would set as a nullable property on the scene.

This is working and the game no longer crashes. Crisis over!