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#.
- To install, visit the Godot Download Page.
- .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.- To install, head to the .NET Download Page. At time of writing, Godot targets .NET 6.0.
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
- To do this:
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 isGodot.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
saysnet6.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
andFSPROJ_NAME
with the actual file names.
- Note: Replace
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 partial
s 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.