Skip to content

C sharp in Godot

  • Follow these instructions and don’t forget to enable auto-reload:
    • Pasted image 20231227101015.png
  • It may be a good idea to change GodotEditor Settings…Project ManagerDirectory Naming Convention to be “PascalCase” since that’s typically what you want for C# directories. Script names will already be in PascalCase, but file names won’t be for some reason.
  • To set up debugging, follow these instructions:
    • NOTE (Fri 03/08/2024): perhaps this whole process is simplified with this plugin.
      • Also, I think you can just run “.NET: Generate Assets for Build and Debug” from VSCode to have it create launch.json and tasks.json for you.
        • By default, launch.json won’t disable module-load logging, which is noisy on start-up. I suggest disabling it by adding this to your configurations (reference): "logging": { "moduleLoad": false },
        • There’s another spammy message at the beginning that says “You may only use the Microsoft .NET Core Debugger (vsdbg) […]“. You can use the integrated terminal to remove that (reference) or you can simply type !vsdbg into the filter in VSCode’s “Debug Console” to remove that message.
          • Note: build failures will go into the “Terminal” tab in VSCode
    • Make sure you put those JSON files in a .vscode folder
    • If it says it can’t find the program (which refers to Godot), then make sure you set an environment variable in your shell file and that you reloaded it.
    • If you get an issue like No loader found for resource: res://NameOfScript.cs , it could be that the environment variable you set was for Godot rather than Godot_mono. Godot_mono is needed to be able to interpret C#.
  • To install packages with NuGet, you just need to run something like dotnet add package TwitchLib from the directory with your .csproj file.
  • To use secrets for your project (e.g. for any API/chat connections), do the following from your project directory:
    Terminal window
    dotnet user-secrets init
    dotnet user-secrets set fake-password hunter2
    • This puts your secret into ls ~/.microsoft/usersecrets/ with the UUID that printed from the init command.
    • To read the secrets, you’ll need to run dotnet add package Microsoft.Extensions.Configuration.UserSecrets and then do something like this from the code:
    using Microsoft.Extensions.Configuration;
    var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
    string username = config["username"];
    string password = config["password"];
  • For hot reloading from VSCode on macOS, simply enable csharp.experimental.debug.hotReload (which comes from C# Dev Kit) and make sure you have a .NET SDK > 8.0.100 (e.g. 8.0.303 works). Then, debug through VSCode and it’ll reload changes automatically.
  • See my C# notes for any general C# knowledge.
  • Predefined colors exist in Colors (plural), not Color, e.g. Colors.White.
  • abstract classes don’t play nice with Godot classes (reference). You may get an error about a script not loading.
  • Prefer the C# collections where you can since they don’t require marshaling to C++ like the Godot collections do. See this for more information.
  • Godot’s signals don’t always automatically disconnect (e.g. if you’re using a lambda that captures a variable) (reference). For those, you still need to -= the lambda which you save as a member variable.
    • Plus, in VSCode, F12 doesn’t work how you would expect on a Godot signal, so overall, I don’t think they’re really worth using unless you need interoperability with other languages (like GDScript)
    • There’s more information in this SO post, and there’s also this Godot proposal to add weak events to C#. In general, if the subscriber (i.e. the thing calling +=) outlives the publisher (i.e. the thing calling someEvent.Invoke()), then you don’t have to do anything.
  • Rather than use System.Timers.Timer, which will require deferred calls due to threading, just use a Godot timer (AddChild(new Timer())).
    • If you don’t want to add a Timer to the scene tree, use this code (await ToSignal(GetTree().CreateTimer(1.0f), SceneTreeTimer.SignalName.Timeout);)
  • There is no way to Instantiate a scene and call the constructor of the script at the same time (reference). IMO, the next best thing is probably something like this to set private variables in a static factory method:
    public partial class Character : Sprite2D
    {
    // Prevent construction of Character instances outside of the CreateCharacter method
    private Character() { }
    // Private setter means only this class can set the Town
    public Town Town { get; private set; } = null!;
    public static Character CreateCharacter(Town town)
    {
    PackedScene scene = ResourceLoader.Load<PackedScene>("res://Character.tscn");
    Character c = scene.Instantiate<Character>();
    // Change anything here
    c.Town = town;
    return c;
    }
    }
  • Install dotnet-trace: dotnet tool install --global dotnet-trace
  • I suggest creating a folder to store the output of dotnet-trace: mkdir profiling
  • ⚠️ Build your game first! dotnet-trace doesn’t automatically build.
  • dotnet-trace collect --format Chromium -o profiling/Skeleseller.nettrace -- "/Applications/Godot_mono.app/Contents/MacOS/Godot" --path ./Game
    • Then, do whatever you want to do in your game.
    • ⚠️ When finished, press enter or ctrl+C in your terminal where you started dotnet-trace. If you don’t do this (e.g. if you exit through the game itself rather than through the terminal), you won’t have method information, so it won’t be very helpful at all (reference).
    • This’ll output a .chromium.json file.
    • In Chrome, press F12 and go to the “Performance” tab.
    • Click the ”↑“-lookin’ button (it’s “Load profile…”) and choose the .chromium.json file.

Unlike GDScript, the C# version of Godot doesn’t have a preload function for scenes (reference). Instead, you just have Load. When you do this, the documentation (e.g. here) makes you think that you’re caching scenes in a reliable way, but that’s not actually what’s happening (reference). Instead, I tested this out by having a character instantiate Fireball.tscn every 3 seconds, and some of the time, it wouldn’t remain cached even though only 3 seconds had passed.

To work around this, you’ll need to keep a reference to the scene in memory, that way the scene won’t get unloaded. It’s as simple as something like this:

public static class SceneLoader {
public static Dictionary<string, PackedScene> CachedScenes = new();
public static T CreateInstanceFromScene<T>(string path) where T : Node {
if (CachedScenes.ContainsKey(path) == false) {
CachedScenes[path] = ResourceLoader.Load<PackedScene>(path, null, ResourceLoader.CacheMode.IgnoreDeep);
}
return CachedScenes[path].Instantiate<T>();
}
}

If you don’t do something like this, it seems like you can hit a very peculiar issue—a scene will sometimes not load at all and fails with ERROR: Scene instance is missing. Then, attempting to use the scene will cause another error, e.g.:

YourNode yourNode = ResourceLoader.Load<PackedScene>(scenePath).Instantiate<YourNode>(); // This fails with "Scene instance is missing"
yourNode.SomeSignal += SomeListener; // This fails with "System.NullReferenceException: Object reference not set to an instance of an object"
  • Calling a function during a tween:
tween.TweenCallback(
Callable.From(() =>
{
// code goes here
})
);
// Note: Callable.From is generic; you can use Callable.From<float> if the function takes a float any

Dispose is built-in to C# and is required by any IDisposable. It is called when an object is garbage-collected. All Godot objects are IDisposables, which means they will have their Dispose functions called.

  • Make sure to unsubscribe from any listeners.
    • Keep in mind that some of your member variables may be null even if it seems like they shouldn’t be. E.g. I ran into this scenario:
    public partial class Battler : Node2D
    {
    private HealthComponent _healthComponent = null!;
    public override void _Ready()
    {
    _healthComponent = GetHealthComponent();
    _healthComponent.OnHealthChanged += OnHealthChanged;
    }
    protected override void Dispose(bool disposing)
    {
    _healthComponent.OnHealthChanged -= OnHealthChanged;
    base.Dispose(disposing);
    }
    }
    • It looks like _healthComponent should never be null, but it will be if you never add this Battler to the scene tree (which is the only time _Ready would be called). The fix in this case is to add a null check in Dispose and only remove the listener if it was actually set in the first place.

There are a hundred possible reasons for this. In my particular case, it came down to a casing issue in a .sln file (reference).

If you click in the VSCode gutter while running and get a gray breakpoint, then it’s possible you’re hitting an issue where Godot has the wrong assembly_name in project.godot. For example, I had this folder structure:

  • Skeleseller
    • Game
      • project.godot

…and originally, project.godot had project/assembly_name="Skeleseller". When I changed it to Game, it worked.

Perhaps your assembly name doesn’t match your solution/project names (reference)?

I filed a bug on this here.