~2 мин чтения

C# в Godot — практика и нюансы

Godot .NET edition, отличия от GDScript, миграция, gotcha'и для разработчиков из мира Unity.

GDScript — отличный язык, но если вы пришли из Unity (или серьёзного .NET-проекта), хочется работать на C#. Godot поддерживает это через отдельную сборку — Godot .NET edition.

Чем .NET edition отличается

ПараметрGodot StandardGodot .NET edition
Размер редактора~60 MB~120 MB
СкриптингGDScript, GDExtension+ C# (.NET 8)
Веб-экспорт❌ (не работает на 2026 май)
C# AOT-экспортn/aNativeAOT (с оговорками)
.csprojn/aСтандартный

Скачайте .NET-вариант с godotengine.org/download. В Steam-версии есть отдельный SKU “Godot Engine (.NET)”.

Веб

Использование Node.js + TypeScript в Express-проекте VS использование Deno или Bun. Базовая логика та же, но runtime и tooling отличаются.

Unity

Первый 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#

UnityGodot C#
class : MonoBehaviourpartial 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.positionPosition (без transform)
Vector3.forwardVector3.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)

Не молниеносно (рефлексия под капотом), но работает.