← Назад к вопросам

Реализовать систему сохранения и загрузки игры

1.7 Middle🔥 261 комментариев
#C# и ООП#Unity Core#Коллекции и структуры данных#Ресурсы и ассеты

Условие

Реализуйте систему сохранения и загрузки игрового прогресса.

Требования

  1. Создайте класс SaveData для хранения данных (позиция игрока, здоровье, инвентарь, уровень)
  2. Реализуйте сериализацию в JSON
  3. Сохраняйте файл в Application.persistentDataPath
  4. Добавьте обработку ошибок (файл не найден, поврежден)
  5. Реализуйте автосохранение каждые N секунд

Структура данных

  • Позиция игрока (Vector3)
  • Текущее здоровье и максимальное
  • Список предметов в инвентаре
  • Номер текущего уровня
  • Время игры

Бонус

  • Добавьте шифрование данных
  • Реализуйте несколько слотов сохранения

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Архитектура системы сохранения

Хорошая система сохранения должна быть надёжной, простой в использовании и защищённой от ошибок. Я реализую трёхуровневую архитектуру: 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. События — интеграция с остальной игрой через делегаты.

Эта система обеспечивает надёжность, гибкость и простоту использования.

Реализовать систему сохранения и загрузки игры | PrepBro