C# in Godot
C#-in-Godot idioms for editing a C# project. Pair with the csharp group: setup scaffolds the solution, build compiles with structured diagnostics.
A reference for working in a C# Godot project. Idioms verified against a shipped commercial Godot C# game (decompiled), so they reflect real shipping practice — not tutorials.
Scope / fit. The
godot-mcpCLI authors GDScript (script.create/script.editwrite.gd); it does not compile C#. So this file is guidance for when you’re editing a C# Godot codebase directly (in the filesystem, building withdotnet/Godot’s build), not something you drive through the addon. Theengine.*discovery commands still help — the liveClassDBAPI is identical whether you call it from C# or GDScript, soengine class-info --class Tweenetc. remain your source of truth for current signatures. For what to build,project-structure.mdanddeckbuilder-patterns.mdare language-agnostic and apply here too.
Node classes: partial, inherit a Godot type#
Every script that Godot instantiates is a partial class deriving from a Godot node type.
partial is mandatory — Godot’s source generator emits the other half (the
MethodName/PropertyName/SignalName lookup classes, marshalling, signal glue) into a
sibling partial. Omitting it breaks generation.
using Godot;
public partial class CardView : Control // partial + Godot base type{ // ...}- File-scoped namespaces (
namespace Game.Cards;) and one public type per file. [GlobalClass]to register the type for the editor’s “create node” / inspector lists.[Tool]only for scripts that must run in the editor (editor plugins,@tool-style preview behavior) — not for normal gameplay nodes.
Reference child nodes: GetNode<T>("%Unique") in _Ready#
Cache children in _Ready() as private fields. The dominant idiom in the shipped game is
unique-name access (%Name), with plain relative paths for simple cases:
private MegaLabel _goldLabel;private TextureRect _icon;
public override void _Ready(){ _goldLabel = GetNode<MegaLabel>("%GoldLabel"); // % = scene-unique, survives reparenting _icon = GetNode<TextureRect>("Icon"); // relative path for a stable direct child}- Prefer
%Uniqueover deep"A/B/C"paths — same rule as GDScript. [Export]aNode/NodePathfield when the wiring should be done in the inspector (the.tscnstores it asnode_paths=PackedStringArray(...)).- Resolve
GetNodein_Ready, never in the constructor (the tree isn’t ready yet). - Pass non-node dependencies via an explicit
Initialize(state)method after instantiation rather than the constructor.
Signals: [Signal] for nodes, event Action for models#
Two distinct mechanisms, used deliberately:
// Node-to-node: Godot signal (shows in editor, crosses the C#/GDScript boundary)[Signal] public delegate void ReleasedEventHandler(NClickableControl button);// emit:EmitSignal(SignalName.Released, this);
// Plain C# data/model class (not a Node): use a C# eventpublic event Action<int, int> BlockChanged;public event Action Died;- Use
[Signal] delegate …EventHandlerfor Node classes and anything the editor or GDScript must see/connect. - Use plain C#
event Action<…>for non-Node model/state classes (aCreature,RunManager) — lighter, strongly typed, no marshalling. The shipped game’s data layer is almost entirely C# events; its nodes use[Signal]. - Connect built-in signals with
Connect(Control.SignalName.MouseEntered, Callable.From(OnMouseEntered))—Callable.Fromwraps a C# method with no boilerplate.
Exports#
[Export] private float _duration = 0.5f;[Export] private Godot.Collections.Array<NParticlesContainer> _particles;[Export] private Curve _particlesCurve;Inline C# defaults; use Godot collection types (Godot.Collections.Array<T>,
Dictionary<,>) for exported containers so they marshal. Custom Resource subclasses
export and edit in the inspector like built-ins.
Lifecycle and async#
_Ready— grab children, set up._EnterTree/_ExitTree— subscribe/unsubscribe external events here (subscribe in enter, unsubscribe in exit; nodes get re-parented and leak otherwise):
public override void _EnterTree() { RunManager.Instance.ActEntered += OnActEntered; }public override void _ExitTree() { RunManager.Instance.ActEntered -= OnActEntered; }- Async is
Task-based and integrates with Godot viaToSignaland tweens:
Tween t = CreateTween().SetParallel();t.TweenProperty(_banner, "modulate:a", 1f, 0.5) .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Quad);await ToSignal(t, Tween.SignalName.Finished); // await an animationawait ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); // wait one frameawait Task.Delay(ms)for plain delays (thread aCancellationTokenfor cancelable waits). Chain tween steps with.Chain(); intervals with.TweenInterval().
Data: Resource for inspector data, plain classes for logic#
- Serializable/inspector-editable data → a
Resourcesubclass with[Export]fields. - Transient runtime models / game logic → plain C# classes (not
Resource/Node), with C# events for change notification. Keep rules out of the scene tree so they’re unit-testable. (This is the model/entity/view split fromdeckbuilder-patterns.md.)
Interop: calling GDScript from C##
Some engine features ship as GDExtension/GDScript-only APIs (FMOD audio is the canonical
case). Bridge them with a GDScript autoload proxy and call it via Node.Call, caching
method names as StringName to avoid per-call allocation:
private static readonly StringName _playOneShot = new StringName("play_one_shot");private Node _audio;public override void _EnterTree() => _audio = GetNode<Node>("%AudioProxy");public void PlaySfx(string path) => _audio.Call(_playOneShot, path);Keep GDScript to these proxies, [Tool]/EditorPlugin scripts, and quick VFX-iteration
tools; keep gameplay/logic in C#. (See the GDScript-usage notes in this skill set.)
Safe-node extension helpers#
Build a small set of static extension methods on Node/Control and use them
everywhere — the shipped game leans on these heavily:
node.QueueFreeSafely(); // validity check (+ object-pool return) before freeingparent.AddChildSafely(child); // main-thread check, else CallDeferred(AddChild)control.TryGrabFocus(); // no-op if invalidvar hud = node.GetAncestorOfType<NHud>();foreach (var c in node.GetChildrenRecursive<NCard>()) { ... }They centralize the recurring guards (is-valid, on-main-thread, deferred tree mutation) that otherwise get forgotten at individual call sites.
Top idioms#
public partial class X : <GodotType>—partialis required for source generation.- Cache children in
_ReadyviaGetNode<T>("%Unique"); avoid../../paths and constructors. [Signal] delegate …EventHandlerfor nodes; plainevent Action<…>for non-node models.Connect(SignalName.X, Callable.From(Handler))— no lambda boilerplate.- Subscribe in
_EnterTree, unsubscribe in_ExitTree— prevent leaks on reparent. [Export]with inline defaults;Godot.Collections.Array<T>/Dictionaryfor exported containers.await ToSignal(tween, Tween.SignalName.Finished)/ProcessFrame;Task.Delaywith a token.Resourcesubclass for inspector data; plain C# class for logic (testable, tree-free).- Bridge GDScript-only APIs via an autoload proxy +
Node.Call(StringName); cache theStringName. - Wrap node ops in safe extension helpers (
QueueFreeSafely,AddChildSafely, …). - Verify any API against the live engine (
engine class-info/engine search) — never from memory.