To test out development in Godot and in particular GDScript I am going to do a few days in Advent of Code in a year I haven’t done yet. The goal is to get a feel for the language, and experiment with some command line centric workflows I know and love from my time with bevy. I’ve got some bevy-fatigue and maybe this is the time to explore other options.

Why godot

It has almost opposite goals than bevy. Not performance centric, no ECS, scripting, finished workflows.

I’ve been running into some roadblocks with bevy when I had to deal with less ergonomic parts of the engine. UI, spawning scenes and I think in general some friction when I rather want to experiment than work in Rust’s straitjacket.

Goals

  1. How much can I work from my code editor vs the godot editor?
  2. How mature is the UI stuff?
  3. GDScript, how much will I miss rust?
  4. How do shaders / gpu programming work in Godot?
  5. How feasible is the godot-rust escape hatch?

Running from the command line

To work from my editor (#kakoune) I would want to be able to run godot scripts from the command line.

godot --headless -s /path/to/script.gd

This generally results in errors: “Can’t load the script “/path/to/script.gd” as it doesn’t inherit from SceneTree or MainLoop”

An example of a file that does work:

#/path/to/script.gd
extends SceneTree
 
func _init():
    print("hello world")
    quit()
 

This means the actual code you want to develop should live in a file you can include.

#/actual-game-code.gd
extends Object # otherwise the LSP will complain
 
class WireSegment:
    var dir: SegmentDirection
    var length: int
 
    func _init(new_dir: SegmentDirection, new_length: int):
        dir = new_dir
        length = new_length
 
 
static func dir_vector(dir: SegmentDirection) -> Vector2:
    return Vector2.ZERO
 
# Example usage
#/file-to-run-from-cli.gd
const Wire = preload("res://actual-game-code.gd")
 
var dir = Wire.dir_vector(SegmentDirection.Up)

Testing

I added the Godot Unit Test addon to my project and it worked fine from the cli.

godot -d -s --path "$PWD" addons/gut/gut_cmdln.gd --headless

Added a small config:

{
  "dirs": ["res://"],
  "should_exit": true,
  "disable_colors": true,
  "prefix": "test_"
}

Every file will immediately be picked up if it starts with test_.

Kakoune integration

I’m using kakoune and found it really extensible for tools that work well from the cli. I’m not very happy with the output of those cli commands, it’s not a very structured way.

External editor

In godot you want to set your editor to external and add a shell script to open a file in kakoune.

#!/bin/bash
 
FILE_PATH="$1"
LINE="$2"
COLUMN="$3"
 
 
echo "eval -try-client %opt{jumpclient} %{edit ${FILE_PATH} ${LINE} ${COLUMN}}" | kak -p godot

The above shell script will try to open a file in a kakoune session with the name “godot”. This means you need to have it running somewhere kak -s godot file.gd. The jumpclient refers to the name of the client, a kakoune session can have multiple clients and jumpclient is generally used to view code.

Neat!

LSP

Kakoune while great when working with cli tools, has a more dificult time integrating with tools that require tighter integration. Kakoune-lsp is a very impressive project that works great, but can be finicky about config. Got it to work after dozens of threads about vim integration with the godot lsp.

hook -group lsp-filetype-gdscript global BufSetOption filetype=gdscript %{
    set-option buffer lsp_servers %{
        [gdscript]
        filetypes = ["gd", "gdscript"]
        root_globs = [".import", "project.godot"]
        command = "nc"
        args = ["localhost", "6008"]
        offset_encoding = "utf-16"
    }
}

The trick was in using nc instead of starting a godot instance from the command line. It does require the editor to be open, but for now this will do.

I did need to make a small change to the godot source and compile myself to get it fully working. Autocomplete removes text when you select an option with Godot 4.3, this PR fixes that for me: https://github.com/godotengine/godot/pull/96725/files. I cherry picked the commit and build, and it works!

Running files

The above CLI commands are combined with running the code from kakoune using compilation-mode.md. eg.

define-command godot-run-test -docstring 'Run godot tests in current file' %{
    evaluate-commands %sh{
        rootdir="$(git rev-parse --show-toplevel)"
        absolute_path="$kak_buffile"
        relative_path="${absolute_path#"$rootdir/"}"
        echo "run-in-fifo ' godot -d -s --path "$rootdir" addons/gut/gut_cmdln.gd -gtest=res://$relative_path --headless -gprefix="XX"' godot"
    } 
}

Parse the current file and get the relative path from the git root, and prefix it with res:// and see the result in the toolsclient right from the editor! Turns out you can actually run scenes from kakoune as well.

define-command \
  -docstring "Run Godot scene file based on the current buffer file name" \
  -params 0 \
  godot-current %{ evaluate-commands %sh{
    rootdir="$(git rev-parse --show-toplevel)"
    absolute_path="$kak_buffile"
    relative_path="${absolute_path#"$rootdir/"}"
    echo "run-in-fifo 'godot $relative_path' godot"

  }
}

Open my-scene.tscn in kakoune and run :godot-current and it will run the Godot Scene in a Player window.

Auto formatting

Indentation sensitive code is never a good idea in my book, but I’m giving it a fair chance. Running an auto format command from you editor helps, as unfortunately the LSP doesn’t come with one.

To install it:

# It's a good idea to not install it globally
mkdir ~/.venv
python3 -m godot ~/.venv
python3 -m venv ~/.venv/godot 
source ~/.venv/godot/bin/activate
pip install "gdtoolkit==4.*"
which gdformat
# echo's something like /Users/me/.venv/godot/bin/gdformat

You need to get that executable on your path and bind it to a kakoune command:

define-command godot-run-format -docstring 'Run gdformat' %{
    nop %sh{
        ~/.venv/godot/bin/gdformat --use-spaces=4 $kak_buffile
    }
    # needed to reload the file as gdformat writes to disk
    edit!
}