From godot-prompter
Provides C# conventions, GodotSharp API differences from GDScript, project setup with .csproj/NuGet, and interop for Godot 4.3+ development.
npx claudepluginhub jame581/godotprompter --plugin godot-prompterThis skill uses the workspace's default tool permissions.
This skill covers C#-specific conventions, API differences from GDScript, project setup, and interop patterns. All examples are C# only. Target Godot 4.3+ with the GodotSharp NuGet package.
Provides C# patterns for Godot with clear interop boundaries, node ownership, and engine lifecycle awareness. Use when C# interop with scenes/resources grows or architecture needs review.
Implements Godot 4.x signals in C# using [Signal] delegates, EmitSignal patterns, async awaiting, and event-driven architecture.
Builds and optimizes Godot 4 games using GDScript, node/scene architecture, signals, resources, physics, animations, UI, tilemaps, shaders, multiplayer, and best practices from prototypes to production.
Share bugs, ideas, or general feedback.
This skill covers C#-specific conventions, API differences from GDScript, project setup, and interop patterns. All examples are C# only. Target Godot 4.3+ with the GodotSharp NuGet package.
Related skills: csharp-signals for C# signal patterns, godot-project-setup for C# project scaffolding, godot-testing for C# testing with gdUnit4.
| GDScript | C# Equivalent | Notes |
|---|---|---|
var x = 5 | var x = 5; or typed int x = 5; | C# var infers type at compile time |
func MyMethod() -> void: | public void MyMethod() { } | Methods are PascalCase in C# |
signal health_changed(amount: int) | [Signal] delegate void HealthChangedEventHandler(int amount); | Must use EventHandler suffix |
@export var speed: float = 100.0 | [Export] public float Speed { get; set; } = 100f; | PascalCase, property syntax |
@onready var label = $Label | private Label _label; + _label = GetNode<Label>("Label"); in _Ready() | No @onready equivalent; use _Ready() |
match value: | switch (value) { case X: break; } | C# switch also supports pattern matching |
class_name MyClass | [GlobalClass] public partial class MyClass : GodotObject { } | Requires [GlobalClass] attribute |
extends Node | public partial class MyScript : Node { } | Inheritance via : |
preload("res://scene.tscn") | GD.Load<PackedScene>("res://scene.tscn") | Loaded at runtime, not compile time |
push_error("msg") | GD.PushError("msg"); | Prints to Godot error log |
print("msg") | GD.Print("msg"); | Also: GD.PrintS(), GD.PrintT() |
node is CharacterBody2D | node is CharacterBody2D | Same keyword, same semantics |
node as CharacterBody2D | node as CharacterBody2D | Returns null on failure in both |
await signal_name | await ToSignal(source, SignalName.X); | Must use ToSignal() wrapper |
Array | Godot.Collections.Array | Not System.Collections.Generic.List<T> |
Dictionary | Godot.Collections.Dictionary | Not System.Collections.Generic.Dictionary<K,V> |
Godot auto-generates the .csproj when you create the first C# script via Script > New Script > C#. Do not edit the generated file structure manually — let the editor manage it.
MyProject/
├── MyProject.csproj # Auto-generated, edit only for NuGet packages
├── MyProject.sln # Auto-generated solution file
├── project.godot
└── scripts/
└── Player.cs
Add packages in MyProject.csproj inside <ItemGroup>:
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<!-- Example: add a third-party NuGet package -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
Run dotnet restore or let the IDE restore automatically after editing.
| IDE | Setup Required | Notes |
|---|---|---|
| JetBrains Rider | Install Godot plugin (bundled in Rider 2023.3+) | Best Godot C# support; debugger works out of the box |
| VS Code | Install C# Dev Kit + Godot Tools extensions | Requires launch.json for debugger attachment |
| Visual Studio | Install Godot Visual Studio extension | Windows only; debugger via Tools > Attach to Process |
For all IDEs, open the .sln file (not just a folder) to get full solution resolution.
partial class RequirementEvery class that extends a Godot type must be declared partial. This is not optional.
Godot uses C# source generators to emit the signal registration, property binding, and RPC code alongside your class. Source generators require partial to inject into the same class declaration.
Error CS0260: Missing partial modifier on declaration of type 'Player';
another partial declaration of this type exists.
Or the class compiles but signals and [Export] properties silently fail to register.
// CORRECT
public partial class Player : CharacterBody2D { }
// WRONG — will cause source generator errors
public class Player : CharacterBody2D { }
This applies to every class in the inheritance chain that extends a Godot type, including intermediate base classes.
| Element | Convention | Example |
|---|---|---|
| Methods | PascalCase | public void TakeDamage(int amount) |
| Properties | PascalCase | public float MaxHealth { get; set; } |
| Signals (delegate) | PascalCase + EventHandler suffix | HealthChangedEventHandler |
[Export] properties | PascalCase | [Export] public float Speed { get; set; } |
| Private fields | _camelCase with underscore prefix | private float _currentSpeed; |
| Local variables | camelCase | var newPosition = ... |
| Parameters | camelCase | void SetHealth(int newHealth) |
| Godot API names | Match Godot's PascalCase exactly | GlobalPosition not global_position |
| Enums | PascalCase type, PascalCase members | enum State { Idle, Running, Dead } |
| Constants | PascalCase or SCREAMING_SNAKE per team style | const float MaxSpeed = 200f; |
Always match GodotSharp property and method names exactly — they are PascalCase translations of the GDScript snake_case names (e.g. is_on_floor() → IsOnFloor()).
Signals require a delegate declaration with the [Signal] attribute. The delegate name must end with EventHandler.
using Godot;
public partial class Player : CharacterBody2D
{
// Declaration
[Signal] public delegate void HealthChangedEventHandler(int newHealth);
[Signal] public delegate void DiedEventHandler();
private int _health = 100;
public void TakeDamage(int amount)
{
_health -= amount;
EmitSignal(SignalName.HealthChanged, _health); // Use SignalName.X, not a string
if (_health <= 0)
EmitSignal(SignalName.Died);
}
}
// Connect with +=
player.HealthChanged += OnHealthChanged;
player.Died += OnPlayerDied;
// Disconnect with -=
player.HealthChanged -= OnHealthChanged;
player.Died -= OnPlayerDied;
// Handler signatures must match the delegate
private void OnHealthChanged(int newHealth) { ... }
private void OnPlayerDied() { ... }
For full signal patterns including one-shot connections, static typed signals, and cross-language signal wiring, see the csharp-signals skill.
GDScript's await maps to C#'s await ToSignal(...). Godot signals return a SignalAwaiter that is compatible with C# await.
public async void StartCutscene()
{
// Wait for a timer to finish
await ToSignal(GetTree().CreateTimer(2.0), Timer.SignalName.Timeout);
// Wait for an animation to finish
var anim = GetNode<AnimationPlayer>("AnimationPlayer");
anim.Play("intro");
await ToSignal(anim, AnimationPlayer.SignalName.AnimationFinished);
GD.Print("Cutscene complete");
}
For CPU-bound work, use Task.Run — but never touch Godot objects from a non-main thread:
public async void LoadHeavyData()
{
// Off main thread: pure C# computation only
var result = await Task.Run(() => ComputeSomethingExpensive());
// Back on main thread: safe to use Godot API
ApplyResult(result);
}
private int ComputeSomethingExpensive()
{
// No Godot API calls here
return Enumerable.Range(0, 1_000_000).Sum();
}
await Equivalents| GDScript | C# |
|---|---|
await get_tree().create_timer(1.0).timeout | await ToSignal(GetTree().CreateTimer(1.0), Timer.SignalName.Timeout) |
await animation_player.animation_finished | await ToSignal(animPlayer, AnimationPlayer.SignalName.AnimationFinished) |
await signal_name | await ToSignal(this, SignalName.YourSignal) |
Use Call, Get, and Set on any GodotObject. Values are marshalled through Variant.
// Assume "enemy" is a GodotObject backed by a GDScript with func take_damage(amount)
GodotObject enemy = GetNode("Enemy");
// Call a GDScript method
enemy.Call("take_damage", 25);
// Get a GDScript property
float health = enemy.Get("health").AsSingle();
// Set a GDScript property
enemy.Set("is_stunned", true);
If a C# class is registered as a [GlobalClass], GDScript can instantiate and use it directly without any extra wiring:
[GlobalClass]
public partial class WeaponData : Resource
{
[Export] public float Damage { get; set; } = 10f;
[Export] public float Cooldown { get; set; } = 0.5f;
}
# GDScript — works because WeaponData is a [GlobalClass]
var data := WeaponData.new()
data.damage = 50.0
Non-[GlobalClass] C# types are not visible to GDScript by name but can still be passed as Variant/Object references.
| Scenario | Issue | Fix |
|---|---|---|
Passing null across boundary | GDScript null becomes default(Variant), not C# null | Check variant.VariantType == Variant.Type.Nil |
Returning int[] from C# | GDScript receives a PackedInt32Array, not an Array | Return Godot.Collections.Array<int> for consistent typing |
Passing System.Collections.Generic.List<T> | Not marshallable — Godot doesn't know this type | Convert to Godot.Collections.Array<T> first |
Godot Color struct | Passed by value through Variant correctly | No issue |
| Workload | Winner | Reason |
|---|---|---|
| Math-heavy loops (pathfinding, simulation) | C# (significantly faster) | Compiled JIT vs interpreted GDScript |
| Large array/collection processing | C# | Value type arrays avoid boxing |
| Godot API calls (move_and_slide, etc.) | Roughly equal | Both route through the same C++ engine |
| Scene tree operations | Roughly equal | Bottleneck is C++ overhead, not language |
| Rapid prototyping | GDScript | Less boilerplate, hot-reload without recompile |
| Editor tooling (plugins, @tool) | GDScript | C# tool scripts require a full build cycle |
@tool scripts, and rapid iteration on gameplay logic.Godot.Collections.Array for hot paths; prefer typed arrays (Array<T>) or native C# arrays (T[]) converted at boundaries.StringName lookups are O(1) but StringName construction is not — cache them if created frequently.| Gotcha | Problem | Fix |
|---|---|---|
Variant to C# type conversion | (float)someVariant throws if the underlying type is int | Use .AsSingle(), .AsInt32(), etc. instead of casts |
null vs default(Variant) | Godot signals passing no value give default(Variant), not C# null | Check .VariantType == Variant.Type.Nil |
Godot.Collections vs System.Collections | Godot API methods return Godot.Collections.Array; passing List<T> causes runtime error | Always use Godot.Collections.Array/Dictionary at Godot API boundaries |
| Disposing native objects | Calling methods on a freed Godot object throws ObjectDisposedException | Check IsInstanceValid(obj) before use |
StringName construction in hot loops | new StringName("my_signal") allocates each call | Cache as private static readonly StringName _signalName = "my_signal"; |
| Export array types | [Export] public Array Items; exports untyped array | Use [Export] public Godot.Collections.Array<MyResource> Items { get; set; } |
| Node path strings | GetNode("../UI/Label") fails silently if the path changes | Use GetNode<Label>("%Label") with unique names or typed [Export] node references |
partial class forgotten | Source generators silently fail; [Export] and [Signal] don't register | Every class extending a Godot type must be partial |
async void vs async Task | async void swallows exceptions | Use async Task except for top-level event handlers that Godot calls |
partial_Ready, _PhysicsProcess, IsOnFloor())[Export] properties are PascalCase and use property syntax ({ get; set; })EventHandler and use EmitSignal(SignalName.X)+= and disconnected with -= when no longer neededawait ToSignal(...) used for Godot signals — not raw Task.Delay for game timingGodot.Collections.Array/Dictionary used at Godot API boundaries, not System.Collections typesIsInstanceValid(obj) checked before using any potentially freed Godot objectStringName instances cached if constructed in loops or frequently called methodsTask.Run(...) callbacks — all engine calls happen on the main thread.csproj opened via the .sln file in the IDE for full solution resolution@tool scripts