C# in Godot
I still remember the first time I hit Run in Godot and watched a simple sprite move across the screen with a few lines of GDScript.
For years, GDScript has been my home in Godot. It’s fast, readable, and deeply woven into the engine’s workflow. But as my projects grew more complex, I started exploring C# - not just for the performance and stricter type safety, but for the broader ecosystem, better tooling, and long-term flexibility it offers.
I started collecting tips as my transition goes. There are syntax gotchas, namespace surprises, and bunch of "wait, how does this work in C#?" moments.
Let’s get into it.
Object initializers
Creates an instance and sets properties in one block.
Object initializer:
var timer = new Timer
{
WaitTime = BulletResources[i].Cooldown,
OneShot = true
};
Is equivalent to:
var timer = new Timer();
timer.WaitTime = BulletResources[i].Cooldown;
timer.OneShot = true;
Just a more compact way to write it. Works for any class - Godot or your own.
Property accessors
public BulletResource CurrentBulletResource { get; private set; }
They control how a property can be read and written.
get- anyone can read this (it'spublicby default, matching the property's access level)private set- only code inside this class can write to it
So from outside this class you can do:
var res = shootingComponent.CurrentBulletResource; // fine
shootingComponent.CurrentBulletResource = something; // compile error
But inside this class:
CurrentBulletResource = (BulletResource)bullet.Duplicate(true); // fine
Other common combinations:
| Syntax | Meaning |
|---|---|
{ get; set; } |
anyone can read and write |
{ get; private set; } |
anyone reads, only this class writes |
{ get; } |
read-only, can only be set in constructor |
{ get; init; } |
read-only after construction, can be set during object initialization |
Method Overloading
You can define two similarly called methods like the following in the same class:
// The main implementation - handles the actual logic
private void shoot(Vector2 direction)
{
var bulletNode = instantiateBullet(br.Id);
AddChild(bulletNode);
}
// The convenience overload - converts angle to vector and calls shoot(Vector2)
private void shoot(float angle)
{
shoot(Vector2.FromAngle(angle));
}
Then when you call shoot(...), C# automatically picks the right one based on what you pass in.
Why this is useful? it makes your code cleaner for whoever calls shoot(). If you direction vector, call shoot() and give it the direction. If you have angle instead, call shoot() and give it the angle value.
It's a small convenience that could keep your code flexible without adding complexity.