C# в Godot — практика и нюансы
Godot .NET edition, отличия от GDScript, миграция, gotcha'и для разработчиков из мира Unity.
GDScript — отличный язык, но если вы пришли из Unity (или серьёзного .NET-проекта), хочется работать на C#. Godot поддерживает это через отдельную сборку — Godot .NET edition.
Чем .NET edition отличается
| Параметр | Godot Standard | Godot .NET edition |
|---|---|---|
| Размер редактора | ~60 MB | ~120 MB |
| Скриптинг | GDScript, GDExtension | + C# (.NET 8) |
| Веб-экспорт | ✅ | ❌ (не работает на 2026 май) |
| C# AOT-экспорт | n/a | NativeAOT (с оговорками) |
| .csproj | n/a | Стандартный |
Скачайте .NET-вариант с godotengine.org/download. В Steam-версии есть отдельный SKU “Godot Engine (.NET)”.
Использование Node.js + TypeScript в Express-проекте VS использование Deno или Bun. Базовая логика та же, но runtime и tooling отличаются.
Первый C#-скрипт
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 — типа double (не float как в Unity)
Position += Vector3.Forward * Speed * (float)delta;
}
public void TakeDamage(int amount, Node attacker) {
_hp -= amount;
if (_hp <= 0) {
EmitSignal(SignalName.Died, attacker);
QueueFree();
}
}
}
Ключевые отличия от Unity:
partial— обязательно (генератор кода добавляет код для signals).[Export]вместо[SerializeField]— атрибут видимости в Inspector.PropertyHint.Rangeвместо[Range(1, 10)]— другой синтаксис.[Signal] public delegate— сигналы декларируются как делегаты сEventHandlerсуффиксом._Ready,_PhysicsProcess(PascalCase) — а не_ready,_physics_process.(double)delta— нужно явно кастовать в float если используете с Vector3.
Сравнение синтаксиса с Unity C#
| Unity | Godot C# |
|---|---|
class : MonoBehaviour | partial class : Node (или 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 (без transform) |
Vector3.forward | Vector3.Forward (PascalCase) |
gameObject.SetActive(false) | Visible = false (или ProcessMode = Disabled) |
GetComponent<T>() | GetNode<T>("Name") |
Instantiate(prefab) | prefab.Instantiate<T>() |
Destroy(go) | QueueFree() |
Time.deltaTime | параметр delta метода |
Time.time | (float)Time.GetTicksMsec() / 1000.0f |
Input.GetKey(...) | Input.IsActionPressed("...") |
Debug.Log(...) | GD.Print(...) |
Сигналы — два пути
Декларация:
[Signal]
public delegate void DamagedEventHandler(int amount);
[Signal]
public delegate void DiedEventHandler();
Эмит:
EmitSignal(SignalName.Damaged, 10);
EmitSignal(SignalName.Died);
Подписка:
// Type-safe
enemy.Damaged += OnEnemyDamaged;
void OnEnemyDamaged(int amount) {
GD.Print($"Enemy took {amount} damage");
}
SignalName — автогенерированный класс с константами. IDE даёт autocomplete.
Resources и Exports
C# Custom Resource — почти как 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; }
}
[GlobalClass] атрибут регистрирует класс глобально — он появится в FileSystem → New Resource →
WeaponData (как scriptable object в Unity).
Применение:
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
Стандартный .NET async/await работает:
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") — ключевой helper: возвращает Task, который завершается при
получении сигнала.
Ограничения C# в Godot
1. Веб-экспорт не работает
На май 2026 — это главное ограничение. C# не экспортируется в HTML5. Команда работает над решением через .NET 10 и NativeAOT, но в production пока нет. Если цель — браузер, остаётся GDScript.
2. Hot reload ограниченный
GDScript перекомпилируется на лету; C# нужно остановить Play Mode → пересборка → запустить. Это дольше итерации, чем в GDScript.
3. .NET 8 (или 9 в зависимости от Godot)
Старые C# фичи доступны; самые свежие — иногда нет. Проверяйте target framework в .csproj.
4. PackedScene.Instantiate в C# чуть многословнее
// GDScript: var enemy = enemy_scene.instantiate()
// C#:
Node enemy = enemyScene.Instantiate();
// Или с generic:
var enemy3D = enemyScene.Instantiate<Node3D>();
5. NativeAOT export — экспериментально
NativeAOT даёт быстрый startup и меньший размер билда, но не все C# фичи работают (рефлексия, JIT-генерация кода). С Godot 4.6 — limited support; следите за релизами.
Когда выбирать C# vs GDScript
✅ C# — хороший выбор, если:
- У вас .NET-команда / .NET-задний план.
- Нужны существующие .NET-библиотеки (Newtonsoft.Json, AutoMapper, и др.).
- Большой проект с сильной типизацией важна для maintenance.
- Веб-таргет не нужен.
✅ GDScript лучше, если:
- Маленький-средний проект.
- Веб-экспорт обязателен.
- Команда — gamedev’ы, не senior .NET-разработчики.
- Скорость итерации (hot reload) критична.
- Опираетесь на community-плагины (большинство — GDScript).
Производительность
C# в Godot примерно ×5–10 быстрее GDScript untyped, ×2–3 быстрее typed GDScript. Это заметно на CPU-bound workload’ах: процедурная генерация, AI с большим количеством агентов, сложная математика.
Для gameplay-логики (90% игрового кода) разница не критична — оба языка справляются.
Mixing GDScript and C#
Можно. В одном проекте разные узлы могут иметь разные языки. Вызов между ними работает через общий ClassDB API.
// C# вызывает GDScript-метод
var gdNode = GetNode("GDScriptNode");
gdNode.Call("some_method", arg1, arg2);
# GDScript вызывает C#-метод
var cs_node = $CSharpNode
cs_node.Call("SomeMethod", arg1, arg2)
Не молниеносно (рефлексия под капотом), но работает.