C# in Godot — practice and nuances
Godot .NET edition, differences from GDScript, migration, gotchas for developers coming from the Unity world.
GDScript is a great language, but if you come from Unity (or a serious .NET project), you’ll want to work in C#. Godot supports this via a separate build — the Godot .NET edition.
How the .NET edition differs
| Parameter | Godot Standard | Godot .NET edition |
|---|---|---|
| Editor size | ~60 MB | ~120 MB |
| Scripting | GDScript, GDExtension | + C# (.NET 8) |
| Web export | ✅ | ❌ (does not work as of May 2026) |
| C# AOT export | n/a | NativeAOT (with caveats) |
| .csproj | n/a | Standard |
Download the .NET variant from godotengine.org/download. The Steam version has a separate SKU “Godot Engine (.NET)”.
Using Node.js + TypeScript in an Express project VS using Deno or Bun. The basic logic is the same, but the runtime and tooling differ.
Your first C# script
using Godot;
public partial class Enemy : Node3D
{
[Export]
public int MaxHp { get; set; } = 100;
[Export(PropertyHint.Range, "1,10,0.1")]
public float Speed { get; set; } = 3.0f;
[Signal]
public delegate void DiedEventHandler(Node killer);
private int _hp;
public override void _Ready() {
_hp = MaxHp;
GD.Print($"Enemy spawned with {MaxHp} HP");
}
public override void _PhysicsProcess(double delta) {
// delta is of type double (not float like in Unity)
Position += Vector3.Forward * Speed * (float)delta;
}
public void TakeDamage(int amount, Node attacker) {
_hp -= amount;
if (_hp <= 0) {
EmitSignal(SignalName.Died, attacker);
QueueFree();
}
}
}
Key differences from Unity:
partial— mandatory (the code generator adds code for signals).[Export]instead of[SerializeField]— the attribute for Inspector visibility.PropertyHint.Rangeinstead of[Range(1, 10)]— a different syntax.[Signal] public delegate— signals are declared as delegates with theEventHandlersuffix._Ready,_PhysicsProcess(PascalCase) — not_ready,_physics_process.(double)delta— you need to explicitly cast to float if you use it with Vector3.
Comparing syntax with Unity C#
| Unity | Godot C# |
|---|---|
class : MonoBehaviour | partial class : Node (or Node2D/Node3D) |
[SerializeField] | [Export] |
void Update() | public override void _Process(double delta) |
void FixedUpdate() | public override void _PhysicsProcess(double delta) |
void Start() | public override void _Ready() |
transform.position | Position (no transform) |
Vector3.forward | Vector3.Forward (PascalCase) |
gameObject.SetActive(false) | Visible = false (or ProcessMode = Disabled) |
GetComponent<T>() | GetNode<T>("Name") |
Instantiate(prefab) | prefab.Instantiate<T>() |
Destroy(go) | QueueFree() |
Time.deltaTime | the delta parameter of the method |
Time.time | (float)Time.GetTicksMsec() / 1000.0f |
Input.GetKey(...) | Input.IsActionPressed("...") |
Debug.Log(...) | GD.Print(...) |
Signals — two ways
Declaration:
[Signal]
public delegate void DamagedEventHandler(int amount);
[Signal]
public delegate void DiedEventHandler();
Emit:
EmitSignal(SignalName.Damaged, 10);
EmitSignal(SignalName.Died);
Subscription:
// Type-safe
enemy.Damaged += OnEnemyDamaged;
void OnEnemyDamaged(int amount) {
GD.Print($"Enemy took {amount} damage");
}
SignalName is an auto-generated class with constants. The IDE provides autocomplete.
Resources and Exports
A C# Custom Resource is almost like a Unity ScriptableObject:
[GlobalClass]
public partial class WeaponData : Resource
{
[Export] public string DisplayName { get; set; } = "Pistol";
[Export] public int Damage { get; set; } = 10;
[Export] public float FireRate { get; set; } = 0.5f;
[Export] public PackedScene ProjectilePrefab { get; set; }
}
The [GlobalClass] attribute registers the class globally — it will appear in FileSystem → New Resource →
WeaponData (like a scriptable object in Unity).
Usage:
public partial class Weapon : Node3D
{
[Export] public WeaponData Data { get; set; }
[Export] public Marker3D Muzzle { get; set; }
private double _nextFireTime = 0;
public void TryFire() {
var now = Time.GetTicksMsec() / 1000.0;
if (now < _nextFireTime) return;
_nextFireTime = now + Data.FireRate;
var bullet = Data.ProjectilePrefab.Instantiate<Node3D>();
bullet.GlobalTransform = Muzzle.GlobalTransform;
GetTree().CurrentScene.AddChild(bullet);
}
}
Async / Await
The standard .NET async/await works:
public async Task LoadAndShow() {
await ToSignal(GetTree().CreateTimer(1.0), Timer.SignalName.Timeout);
GD.Print("1 second passed");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
GD.Print("Next frame");
}
ToSignal(node, "signal_name") is the key helper: it returns a Task that completes when the
signal is received.
Limitations of C# in Godot
1. Web export doesn’t work
As of May 2026, this is the main limitation. C# does not export to HTML5. The team is working on a solution via .NET 10 and NativeAOT, but it’s not in production yet. If the target is the browser, GDScript remains.
2. Limited hot reload
GDScript recompiles on the fly; C# requires stopping Play Mode → rebuild → run again. This iterates slower than GDScript.
3. .NET 8 (or 9 depending on Godot)
Older C# features are available; the very latest ones — sometimes not. Check the target framework in .csproj.
4. PackedScene.Instantiate in C# is a bit more verbose
// GDScript: var enemy = enemy_scene.instantiate()
// C#:
Node enemy = enemyScene.Instantiate();
// Or with a generic:
var enemy3D = enemyScene.Instantiate<Node3D>();
5. NativeAOT export — experimental
NativeAOT provides fast startup and a smaller build size, but not all C# features work (reflection, runtime code generation). With Godot 4.6 — limited support; keep an eye on releases.
When to choose C# vs GDScript
✅ C# is a good choice if:
- You have a .NET team / .NET background.
- You need existing .NET libraries (Newtonsoft.Json, AutoMapper, etc.).
- It’s a large project where strong typing is important for maintenance.
- A web target is not needed.
✅ GDScript is better if:
- It’s a small-to-medium project.
- Web export is mandatory.
- The team are gamedevs, not senior .NET developers.
- Iteration speed (hot reload) is critical.
- You rely on community plugins (most are GDScript).
Performance
C# in Godot is roughly 5–10× faster than untyped GDScript, 2–3× faster than typed GDScript. This is noticeable on CPU-bound workloads: procedural generation, AI with a large number of agents, complex math.
For gameplay logic (90% of game code) the difference isn’t critical — both languages handle it.
Mixing GDScript and C#
It’s possible. In a single project, different nodes can have different languages. Calling between them works through the shared ClassDB API.
// C# calls a GDScript method
var gdNode = GetNode("GDScriptNode");
gdNode.Call("some_method", arg1, arg2);
# GDScript calls a C# method
var cs_node = $CSharpNode
cs_node.Call("SomeMethod", arg1, arg2)
Not lightning-fast (reflection under the hood), but it works.