GUI - Практична - Збереження
Передумови
Скачайте даний проєкт та працюйте у ньому або продовжуйте працювати у своєму, але скопіюйте ключові нові файли з цього.
Основне завдання - ціль
Приклад результату виконання основного завдання.
Нова інформація
Нова інформація яку потрібно знати перед початком виконання завдання.
Dictionary - словники (колекції):
Уявіть собі звичайний паперовий словник: щоб знайти значення слова, ви не гортаєте всі сторінки послідовно, а одразу шукаєте потрібне слово за алфавітом.
Dictionary - це колекція, яка працює за схожим принципом. Вона зберігає дані у вигляді пар Key (ключ) та Value (значення).
- Key: це унікальний ідентифікатор, за допомогою якого ми шукаємо дані (як слово у словнику).
- Value: це дані, які прив'язані до цього ключа (як визначення слова).
Для роботи з цією колекцією необхідно під'єднати System.Collections.Generic.
using System;
using System.Collections.Generic; // Необхідний Namespace для роботи з Dictionary
class Program
{
static void Main()
{
// Створення Dictionary, де Key — це рядок (string), а Value — ціле число (int)
Dictionary<string, int> ages = new Dictionary<string, int>();
// --- 1. ДОДАВАННЯ ЕЛЕМЕНТІВ ---
// Метод Add: додає пару. Видасть помилку, якщо такий Key вже існує в колекції
ages.Add("Олексій", 20);
ages.Add("Марія", 22);
// Індексатор []: якщо ключ існує — оновить значення, якщо ні — створить новий запис
ages["Іван"] = 25;
// --- 2. ОТРИМАННЯ ЗНАЧЕННЯ ---
// Пряме звернення за ключем (видасть помилку KeyNotFoundException, якщо ключа немає)
int studentAge = ages["Олексій"];
Console.WriteLine($"Олексію {studentAge} років.");
// --- 3. БЕЗПЕЧНИЙ ПОШУК ---
// Метод TryGetValue: перевіряє наявність ключа та повертає значення у змінну 'age'
if (ages.TryGetValue("Олена", out int age))
{
Console.WriteLine($"Вік Олени: {age}");
}
else
{
Console.WriteLine("Користувача не знайдено.");
}
// --- 4. ПЕРЕВІРКА ТА ВИДАЛЕННЯ ---
// Перевірка, чи міститься конкретний Key у словнику
if (ages.ContainsKey("Марія"))
{
Console.WriteLine("Марія є в списку.");
}
// Видалення пари за її ключем
ages.Remove("Іван");
// --- 5. ПЕРЕБІР ЕЛЕМЕНТІВ (ІТЕРАЦІЯ) ---
// Використання циклу foreach. Кожен елемент — це об'єкт KeyValuePair
foreach (KeyValuePair<string, int> entry in ages)
{
Console.WriteLine($"Студент: {entry.Key}, Вік: {entry.Value}");
}
}
}Файли на які потрібно звернути увагу.
Файли у яких знаходиться важливий код/у яких потрібно працювати.
Дерево-файлова структура проєкту:
- theme-17-core
- Code
- Libraries
- RedGodot.cs
- 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
- GameManager
- GameManager.cs
- InteractionManager
- InteractionManager.cs
- InteractionManager.tscn
- SaveManager
- ISavable.cs
- 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-17-core.csproj
RedGodot.cs:
using System;
using System.Collections.Generic;
using Godot;
using Newtonsoft.Json.Linq;
namespace Red
{
// Статичний клас-помічник для централізованої роботи з серіалізацією стану нодів у Godot
public static class RedGodot
{
// Рекурсивно збирає всі ноди, що реалізують інтерфейс ISavable, починаючи з кореневого елемента
public static List<ISavable> GetAllISavableNodes(Node root)
{
// Отримуємо повний плоский список усіх нодів у дереві сцени
List<Node> allNodes = GetAllNodes(root);
List<ISavable> allISavablenodes = new List<ISavable>();
// Фільтруємо об'єкти, залишаючи лише ті, що підтримують кастомний інтерфейс збереження
foreach (Node node in allNodes)
{
if (node is ISavable saveble)
allISavablenodes.Add(saveble);
}
return allISavablenodes;
}
// Допоміжний рекурсивний метод для збору повної ієрархії нодів (включаючи корінь та всіх нащадків)
public static List<Node> GetAllNodes(Node root)
{
var result = new List<Node>();
result.Add(root);
// Рекурсивно обходимо всіх дочірніх елементів і додаємо їх до результату
foreach (Node child in root.GetChildren())
result.AddRange(GetAllNodes(child));
return result;
}
// Записує значення у словник, використовуючи унікальний ключ на основі шляху до ноуди
public static Dictionary<string, object> SaveData(Node node, Dictionary<string, object> dict, string key, object data)
{
// Формуємо унікальний ідентифікатор, щоб уникнути колізій ключів між різними об'єктами гри
string fullKey = node.GetPath() + ":" + key;
// Зберігаємо або оновлюємо значення у словнику
dict[fullKey] = data;
return dict;
}
// Завантажує значення типу T зі словника за унікальним ключем ноуди
public static T LoadData<T>(Node node, Dictionary<string, object> dict, string key)
{
string fullKey = node.GetPath() + ":" + key;
if (dict.ContainsKey(fullKey))
{
var value = dict[fullKey];
// Якщо дані були десеріалізовані як JObject (наприклад, складний об'єкт, Vector2 або struct),
// перетворюємо їх назад у цільовий тип за допомогою Newtonsoft.Json
if (value is JObject jObj)
{
return jObj.ToObject<T>();
}
// Якщо тип вже співпадає або сумісний з цільовим, виконуємо пряме приведення
if (value is T typed)
{
return typed;
}
// Фолбек для примітивних типів: намагаємося використати вбудовану конвертацію значень
return (T)Convert.ChangeType(value, typeof(T));
}
// Якщо ключ відсутній у словнику, повертаємо значення за замовчуванням для типу T
return default;
}
}
}Lever.cs:
using System.Collections.Generic;
using Godot;
public partial class Lever : StaticBody2D, ISavable
{
//
//
// --------
//
//
public bool Save(Dictionary<string, object> data)
{
Red.RedGodot.SaveData(this, data, "LeverStatus", LeverStatus);
return true;
}
public bool Load(Dictionary<string, object> data)
{
LeverStatus = Red.RedGodot.LoadData<int>(this, data, "LeverStatus");
UpdateTexture();
ObjectUnderControlILeverable.OnLeverStateChanged(LeverStatus);
return true;
}
}
// Інтерфейс для об'єктів, які можуть реагувати на зміну стану важеля.
public interface ILeverable
{
// Метод, який викликається при зміні стану важеля.
public void OnLeverStateChanged(int state);
}ISavable.cs:
using System.Collections.Generic;
/// <summary>
/// Інтерфейс-контракт для всіх ігрових об'єктів, які підтримують збереження та завантаження стану.
/// Класи, що реалізують цей інтерфейс, зобов'язані надати логіку серіалізації своїх внутрішніх змінних
/// у єдиний словник даних, який пізніше перетворюється на JSON.
/// </summary>
public interface ISavable
{
/// <summary>
/// Метод зберігає поточний стан об'єкта, додаючи значення у переданий Dictionary<string, object>.
/// Повертає true у разі успішного збереження, false - при виникненні помилок або відсутності даних.
/// </summary>
bool Save(Dictionary<string, object> data);
/// <summary>
/// Метод завантажує попередньо збережений стан об'єкта, читаючи дані з Dictionary<string, object>.
/// Повертає true, якщо дані успішно відновлені, ініціалізуючи поля класу відповідними значеннями.
/// </summary>
bool Load(Dictionary<string, object> data);
}SaveManager.cs:
using System.Collections.Generic;
using System.IO;
using Godot;
using Newtonsoft.Json;
using Red;
public partial class SaveManager : Node
{
// Статичний словник для агрегації всіх змінних стану гри у форматі ключ-значення
private static Dictionary<string, object> Data = new Dictionary<string, object>();
// Шлях/ім'я файлу збереження
public const string SaveFile = "save.json";
// Метод для відновлення збереженого стану гри
public bool Load()
{
// Спочатку читаємо наявний файл, щоб словник Data містив актуальні дані з диску
LoadSave();
// Отримуємо рекурсивний список всіх нодів у дереві сцени, що реалізують інтерфейс ISavable
List<ISavable> allISavablenodes = RedGodot.GetAllISavableNodes(GetTree().Root);
// TODO
// Ітеруємо усі ноди та загружаємо їх данні з словнику Data
//
return true;
}
// Метод для фіксації поточного стану гри на диску
public bool Save()
{
// Завантажуємо попередні дані, щоб не втрачати поля, які не змінилися під час поточної сесії
LoadSave();
// Скануємо дерево сцени на наявність об'єктів, що підтримують збереження
List<ISavable> allISavablenodes = RedGodot.GetAllISavableNodes(GetTree().Root);
// TODO
// Ітеруємо усі ноди та зберігаємо їх данні в словник Data
//
// Серіалізуємо словник у читабельний JSON-рядок за допомогою Newtonsoft.Json
string saveSerial = JsonConvert.SerializeObject(Data, Formatting.Indented);
// TODO
// Зберігаємо данні у файл
//
return true;
}
// Приватний допоміжний метод для читання файлу з диску в пам'ять
private void LoadSave()
{
// TODO
// Якщо файл збереження існує тоді зчитуємо його та десеріалізуємо його в наш словник Data
//
}
}PauseMenu.cs:
using Godot;
// Клас реалізує меню паузи, що відображається як шар інтерфейсу (CanvasLayer) поверх ігрового світу.
// Він дозволяє гравцю зупинити гру, зберегти/завантажити стан або перейти до головного меню.
public partial class PauseMenu : CanvasLayer
{
//
//
// --------
//
//
// Обробник натискання кнопки збереження.
private void OnSave()
{
// Отримуємо посилання на менеджер збережень (мовляв, Autoload або синглтон у корені сцени).
SaveManager sm = GetNode<SaveManager>("/root/SaveManager");
// Викликаємо метод збереження, який серіалізує стан гри.
sm.Save();
}
// Обробник натискання кнопки завантаження.
private void OnLoad()
{
// Отримуємо посилання на менеджер збережень.
SaveManager sm = GetNode<SaveManager>("/root/SaveManager");
// Викликаємо метод завантаження, який десеріалізує стан гри та оновлює об'єкти.
sm.Load();
}
}Player.cs:
using Godot;
using System;
using System.Collections.Generic;
// Клас PlayerCharacterBody2d успадковує CharacterBody2D, що дозволяє йому мати фізичні властивості, такі як маса, прискорення, колізії.
public partial class Player : CharacterBody2D, ISavable
{
//
//
// --------
//
//
public bool Save(Dictionary<string, object> data)
{
Red.RedGodot.SaveData(this, data, "Position", Position);
Red.RedGodot.SaveData(this, data, "Velocity", Velocity);
return true;
}
public bool Load(Dictionary<string, object> data)
{
Position = Red.RedGodot.LoadData<Vector2>(this, data, "Position");
Velocity = Red.RedGodot.LoadData<Vector2>(this, data, "Velocity");
return true;
}
}build info
- compiler
- 2.0.5.1 · 4054EF9BF6C4510A
- built
- 2026-11-08 14:31:25 UTC
WebUniverse