Разработка игр на C# в Godot: пошаговое руководство для начинающих
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# включает несколько простых шагов:
- Запустите Godot и выберите "Новый проект"
- Укажите имя проекта и путь для сохранения
- В настройках проекта активируйте опцию "Использовать C# (.NET)"
- После создания проекта 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:
- В редакторе Godot щелкните правой кнопкой мыши на узле в дереве сцены
- Выберите "Создать скрипт"
- В выпадающем меню языков выберите "C#"
- Укажите имя файла (придерживайтесь стиля PascalCase для C# классов)
После создания скрипта Godot сгенерирует базовый шаблон класса, наследующего от типа выбранного узла. Например, для узла Node2D скрипт будет выглядеть так:
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 в определении класса:
public partial class Player : CharacterBody2D
{
// Код класса
}
Это связано с тем, что Godot генерирует дополнительный код для вашего класса, включая экспортируемые свойства и сигналы. Без модификатора partial ваш код просто не скомпилируется.
В отличие от GDScript, C# строго типизирован, что требует явного указания типов переменных и возвращаемых значений функций. Это может показаться излишним на первый взгляд, но значительно уменьшает количество ошибок во время выполнения:
// В GDScript
var speed = 5 # Тип определяется автоматически
// В C#
private int _speed = 5; // Явное указание типа
Для экспорта переменных в инспектор Godot используется атрибут [Export]. В C# можно комбинировать этот атрибут с дополнительными параметрами для более детальной настройки:
[Export] public float Speed { get; set; } = 5.0f;
[Export(PropertyHint.Range, "0,100,1")]
public int Health { get; set; } = 100;
Еще одна важная особенность – работа с сигналами (аналог событий). В C# для определения и подписки на сигналы используется следующий синтаксис:
// Определение сигнала
[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# скрипт наследуется от определенного типа узла и расширяет его функциональность:
// Скрипт для узла Sprite2D
public partial class PlayerSprite : Sprite2D
{
// Теперь класс имеет доступ ко всем методам и свойствам Sprite2D
public override void _Ready()
{
// Изменение свойства из базового класса
Texture = ResourceLoader.Load<Texture2D>("res://player.png");
}
}
Для получения ссылок на другие узлы в сцене C# предоставляет несколько подходов:
- Использование метода GetNode() с приведением типа:
private Label _scoreLabel;
public override void _Ready()
{
_scoreLabel = GetNode<Label>("UI/ScoreLabel");
_scoreLabel.Text = "Score: 0";
}
- Использование атрибута [NodePath] для автоматического связывания:
[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% в сценах с сложной иерархией. Теперь вся команда использует этот паттерн как стандарт.
Для динамического создания и управления узлами используется следующий подход:
// Создание нового узла
Label newLabel = new Label();
newLabel.Text = "Dynamically created!";
// Добавление узла в сцену
AddChild(newLabel);
// Позиционирование (если это узел с пространственными свойствами)
if (newLabel is Control controlLabel)
{
controlLabel.Position = new Vector2(100, 100);
}
Одной из ключевых особенностей Godot является система сигналов, которая позволяет узлам обмениваться сообщениями без жесткой связи между ними. В C# работа с сигналами выглядит так:
// Определение сигнала в классе
[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);
}
Для эффективного управления состоянием узлов и группирования их можно использовать группы:
// Добавление узла в группу
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 для правильной организации кода:
- _EnterTree() – вызывается, когда узел добавляется в дерево сцены
- _Ready() – вызывается, когда узел и все его дочерние узлы готовы
- _Process(delta) – вызывается каждый кадр для обновления логики
- _PhysicsProcess(delta) – вызывается на каждом физическом шаге (обычно 60 раз в секунду)
- _ExitTree() – вызывается, когда узел удаляется из дерева сцены
Распределение логики между этими методами – ключ к созданию хорошо структурированных и производительных игр на C# в Godot.
Создание игровых механик на C# в проектах Godot
Разработка игровых механик – это то, ради чего мы, собственно, и погружаемся в мир геймдева. C# в Godot предлагает невероятно гибкие инструменты для воплощения практически любых игровых идей – от простых аркад до сложных RPG с продвинутыми системами. 🎲
Начнем с базового примера – создания движения персонажа в 2D-игре:
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() автоматически обрабатывает коллизии
Для более сложных игр требуется разработка различных систем. Рассмотрим пример простой системы инвентаря:
// Определение предмета
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 (Паттерн состояний):
// Базовый интерфейс состояния
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# классами:
[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. 🚀
Наш проект будет состоять из следующих компонентов:
- Игрок (космический корабль с возможностью движения и стрельбы)
- Враги (простые астероиды и вражеские корабли)
- Система очков и жизней
- Простое игровое меню
Начнем с создания основных сцен нашего проекта:
| Сцена | Тип корневого узла | Назначение |
|---|---|---|
| Main.tscn | Node2D | Основная сцена, содержащая игровую логику |
| Player.tscn | CharacterBody2D | Корабль игрока с контролем движения и стрельбы |
| Enemy.tscn | RigidBody2D | Базовый враг (астероид) |
| Bullet.tscn | Area2D | Снаряд, выстреливаемый игроком |
| UI.tscn | CanvasLayer | Игровой интерфейс для отображения очков и жизней |
| MainMenu.tscn | Control | Стартовое меню игры |
Теперь реализуем контроллер игрока в файле Player.cs:
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:
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:
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:
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:
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:
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 4: секреты профессионалов
- Godot Engine: 20 успешных игр на бесплатном движке – примеры
- Где опубликовать игру на Godot: платформы для инди-разработчиков
- Godot Engine: создаем первую игру от установки до публикации
- Godot Engine для начинающих: создаем первую игру с нуля
- Ресурсы в Godot Engine: полное руководство для разработчиков игр
- GDScript для начинающих: основы языка программирования Godot
- Визуальное программирование в Godot: создание игр без кода
- Настройка физики и столкновений для идеальной 2D игры в Godot
- Оптимизация и архитектура Godot: избегаем ошибок в разработке