Настройка проекта Godot 4.5.1 + .NET 10

Важно!

Godot 4.5.1 требует .NET SDK 8.0 или новее для работы с C#. Убедитесь, что у вас установлен .NET 10 для максимальной совместимости.

Создание 2D C# проекта
Настройка
Шаги для создания нового 2D проекта с поддержкой C# в Godot 4.5.1:
// 1. Создайте новый проект через менеджер проектов // 2. Выберите шаблон "2D" // 3. В настройках проекта включите C# поддержку // 4. Убедитесь, что в Project Settings → .NET: // - Build Configuration: Debug // - Target Framework: net8.0 (или net10.0) // 5. Создайте главную сцену с Node2D корнем
csproj файл для .NET 10
Конфигурация
Пример файла проекта для Godot 4.5.1 с .NET 10:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="GodotSharp" Version="4.5.1" />
    <PackageReference Include="GodotSharp.SourceGenerators" Version="4.5.1" />
  </ItemGroup>
</Project>

Основные 2D Ноды

Node2D
Базовый класс
public class MyNode2D : Node2D
Базовый класс для всех 2D объектов. Имеет позицию, поворот и масштаб.

Основные свойства:

Свойство Тип Описание
Position Vector2 Позиция ноды в пикселях
Rotation float Поворот в радианах
Scale Vector2 Масштаб по осям X и Y
GlobalPosition Vector2 Глобальная позиция в дереве сцен
using Godot;

public partial class Player : Node2D
{
    [Export]
    public float Speed { get; set; } = 200.0f;

    private Sprite2D _sprite;

    public override void _Ready()
    {
        // Получаем дочерний Sprite2D
        _sprite = GetNode<Sprite2D>("Sprite2D");
    }

    public override void _Process(double delta)
    {
        // Движение с клавиатуры
        Vector2 direction = Input.GetVector("move_left", "move_right", "move_up", "move_down");
        Position += direction * Speed * (float)delta;

        // Поворот спрайта к курсору мыши
        if (direction != Vector2.Zero)
        {
            Rotation = direction.Angle();
        }
    }
}
Sprite2D
2D Спрайт
public class MySprite : Sprite2D
Нода для отображения 2D текстур. Поддерживает анимацию через SpriteFrames.
public partial class AnimatedSprite : Sprite2D
{
    [Export]
    public SpriteFrames SpriteFrames { get; set; }

    private string _currentAnimation = "idle";

    public override void _Ready()
    {
        if (SpriteFrames != null)
        {
            PlayAnimation("idle");
        }
    }

    public void PlayAnimation(string animationName)
    {
        if (SpriteFrames.HasAnimation(animationName) && _currentAnimation != animationName)
        {
            _currentAnimation = animationName;
            Frame = 0;
            // В Sprite2D анимация управляется вручную
            // Для автоматической анимации используйте AnimatedSprite2D
        }
    }

    public override void _Process(double delta)
    {
        // Ручное управление кадрами
        if (SpriteFrames != null)
        {
            int frameCount = SpriteFrames.GetFrameCount(_currentAnimation);
            Frame = (Frame + 1) % frameCount;
        }
    }
}
Camera2D
Камера 2D
Камера для отображения определенной области 2D мира. Поддерживает сглаживание, ограничения и зум.
public partial class PlayerCamera : Camera2D
{
    [Export]
    public Node2D Target { get; set; }

    [Export]
    public float FollowSpeed { get; set; } = 5.0f;

    [Export]
    public Vector2 LimitsMin { get; set; } = new Vector2(-1000, -1000);

    [Export]
    public Vector2 LimitsMax { get; set; } = new Vector2(1000, 1000);

    public override void _Ready()
    {
        // Установка ограничений камеры
        LimitLeft = (int)LimitsMin.X;
        LimitTop = (int)LimitsMin.Y;
        LimitRight = (int)LimitsMax.X;
        LimitBottom = (int)LimitsMax.Y;
    }

    public override void _Process(double delta)
    {
        if (Target != null)
        {
            // Плавное следование за целью
            Vector2 targetPosition = Target.GlobalPosition;
            GlobalPosition = GlobalPosition.Lerp(targetPosition, FollowSpeed * (float)delta);
        }
    }

    // Метод для встряски камеры (screen shake)
    public async Task Shake(float intensity, float duration)
    {
        Vector2 originalOffset = Offset;
        float elapsed = 0;

        while (elapsed < duration)
        {
            Vector2 shakeOffset = new Vector2(
                GD.Randf() * 2 - 1,
                GD.Randf() * 2 - 1
            ) * intensity * (1 - elapsed / duration);

            Offset = originalOffset + shakeOffset;
            elapsed += (float)GetProcessDeltaTime();
            await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
        }

        Offset = originalOffset;
    }
}

Движение и физика 2D

CharacterBody2D
Важно в 4.5.1
В Godot 4.5.1 KinematicBody2D заменен на CharacterBody2D. Это основной класс для управляемых персонажей с коллизиями.
public partial class Player : CharacterBody2D
{
    [Export]
    public float Speed { get; set; } = 300.0f;

    [Export]
    public float JumpVelocity { get; set; } = -400.0f;

    // Получаем гравитацию из Project Settings
    private float _gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();

    public override void _PhysicsProcess(double delta)
    {
        Vector2 velocity = Velocity;

        // Добавляем гравитацию если не на земле
        if (!IsOnFloor())
            velocity.Y += _gravity * (float)delta;

        // Обработка прыжка
        if (Input.IsActionJustPressed("jump") && IsOnFloor())
            velocity.Y = JumpVelocity;

        // Получение ввода движения
        Vector2 direction = Input.GetVector("move_left", "move_right", "move_up", "move_down");
        if (direction != Vector2.Zero)
        {
            velocity.X = direction.X * Speed;
        }
        else
        {
            // Постепенная остановка
            velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
        }

        Velocity = velocity;
        MoveAndSlide();

        // Обработка скольжения по стенам (wall sliding)
        if (IsOnWall() && !IsOnFloor() && velocity.Y > 0)
        {
            // Уменьшаем скорость падения при скольжении по стене
            velocity.Y *= 0.7f;
        }
    }
}
RigidBody2D
Физическое тело
Тело с физикой, управляемое движком Godot. Подходит для объектов, которые должны реагировать на физические силы.
public partial class PhysicsObject : RigidBody2D
{
    [Export]
    public float ExplosionForce { get; set; } = 500.0f;

    private bool _hasExploded = false;

    public void ApplyExplosion(Vector2 explosionOrigin)
    {
        if (_hasExploded) return;

        _hasExploded = true;

        // Вычисляем направление взрыва
        Vector2 direction = (GlobalPosition - explosionOrigin).Normalized();
        float distance = GlobalPosition.DistanceTo(explosionOrigin);

        // Уменьшаем силу взрыва с расстоянием
        float force = ExplosionForce / (distance + 1);

        // Применяем импульс
        ApplyCentralImpulse(direction * force);

        // Добавляем случайное вращение
        ApplyTorqueImpulse(GD.Randf() * 1000 - 500);

        // Запускаем таймер самоуничтожения
        GetNode<Timer>("DestroyTimer").Start();
    }

    // Обработка столкновений
    private void OnBodyEntered(Node body)
    {
        if (body is Player player)
        {
            player.TakeDamage(10);
        }
    }
}

Коллизии и детекты

Area2D
Область детекции
Нода для создания зон детекции. Может обнаруживать вход/выход других Area2D или PhysicsBody2D.
public partial class DetectionZone : Area2D
{
    [Signal]
    public delegate void PlayerDetectedEventHandler(Node2D player);

    [Signal]
    public delegate void PlayerLostEventHandler();

    private Player _detectedPlayer;

    public override void _Ready()
    {
        // Подключаем сигналы
        BodyEntered += OnBodyEntered;
        BodyExited += OnBodyExited;
    }

    private void OnBodyEntered(Node2D body)
    {
        if (body is Player player)
        {
            _detectedPlayer = player;
            EmitSignal(SignalName.PlayerDetected, player);
        }
    }

    private void OnBodyExited(Node2D body)
    {
        if (body == _detectedPlayer)
        {
            _detectedPlayer = null;
            EmitSignal(SignalName.PlayerLost);
        }
    }

    public bool CanSeePlayer()
    {
        return _detectedPlayer != null;
    }

    public Vector2 GetPlayerPosition()
    {
        if (_detectedPlayer != null)
            return _detectedPlayer.GlobalPosition;

        return Vector2.Zero;
    }
}
RayCast2D
Луч для детекции
Используется для проверки линии прямой видимости или расстояния до объектов.
public partial class EnemySight : RayCast2D
{
    [Export]
    public float SightDistance { get; set; } = 300.0f;

    [Export]
    public Node2D Target { get; set; }

    public override void _Ready()
    {
        TargetPosition = new Vector2(SightDistance, 0);
    }

    public override void _Process(double delta)
    {
        if (Target != null)
        {
            // Направляем луч к цели
            Vector2 direction = (Target.GlobalPosition - GlobalPosition).Normalized();
            TargetPosition = direction * SightDistance;
        }

        // Обновляем луч
        ForceRaycastUpdate();
    }

    public bool CanSeeTarget()
    {
        if (!IsColliding()) return false;

        var collider = GetCollider();
        return collider == Target;
    }

    public Vector2 GetCollisionPoint()
    {
        if (IsColliding())
            return GetCollisionPoint();

        return GlobalPosition + TargetPosition;
    }
}

Сигналы и события

Новое в 4.5.1

В Godot 4.5.1 улучшена работа с сигналами в C#. Сигналы теперь могут быть объявлены с использованием атрибута [Signal].

Объявление сигналов
C# 4.5.1
Правильное объявление и использование сигналов в Godot 4.5.1 C#.
public partial class GameEvents : Node
{
    // Объявление сигналов с атрибутом [Signal]
    [Signal]
    public delegate void PlayerHealthChangedEventHandler(int currentHealth, int maxHealth);

    [Signal]
    public delegate void ScoreChangedEventHandler(int newScore);

    [Signal]
    public delegate void GameOverEventHandler(bool isWin);

    private int _score = 0;

    public void AddScore(int points)
    {
        _score += points;
        EmitSignal(SignalName.ScoreChanged, _score);
    }

    public void OnPlayerHealthChanged(int current, int max)
    {
        EmitSignal(SignalName.PlayerHealthChanged, current, max);
    }
}
Подписка на сигналы
Методы подписки
Различные способы подписки на сигналы в Godot 4.5.1 C#.
public partial class UI : Control
{
    [Export]
    public GameEvents GameEvents { get; set; }

    private Label _scoreLabel;
    private ProgressBar _healthBar;

    public override void _Ready()
    {
        _scoreLabel = GetNode<Label>("ScoreLabel");
        _healthBar = GetNode<ProgressBar>("HealthBar");

        if (GameEvents != null)
        {
            // Способ 1: Через Connect (старый способ)
            GameEvents.Connect(
                GameEvents.SignalName.ScoreChanged,
                Callable.From<int>(OnScoreChanged)
            );

            // Способ 2: Через += (новый способ в 4.5.1)
            GameEvents.PlayerHealthChanged += OnPlayerHealthChanged;
            GameEvents.GameOver += OnGameOver;
        }
    }

    private void OnScoreChanged(int newScore)
    {
        _scoreLabel.Text = $"Score: {newScore}";
    }

    private void OnPlayerHealthChanged(int current, int max)
    {
        _healthBar.MaxValue = max;
        _healthBar.Value = current;
    }

    private void OnGameOver(bool isWin)
    {
        var gameOverScreen = GetNode<Control>("GameOverScreen");
        gameOverScreen.Visible = true;

        var message = GetNode<Label>("GameOverScreen/Message");
        message.Text = isWin ? "You Win!" : "Game Over";
    }

    public override void _ExitTree()
    {
        // Важно отписаться от сигналов при уничтожении ноды
        if (GameEvents != null)
        {
            GameEvents.PlayerHealthChanged -= OnPlayerHealthChanged;
            GameEvents.GameOver -= OnGameOver;
        }
    }
}

Интерфейс 2D

Control ноды
UI система
Основные Control ноды для создания пользовательского интерфейса в 2D играх.
public partial class GameUI : Control
{
    [Export]
    public PackedScene PauseMenuScene { get; set; }

    private Control _pauseMenu;
    private Label _ammoLabel;
    private TextureProgressBar _healthBar;

    public override void _Ready()
    {
        _ammoLabel = GetNode<Label>("AmmoLabel");
        _healthBar = GetNode<TextureProgressBar>("HealthBar");

        // Создаем меню паузы
        if (PauseMenuScene != null)
        {
            _pauseMenu = PauseMenuScene.Instantiate<Control>();
            AddChild(_pauseMenu);
            _pauseMenu.Visible = false;
        }

        // Обработка нажатия Escape
        SetProcessInput(true);
    }

    public override void _Input(InputEvent event)
    {
        if (event.IsActionPressed("ui_cancel"))
        {
            TogglePauseMenu();
        }
    }

    public void UpdateAmmo(int current, int max)
    {
        _ammoLabel.Text = $"{current}/{max}";
    }

    public void UpdateHealth(float current, float max)
    {
        _healthBar.MaxValue = max;
        _healthBar.Value = current;

        // Меняем цвет в зависимости от здоровья
        float ratio = current / max;
        if (ratio > 0.6)
            _healthBar.TintProgress = Colors.Green;
        else if (ratio > 0.3)
            _healthBar.TintProgress = Colors.Yellow;
        else
            _healthBar.TintProgress = Colors.Red;
    }

    private void TogglePauseMenu()
    {
        if (_pauseMenu != null)
        {
            bool isVisible = !_pauseMenu.Visible;
            _pauseMenu.Visible = isVisible;
            GetTree().Paused = isVisible;

            // Захват/освобождение мыши
            if (isVisible)
                Input.MouseMode = Input.MouseModeEnum.Visible;
            else
                Input.MouseMode = Input.MouseModeEnum.Captured;
        }
    }
}

Ресурсы 2D

Загрузка ресурсов
ResourceLoader
Методы загрузки и управления ресурсами в Godot 4.5.1 C#.
public partial class ResourceManager : Node
{
    // Кэш загруженных ресурсов
    private Dictionary<string, Resource> _resourceCache = new();

    // Предзагрузка ресурсов
    public void PreloadResources()
    {
        string[] resourcesToPreload = new[]
        {
            "res://assets/player.png",
            "res://scenes/enemy.tscn",
            "res://audio/hit.wav"
        };

        foreach (var path in resourcesToPreload)
        {
            LoadResource<Resource>(path);
        }
    }

    // Универсальный метод загрузки с кэшированием
    public T LoadResource<T>(string path) where T : Resource
    {
        if (_resourceCache.TryGetValue(path, out var cachedResource))
        {
            return cachedResource as T;
        }

        try
        {
            var resource = GD.Load<T>(path);
            if (resource != null)
            {
                _resourceCache[path] = resource;
                return resource;
            }
        }
        catch (Exception e)
        {
            GD.PrintErr($"Failed to load resource: {path}. Error: {e.Message}");
        }

        return null;
    }

    // Загрузка текстуры
    public Texture2D LoadTexture(string path)
    {
        return LoadResource<Texture2D>(path);
    }

    // Загрузка сцены
    public PackedScene LoadScene(string path)
    {
        return LoadResource<PackedScene>(path);
    }

    // Загрузка аудио
    public AudioStream LoadAudio(string path)
    {
        return LoadResource<AudioStream>(path);
    }

    // Очистка кэша
    public void ClearCache()
    {
        _resourceCache.Clear();
        GC.Collect();
    }
}

Оптимизация 2D игр

Оптимизация производительности
Godot 4.5.1
Ключевые техники оптимизации для 2D игр в Godot 4.5.1.
public partial class PerformanceOptimizer : Node
{
    // Использование VisibleOnScreenNotifier2D
    public void SetupVisibilityOptimization(Node2D target)
    {
        var notifier = new VisibleOnScreenNotifier2D();
        target.AddChild(notifier);

        // Отключаем обработку когда объект не виден
        notifier.ScreenEntered += () => target.SetProcess(true);
        notifier.ScreenExited += () => target.SetProcess(false);
    }

    // Оптимизация пула объектов
    public class ObjectPool<T> where T : Node2D, new()
    {
        private Queue<T> _pool = new Queue<T>();
        private int _maxSize;

        public ObjectPool(int initialSize, int maxSize)
        {
            _maxSize = maxSize;
            for (int i = 0; i < initialSize; i++)
            {
                _pool.Enqueue(new T());
            }
        }

        public T GetObject()
        {
            if (_pool.Count > 0)
                return _pool.Dequeue();

            if (_pool.Count < _maxSize)
                return new T();

            return null;
        }

        public void ReturnObject(T obj)
        {
            if (_pool.Count < _maxSize)
            {
                obj.Visible = false;
                obj.SetProcess(false);
                _pool.Enqueue(obj);
            }
            else
            {
                obj.QueueFree();
            }
        }
    }

    // Оптимизация через Level of Detail (LOD)
    public void SetupLOD(Node2D highDetail, Node2D lowDetail, float distanceThreshold)
    {
        var camera = GetViewport().GetCamera2D();
        if (camera == null) return;

        float distance = highDetail.GlobalPosition.DistanceTo(camera.GlobalPosition);

        if (distance > distanceThreshold)
        {
            highDetail.Visible = false;
            lowDetail.Visible = true;
        }
        else
        {
            highDetail.Visible = true;
            lowDetail.Visible = false;
        }
    }
}