HUD - Інтерфейс - GUI
У цій темі ми розглянемо проєкт у якому реалізовано HUD.
(Опціонально) Скачайте заготовку проєкту.
Підготовка основної сцени
Додайте Node HUD (та його наповнення) для основи нашого GUI.
Що у вас повинно вийти
- Node2D
- Map
- {...}
- Player
- {...}
- HUD
- PanelContainer
- MarginContainer
- VBoxContainer
- Label
- Label2
Перейменуйте Label та Label2 на LabelBallsLeft та LabelTimeout:
Що у вас повинно вийти
- Node2D
- Map
- {...}
- Player
- {...}
- HUD
- PanelContainer
- MarginContainer
- VBoxContainer
- LabelBallsLeft
- LabelTimeout
Настройка HUD елементів.
Конфігурація HUD.
Додайте placeholder текст:
Настройте PanelContainer, змініть значення Layout>Anchor Preset на Top Right:
Настройте MarginContainer, змініть значення Theme Overrides>Constants>Margin Left/Right/Top/Bottom на 20:
Настройте LabelBallsLeft та LabelTimeout, змініть значення Theme Overrides>Font Sizes>Font Size на 30:
Додавання коду та конфігурація проєкту.
Приєднання коду до Node-ів та до проєкту.
PlayerCharacterBody2d.cs
Додайте код гравця:
using System;
using Godot;
// Клас PlayerCharacterBody2d успадковує CharacterBody2D, що дозволяє йому мати фізичні властивості, такі як маса, прискорення, колізії.
public partial class PlayerCharacterBody2d : CharacterBody2D
{
// Константи для фізичних параметрів гравця
private const float Acceleration = 300.0f; // Прискорення при русі
private const float JumpAcceleration = 700f; // Сила стрибка
private const float MaxSpeed = 350; // Максимальна швидкість руху
private const float AnimationMaxFPS = 20; // Максимальна швидкість анимації для ходьби
private const float PushForce = 1000; // Сила відштовхування при колізії з іншими об'єктами
// Вектор гравітації (вниз)
private Vector2 Gravity = new Vector2(0, 50f);
// Посилання на AnimatedSprite2D для відображення анімації
private AnimatedSprite2D AnimatedSprite2D;
// Метод викликається один раз під час ініціалізації об'єкта
public override void _Ready()
{
// Отримуємо посилання на AnimatedSprite2D з сцени
AnimatedSprite2D = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
}
// Основний метод фізичного процесу, викликається кожен кадр
public override void _PhysicsProcess(double delta)
{
// Отримуємо напрямок руху з методу GetDirection()
Vector2 direction = GetDirection();
Vector2 velocity = Velocity;
// Додаємо прискорення в напрямку руху
velocity += direction * Acceleration * (float)delta;
// Додаємо гравітацію
velocity += Gravity;
// Обмежуємо швидкість по X, щоб не перевищувала MaxSpeed
velocity.X = Mathf.Clamp(velocity.X, -MaxSpeed, MaxSpeed);
// Якщо гравець рухається по X
if (velocity.X != 0)
{
// Якщо рух відбувається вправо
if (velocity.X > 0)
{
AnimatedSprite2D.FlipH = true; // Перевертаємо спрайт вправо
}
// Якщо рух відбувається вліво
else if (velocity.X < 0)
{
AnimatedSprite2D.FlipH = false; // Перевертаємо спрайт вліво
}
// Встановлюємо анімацію ходьби
AnimatedSprite2D.Animation = "walk";
// Змінюємо швидкість анімації залежно від швидкості руху
AnimatedSprite2D.SpriteFrames.SetAnimationSpeed("walk", AnimationMaxFPS * (Math.Abs(velocity.X) / MaxSpeed));
}
else
{
// Якщо гравець не рухається по X, встановлюємо анімацію "idle"
AnimatedSprite2D.Animation = "idle";
}
// Запускаємо анімацію
AnimatedSprite2D.Play();
// Реалізуємо інерцію при зміні напрямку руху
if (direction.X == 1 && velocity.X < 0 || direction.X == -1 && velocity.X > 0)
{
velocity.X *= 0.90f; // Зменшуємо швидкість по X
}
else if (direction.X == 0)
{
velocity.X *= 0.90f; // Зменшуємо швидкість по X при відсутності напрямку
}
// Якщо натиснута клавіша пробіл і гравець на землі, робимо стрибок
if (Input.IsPhysicalKeyPressed(Key.Space) && IsOnFloor())
{
velocity.Y -= JumpAcceleration; // Змінюємо швидкість по Y вгору
}
// Встановлюємо нову швидкість гравця
Velocity = velocity;
// Викликаємо логіку створення куль
BallSpawnLogic();
// Виконуємо фізичний рух об'єкта
if (MoveAndSlide())
{
// Отримуємо кількість колізій
int collisionCount = GetSlideCollisionCount();
// Перебираємо всі колізії
for (int i = 0; i < collisionCount; i++)
{
KinematicCollision2D collision = GetSlideCollision(i);
// Якщо колізія відбувається з RigidBody2D
if (collision.GetCollider() is RigidBody2D collider)
{
// Застосовуємо силу відштовхування в напрямку нормалі колізії
collider.ApplyForce(collision.GetNormal() * -PushForce);
}
}
}
}
// Метод для створення куль при натисканні клавіші E
private void BallSpawnLogic()
{
// Якщо натиснута клавіша E
if (Input.IsPhysicalKeyPressed(Key.E))
{
// Отримуємо посилання на Global
Global global = GetNode<Global>("/root/Global");
// Викликаємо метод SpawnBall()
global.SpawnBall();
}
}
// Метод для визначення напрямку руху гравця
public Vector2 GetDirection()
{
Vector2 direction = new Vector2(); // Створюємо вектор напрямку
// Якщо натиснута клавіша W — рух вгору
if (Input.IsPhysicalKeyPressed(Key.W))
{
direction.Y += -1;
}
// Якщо натиснута клавіша S — рух вниз
if (Input.IsPhysicalKeyPressed(Key.S))
{
direction.Y += 1;
}
// Якщо натиснута клавіша A — рух вліво
if (Input.IsPhysicalKeyPressed(Key.A))
{
direction.X += -1;
}
// Якщо натиснута клавіша D — рух вправо
if (Input.IsPhysicalKeyPressed(Key.D))
{
direction.X += 1;
}
return direction; // Повертаємо напрямок руху
}
}Цей код представляє клас PlayerCharacterBody2d, який визначає поведінку гравця у 2D-грі, створеній за допомогою рушія Godot. Клас успадковує функціональність від CharacterBody2D, що дозволяє реалізувати фізичну взаємодію з ігровим світом, таку як колізії та рух.
Основна логіка класу реалізована в методі _PhysicsProcess, який викликається рушієм Godot кожного фізичного кадру. У цьому методі визначається рух гравця на основі вхідних даних від користувача (натискання клавіш W, A, S, D), застосовується гравітація та обробляються колізії.
- Рух:Клас визначає напрямок руху гравця на основі натиснутих клавіш та застосовує прискорення та гравітацію для симуляції реалістичного руху.
- Колізії:Після симуляції руху, код перевіряє наявність колізій з іншими об'єктами у грі. При виявленні колізії з
RigidBody2D, до нього застосовується сила відштовхування. - Анімація:Код змінює анімацію гравця залежно від напрямку його руху.
- Створення куль:При натисканні клавіші E, викликається метод SpawnBall глобального об'єкта Global, який відповідає за створення нових м'ячів у грі.
- Інерція:Реалізовано базову інерцію для більш плавного руху гравця, особливо при зміні напрямку.
Ball.cs
Додайте код м'яча:
using System;
using System.Collections.Generic;
using Godot;
// Клас Ball наслідується від RigidBody2D, що дозволяє йому мати фізичні властивості в Godot
public partial class Ball : RigidBody2D
{
// Змінна для відстеження часу життя м'яча (в секундах)
private double LifespanLeft = 2;
// Прапорець, що вказує, чи гравець знаходиться в зоні дії м'яча
private bool PlayerInZone = false;
// Посилання на Area2D, яке використовується для виявлення колізій
private Area2D Area2D { get; set; }
// Посилання на Sprite2D, яке відображає зображення м'яча
private Sprite2D Sprite2D { get; set; }
// Статичний список текстур для м'ячів, які завантажуються один раз при першому запуску
private static List<Texture2D> Texture2Ds = null;
// Генератор випадкових чисел для вибору текстури
private static Random Random = new Random();
// Метод викликається під час ініціалізації об'єкта
public override void _Ready()
{
// Перевіряємо, чи текстури ще не завантажені
if (Texture2Ds == null)
{
Texture2Ds = new List<Texture2D>();
// Список кольорів для м'ячів
string[] colors = [
"black",
"blue",
"green",
"purple",
"red",
"yellow"
];
// Список емоцій, які можуть бути на м'яча
string[] expresions = [
"angry",
"ashamed",
"confused",
"expresionless_blink",
"expresionless",
"smile_big",
"smile",
"surprised"
];
// Цикл для завантаження всіх комбінацій кольорів та емоцій
for (int i = 0; i < colors.Length; i++)
{
for (int j = 0; j < expresions.Length; j++)
{
// Завантажуємо текстуру з файлової системи та додаємо до списку
Texture2Ds.Add((Texture2D)GD.Load($"res://assets/Spritesheets/Balls/{colors[i]}/{expresions[j]}.png"));
}
}
}
// Отримуємо посилання на Area2D, яке знаходиться в складі об'єкта
Area2D = GetNode<Area2D>("Area2D");
// Отримуємо посилання на Sprite2D, яке знаходиться в складі об'єкта
Sprite2D = GetNode<Sprite2D>("Sprite2D");
// Підписуємося на подію BodyEntered від Area2D
Area2D.BodyEntered += OnBodyEntered;
// Встановлюємо випадкову текстуру при створенні м'яча
Sprite2D.Texture = Texture2Ds[Random.Next(0, Texture2Ds.Count)];
// Підписуємося на події BodyEntered та BodyExited
Area2D.BodyEntered += OnBodyEntered;
Area2D.BodyExited += OnBodyExited;
}
// Метод викликається на кожному кадрі фізичного процесу
public override async void _PhysicsProcess(double delta)
{
// Якщо гравець знаходиться в зоні дії м'яча
if (PlayerInZone)
{
// Якщо час життя м'яча закінчився
if (LifespanLeft <= 0)
{
// Отримуємо глобальний об'єкт Global
Global global = GetNode<Global>("/root/Global");
// Викликаємо метод DespawnBall для видалення м'яча зі списку
global.DespawnBall(this);
// Видаляємо об'єкт з сцени
QueueFree();
}
else
{
// Зменшуємо залишковий час життя
LifespanLeft -= delta;
// Виводимо інформацію про залишковий час у консоль
GD.Print($"Life left:[{LifespanLeft}]");
}
}
}
// Обробник події, коли об'єкт ввійшов у зону Area2D
private void OnBodyEntered(Node body)
{
// Перевіряємо, чи це гравець (CharacterBody2D)
if (body is CharacterBody2D)
{
// Встановлюємо прапорець, що вказує, що гравець у зоні
PlayerInZone = true;
// Змінюємо текстуру м'яча на випадкову зі списку
Sprite2D.Texture = Texture2Ds[Random.Next(0, Texture2Ds.Count)];
}
}
// Обробник події, коли об'єкт вийшов з зони Area2D
private void OnBodyExited(Node body)
{
// Перевіряємо, чи це гравець (CharacterBody2D)
if (body is CharacterBody2D)
{
// Скидаємо прапорець, що вказує, що гравець поза зоною
PlayerInZone = false;
}
}
}Клас Ball:
Цей клас представляє логіку для М'яча. Він успадковує функціональність від RigidBody2D, що дозволяє м'ячу взаємодіяти з фізичним світом Godot.
- LifespanLeft:Змінна типу
double, яка зберігає час життя м'яча в секундах. Зменшується з часом, і коли досягає нуля, м'яч видаляється. - PlayerInZone:Булева змінна, яка вказує, чи знаходиться гравець (об'єкт
CharacterBody2D) в зоні дії м'яча (в зоні діїArea2D). Area2D:Посилання на вузолArea2D, який використовується для виявлення зіткнень і визначення, коли гравець входить або виходить із зони дії м'яча.Sprite2D:Посилання на вузолSprite2D, який відповідає за відображення візуального представлення м'яча.- Texture2Ds:Статичний список (
List), який містить всі можливі текстури для м'яча. Завантажується лише один раз при запуску гри. - Random:Статичний генератор випадкових чисел (Random), який використовується для випадкового вибору текстури для м'яча.
HudController.cs
Додайте код контролера HUD:
using Godot;
using System;
// Клас HudController наслідується від CanvasLayer для відображення інтерфейсу користувача
public partial class HudController : CanvasLayer
{
// Приватне поле для зберігання посилання на Label, який відображає кількість залишившихся м'ячів
private Label LabelBallsLeft { get; set; }
// Приватне поле для зберігання посилання на Label, який відображає таймер очікування на наступну м'яч
private Label LabelTimeout { get; set; }
// Приватне поле для зберігання таймера, який оновлює інтерфейс з інтервалом у 0.1 секунди
private Timer TimerBallTimeout = new Timer();
// Приватне поле для зберігання посилання на Global, який є глобальним об'єктом для управління м'ячами
private Global Global { get; set; }
// Метод, який викликається під час ініціалізації об'єкта, коли він готовий до роботи
public override void _Ready()
{
// Отримуємо посилання на дочірній Node з назвою "PanelContainer/MarginContainer/VBoxContainer/LabelBallsLeft" і приводимо його до типу Label
LabelBallsLeft = GetNode<Label>("PanelContainer/MarginContainer/VBoxContainer/LabelBallsLeft");
// Отримуємо посилання на дочірній Node з назвою "PanelContainer/MarginContainer/VBoxContainer/LabelTimeout" і приводимо його до типу Label
LabelTimeout = GetNode<Label>("PanelContainer/MarginContainer/VBoxContainer/LabelTimeout");
// Встановлюємо текст для LabelBallsLeft, щоб показати, що він завантажений
LabelBallsLeft.Text = "LabelBallsLeft - Loaded";
// Встановлюємо текст для LabelTimeout, щоб показати, що він завантажений
LabelTimeout.Text = "LabelTimeout - Loaded";
// Отримуємо посилання на глобальний об'єкт Global за шляхом "/root/Global"
Global = GetNode<Global>("/root/Global");
// Підписуємося на подію BallStatusChange, яка виникає коли статус м'ячів змінюється
// При виклику події буде викликано метод OnBallStatusChange
Global.BallStatusChange += OnBallStatusChange;
// Додаємо таймер до сцени
AddChild(TimerBallTimeout);
// Встановлюємо час очікування таймера на 0.1 секунди
TimerBallTimeout.WaitTime = 0.1f;
// Підписуємося на подію Timeout, яка виникає коли таймер завершує свій цикл
// При виклику події буде викликано метод UpdateBallTimeoutLabel
TimerBallTimeout.Timeout += UpdateBallTimeoutLabel;
// Встановлюємо параметр OneShot на false, щоб таймер працював безперервно
TimerBallTimeout.OneShot = false; // Repeat indefinitely
// Запускаємо таймер
TimerBallTimeout.Start();
}
// Обробник події, який викликається коли змінюється статус м'ячів
// Параметр balls містить масив м'ячів, а ballLimit - максимальну кількість м'ячів
private void OnBallStatusChange(Ball[] balls, int ballLimit)
{
// Оновлюємо текст LabelBallsLeft, щоб показати кількість залишившихся м'ячів
LabelBallsLeft.Text = "BallsLeft:" + (ballLimit - balls.Length);
}
// Метод, який оновлює текст LabelTimeout з інтервалом у 0.1 секунди
private void UpdateBallTimeoutLabel()
{
// Обчислюємо залишковий час до наступного спавну м'яча
TimeSpan timeLeftOnBallTimeout = Global.MinimalNextBallSpawnTime - DateTime.Now;
// Конвертуємо залишковий час у секунди
int secondsUntilSpawn = Convert.ToInt32(timeLeftOnBallTimeout.TotalSeconds);
// Якщо залишковий час менше нуля, встановлюємо його рівним нулю
if (secondsUntilSpawn < 0)
secondsUntilSpawn = 0;
// Оновлюємо текст LabelTimeout, щоб показати залишковий час до наступного спавну м'яча
LabelTimeout.Text = "Timeout:" + secondsUntilSpawn;
}
}Клас HudController:
Клас HudController успадковується від CanvasLayer і призначений для відображення інтерфейсу користувача (HUD – heads-up display). Він відповідає за відображення інформації про кількість доступних м'ячів та часу, що залишився до появи наступного м'яча.
Приватні поля:
- Label LabelBallsLeft:Зберігає посилання на елемент інтерфейсу Label, який відображає кількість м'ячів, що залишилися.
- Label LabelTimeout:Зберігає посилання на елемент інтерфейсу Label, який відображає час, що залишився до появи наступного м'яча.
- Timer TimerBallTimeout:Зберігає посилання на об'єкт Timer, який використовується для періодичного оновлення відображення часу.
- Global Global:Зберігає посилання на об'єкт Global, який відповідає за керування м'ячами та містить інформацію про час їх появи.
Метод _Ready():
Цей метод викликається після ініціалізації об'єкта. Він виконує наступні дії:
- Отримує посилання на елементи інтерфейсу BallsLeft та Timeout за їхніми шляхами в сцені.
- Встановлює початковий текст для елементів інтерфейсу, щоб показати, що вони завантажені.
- Отримує посилання на глобальний об'єкт Global за шляхом /root/Global.
- Підписується на подію BallStatusChange об'єкта Global. Коли відбувається зміна статусу м'ячів (наприклад, з'являється або зникає м'яч), буде викликано метод OnBallStatusChange.
- Додає таймер TimerBallTimeout до поточної сцени.
- Встановлює час очікування таймера WaitTime на 0.1 секунди.
- Підписується на подію Timeout таймера. Коли таймер завершує свій цикл, буде викликано метод UpdateBallTimeoutLabel.
- Встановлює параметр OneShot на false, щоб таймер працював безперервно.
- Запускає таймер.
Global.cs
Додайте основний глобальний клас Global (Public) та прив'яжіть його до проєкту:
using System;
using System.Collections.Generic;
using Godot;
//(autoload)
public partial class Global : Node
{
// Вузол, в який додаються створені м'ячі
private Node ItemsNode { get; set; }
// Посилання на гравця для отримання його позиції
private PlayerCharacterBody2d Player { get; set; }
// Сцена м'яча для інстанціювання
private PackedScene BallScene { get; set; }
// Список активних м'ячів
private List<Ball> Balls { get; set; } = new List<Ball>();
// Максимальна кількість м'ячів, які можна створити
private int BallLimit { get; set; } = 3;
// Час між створенням м'ячів
private TimeSpan BallSpawnTimeout { get; set; } = TimeSpan.FromSeconds(3);
// Час останнього створення м'яча
private DateTime LastBallSpawnTime { get; set; } = DateTime.Now;
// Властивість, яка визначає, чи можна створити новий м'яч
public bool CanSpawnBall { get { return MinimalNextBallSpawnTime < DateTime.Now; } }
// Мінімальний час, коли можна створити наступний м'яч
public DateTime MinimalNextBallSpawnTime { get { return LastBallSpawnTime + BallSpawnTimeout; } }
// Сигнал для оновлення інтерфейсу при зміні кількості м'ячів
[Signal]
public delegate void BallStatusChangeEventHandler(Ball[] balls, int ballLimit);
// Метод, який викликається під час ініціалізації об'єкта
public override void _Ready()
{
// Отримуємо посилання на вузол ItemsNode за шляхом
ItemsNode = GetNode<Node>("/root/Node2D/Map/Items");
// Отримуємо посилання на гравця за шляхом
Player = GetNode<PlayerCharacterBody2d>("/root/Node2D/Player/PlayerCharacterBody2d");
// Завантажуємо сцену м'яча
BallScene = GD.Load<PackedScene>("res://Scenes/ball.tscn");
}
// Метод для створення нового м'яча
public void SpawnBall()
{
// Перевіряємо, чи можна створити новий м'яч (відповідає умовам таймера та ліміту)
if (CanSpawnBall && Balls.Count < BallLimit)
{
// Оновлюємо час останнього створення м'яча
LastBallSpawnTime = DateTime.Now;
// Створюємо новий м'яч з сцени
Ball ball = (Ball)BallScene.Instantiate();
// Встановлюємо позицію м'яча над гравцем
Vector2 gp = Player.GlobalPosition;
gp.Y -= 30;
ball.GlobalPosition = gp;
// Додаємо м'яч до списку активних м'ячів
Balls.Add(ball);
// Додаємо м'яч до сцени
ItemsNode.AddChild(ball);
// Генеруємо сигнал для оновлення інтерфейсу
EmitSignal(SignalName.BallStatusChange, Balls.ToArray(), BallLimit);
}
}
// Метод для видалення м'яча
internal void DespawnBall(Ball ball)
{
// Видаляємо м'яч зі списку активних м'ячів
Balls.Remove(ball);
// Генеруємо сигнал для оновлення інтерфейсу
EmitSignal(SignalName.BallStatusChange, Balls.ToArray(), BallLimit);
}
}Клас Global:
Клас *Цей клас відповідає за глобальне керування м'ячами у грі. Він забезпечує логіку для створення, видалення та відстеження м'ячів, а також керує їх відображенням на сцені.
Приватні поля:
- ItemsNode:Вузол сцени, до якого додаються створені м'ячі. Використовується для керування їх відображенням у грі.
- Player:Поле, яке містить посилання на об'єкт гравця (PlayerCharacterBody2d). Необхідне для визначення позиції, відносно якої створюються нові м'ячі.
- BallScene:Змінна, що зберігає завантажену сцену м'яча (PackedScene). Це шаблон, який використовується для створення нових екземплярів м'ячів.
- Balls:Список (List
) активних м'ячів у грі. Використовується для відстеження та керування всіма м'ячами, які присутні на сцені. - BallLimit:Ціле число, що визначає максимальну кількість м'ячів, які можуть бути створені одночасно. Обмежує кількість м'ячів на сцені.
- BallSpawnTimeout:Інтервал часу (TimeSpan), який визначає мінімальний час між створенням двох м'ячів. Запобігає надмірному створенню м'ячів.
- LastBallSpawnTime:Змінна типу DateTime, яка зберігає час останнього створення м'яча. Використовується для реалізації обмеження на частоту створення м'ячів.
Властивості:
- CanSpawnBall: Логічна властивість, яка визначає, чи можна створити новий м'яч. Вона перевіряє, чи минув час, необхідний для створення нового м'яча (MinimalNextBallSpawnTime), і чи не досягнуто ліміту м'ячів (BallLimit).
- MinimalNextBallSpwnTime: Властивість, яка обчислює мінімальний час, який повинен минути, перш ніж можна буде створити новий м'яч. Обчислюється на основі часу останнього створення м'яча (LastBallSpawnTime) та інтервалу між створенням м'ячів (BallSpawnTimeout).
Методи:
_Ready():Метод, який викликається під час ініціалізації вузла. Він отримує посилання на вузол ItemsNode та об'єкт гравця (Player), а також завантажує сцену м'яча (BallScene).- SpawnBall():Метод, який створює новий м'яч. Він перевіряє, чи можна створити новий м'яч (CanSpawnBall) та чи не перевищено ліміт м'ячів (BallLimit). Якщо всі умови виконано, він створює новий екземпляр м'яча зі сцени (BallScene), встановлює його позицію над гравцем, додає його до списку активних м'ячів (Balls) та до сцени (ItemsNode), а також генерує сигнал (BallStatusChange) для оновлення інтерфейсу.
- DespawnBall(Ball ball):Метод, який видаляє м'яч зі списку активних м'ячів (Balls) та генерує сигнал (BallStatusChange) для оновлення інтерфейсу.
Сигнали:
- BallStatusChangeEventHandler(Ball[] balls,
intballLimit):Сигнал, який генерується при зміні кількості активних м'ячів. Він передає масив активних м'ячів (balls) та ліміт м'ячів (ballLimit) для оновлення інтерфейсу користувача.
Приклад результату.
E - для створення м'яча.
Що у вас повинно вийти:
build info
- compiler
- 2.0.5.1 · 4054EF9BF6C4510A
- built
- 2026-11-08 14:31:25 UTC
WebUniverse