Разработка игр на C# в Godot: пошаговое руководство для начинающих AI: Разработка игр на C# в Godot: пошаговое руководство для начинающих
Пройти тест: моя идеальная работа
Узнай, какая работа подходит именно тебе по характеру и способностям
Пройти тест

Разработка игр на C# в Godot: пошаговое руководство для начинающих

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

AI: Разработка игр на C# в Godot: пошаговое руководство для начинающих Для кого эта статья:

  • Студенты и новички в сфере разработки игр
  • Энтузиасты, заинтересованные в создании игр на C# и Godot
  • Разработчики, переходящие с Unity на Godot

    Переход от идеи к первой работающей игре может казаться непреодолимой пропастью, особенно если вы только начинаете путь в геймдеве. C# в связке с Godot предлагает удивительно гибкий и мощный инструментарий, который помогает преодолеть эту пропасть гораздо быстрее, чем можно представить. Будь вы студент, энтузиаст или разработчик, мигрирующий с Unity — это руководство станет вашей дорожной картой в мир создания игр, где код не просто работает, а открывает бесконечные творческие возможности. 🎮

Изучение C# в контексте разработки игр на Godot может стать идеальной стартовой точкой для входа в мир программирования. Однако если вам интересно расширить свои возможности на рынке труда и освоить востребованный язык корпоративной разработки, обратите внимание на Курс Java-разработки от Skypro. Java и C# имеют схожий синтаксис, что делает переход между ними естественным, а навыки Java-разработки открывают двери в крупнейшие технологические компании мира.

C# в Godot: настройка среды и первые шаги

Начнем с самого важного – подготовки рабочей среды для разработки игр на C# в Godot. В отличие от GDScript, который встроен непосредственно в движок, C# требует дополнительных настроек, но предлагает взамен мощную типизацию и высокую производительность.

Прежде всего, необходимо убедиться, что вы используете правильную версию Godot с поддержкой C#. Загрузите официальную версию Godot с пометкой ".NET" или "Mono" с официального сайта. Эта версия включает интеграцию с .NET, необходимую для работы C# скриптов.

После установки Godot, вам потребуется установить .NET SDK. Это набор инструментов, необходимых для компиляции и выполнения C# кода. Минимальная рекомендуемая версия – .NET 6.0, которая обеспечивает стабильную работу с Godot 4.x.

Компонент Версия Примечание
Godot Engine 4.x (.NET/Mono) Специальная сборка с поддержкой C#
.NET SDK 6.0 или выше Необходим для компиляции C# кода
IDE Visual Studio Code/Rider Рекомендуются для удобной разработки

Для комфортной разработки рекомендую настроить внешнюю IDE. Хотя встроенный редактор Godot вполне функционален, специализированные среды разработки предоставляют лучшую поддержку C#:

  • Visual Studio Code с расширениями C# Dev Kit и Godot Tools
  • JetBrains Rider с плагином Godot
  • Visual Studio с установленной рабочей нагрузкой .NET

Создание первого проекта с C# включает несколько простых шагов:

  1. Запустите Godot и выберите "Новый проект"
  2. Укажите имя проекта и путь для сохранения
  3. В настройках проекта активируйте опцию "Использовать C# (.NET)"
  4. После создания проекта Godot автоматически сгенерирует файлы .csproj и .sln

После первого запуска проекта Godot компилирует все C# скрипты, что может занять некоторое время. Это нормально и происходит только при существенных изменениях кодовой базы.

Алексей Петров, преподаватель программирования игровых механик

Один из моих студентов, ранее работавший исключительно с Unity, испытывал серьезные трудности при переходе на Godot. Документация по C# в Godot казалась ему недостаточной, а примеры часто были написаны на GDScript. Мы решили создать пошаговый процесс настройки, который теперь использует вся наша группа.

Ключевым моментом стала правильная интеграция внешней IDE с Godot. Когда мы настроили Visual Studio Code с расширениями C# Dev Kit и Godot Tools, производительность студента выросла драматически. Возможность отлаживать код прямо из IDE, видеть ошибки компиляции в реальном времени и использовать автодополнение специфичных для Godot классов сделала процесс разработки намного комфортнее.

Теперь этот студент не только догнал группу, но и помогает другим с переходом с Unity на Godot, сохраняя знания C#.

Для создания вашего первого C# скрипта в Godot:

  1. В редакторе Godot щелкните правой кнопкой мыши на узле в дереве сцены
  2. Выберите "Создать скрипт"
  3. В выпадающем меню языков выберите "C#"
  4. Укажите имя файла (придерживайтесь стиля PascalCase для C# классов)

После создания скрипта Godot сгенерирует базовый шаблон класса, наследующего от типа выбранного узла. Например, для узла Node2D скрипт будет выглядеть так:

csharp
Скопировать код
using Godot;
using System;

public partial class MyScript : Node2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
}

Этот шаблон включает два основных метода жизненного цикла: _Ready() выполняется один раз при добавлении узла в сцену, а _Process() вызывается каждый кадр.

Пошаговый план для смены профессии

Особенности синтаксиса C# при разработке в Godot

C# в Godot следует стандартным правилам языка, но имеет ряд особенностей, связанных со спецификой игрового движка. Понимание этих нюансов поможет избежать распространенных ошибок и эффективно использовать все возможности языка.

Первое, что необходимо отметить – все скрипты C# в Godot должны включать ключевое слово partial в определении класса:

csharp
Скопировать код
public partial class Player : CharacterBody2D
{
// Код класса
}

Это связано с тем, что Godot генерирует дополнительный код для вашего класса, включая экспортируемые свойства и сигналы. Без модификатора partial ваш код просто не скомпилируется.

В отличие от GDScript, C# строго типизирован, что требует явного указания типов переменных и возвращаемых значений функций. Это может показаться излишним на первый взгляд, но значительно уменьшает количество ошибок во время выполнения:

csharp
Скопировать код
// В GDScript
var speed = 5 # Тип определяется автоматически

// В C#
private int _speed = 5; // Явное указание типа

Для экспорта переменных в инспектор Godot используется атрибут [Export]. В C# можно комбинировать этот атрибут с дополнительными параметрами для более детальной настройки:

csharp
Скопировать код
[Export] public float Speed { get; set; } = 5.0f;

[Export(PropertyHint.Range, "0,100,1")] 
public int Health { get; set; } = 100;

Еще одна важная особенность – работа с сигналами (аналог событий). В C# для определения и подписки на сигналы используется следующий синтаксис:

csharp
Скопировать код
// Определение сигнала
[Signal] public delegate void HealthChanged(int newHealth);

// Вызов сигнала
EmitSignal(SignalName.HealthChanged, currentHealth);

// Подписка на сигнал (обычно в _Ready)
otherNode.Connect(OtherNode.SignalName.SomethingHappened, Callable.From(OnSomethingHappened));

Обратите внимание на использование SignalName – это автоматически генерируемое пространство имен, содержащее константы для всех сигналов класса.

Важно понимать разницу между синтаксисом C# и GDScript при доступе к узлам и ресурсам:

Операция GDScript C#
Доступ к дочернему узлу $ChildNode GetNode<Sprite2D>("ChildNode")
Получение родителя get_parent() GetParent<Node>()
Загрузка ресурса preload("res://path.tres") ResourceLoader.Load<Resource>("res://path.tres")
Создание экземпляра сцены scene.instantiate() scene.Instantiate<Node>()

Для улучшения читаемости кода в C# рекомендуется использовать следующие соглашения об именовании:

  • Имена классов и методов: PascalCase (например, PlayerController, CalculateDamage)
  • Приватные поля: camelCase с подчеркиванием (например, health, _movementSpeed)
  • Константы: UPPERSNAKECASE (например, MAXHEALTH, DEFAULTSPEED)
  • Свойства: PascalCase (например, Health, MovementSpeed)

Следование этим соглашениям сделает ваш код более понятным для других разработчиков и соответствующим общепринятым стандартам C#.

Основы взаимодействия C# скриптов с узлами Godot

Успешное программирование на C# в Godot во многом зависит от правильного взаимодействия ваших скриптов с узлами движка. Узлы (Nodes) являются фундаментальными строительными блоками всех игр в Godot, и C# предоставляет мощные инструменты для управления ими.

В основе взаимодействия с узлами лежит система наследования. Каждый C# скрипт наследуется от определенного типа узла и расширяет его функциональность:

csharp
Скопировать код
// Скрипт для узла Sprite2D
public partial class PlayerSprite : Sprite2D
{
// Теперь класс имеет доступ ко всем методам и свойствам Sprite2D
public override void _Ready()
{
// Изменение свойства из базового класса
Texture = ResourceLoader.Load<Texture2D>("res://player.png");
}
}

Для получения ссылок на другие узлы в сцене C# предоставляет несколько подходов:

  1. Использование метода GetNode() с приведением типа:
csharp
Скопировать код
private Label _scoreLabel;

public override void _Ready()
{
_scoreLabel = GetNode<Label>("UI/ScoreLabel");
_scoreLabel.Text = "Score: 0";
}

  1. Использование атрибута [NodePath] для автоматического связывания:
csharp
Скопировать код
[NodePath("UI/ScoreLabel")]
private Label _scoreLabel = null;

public override void _Ready()
{
// Узел уже доступен, нет необходимости в GetNode
_scoreLabel.Text = "Score: 0";
}

Второй подход более декларативный и позволяет избежать дублирования строковых путей, но требует точного соответствия путей в дереве сцены.

Михаил Соколов, разработчик игровых механик

Когда я начал переводить нашу игровую студию с Unity на Godot, самым сложным оказалось переучить команду эффективно работать с деревом узлов через C#. Особенно много проблем возникало с доступом к узлам и обработкой их сигналов.

Мы разработали подход, который значительно упростил код и уменьшил количество ошибок. Вместо традиционных GetNode() вызовов для часто используемых узлов мы создали класс-расширение, который кэширует ссылки:

csharp
Скопировать код
public static class NodeExtensions
{
private static Dictionary<(Node, string), Node> _cachedNodes = new();

public static T GetCached<T>(this Node node, string path) where T : Node
{
var key = (node, path);
if (!_cachedNodes.ContainsKey(key))
_cachedNodes[key] = node.GetNode(path);

return _cachedNodes[key] as T;
}
}

Этот простой метод расширения позволил нам избежать повторных поисков узлов и повысил производительность игры на 15% в сценах с сложной иерархией. Теперь вся команда использует этот паттерн как стандарт.

Для динамического создания и управления узлами используется следующий подход:

csharp
Скопировать код
// Создание нового узла
Label newLabel = new Label();
newLabel.Text = "Dynamically created!";

// Добавление узла в сцену
AddChild(newLabel);

// Позиционирование (если это узел с пространственными свойствами)
if (newLabel is Control controlLabel)
{
controlLabel.Position = new Vector2(100, 100);
}

Одной из ключевых особенностей Godot является система сигналов, которая позволяет узлам обмениваться сообщениями без жесткой связи между ними. В C# работа с сигналами выглядит так:

csharp
Скопировать код
// Определение сигнала в классе
[Signal]
public delegate void PlayerDied(Vector2 position, int score);

// Вызов сигнала
EmitSignal(SignalName.PlayerDied, GlobalPosition, _currentScore);

// Подключение к сигналу в другом скрипте
public override void _Ready()
{
var player = GetNode<Player>("Player");
player.Connect(Player.SignalName.PlayerDied, Callable.From(OnPlayerDied));
}

// Обработчик сигнала
private void OnPlayerDied(Vector2 position, int score)
{
ShowGameOver(score);
SpawnExplosion(position);
}

Для эффективного управления состоянием узлов и группирования их можно использовать группы:

csharp
Скопировать код
// Добавление узла в группу
public override void _Ready()
{
AddToGroup("enemies");
}

// В другом скрипте: получение всех узлов из группы
private void DestroyAllEnemies()
{
foreach (Node enemy in GetTree().GetNodesInGroup("enemies"))
{
if (enemy is Enemy enemyNode)
{
enemyNode.TakeDamage(999);
}
}
}

Важно понимать жизненный цикл узла в Godot для правильной организации кода:

  1. _EnterTree() – вызывается, когда узел добавляется в дерево сцены
  2. _Ready() – вызывается, когда узел и все его дочерние узлы готовы
  3. _Process(delta) – вызывается каждый кадр для обновления логики
  4. _PhysicsProcess(delta) – вызывается на каждом физическом шаге (обычно 60 раз в секунду)
  5. _ExitTree() – вызывается, когда узел удаляется из дерева сцены

Распределение логики между этими методами – ключ к созданию хорошо структурированных и производительных игр на C# в Godot.

Создание игровых механик на C# в проектах Godot

Разработка игровых механик – это то, ради чего мы, собственно, и погружаемся в мир геймдева. C# в Godot предлагает невероятно гибкие инструменты для воплощения практически любых игровых идей – от простых аркад до сложных RPG с продвинутыми системами. 🎲

Начнем с базового примера – создания движения персонажа в 2D-игре:

csharp
Скопировать код
public partial class Player : CharacterBody2D
{
[Export] public float Speed { get; set; } = 300.0f;
[Export] public float JumpVelocity { get; set; } = -400.0f;

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("ui_accept") && IsOnFloor())
velocity.Y = JumpVelocity;

// Получаем направление горизонтального движения
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
velocity.X = direction.X * Speed;

Velocity = velocity;
MoveAndSlide();
}
}

Этот код демонстрирует несколько важных паттернов разработки игровых механик:

  • Использование _PhysicsProcess для физических расчетов обеспечивает стабильное поведение, независимое от частоты кадров
  • Экспортируемые свойства Speed и JumpVelocity позволяют настраивать механику без изменения кода
  • Использование встроенного класса CharacterBody2D с методом MoveAndSlide() автоматически обрабатывает коллизии

Для более сложных игр требуется разработка различных систем. Рассмотрим пример простой системы инвентаря:

csharp
Скопировать код
// Определение предмета
public class Item
{
public string Name { get; set; }
public string Description { get; set; }
public Texture2D Icon { get; set; }
public int MaxStackSize { get; set; } = 1;

public virtual void Use(Player player)
{
// Базовая реализация
GD.Print($"Using item: {Name}");
}
}

// Класс конкретного предмета
public class HealthPotion : Item
{
public int HealAmount { get; set; } = 20;

public override void Use(Player player)
{
player.Heal(HealAmount);
GD.Print($"Healed for {HealAmount} HP");
}
}

// Система инвентаря
public partial class Inventory : Node
{
[Signal] public delegate void InventoryChangedEventHandler();

private List<(Item Item, int Count)> _items = new();
[Export] public int MaxSlots { get; set; } = 20;

public bool AddItem(Item item, int count = 1)
{
// Ищем существующий стек этого предмета
var existingStack = _items.FindIndex(i => i.Item.Name == item.Name && i.Count < i.Item.MaxStackSize);

if (existingStack >= 0)
{
var currentStack = _items[existingStack];
int canAdd = Math.Min(count, currentStack.Item.MaxStackSize – currentStack.Count);

_items[existingStack] = (currentStack.Item, currentStack.Count + canAdd);
count -= canAdd;

if (count <= 0)
{
EmitSignal(SignalName.InventoryChanged);
return true;
}
}

// Если остались предметы или нет подходящего стека, создаем новый
if (_items.Count >= MaxSlots)
return false; // Инвентарь полон

_items.Add((item, count));
EmitSignal(SignalName.InventoryChanged);
return true;
}

public Item GetItem(int slot)
{
if (slot >= 0 && slot < _items.Count)
return _items[slot].Item;
return null;
}

public bool UseItem(int slot, Player player)
{
if (slot >= 0 && slot < _items.Count)
{
var (item, count) = _items[slot];
item.Use(player);

// Удаляем предмет после использования
if (count > 1)
_items[slot] = (item, count – 1);
else
_items.RemoveAt(slot);

EmitSignal(SignalName.InventoryChanged);
return true;
}
return false;
}
}

При разработке более сложных игр стоит рассмотреть использование различных архитектурных паттернов. Одним из наиболее полезных в геймдеве является State Pattern (Паттерн состояний):

csharp
Скопировать код
// Базовый интерфейс состояния
public interface IState
{
void Enter();
void Exit();
void Update(double delta);
void PhysicsUpdate(double delta);
void HandleInput();
}

// Конкретное состояние – бег
public class RunningState : IState
{
private PlayerController _player;

public RunningState(PlayerController player)
{
_player = player;
}

public void Enter()
{
_player.Animator.Play("run");
}

public void Exit()
{
// Очистка при выходе из состояния
}

public void HandleInput()
{
if (Input.IsActionJustPressed("jump") && _player.IsOnFloor())
_player.StateMachine.ChangeState(_player.JumpingState);
else if (Input.GetActionStrength("move_right") == 0 && Input.GetActionStrength("move_left") == 0)
_player.StateMachine.ChangeState(_player.IdleState);
}

public void Update(double delta)
{
// Логика не связанная с физикой
}

public void PhysicsUpdate(double delta)
{
float moveInput = Input.GetActionStrength("move_right") – Input.GetActionStrength("move_left");
_player.Velocity = new Vector2(moveInput * _player.RunSpeed, _player.Velocity.Y);
_player.MoveAndSlide();
}
}

// Машина состояний
public class StateMachine
{
public IState CurrentState { get; private set; }

public void Initialize(IState startingState)
{
CurrentState = startingState;
CurrentState.Enter();
}

public void ChangeState(IState newState)
{
CurrentState.Exit();
CurrentState = newState;
CurrentState.Enter();
}

public void Update(double delta)
{
CurrentState?.HandleInput();
CurrentState?.Update(delta);
}

public void PhysicsUpdate(double delta)
{
CurrentState?.PhysicsUpdate(delta);
}
}

Для организации диалоговых систем и повествования можно использовать ресурсы в сочетании с C# классами:

csharp
Скопировать код
[Tool]
public partial class DialogueResource : Resource
{
[Export] public string SpeakerName { get; set; } = "";
[Export] public string DialogueText { get; set; } = "";
[Export] public Texture2D SpeakerPortrait { get; set; }
[Export] public DialogueResource[] Responses { get; set; } = Array.Empty<DialogueResource>();

// Событие, которое произойдёт при завершении диалога
[Export] public string EventName { get; set; } = "";
}

// Менеджер диалогов
public partial class DialogueManager : CanvasLayer
{
[Export] private NodePath _dialoguePanelPath;
[Export] private NodePath _speakerLabelPath;
[Export] private NodePath _dialogueLabelPath;
[Export] private NodePath _portraitTexturePath;
[Export] private NodePath _responseContainerPath;
[Export] private PackedScene _responseButtonScene;

private Control _dialoguePanel;
private Label _speakerLabel;
private RichTextLabel _dialogueLabel;
private TextureRect _portraitTexture;
private VBoxContainer _responseContainer;

private DialogueResource _currentDialogue;

public override void _Ready()
{
_dialoguePanel = GetNode<Control>(_dialoguePanelPath);
_speakerLabel = GetNode<Label>(_speakerLabelPath);
_dialogueLabel = GetNode<RichTextLabel>(_dialogueLabelPath);
_portraitTexture = GetNode<TextureRect>(_portraitTexturePath);
_responseContainer = GetNode<VBoxContainer>(_responseContainerPath);

_dialoguePanel.Visible = false;
}

public void StartDialogue(DialogueResource dialogue)
{
_currentDialogue = dialogue;
_dialoguePanel.Visible = true;

_speakerLabel.Text = dialogue.SpeakerName;
_dialogueLabel.Text = dialogue.DialogueText;
_portraitTexture.Texture = dialogue.SpeakerPortrait;

// Очищаем предыдущие варианты ответов
foreach (Node child in _responseContainer.GetChildren())
{
child.QueueFree();
}

// Добавляем новые варианты ответов
foreach (var response in dialogue.Responses)
{
var button = _responseButtonScene.Instantiate<Button>();
button.Text = response.DialogueText;
button.Connect("pressed", Callable.From(() => OnResponseSelected(response)));
_responseContainer.AddChild(button);
}

// Если нет вариантов ответов, добавляем кнопку "Продолжить"
if (dialogue.Responses.Length == 0)
{
var button = _responseButtonScene.Instantiate<Button>();
button.Text = "Продолжить";
button.Connect("pressed", Callable.From(() => EndDialogue()));
_responseContainer.AddChild(button);
}
}

private void OnResponseSelected(DialogueResource response)
{
StartDialogue(response);
}

private void EndDialogue()
{
_dialoguePanel.Visible = false;

if (!string.IsNullOrEmpty(_currentDialogue.EventName))
{
EmitSignal(_currentDialogue.EventName);
}
}
}

Эти примеры демонстрируют, как C# позволяет создавать модульные, хорошо структурированные игровые системы в Godot. При разработке сложных механик всегда стоит придерживаться принципов SOLID, что особенно просто делать в строго типизированном языке, таком как C#.

Практический проект: от идеи к первой игре на C# в Godot

Теория без практики остается лишь абстрактным знанием. Давайте применим полученные знания для создания небольшой, но полноценной 2D-игры – космического шутера с базовой механикой стрельбы и уклонения от препятствий. Этот проект охватит все основные аспекты разработки игры на C# в Godot. 🚀

Наш проект будет состоять из следующих компонентов:

  1. Игрок (космический корабль с возможностью движения и стрельбы)
  2. Враги (простые астероиды и вражеские корабли)
  3. Система очков и жизней
  4. Простое игровое меню

Начнем с создания основных сцен нашего проекта:

Сцена Тип корневого узла Назначение
Main.tscn Node2D Основная сцена, содержащая игровую логику
Player.tscn CharacterBody2D Корабль игрока с контролем движения и стрельбы
Enemy.tscn RigidBody2D Базовый враг (астероид)
Bullet.tscn Area2D Снаряд, выстреливаемый игроком
UI.tscn CanvasLayer Игровой интерфейс для отображения очков и жизней
MainMenu.tscn Control Стартовое меню игры

Теперь реализуем контроллер игрока в файле Player.cs:

csharp
Скопировать код
public partial class Player : CharacterBody2D
{
[Export] public float Speed { get; set; } = 300.0f;
[Export] public PackedScene BulletScene { get; set; }
[Export] public int MaxHealth { get; set; } = 3;

[Signal] public delegate void HealthChangedEventHandler(int health);
[Signal] public delegate void PlayerDiedEventHandler();

private int _health;
private float _shootCooldown = 0.0f;
private const float ShootDelay = 0.2f;

public override void _Ready()
{
_health = MaxHealth;
EmitSignal(SignalName.HealthChanged, _health);
}

public override void _Process(double delta)
{
// Обработка стрельбы
_shootCooldown -= (float)delta;

if (Input.IsActionPressed("shoot") && _shootCooldown <= 0)
{
Shoot();
_shootCooldown = ShootDelay;
}
}

public override void _PhysicsProcess(double delta)
{
// Обработка движения
Vector2 direction = Input.GetVector("move_left", "move_right", "move_up", "move_down");
Velocity = direction * Speed;

MoveAndSlide();

// Ограничиваем движение в пределах экрана
Vector2 viewportSize = GetViewportRect().Size;
Vector2 position = Position;

position.X = Mathf.Clamp(position.X, 0, viewportSize.X);
position.Y = Mathf.Clamp(position.Y, 0, viewportSize.Y);

Position = position;
}

private void Shoot()
{
if (BulletScene != null)
{
var bullet = BulletScene.Instantiate<Bullet>();
GetParent().AddChild(bullet);
bullet.Position = Position + new Vector2(0, -30); // Позиция перед кораблем
bullet.Launch(new Vector2(0, -1)); // Направление вверх
}
}

public void TakeDamage()
{
_health--;
EmitSignal(SignalName.HealthChanged, _health);

if (_health <= 0)
{
Die();
}
else
{
// Визуальный эффект получения урона
var tween = CreateTween();
tween.TweenProperty(this, "modulate", Colors.Red, 0.1f);
tween.TweenProperty(this, "modulate", Colors.White, 0.1f);
}
} 

private void Die()
{
EmitSignal(SignalName.PlayerDied);
QueueFree();
}
}

Далее реализуем класс снаряда в файле Bullet.cs:

csharp
Скопировать код
public partial class Bullet : Area2D
{
[Export] public float Speed { get; set; } = 700.0f;

private Vector2 _direction = Vector2.Up;

public void Launch(Vector2 direction)
{
_direction = direction.Normalized();

// Устанавливаем угол поворота в соответствии с направлением
Rotation = Mathf.Atan2(_direction.Y, _direction.X) + Mathf.Pi/2;
}

public override void _Process(double delta)
{
Position += _direction * Speed * (float)delta;

// Удаление снаряда, если он покинул экран
if (!GetViewportRect().HasPoint(Position))
{
QueueFree();
}
}

private void OnBodyEntered(Node2D body)
{
if (body is Enemy enemy)
{
enemy.Destroy();
QueueFree();
}
}
}

Создадим класс врага в файле Enemy.cs:

csharp
Скопировать код
public partial class Enemy : RigidBody2D
{
[Signal] public delegate void EnemyDestroyedEventHandler(int points, Vector2 position);

[Export] public int PointValue { get; set; } = 10;
[Export] public PackedScene ExplosionEffect { get; set; }

public void Destroy()
{
EmitSignal(SignalName.EnemyDestroyed, PointValue, Position);

// Создание эффекта взрыва
if (ExplosionEffect != null)
{
var explosion = ExplosionEffect.Instantiate<Node2D>();
GetParent().AddChild(explosion);
explosion.GlobalPosition = GlobalPosition;
}

QueueFree();
}

private void OnBodyEntered(Node2D body)
{
if (body is Player player)
{
player.TakeDamage();
Destroy();
}
}
}

Теперь создадим главный скрипт игры в файле Main.cs:

csharp
Скопировать код
public partial class Main : Node2D
{
[Export] public PackedScene EnemyScene { get; set; }
[Export] public NodePath ScoreLabelPath { get; set; }
[Export] public NodePath GameOverPanelPath { get; set; }

private Label _scoreLabel;
private Control _gameOverPanel;
private int _score = 0;
private Random _random = new Random();
private double _enemySpawnTime = 0;

public override void _Ready()
{
_scoreLabel = GetNode<Label>(ScoreLabelPath);
_gameOverPanel = GetNode<Control>(GameOverPanelPath);
_gameOverPanel.Visible = false;

// Подключаемся к сигналу от игрока
var player = GetNode<Player>("Player");
player.Connect(Player.SignalName.PlayerDied, Callable.From(OnPlayerDied));

UpdateScoreLabel();
}

public override void _Process(double delta)
{
_enemySpawnTime -= delta;

if (_enemySpawnTime <= 0)
{
SpawnEnemy();
_enemySpawnTime = 1.0 + _random.NextDouble() * 2.0; // Случайный интервал между 1 и 3 секундами
}
}

private void SpawnEnemy()
{
if (EnemyScene != null)
{
var enemy = EnemyScene.Instantiate<Enemy>();
AddChild(enemy);

// Подключаемся к сигналу врага
enemy.Connect(Enemy.SignalName.EnemyDestroyed, Callable.From<int, Vector2>(OnEnemyDestroyed));

// Устанавливаем случайную позицию врага в верхней части экрана
var viewportWidth = GetViewportRect().Size.X;
enemy.Position = new Vector2((float)_random.NextDouble() * viewportWidth, -50);

// Добавляем небольшой случайный импульс
enemy.LinearVelocity = new Vector2((float)(_random.NextDouble() * 100 – 50), (float)(_random.NextDouble() * 100 + 50));
}
}

private void OnEnemyDestroyed(int points, Vector2 position)
{
_score += points;
UpdateScoreLabel();
}

private void UpdateScoreLabel()
{
_scoreLabel.Text = $"Score: {_score}";
}

private void OnPlayerDied()
{
_gameOverPanel.Visible = true;
GetTree().Paused = true; // Останавливаем игру
}

public void OnRestartButtonPressed()
{
GetTree().Paused = false; // Возобновляем игру
GetTree().ReloadCurrentScene(); // Перезагружаем текущую сцену
}

public void OnMainMenuButtonPressed()
{
GetTree().Paused = false; // Возобновляем игру
GetTree().ChangeSceneToFile("res://MainMenu.tscn"); // Переходим в главное меню
}
}

Для интерфейса создадим простой класс UI.cs:

csharp
Скопировать код
public partial class UI : CanvasLayer
{
[Export] public NodePath HealthContainerPath { get; set; }
[Export] public Texture2D HealthIconTexture { get; set; }

private HBoxContainer _healthContainer;

public override void _Ready()
{
_healthContainer = GetNode<HBoxContainer>(HealthContainerPath);

// Подключаемся к сигналу изменения здоровья игрока
var player = GetTree().GetFirstNodeInGroup("Player") as Player;
if (player != null)
{
player.Connect(Player.SignalName.HealthChanged, Callable.From<int>(UpdateHealthDisplay));
UpdateHealthDisplay(player.MaxHealth); // Инициализация начального отображения
}
}

private void UpdateHealthDisplay(int health)
{
// Удаляем все существующие иконки здоровья
foreach (var child in _healthContainer.GetChildren())
{
child.QueueFree();
}

// Добавляем новые иконки в соответствии с текущим здоровьем
for (int i = 0; i < health; i++)
{
var healthIcon = new TextureRect
{
Texture = HealthIconTexture,
StretchMode = TextureRect.StretchModeEnum.KeepAspect,
CustomMinimumSize = new Vector2(30, 30)
};

_healthContainer.AddChild(healthIcon);
}
}
}

И, наконец, создадим простое главное меню в файле MainMenu.cs:

csharp
Скопировать код
public partial class MainMenu : Control
{
public void OnStartButtonPressed()
{
GetTree().ChangeSceneToFile("res://Main.tscn");
}

public void OnQuitButtonPressed()
{
GetTree().Quit();
}
}

Это основные компоненты нашей простой игры. Для полноценной реализации вам потребуется также создать соответствующие сцены в редакторе Godot и настроить их согласно кодовой структуре. Проект демонстрирует применение принципов ООП, обработку ввода, сигналы, физику и другие основные концепции разработки игр в Godot на C#.

Для улучшения игры можно добавить:

  • Различные типы врагов и оружия
  • Систему уровней с нарастающей сложностью
  • Визуальные и звуковые эффекты
  • Систему усилений (power-ups)
  • Сохранение рекордов

Этот проект послужит отличной основой для дальнейшего обучения и экспериментов с C# в Godot. По мере развития ваших навыков, вы сможете реализовать более сложные игровые механики и системы, постепенно превращая простую игру в полноценный проект. 🎯

Освоение C# в связке с Godot открывает перед вами двери в мир профессиональной разработки игр. Мощь строгой типизации, обширные библиотеки .NET и гибкость Godot создают идеальную комбинацию для воплощения самых смелых игровых идей. Не останавливайтесь на теории — практикуйтесь, экспериментируйте и постепенно расширяйте границы созданного проекта. Каждая строчка кода, каждый игровой механизм, реализованный вами, приближает вас к мастерству в этой увлекательной сфере. Разработка игр — это не просто профессия, это искусство превращения воображения в интерактивные миры, доступные миллионам.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой язык программирования используется в Godot для написания скриптов?
1 / 5

Загрузка...