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.
    • 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;
    }
    }

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

VSCode doesn’t show errors/warnings

Section titled VSCode doesn’t show errors/warnings

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

Can’t set breakpoints while debugging

Section titled Can’t set breakpoints while debugging

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)?

Dispose being called on base scenes

Section titled Dispose being called on base scenes

I filed a bug on this here.