Godot 4 C# skill for Codex
Godot with .NET, minus the generator surprises. Codex checks that the Godot.NET.Sdk version matches the editor before touching code, writes partial node classes that satisfy the source generators, uses SignalName and cached StringName values instead of strings, and keeps engine-free rules in a plain class library tested with dotnet test. It flags platform limits such as the missing C# web export before you commit to a design.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- godot-csharp Installs to ~/.agents/skills/godot-csharp/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install godot-csharp The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project godot-csharp Installs to .agents/skills/godot-csharp/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/godot-csharp.zipunzip -o godot-csharp.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) godot-csharp PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $godot-csharp, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds Godot 4 games in C# with the .NET edition - partial node classes, [Export] and [Signal] delegates, SignalName and PropertyName caches, typed GetNode lookups, Godot vs .NET collections, interop with GDScript, dotnet build and xUnit tests for engine-free logic. Use when a Godot 4 project has a .csproj, when converting GDScript to C#, when signals or exports do not appear in the editor, when C# builds fail after an engine upgrade, or when choosing C# vs GDScript for a platform such as web export.
Use it when
- A Godot 4 project uses the .NET edition and a .csproj
- Signals, exports or scripts fail to appear after building
- Builds break after upgrading the Godot editor version
- You want engine-free game rules covered by dotnet test
Use something else when
- The project is GDScript only: use godot-gdscript
- You need a modern .NET language question unrelated to Godot: use csharp-modern
How to check the result
dotnet build passes with no generator warnings, dotnet test passes for the core library, and new exports appear in the Inspector after a rebuild.
The mistake it prevents
Forgetting the partial keyword or naming the file differently from the class, which stops Godot from attaching the script.
Done well means: the project builds with dotnet build with no warnings from the Godot
source generators, every node script is a partial class whose file name matches the class,
engine-free logic lives in plain C# that unit tests can reach, and hot paths avoid
marshalling costs between C# and the engine.
Before writing code
- Read
AGENTS.md,project.godot(config/featuresgives the engine version, e.g. “4.4”, plus “C#”) and the.csproj. - In the
.csproj,<Project Sdk="Godot.NET.Sdk/4.x.y">must match the editor version exactly, andTargetFrameworkmust be one that SDK supports (Godot 4.4+ usesnet8.0). A mismatch causes “assembly not found” or generator errors after an engine upgrade. - Check target platforms. C# export works for desktop, Android and iOS (mobile support was still marked experimental in early 4.x releases); Godot 4 C# projects cannot export to the web. If web is a requirement, stop and tell the user GDScript is needed for that target.
dotnet buildneeds the .NET SDK installed. Restoring NuGet packages needs network; in a sandbox without it, the build only works if packages are already in the local cache.
Node scripts
using Godot;
public partial class Player : CharacterBody2D
{
[Signal] public delegate void HealthChangedEventHandler(int current, int maximum);
[Signal] public delegate void DiedEventHandler();
[Export] public float MaxSpeed { get; set; } = 220f;
[Export] public float Acceleration { get; set; } = 1400f;
[Export] public PlayerStats Stats { get; set; } // custom Resource
private AnimatedSprite2D _sprite;
private int _health;
public override void _Ready()
{
_sprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
_health = Stats.MaxHealth;
GetNode<Area2D>("%Hurtbox").AreaEntered += OnHurtboxAreaEntered;
}
public override void _PhysicsProcess(double delta)
{
Vector2 v = Velocity;
if (!IsOnFloor())
v += GetGravity() * (float)delta;
float dir = Input.GetAxis(InputActions.MoveLeft, InputActions.MoveRight);
v.X = Mathf.MoveToward(v.X, dir * MaxSpeed, Acceleration * (float)delta);
Velocity = v; // Vector2 is a struct: copy, modify, assign back
MoveAndSlide();
}
public void TakeDamage(int amount)
{
_health = Mathf.Max(_health - amount, 0);
EmitSignal(SignalName.HealthChanged, _health, Stats.MaxHealth);
if (_health == 0)
EmitSignal(SignalName.Died);
}
private void OnHurtboxAreaEntered(Area2D area)
{
if (area is Hitbox hit)
TakeDamage(hit.Damage);
}
}
internal static class InputActions
{
public static readonly StringName MoveLeft = "move_left";
public static readonly StringName MoveRight = "move_right";
}
Rules the source generators enforce:
- Classes deriving from
GodotObjectmust bepartial, and the file name must match the class name, or the script cannot be attached. - Signal delegates must end in
EventHandler; the signal name drops the suffix (HealthChanged). Subscribe with the generated C# event:player.HealthChanged += OnHp;. [Export]works on fields and properties of Variant-compatible types (primitives, Godot structs,GodotObjectsubclasses,Godot.Collections, arrays of those). After adding one, build (the hammer button ordotnet build) before it appears in the Inspector.- Use generated
SignalName.X,PropertyName.X,MethodName.Xinstead of string literals.
Collections and marshalling
| Need | Use |
|---|---|
| Internal logic, hot loops | System.Collections.Generic.List<T> / Dictionary<,> |
| Exported to Inspector, passed to engine APIs, signals | Godot.Collections.Array<T> / Dictionary<,> |
| Strings passed repeatedly to engine (input actions, animation names, group names) | Cache as static readonly StringName |
| Node paths used repeatedly | Cache NodePath or resolve once in _Ready |
Every call that crosses into the engine marshals arguments. A string converts to
StringName on each call, which allocates; that is why the InputActions cache above exists.
Godot collections live on the engine side, so iterating one from C# marshals every element.
Interop with GDScript
- C# calling GDScript:
node.Call(MethodName_or_"method_name", args),node.Get("prop"),node.Set("prop", value); returnsVariant, convert with.As<int>(). - GDScript calling C#: works through normal method calls and properties; names stay PascalCase as written in C#.
- Connecting to a GDScript signal from C#:
node.Connect("signal_name", Callable.From<int>(OnX)). - Keep the boundary narrow: one adapter class per GDScript addon rather than
Callstrings spread through the codebase.
Async
public async void Respawn()
{
await ToSignal(GetTree().CreateTimer(2.0), SceneTreeTimer.SignalName.Timeout);
if (!IsInstanceValid(this) || !IsInsideTree())
return;
GlobalPosition = _spawnPoint;
}
async voidis acceptable for engine callbacks and fire-and-forget gameplay; wrap the body in try/catch and log withGD.PushError, since exceptions otherwise only print.- Engine APIs are not thread-safe for nodes in the scene tree. Use
Task.Runonly for pure computation, then apply results withCallDeferredor on the next_Process.
Testable structure
Put rules that need no engine types (damage formulas, inventory, save migration) in a
separate class library project without the Godot SDK, referenced by the game project. Test
it with xUnit or NUnit through dotnet test. Node scripts become thin adapters that call it.
Game.sln
├── Game.csproj (Godot.NET.Sdk, node scripts)
├── Game.Core/ (plain net8.0 class library)
└── Game.Core.Tests/ (xUnit, references Game.Core)
Code that needs real nodes is tested in-engine with gdUnit4 (has C# support) or by running a test scene; ask before adding an addon.
Gotchas
- Struct properties:
Position.X = 5does not compile (CS1612) becausePositionreturns a copy. Read into a local, modify, assign back. _Process(double delta)and_PhysicsProcess(double delta)passdouble; cast once.- Disposed objects: holding a C# reference to a freed node throws
ObjectDisposedException. CheckIsInstanceValid(node)for references kept across frames. - Unsubscribe C# event handlers (
-=) in_ExitTreewhen the publisher outlives the subscriber; otherwise the subscriber leaks and may be called after being freed. - Renaming a class requires renaming the file and updating the script path in every
.tscnthat references it ([ext_resource type="Script" path=...]); the editor does this if the rename is done in the FileSystem dock. - Mixing GDScript and C# in one project is fine, but a GDScript
class_nameis not visible to C# as a type. Interop goes throughCall/Get. - Hot reload of C# while the game runs is limited; restart the scene after structural changes.
Verification
dotnet build # zero errors, zero generator warnings
dotnet test Game.Core.Tests # engine-free logic
godot --headless --build-solutions --quit # build through Godot, as the editor would
- Open the editor, confirm new
[Export]members appear after building, and run the scene with the Debugger > Errors tab empty. - If the Godot binary is not available, report which checks you could not run.
Hand-off summary
**Engine:** Godot 4.4 .NET, Godot.NET.Sdk/4.4.1, net8.0
**Changed:** Player.cs (partial, signals HealthChanged/Died), InputActions.cs (StringName cache)
**New project:** Game.Core (Damage, Inventory) + Game.Core.Tests (12 xUnit tests passing)
**Editor steps:** rebuild, then assign PlayerStats resource on player.tscn
**Platform note:** web export not available for C#; desktop and Android targets only What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Godot 4 C#"
short_description: "Godot with .NET, minus the generator surprises"
default_prompt: "$godot-csharp Convert our GDScript player and enemy scripts to C#" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.