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.

  • Game Development
  • $godot-csharp
  • 187 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- godot-csharp

Installs to ~/.agents/skills/godot-csharp/ for every project on this machine.

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

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.

~/.agents/skills/godot-csharp/SKILL.md

--- name: godot-csharp description: Builds Godot 4 games in C# with the .NET edition - partial node classes, [Export] and [Signal] delegates, SignalName and PropertyName caches, typed… license: MIT ---

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

  1. Read AGENTS.md, project.godot (config/features gives the engine version, e.g. “4.4”, plus “C#”) and the .csproj.
  2. In the .csproj, <Project Sdk="Godot.NET.Sdk/4.x.y"> must match the editor version exactly, and TargetFramework must be one that SDK supports (Godot 4.4+ uses net8.0). A mismatch causes “assembly not found” or generator errors after an engine upgrade.
  3. 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.
  4. dotnet build needs 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 GodotObject must be partial, 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, GodotObject subclasses, Godot.Collections, arrays of those). After adding one, build (the hammer button or dotnet build) before it appears in the Inspector.
  • Use generated SignalName.X, PropertyName.X, MethodName.X instead of string literals.

Collections and marshalling

NeedUse
Internal logic, hot loopsSystem.Collections.Generic.List<T> / Dictionary<,>
Exported to Inspector, passed to engine APIs, signalsGodot.Collections.Array<T> / Dictionary<,>
Strings passed repeatedly to engine (input actions, animation names, group names)Cache as static readonly StringName
Node paths used repeatedlyCache 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); returns Variant, 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 Call strings 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 void is acceptable for engine callbacks and fire-and-forget gameplay; wrap the body in try/catch and log with GD.PushError, since exceptions otherwise only print.
  • Engine APIs are not thread-safe for nodes in the scene tree. Use Task.Run only for pure computation, then apply results with CallDeferred or 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 = 5 does not compile (CS1612) because Position returns a copy. Read into a local, modify, assign back.
  • _Process(double delta) and _PhysicsProcess(double delta) pass double; cast once.
  • Disposed objects: holding a C# reference to a freed node throws ObjectDisposedException. Check IsInstanceValid(node) for references kept across frames.
  • Unsubscribe C# event handlers (-=) in _ExitTree when 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 .tscn that 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_name is not visible to C# as a type. Interop goes through Call/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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem