Skills/godot-csharp-patterns/SKILL.md

5.5 KiB

name description license compatibility metadata
godot-csharp-patterns C# Mono patterns for Godot 4.x: Export attributes, signal handling, performance optimization, GDExtension interop, and best practices for first-class C# support. Use when writing C# code in Godot. MIT
godot-4.0
godot-4.1
godot-4.2
godot-4.3
godot-4.4
godot-4.5
godot-4.6
godot-4.7
author version tags created
vl4dt 0.1.0
godot
csharp
mono
performance
2026-06-20

C# Patterns for Godot 4.x

Best practices for Godot's first-class C# (Mono) support.

Export Attributes

Inspector Properties

using Godot;

public partial class Player : CharacterBody2D
{
    // Basic export
    [Export] public float Speed { get; set; } = 200f;

    // With range
    [Export(PropertyHint.Range, "0,100,0.5")]
    public float JumpForce { get; set; } = 400f;

    // File path
    [Export(FileFilter = "*.png,*.svg")]
    public string IconPath { get; set; } = "";

    // Resource export
    [Export] public PackedScene BulletScene { get; set; }

    // Enum with hint
    [Export] public MovementMode MoveMode { get; set; }

    public enum MovementMode
    {
        Walk,
        Run,
        Sprint
    }
}

Export Categories

[ExportGroup("Combat")]
[Export] public int AttackDamage { get; set; } = 10;
[Export] public float AttackCooldown { get; set; } = 0.5f;

[ExportGroup("Movement")]
[Export] public float MoveSpeed { get; set; } = 200f;
[Export] public float JumpForce { get; set; } = 400f;

Signal Handling

C# Event Syntax vs Godot Signals

// Godot signal declaration in GDScript (autoload)
// class_name EventBus extends Node
// signal player_died(Vector2 position)

// C# subscriber
public override void _Ready()
{
    // Method reference (recommended — auto-disconnect on free)
    EventBus.PlayerDied += OnPlayerDied;

    // Lambda (requires manual disconnect)
    EventBus.PlayerDied += (pos) => GD.Print($"Died at {pos}");
}

public override void _ExitTree()
{
    // Clean up lambda subscriptions
    EventBus.PlayerDied -= OnPlayerDied;
}

private void OnPlayerDied(Vector2 position)
{
    GD.Print($"Player died at {position}");
}

Using Godot.Signal for C#-Side Signals

public partial class Enemy : CharacterBody2D
{
    [Signal] public delegate void HealthChangedEventHandler(int newHealth);
    [Signal] public delegate void DefeatedEventHandler();

    private int health = 100;

    public void TakeDamage(int amount)
    {
        health -= amount;
        EmitSignal(SignalName.HealthChanged, health);

        if (health <= 0)
        {
            EmitSignal(SignalName.Defeated);
            QueueFree();
        }
    }
}

Performance Considerations

Avoiding GC Pressure

// Bad: Creates garbage every frame
public override void _Process(double delta)
{
    var text = $"Score: {score}";  // String allocation
    label.Text = text;
}

// Good: Reuse objects, avoid allocations
private readonly StringBuilder _sb = new();

public override void _Process(double delta)
{
    _sb.Clear();
    _sb.Append("Score: ");
    _sb.Append(score);
    label.Text = _sb.ToString();
}

Structs vs Classes

// Use structs for small, short-lived data
public struct Vector2Int
{
    public int X { get; }
    public int Y { get; }
    public Vector2Int(int x, int y) => (X, Y) = (x, y);
}

// Use classes for objects with identity/lifetime
public class PlayerData : Resource
{
    public string Name { get; set; }
    public int Level { get; set; }
}

Instance Pooling

public partial class BulletPool : Node
{
    private readonly Queue<Bullet> _pool = new();
    private readonly PackedScene _bulletScene;

    public BulletPool(PackedScene bulletScene) => _bulletScene = bulletScene;

    public Bullet Get()
    {
        if (_pool.TryDequeue(out var bullet))
        {
            bullet.Visible = true;
            return bullet;
        }
        return _bulletScene.Instantiate<Bullet>();
    }

    public void Return(Bullet bullet)
    {
        bullet.Visible = false;
        bullet.QueueFree();
        _pool.Enqueue(bullet);
    }
}

GDExtension Interop

Calling GDScript from C#

// GDScript (autoload "GameData")
// class_name GameData extends Node
// var high_score: int = 0

// C# access
var gameData = Engine.GetSingleton("GameData") as GodotObject;
var highScore = gameData.Get("high_score");

Exposing C# to GDScript via GDExtension

Create a .gdextension file to expose C# classes to GDScript projects.

Godot 4.7 C# Notes

  • C# fully supports new 4.7 features including VirtualJoystick and tween_await()
  • Use await with tweens for async animation sequences
  • Export attributes work the same — no changes in 4.7

MCP Bridge Tools (Optional — Live Editor Integration)

When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:

  • script_read — Read C# script files for review
  • script_edit — Update C# scripts directly
  • script_validate — Validate C# scripts for syntax errors
  • scene_tree — Inspect scene hierarchy to verify node composition
  • class_db_info — Query Godot class info for C# interop patterns

Note: MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.

References