GUI - 2 - Інтерфейс та механіка збереження.
Передумови
Скачайте даний проєкт або продовжуйте працювати у своєму.
Сцена - MainMenu
Що потрібно змінити/додати у MainMenu.
Дерево:
Додайте ButtonContinue.
- MainMenu (CanvasLayer)
- ColorRect
- MarginContainer
- HBoxContainer
- VBoxContainer
- LabelTitle
- ButtonContinue %
- ButtonLevels %
- ButtonExit %
Пояснення:
Додано ButtonContinue та код для її обробки.
Код:
using Godot;
using System;
public partial class MainMenu : CanvasLayer
{
// Посилання на кнопки головного меню
private Button ButtonContinue { get; set; }
private Button ButtonLevels { get; set; }
private Button ButtonExit { get; set; }
// Збережена сцена меню вибору рівнів
private PackedScene LevelMenuInstance { get; set; }
// Метод викликається один раз при ініціалізації нода в дереві сцени
public override void _Ready()
{
// Отримання посилань на кнопки за допомогою унікальних імен (%)
ButtonContinue = GetNode<Button>("%ButtonContinue");
ButtonLevels = GetNode<Button>("%ButtonLevels");
ButtonExit = GetNode<Button>("%ButtonExit");
// Встановлення тексту для кнопок
ButtonContinue.Text = "ButtonContinue";
ButtonLevels.Text = "ButtonLevels";
ButtonExit.Text = "ButtonExit";
// Підписка на подію Pressed для кожної кнопки
ButtonContinue.Pressed += ButtonContinueClicked;
ButtonLevels.Pressed += ButtonLevelsClicked;
ButtonExit.Pressed += ButtonExitClicked;
// Завантаження PackedScene для меню рівнів з ресурсів проекту
LevelMenuInstance = GD.Load<PackedScene>("res://Scenes/Menus/LevelMenu/LevelMenu.tscn");
// Завантаження даних збереження через SaveManager
SaveManager.Instance.LoadSave();
// Якщо в збереженні є шлях до останнього завантаженого рівня, завантажуємо його в DataStorageManager
if (SaveManager.Instance.CurrentSave.LastLoadedLevelScenePath != null)
{
DataStorageManager.LastLoadedLevel = (PackedScene)GD.Load(SaveManager.Instance.CurrentSave.LastLoadedLevelScenePath);
}
}
// Обробник натискання кнопки "Продовжити"
private void ButtonContinueClicked()
{
// Перевірка, чи існує рівень, з якого можна продовжити гру
if (DataStorageManager.LastLoadedLevel is null)
{
// Створення та відображення діалогового вікна з помилкою, якщо рівень не знайдено
var dialog = new AcceptDialog();
dialog.Title = "Error";
dialog.DialogText = "There is no last level!";
AddChild(dialog); // Додавання діалогу в дерево сцени для відображення
dialog.PopupCentered();
}
else
{
// Перехід до останнього завантаженого рівня
GetTree().ChangeSceneToPacked(DataStorageManager.LastLoadedLevel);
}
}
// Обробник натискання кнопки "Рівні" — перемикає сцену на меню вибору рівнів
private void ButtonLevelsClicked()
{
GetTree().ChangeSceneToPacked(LevelMenuInstance);
}
// Обробник натискання кнопки "Вихід" — повністю закриває гру
private void ButtonExitClicked()
{
GetTree().Quit();
}
}Сцена - LevelMenu
Що потрібно змінити/додати у LevelMenu.
Дерево:
- LevelMenu (CanvasLayer)
- ColorRect
- MarginContainer
- HBoxContainer
- LevelListVBoxContainer %
- LabelTitle
Пояснення:
Додано код для збереження прогресу через SaveManager.
Код:
using Godot;
using System.Collections.Generic;
// Клас меню вибору рівнів, що наслідується від CanvasLayer для відображення UI поверх ігрового світу
public partial class LevelMenu : CanvasLayer
{
// Посилання на контейнер VBoxContainer, у якому будуть розміщені кнопки рівнів
private VBoxContainer LevelListVBoxContainer { get; set; }
// Список об'єктів PackedScene, що містять завантажені сцени рівнів
private List<PackedScene> Levels = new();
public override void _Ready()
{
// Отримуємо посилання на VBoxContainer з використанням unique name (%)
LevelListVBoxContainer = GetNode<VBoxContainer>("%LevelListVBoxContainer");
// Завантажуємо сцени рівнів за їхніми шляхами та додаємо до списку Levels
Levels.Add((PackedScene)GD.Load("res://Scenes/Levels/Level1.tscn"));
Levels.Add((PackedScene)GD.Load("res://Scenes/Levels/Level2.tscn"));
// Ітеруємо по списку рівнів для динамічного створення UI-елементів
foreach (var level in Levels)
{
// Створюємо новий екземпляр кнопки
Button levelButton = new();
// Встановлюємо текст кнопки (наприклад, "Level 1", "Level 2")
levelButton.Text = "Level " + (Levels.IndexOf(level) + 1);
// Підписуємось на подію Pressed, передаючи обрану сцену в метод LevelButtonClicked через лямбда-вираз
levelButton.Pressed += () => LevelButtonClicked(level);
// Додаємо створену кнопку як дочірній елемент до контейнера
LevelListVBoxContainer.AddChild(levelButton);
// Виводимо шлях до ресурсу рівня в консоль для відладки
GD.Print(level.ResourcePath);
}
}
// Метод, що обробляє натискання кнопки вибору рівня
public void LevelButtonClicked(PackedScene levelToLoad)
{
// Змінює поточну активну сцену гри на обрану PackedScene
GetTree().ChangeSceneToPacked(levelToLoad);
// Оновлює статичне поле в DataStorageManager, щоб гра знала, який рівень завантажено останнім
DataStorageManager.LastLoadedLevel = levelToLoad;
SaveManager.Instance.CurrentSave.PlayerPosition = null;
// Викликає метод запису у SaveManager для збереження прогресу/вибору рівня у файл
SaveManager.Instance.WriteSave();
}
}Сцена - PauseMenu
Огляд структури та функціоналу PauseMenu.
Дерево:
- PauseMenu (CanvasLayer)
- ColorRect
- MarginContainer
- HBoxContainer
- VBoxContainer
- LabelTitle
- ButtonToMenu %
- ButtonResume %
Пояснення:
Додано код для збереження прогресу через SaveManager.
Код:
using Godot;
public partial class PauseMenu : CanvasLayer
{
public override void _Ready()
{
// КРИТИЧНО: Встановлюємо ProcessMode на Always, щоб UI продовжував працювати,
// навіть коли GetTree().Paused = true
ProcessMode = ProcessModeEnum.Always;
// Отримуємо посилання на кнопки за допомогою Unique Names (%)
var resumeBtn = GetNode<Button>("%ButtonResume");
var menuBtn = GetNode<Button>("%ButtonToMenu");
// Підписуємося на подію Pressed для кожної кнопки
resumeBtn.Pressed += OnResume;
menuBtn.Pressed += OnMainMenu;
}
public override void _Input(InputEvent @event)
{
// Перевіряємо, чи натиснута клавіша, що відповідає дії "ui_cancel" (зазвичай ESC)
if (@event.IsActionPressed("ui_cancel"))
{
OnResume();
// Позначаємо подію як оброблену, щоб вона не передавалася далі в інші ноди
GetViewport().SetInputAsHandled();
}
}
// Метод для повернення в гру
private void OnResume()
{
// Знімаємо стан паузи з ігрового дерева
GetTree().Paused = false;
// Видаляємо екземпляр меню з пам'яті та сцени
QueueFree(); // Remove overlay
}
// Метод для виходу в головне меню
private void OnMainMenu()
{
// Знімаємо стан паузи, щоб головне меню працювало коректно
GetTree().Paused = false;
// Шукаємо об'єкт гравця в групі "Player" і приводимо його до типу Player
Player player = GetTree().GetFirstNodeInGroup("Player") as Player;
// Оновлюємо дані в SaveManager, зберігаючи поточну позицію гравця
SaveManager.Instance.CurrentSave.PlayerPosition = player.Position;
// Записуємо оновлені дані у файл (JSON)
SaveManager.Instance.WriteSave();
// Змінюємо поточну сцену на сцену головного меню
GetTree().ChangeSceneToFile("res://Scenes/Menus/MainMenu/MainMenu.tscn");
// Видаляємо меню паузи
QueueFree();
}
}Сцена - Player
Що потрібно змінити/додати у Player.
Пояснення:
Додано код для загрузки позиції персонажа зі збереження.
Код:
using Godot;
using System;
// Клас PlayerCharacterBody2d успадковує CharacterBody2D, що дозволяє йому мати фізичні властивості, такі як маса, прискорення, колізії.
public partial class Player : 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");
if (SaveManager.Instance.CurrentSave.PlayerPosition is not null)
{
Position = (Vector2)SaveManager.Instance.CurrentSave.PlayerPosition;
}
}
// Основний метод фізичного процесу, викликається кожен кадр
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;
// Виконуємо фізичний рух об'єкта
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);
}
}
}
}
// Метод для визначення напрямку руху гравця
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; // Повертаємо напрямок руху
}
}Клас-Singleton - SaveManager
Огляд структури та функціоналу SaveManager.
Singleton (Одинак) — це патерн проєктування, який гарантує, що клас має лише один екземпляр, і надає до нього глобальну точку доступу.
Пояснення:
SaveManager відповідає за збереження та загрузку даних гри у форматі JSON, клас Save.
ВАЖЛИВО:
Додайте SaveManager у Globals, так само як ви робили це на попередньому занятті з GameManager.
Код:
using System;
using System.Collections.Generic;
using System.IO;
using Godot;
using Newtonsoft.Json;
// Клас SaveManager керує процесом збереження та завантаження стану гри
public partial class SaveManager : Node
{
// Поточний об'єкт збереження, що містить дані про прогрес
public Save CurrentSave { get; set; } = new Save();
// Статичний Instance для реалізації патерну Singleton, що дозволяє доступ до менеджера з будь-якого місця коду
public static SaveManager Instance { get; private set; }
// Константа, що визначає шлях до файлу збереження у форматі JSON
private const string SavePath = "./Save.json";
public SaveManager()
{
// Присвоєння поточного екземпляра статичній змінній Instance
Instance = this;
// Перевірка: якщо файл збереження вже існує, завантажуємо його, інакше створюємо новий
if (File.Exists(SavePath))
{
LoadSave();
}
else
{
CreateDefaultSave();
}
}
// Метод для читання JSON файлу та десеріалізації його в об'єкт Save
public void LoadSave()
{
string json = File.ReadAllText(SavePath);
CurrentSave = JsonConvert.DeserializeObject<Save>(json) ?? new Save();
}
// Створює новий об'єкт Save з параметрами за замовчуванням та записує його на диск
private void CreateDefaultSave()
{
CurrentSave = new Save();
WriteSave();
}
// Метод для серіалізації поточного стану в JSON та запису у файл
public void WriteSave()
{
// Оновлення шляху до останнього рівня, використовуючи дані з DataStorageManager
CurrentSave.LastLoadedLevelScenePath = DataStorageManager.LastLoadedLevel.ResourcePath;
string json = JsonConvert.SerializeObject(CurrentSave);
File.WriteAllText(SavePath, json);
}
}
// Клас-контейнер для даних, які необхідно зберегти
public class Save
{
// Шлях до сцени останнього завантаженого рівня
public string LastLoadedLevelScenePath { get; set; } = string.Empty;
// Позиція гравця у світі (може бути null, якщо гравець ще не з'явився)
public Vector2? PlayerPosition { get; set; } = null;
}Статичний клас - DataStorageManager
Огляд структури та функціоналу DataStorageManager.
Пояснення:
DataStorageManager відповідає за зберігання та доступ до глобальних даних.
ВАЖЛИВО:
DataStorageManager це просто статичний клас.
Код:
using Godot;
// Статичний клас, який використовується для глобального зберігання даних,
// що мають бути доступні з будь-якої частини проєкту без створення екземпляра класу.
public static class DataStorageManager
{
// Статична властивість типу PackedScene, яка зберігає посилання на останній завантажений рівень.
// Початкове значення встановлено як null.
public static PackedScene LastLoadedLevel { get; set; } = null;
}Перевірка
Перевірка змін - що було змінено/додано.
Дерево-файлова структура проєкту:
- theme-15-16-base
- Assets
- *
- Code
- Libraries
- RedMath.cs
- icon.svg
- project.godot
- Save.json
- Scenes
- InteractionArea
- InteractionArea.cs
- InteractionArea.tscn
- Levels
- Level1.tscn
- Level2.tscn
- Lever
- Lever.cs
- Lever.tscn
- Managers
- DataStorageManager
- DataStorageManager.cs
- GameManager
- GameManager.cs
- InteractionManager
- InteractionManager.cs
- InteractionManager.tscn
- SaveManager
- SaveManager.cs
- Menus
- LevelMenu
- LevelMenu.cs
- LevelMenu.tscn
- MainMenu
- MainMenu.cs
- MainMenu.tscn
- PauseMenu
- PauseMenu.cs
- PauseMenu.tscn
- MovingPlatform
- MovingPlatform.cs
- MovingPlatform.tscn
- Player
- Player.cs
- Player.tscn
- Portal
- Portal.cs
- Portal.tscn
- ToggleButton
- ToggleButton.cs
- ToggleButton.tscn
- theme-15-16-base.csproj
- theme-15-16-base.sln
Globals:

build info
- compiler
- 2.0.5.1 · 4054EF9BF6C4510A
- built
- 2026-11-08 14:31:25 UTC
WebUniverse