C sharp in Godot
Setting up
Section titled Setting up- Follow these instructions and don’t forget to enable auto-reload:
- It may be a good idea to change Godot → Editor Settings… → Project Manager → Directory 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
andtasks.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.
- By default,
- Also, I think you can just run “.NET: Generate Assets for Build and Debug” from VSCode to have it create
- 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#.
- NOTE (Fri 03/08/2024): perhaps this whole process is simplified with this plugin.
- 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 initdotnet user-secrets set fake-password hunter2- This puts your secret into
ls ~/.microsoft/usersecrets/
with the UUID that printed from theinit
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"]; - This puts your secret into
- 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.
Basics
Section titled Basics- See my C# notes for any general C# knowledge.
- Predefined colors exist in
Colors
(plural), notColor
, 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 callingsomeEvent.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);
)
- If you don’t want to add a
- 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 methodprivate Character() { }// Private setter means only this class can set the Townpublic 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 herec.Town = town;return c;}}
Loading scenes
Section titled Loading scenesUnlike 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"
Tweens in C#
Section titled Tweens in C#- 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
Troubleshooting
Section titled TroubleshootingVSCode doesn’t show errors/warnings
Section titled VSCode doesn’t show errors/warningsThere 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 debuggingIf 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.
An exported build doesn’t work
Section titled An exported build doesn’t workPerhaps your assembly name doesn’t match your solution/project names (reference)?
Dispose
being called on base scenes
Section titled Dispose being called on base scenesI filed a bug on this here.