Реализовать систему сохранения и загрузки игры
Условие
Реализуйте систему сохранения и загрузки игрового прогресса.
Требования
- Создайте класс SaveData для хранения данных (позиция игрока, здоровье, инвентарь, уровень)
- Реализуйте сериализацию в JSON
- Сохраняйте файл в Application.persistentDataPath
- Добавьте обработку ошибок (файл не найден, поврежден)
- Реализуйте автосохранение каждые N секунд
Структура данных
- Позиция игрока (Vector3)
- Текущее здоровье и максимальное
- Список предметов в инвентаре
- Номер текущего уровня
- Время игры
Бонус
- Добавьте шифрование данных
- Реализуйте несколько слотов сохранения
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Архитектура системы сохранения
Хорошая система сохранения должна быть надёжной, простой в использовании и защищённой от ошибок. Я реализую трёхуровневую архитектуру: SaveData (структура), SaveManager (логика), и интеграцию с игровыми системами.
1. Структура данных SaveData
using UnityEngine;
using System;
using System.Collections.Generic;
/// <summary>Данные о позиции и ориентации</summary>
[System.Serializable]
public struct Vector3Serializable
{
public float x, y, z;
public Vector3Serializable(Vector3 v)
{
x = v.x;
y = v.y;
z = v.z;
}
public Vector3 ToVector3() => new Vector3(x, y, z);
}
/// <summary>Данные об одном предмете инвентаря</summary>
[System.Serializable]
public class InventoryItemData
{
public string itemId;
public int quantity;
public int slotIndex;
public InventoryItemData(string id, int qty, int slot)
{
itemId = id;
quantity = qty;
slotIndex = slot;
}
}
/// <summary>Основной класс для сохранённых данных игры</summary>
[System.Serializable]
public class SaveData
{
[Header("Metadata")]
public int saveVersion = 1; // Версия сохранения для совместимости
public string saveTime; // ISO 8601 формат
public int playTimeSeconds; // Общее время игры
[Header("Player State")]
public Vector3Serializable playerPosition;
public Vector3Serializable playerRotation;
public int currentHealth;
public int maxHealth;
public int currentLevel;
[Header("Inventory")]
public List<InventoryItemData> inventoryItems = new List<InventoryItemData>();
[Header("Game State")]
public Dictionary<string, int> checkpoints = new Dictionary<string, int>(); // Пройденные контрольные точки
public Dictionary<string, bool> questFlags = new Dictionary<string, bool>(); // Флаги квестов
/// <summary>Конструктор по умолчанию</summary>
public SaveData()
{
saveTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
playerPosition = new Vector3Serializable(Vector3.zero);
playerRotation = new Vector3Serializable(Vector3.zero);
currentHealth = 100;
maxHealth = 100;
currentLevel = 1;
}
}
/// <summary>Обёртка для сохранения Dictionary (JsonUtility не сериализует Dictionary)</summary>
[System.Serializable]
public class SerializableDictionary<TKey, TValue>
{
[System.Serializable]
public struct KVP
{
public TKey key;
public TValue value;
}
public KVP[] items;
public SerializableDictionary(Dictionary<TKey, TValue> dict)
{
var list = new System.Collections.Generic.List<KVP>();
foreach (var kvp in dict)
{
list.Add(new KVP { key = kvp.Key, value = kvp.Value });
}
items = list.ToArray();
}
public Dictionary<TKey, TValue> ToDictionary()
{
var dict = new Dictionary<TKey, TValue>();
foreach (var kvp in items)
{
dict[kvp.key] = kvp.value;
}
return dict;
}
}
2. SaveManager с автосохранением
using UnityEngine;
using System;
using System.IO;
using System.Collections.Generic;
public class SaveManager : Singleton<SaveManager>
{
[Header("Save Settings")]
[SerializeField] private float autoSaveInterval = 60f; // Автосохранение каждые 60 сек
[SerializeField] private int maxSaveSlots = 10;
[SerializeField] private bool useEncryption = false;
private SaveData currentSaveData;
private float autoSaveTimer;
private string savePath;
private const string SAVE_FOLDER = "SaveData";
// События
public static event Action<SaveData> OnSaveCompleted;
public static event Action<SaveData> OnLoadCompleted;
public static event Action<string> OnSaveError;
protected override void Awake()
{
base.Awake();
// Инициализируем путь сохранения
savePath = Path.Combine(Application.persistentDataPath, SAVE_FOLDER);
// Создаём папку если её нет
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
Debug.Log($"Создана папка сохранений: {savePath}");
}
autoSaveTimer = autoSaveInterval;
}
void Update()
{
// Автосохранение
if (currentSaveData != null)
{
autoSaveTimer -= Time.deltaTime;
if (autoSaveTimer <= 0)
{
SaveGame(0); // Сохраняем в первый слот
autoSaveTimer = autoSaveInterval;
Debug.Log("Автосохранение выполнено");
}
}
}
/// <summary>Создать новое сохранение</summary>
public SaveData CreateNewSaveData()
{
currentSaveData = new SaveData();
return currentSaveData;
}
/// <summary>Сохранить игру в слот</summary>
public bool SaveGame(int slotIndex)
{
if (currentSaveData == null)
{
Debug.LogError("Нет данных для сохранения");
OnSaveError?.Invoke("No save data to save");
return false;
}
if (slotIndex < 0 || slotIndex >= maxSaveSlots)
{
string error = $"Некорректный слот сохранения: {slotIndex}";
Debug.LogError(error);
OnSaveError?.Invoke(error);
return false;
}
try
{
string fileName = $"save_{slotIndex}.json";
string filePath = Path.Combine(savePath, fileName);
// Обновляем время сохранения
currentSaveData.saveTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
// Сериализуем в JSON
string jsonData = JsonUtility.ToJson(currentSaveData, true);
// Опционально: шифруем
if (useEncryption)
{
jsonData = SimpleEncrypt(jsonData);
}
// Записываем файл
File.WriteAllText(filePath, jsonData);
Debug.Log($"Игра сохранена в слот {slotIndex}: {filePath}");
OnSaveCompleted?.Invoke(currentSaveData);
return true;
}
catch (Exception e)
{
string error = $"Ошибка сохранения: {e.Message}";
Debug.LogError(error);
OnSaveError?.Invoke(error);
return false;
}
}
/// <summary>Загрузить игру из слота</summary>
public bool LoadGame(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= maxSaveSlots)
{
string error = $"Некорректный слот сохранения: {slotIndex}";
Debug.LogError(error);
OnSaveError?.Invoke(error);
return false;
}
try
{
string fileName = $"save_{slotIndex}.json";
string filePath = Path.Combine(savePath, fileName);
if (!File.Exists(filePath))
{
string error = $"Файл сохранения не найден: {filePath}";
Debug.LogError(error);
OnSaveError?.Invoke(error);
return false;
}
// Читаем файл
string jsonData = File.ReadAllText(filePath);
// Опционально: расшифровываем
if (useEncryption)
{
jsonData = SimpleDecrypt(jsonData);
}
// Десериализуем из JSON
currentSaveData = JsonUtility.FromJson<SaveData>(jsonData);
// Проверяем версию сохранения
if (currentSaveData.saveVersion != 1)
{
Debug.LogWarning($"Версия сохранения {currentSaveData.saveVersion} может быть несовместима");
}
Debug.Log($"Игра загружена из слота {slotIndex}");
OnLoadCompleted?.Invoke(currentSaveData);
return true;
}
catch (Exception e)
{
string error = $"Ошибка загрузки: {e.Message}";
Debug.LogError(error);
OnSaveError?.Invoke(error);
currentSaveData = null;
return false;
}
}
/// <summary>Получить информацию о сохранении без полной загрузки</summary>
public string GetSaveInfo(int slotIndex)
{
string fileName = $"save_{slotIndex}.json";
string filePath = Path.Combine(savePath, fileName);
if (!File.Exists(filePath))
return "Пусто";
try
{
string jsonData = File.ReadAllText(filePath);
if (useEncryption) jsonData = SimpleDecrypt(jsonData);
var saveData = JsonUtility.FromJson<SaveData>(jsonData);
return $"Уровень {saveData.currentLevel} | Время: {saveData.saveTime} | HP: {saveData.currentHealth}/{saveData.maxHealth}";
}
catch
{
return "Ошибка чтения";
}
}
/// <summary>Удалить сохранение</summary>
public bool DeleteSave(int slotIndex)
{
string fileName = $"save_{slotIndex}.json";
string filePath = Path.Combine(savePath, fileName);
if (File.Exists(filePath))
{
File.Delete(filePath);
Debug.Log($"Сохранение {slotIndex} удалено");
return true;
}
return false;
}
/// <summary>Получить список всех сохранений</summary>
public List<string> GetAllSaves()
{
var saves = new List<string>();
for (int i = 0; i < maxSaveSlots; i++)
{
saves.Add(GetSaveInfo(i));
}
return saves;
}
/// <summary>Получить текущие данные сохранения</summary>
public SaveData GetCurrentSaveData() => currentSaveData;
/// <summary>Обновить позицию игрока в сохранении</summary>
public void UpdatePlayerPosition(Vector3 position)
{
if (currentSaveData != null)
{
currentSaveData.playerPosition = new Vector3Serializable(position);
}
}
/// <summary>Обновить здоровье игрока в сохранении</summary>
public void UpdatePlayerHealth(int health, int maxHealth)
{
if (currentSaveData != null)
{
currentSaveData.currentHealth = health;
currentSaveData.maxHealth = maxHealth;
}
}
/// <summary>Простое шифрование (для демо, используй реальное в продакшене!)</summary>
private string SimpleEncrypt(string text)
{
// Это очень базовое шифрование, в реальных проектах используй AES
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
for (int i = 0; i < data.Length; i++)
{
data[i] ^= 0xAA; // XOR с константой
}
return System.Convert.ToBase64String(data);
}
private string SimpleDecrypt(string text)
{
byte[] data = System.Convert.FromBase64String(text);
for (int i = 0; i < data.Length; i++)
{
data[i] ^= 0xAA;
}
return System.Text.Encoding.UTF8.GetString(data);
}
}
3. Интеграция с системами игры
public class GameManager : Singleton<GameManager>
{
private SaveManager saveManager;
protected override void Awake()
{
base.Awake();
saveManager = SaveManager.Instance;
}
void Start()
{
// Подписываемся на события сохранения
SaveManager.OnLoadCompleted += OnGameLoaded;
SaveManager.OnSaveCompleted += OnGameSaved;
SaveManager.OnSaveError += OnSaveError;
}
/// <summary>Начать новую игру</summary>
public void NewGame()
{
var saveData = saveManager.CreateNewSaveData();
LoadScene("Level_1");
}
/// <summary>Продолжить игру из слота</summary>
public void ContinueGame(int slotIndex)
{
if (saveManager.LoadGame(slotIndex))
{
var saveData = saveManager.GetCurrentSaveData();
LoadScene($"Level_{saveData.currentLevel}");
}
}
private void OnGameLoaded(SaveData data)
{
Debug.Log($"Игра загружена: уровень {data.currentLevel}");
// Восстанавливаем позицию и здоровье игрока
var player = FindObjectOfType<Player>();
if (player != null)
{
player.transform.position = data.playerPosition.ToVector3();
player.SetHealth(data.currentHealth);
}
}
private void OnGameSaved(SaveData data)
{
Debug.Log($"Игра сохранена: уровень {data.currentLevel}");
}
private void OnSaveError(string error)
{
Debug.LogError($"Ошибка: {error}");
// Показываем UIError в интерфейсе
}
private void LoadScene(string sceneName)
{
UnityEngine.SceneManagement.SceneManager.LoadScene(sceneName);
}
}
Пример использования в UI
public class SaveMenuUI : MonoBehaviour
{
[SerializeField] private SaveSlotUI[] saveSlots;
private SaveManager saveManager;
void Start()
{
saveManager = SaveManager.Instance;
DisplayAllSaves();
}
private void DisplayAllSaves()
{
var saves = saveManager.GetAllSaves();
for (int i = 0; i < saveSlots.Length; i++)
{
saveSlots[i].SetSaveInfo(i, saves[i]);
saveSlots[i].OnLoadClicked += () => LoadGame(i);
saveSlots[i].OnDeleteClicked += () => DeleteSave(i);
}
}
private void LoadGame(int slotIndex)
{
if (saveManager.LoadGame(slotIndex))
{
GameManager.Instance.ContinueGame(slotIndex);
}
}
private void DeleteSave(int slotIndex)
{
saveManager.DeleteSave(slotIndex);
DisplayAllSaves();
}
}
Ключевые особенности
1. Простая сериализация — JsonUtility встроен в Unity и не требует доп. библиотек.
2. Обработка ошибок — try-catch для защиты от повреждённых файлов.
3. Версионирование — поле saveVersion позволяет поддерживать совместимость.
4. Автосохранение — автоматически сохраняет прогресс каждые N секунд.
5. Безопасность пути — используем Application.persistentDataPath для кроссплатформности.
6. События — интеграция с остальной игрой через делегаты.
Эта система обеспечивает надёжность, гибкость и простоту использования.