C sharp in Godot
- 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.- Note: build failures will go into the “Terminal” tab in VSCode
- 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.
- 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;}}
Profiling C# code in Godot
Section titled Profiling C# code in Godot- 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.- If you want, you can do
--format Speedscope
instead and then load the resulting file with https://www.speedscope.app/.
- If you want, you can do
- 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
Writing a good Dispose
function
Section titled Writing a good Dispose functionDispose
is built-in to C# and is required by any IDisposable
. It is called when an object is garbage-collected. All Godot objects are IDisposable
s, 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 benull
, but it will be if you never add thisBattler
to the scene tree (which is the only time_Ready
would be called). The fix in this case is to add anull
check inDispose
and only remove the listener if it was actually set in the first place.
- Keep in mind that some of your member variables may be
VSCode 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.