Godot 4: Script with F#

Date: 2023-04-30 | create | ctech | fsharp | godot |

If you are running Godot 3, refer to the Godot 3 guide instead.

Overview

F# continues to be my favorite programming language. Now that I'm focusing fulltime on being a Technologist / Tinypreneur, I've been playing around with F# and creative coding - which means diving back into Godot.

In this post we'll walkthrough setting up a Godot 4 project that can run scripts in both C# and F#.

Requirements

  • Godot 4 (C# / .NET Version) - This version allows us to build / run C# / .NET. We will leverage the .NET support to run F#.
  • .NET SDK - Since we'll be building / running .NET, we need the .NET SDK installed. This will also give us access to the dotnet command which will help us throughout the process.

Create a Godot Project

Access the full Godot 4 + F# example project on GitHub - available to HAMINIONs supporters.

The first thing we'll do is create a new Godot project that is .NET compatible.

  • Open Godot
  • Create a new Godot Project
  • Add a C# script to the project
    • To do this: Inspector > File Icon (Create a new Resource...) > CSharpScript

Once we add a C# script, Godot should do some processing and add a .csproj to the project. To verify this - open your project folder in your file explorer (the .csproj will not show up in the Godot editor). You should see a .csproj file.

Now that we have the .csproj, our project should have C# scripting support. We can verify this by creating an empty node in our project and attaching a simple script that will print to the Godot console when we build / run the game.

SimplePrintCs.cs

using Godot;
using System;

public partial class SimplePrintCs : Node
{
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
		GD.Print("SimplePrintCs: C# Running...");
	}
}

Note: In Godot 4, all C# classes connected to Godot must be a partial so this will be different than Godot 3.

To connect the C# script to Godot:

  • Create a Scene (if you don't already have one)
  • Create a Node (look for Node or Node2D)
  • Create a C# Script
  • Drag the new C# script onto the new Node to attach it

Now when we run our Godot project, we should be able to see our output:

SimplePrintCs: C# Running...

Enable F# in Godot

Access the full Godot 4 + F# example project on GitHub - available to HAMINIONs supporters.

Next, we need to create an F# project and connect it to our C# project.

First create an F# library project:

  • Create a new folder in your project (like ScriptsFs)
  • Navigate to that directory (like cd ScriptsFs)
  • Create a new F# library project using the dotnet CLI:
    • dotnet new classlib -lang "F#"

This should output a minimal F# project complete with an .fsproj and .fs script.

Now that we have both a C# project and an F# project, we need to make sure that they are compatible with each other and the Godot engine.

Open both the .csproj and .fsproj and modify the .fsproj so that it's compatible with .csproj. Here are common / important fields to check:

  • Project Sdk - These must target the same Godot sdk. At time of writing, my .csproj SDK is Godot.NET.Sdk/4.0.2 but use whatever's in your .csproj
  • TargetFramework - This configures the target .NET version to build against so these must match. At time of writing, my .csproj says net6.0 but use whatever's in your .csproj

Our C# and F# projects should now be compatible but we need a way to connect them so they can talk to each other. We can change this by adding a reference to the .fsproj to our .csproj.

This will allow our C# code to call into our F# code.

Add a reference from our C# project to our F# project:

  • Navigate to the root of your Godot project (where your .csproj is located)
  • Run dotnet add ./CSPROJ_NAME.csproj reference ./ScriptsFs/FSPROJ_NAME.fsproj
    • Note: Replace CSPROJ_NAME and FSPROJ_NAME with the actual file names.

This should have modified our .csproj to include a reference to our .fsproj. We should now be able to call our F# code from our C# code.

After modifying my .csproj / .fsproj here's what they look like:

ScriptsFs.fsproj

<Project Sdk="Godot.NET.Sdk/4.0.2">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Library.fs" />
    <Compile Include="SimplePrintFs.fs" />
  </ItemGroup>

</Project>

Godot4Fsharp.csproj

<Project Sdk="Godot.NET.Sdk/4.0.2">
  <ItemGroup>
    <ProjectReference Include="ScriptsFs\ScriptsFs.fsproj" />
  </ItemGroup>
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
  </PropertyGroup>
</Project>

F# Scripts in Godot

Access the full Godot 4 + F# example project on GitHub - available to HAMINIONs supporters.

At this point, Godot knows about our C# project and our C# project knows about our F# project.

Godot -> C# -> F#

However, Godot does not know about our F# project nor does our F# project know about our C# project! For now, that's a limitation we're going to have to live with.

So how do we actually build stuff with this?

The answer is - it's a bit clunky. Basically we'll need a C# script attached to Godot which will then call into our F# project to make it function.

The paradigm looks something like this:

  • Godot Scene - Contains nodes with attached C# script so that it runs with the game
  • C# Script: FSharpCaller.cs -> References the F# project and calls it directly
  • F# Script: Actual Code to be run

There are a few ways I've found to connect these so we'll walk through each and discuss their respective pros / cons. For each method, I'll also provide some sample code which simply prints to Godot's console so we can see how things are called.

Godot 4 Breaking Change: Previously in Godot 3, we were able to execute our F# by simply creating a placeholder C# class that inherits from an F# class. Godot 4 introduced a breaking change where all C# classes must be partials which breaks this integration. In order to execute F# in Godot 4, we must now directly call our F# code from C#.

Option 1: Fake Inheritance

SimplePrintFsInheritanceHolder.cs

using ScriptsFs; 

public partial class SimplePrintFsHolder : SimplePrintFs
{
	public override void _Ready()
	{
		GD.Print("SimplePrintFsHolder: C# Running...");
    // Ham: Without this base call, F# never gets run!
		base._Ready();
	}
}

SimplePrintFsInheritance.fs

namespace ScriptsFs

open Godot

type SimplePrintFsInheritance() =
    inherit Node()

    override this._Ready() =
        GD.Print($"{nameof SimplePrintFsInheritance}: F# Running...")

Option 1: Fake Inheritance - is the most similar to Godot 3. We inherit the F# class in our C# holder in order to connect Godot and F#. The problem is that this will no longer call the F# lifecycle functions (like _Ready()) via inheritance! Instead we have to directly call the F# base class in order for it to execute.

This downgrades something kinda clunky (Godot - C# - F#) to something really clunky. This is mostly because we are still sticking with an inheritance pattern even though it's no longer working for us.

This brings us to Option 2 which tries to be a bit more pragmatic given the new constraints in Godot 4.

[Recommended] Option 2: F# Library

SimplePrintFsLibraryHolder.cs

using Godot;

using ScriptsFs; 

public partial class SimplePrintFsLibraryHolder : Node
{
	public override void _Ready()
	{
		GD.Print($"{nameof(SimplePrintFsLibraryHolder)}: C# Running...");

		SimplePrintFsLibrary.logRunning();
	}
}

SimplePrintFsLibrary.fs

namespace ScriptsFs

open Godot

module SimplePrintFsLibrary =
    // Ham: There is no way to cleanly get current module name, so make hard-coded configuration
    let currentModuleName = "SimplePrintFsLibrary"

    let logRunning = fun() ->
        GD.Print($"{currentModuleName}: F# Running...") 

In Option 2: F# Library we are removing C#'s inheritance from F# and instead treating our F# code more like a library. In many ways this is a better option than 1 as it's simpler and more transparent about what's actually going on here.

If you squint a bit - this look a lot like DDD / Clean Code but in a Godot game environment. We treat the C# code as our Presentation layer. Then our F# is the core App / Domain logic.

We translate Godot game signals from C# -> F# and update our Godot game state based on F# -> C# returns.

This is the option I'll be using as I think there's a cleaner path to using idiomatic F# for fun and profit in our Godot project.

Next Steps

Godot 3 didn't provide the most pleasant F# experience and Godot 4 broke the workaround enough for me to stop recommending it. However these new constraints may have unlocked an even cleaner, better paradigm for Godot to F# interop and I'm excited to see where it goes.

I'll be experimenting with different paradigms in the coming months so make sure to follow / sub for updates.

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.