Save/Load architecture

Home  /  Chronicles of Cyberpunk  /  Save/Load architecture

Реализация сохранения/загрузки — одна из самых сложных задач, с которыми я сталкивался. Какие стояли задачи:


Загружать нужную сцену, номер акта, номер шага, позицию игрока

Часть кода вырезал, оставил главное. При загрузке мы загружаем новую сцену, поэтому нужно найти объекты со скриптами, чтобы потом к ним обратиться. И если есть метод со строчкой, загружающей новую сцену, то код, выполняющийся после этой строчки, выполняется не для новой загруженной сцены, а для предыдущей. Поэтому нужно проверять, чтобы все выполнялось на нужных сценах. Часть переменных сделал статическими, а часть кода загрузки вызывал в Awake

void Awake()
{
    Time.timeScale = 1;
    gameManagerObj = GameObject.FindGameObjectWithTag("GameManager");
    gameManagerScript = gameManagerObj.GetComponent<GameManager>();
    playerTransform = player.GetComponent<Transform>();

    // Если новая сцена загрузилась после загрузки сохранения
    if (isLoadButtonPressed)
    {
        isLoadButtonPressed = false;
        LoadData();
    }
    InitSavedData();
}

public void ButtonSave()
{
    latestSaveSlot = currentActiveSlot;
    PlayerPrefs.SetFloat(
        "transform position x" + currentActiveSlot, playerTransform.position.x);
    // y z
    PlayerPrefs.SetInt("task 1 completed" + currentActiveSlot, isTask1Completed);
    PlayerPrefs.SetInt("latestSaveSlot", latestSaveSlot);
    PlayerPrefs.SetInt(
        "act number" + currentActiveSlot, gameManagerScript.currentActNumber);
    SaveStepNumber();
    PlayerPrefs.SetString(
        "sceneName" + currentActiveSlot, SceneManager.GetActiveScene().name);
    PlayerPrefs.SetInt("slotImage" + currentActiveSlot, imageNumberForCurrentSlot);
    PlayerPrefs.Save();
}

public void ButtonLoad()
{
    CheckForContinueButton();
    isLoadButtonPressed = true;
    SceneManager.LoadScene(PlayerPrefs.GetString("sceneName" + currentActiveSlot));
    gameManagerScript.currentActNumber = 
        PlayerPrefs.GetInt("act number" + currentActiveSlot);
    LoadStepNumber();
}

public void ButtonContinue()
{
    // Если есть хоть 1 сохраненный слот
    if (latestSaveSlot != 0)
    {
        isItContinueButton = true;
        ButtonLoad();
    }
}

// Метод срабатывает при нажатии на один из 6 слотов сохранения
// Таких методов 6 - для каждого слота
public void ButtonSaveSlot1()
{
    currentActiveSlot = 1;
    PlayerPrefs.SetInt("currentActiveSlot", currentActiveSlot);
    PlayerPrefs.Save();
}

// Загружаем это после каждой загрузки новой сцены
private void InitSavedData()
{
    latestSaveSlot = PlayerPrefs.GetInt("latestSaveSlot");
    LoadTasks();
}

// Загружаем это только если сцена загружена из сохранения
private void LoadData()
{
    currentActiveSlot = PlayerPrefs.GetInt("currentActiveSlot");
    CheckForContinueButton();
    isItContinueButton = false;
    playerTransform.position = new Vector3(
        PlayerPrefs.GetFloat("transform position x" + currentActiveSlot),
        PlayerPrefs.GetFloat("transform position y" + currentActiveSlot),
        PlayerPrefs.GetFloat("transform position z" + currentActiveSlot));
    LoadTasks();
}

// ЗЗагружаем состояния второстепенных заданий
private void LoadTasks()
{
    isTask1Completed = PlayerPrefs.GetInt("task 1 completed" + currentActiveSlot);
    //..
}

// Если нажата кнопка "Продолжить", а не "Загрузить"
private void CheckForContinueButton()
{
    if (isItContinueButton)
        currentActiveSlot = PlayerPrefs.GetInt("latestSaveSlot");
}

private void SaveStepNumber()
{
    switch (gameManagerScript.currentActNumber)
    {
        case 0: PlayerPrefs.SetInt(
            "step number" + currentActiveSlot, Act_0.stepNumber); break;
        case 1: PlayerPrefs.SetInt(
            "step number" + currentActiveSlot, Act_1.stepNumber); break;
        //..
        case 28: PlayerPrefs.SetInt(
            "step number" + currentActiveSlot, Act28.stepNumber); break;
    }
}

private void LoadStepNumber()
{
    switch (gameManagerScript.currentActNumber)
    {
        case 0:
            Act_0.stepNumber = PlayerPrefs.GetInt("step number" + currentActiveSlot);
            gameManagerScript.act0.QuickAct(Act_0.stepNumber); break;
        case 1:
            Act_1.stepNumber = PlayerPrefs.GetInt("step number" + currentActiveSlot);
            gameManagerScript.act1.QuickAct(Act_1.stepNumber); break;
        //...
        case 28:
            Act28.stepNumber = PlayerPrefs.GetInt("step number" + currentActiveSlot);
            gameManagerScript.act28.QuickAct(Act28.stepNumber); break;                    
    }
}

Состояния всех объектов должны быть актуальными для данного шага

Для каждого акта есть цепочка шагов, которые игрок проходит один за другим. На акте 2 шагов 22 и если мы на шаге 8 подойдем к док-станции дрона, введем пароль, то дрон улетит на кухню (шаг 11). Если потом начать новую игру и загрузить номер шага 11, то что дрон будет в док-станции, а должен быть на кухне. То есть номер шага мы загружаем корректно, а анимацию перемещения дрона не проигрываем. И таких мелочей куча, хранить их все в переменных, а потом загружать сложно. Поэтому создал метод, который является копией игрового цикла, но убрал оттуда строки, где ожидается активность со стороны игрока. И при загрузке игры мы просто в ускоренном темпе прогоняем все шаги от нулевого до требуемого, устанавливая актуальные состояния для всех объектов. Игра небольшая, поэтому в каждом акте немного шагов, но интересно, как это реализовано в крупных играх

// Быстро перебираем все шаги для загрузки сохранения
public void QuickAct(int lastStepNumber)
{
    Time.timeScale = 5;
    for (int i = 0; i < lastStepNumber; i++)
        switch (i)
        {
            case 0:
                stepNumber++;
                break;
            case 1: // Текстовое интро
                startTextCanvas.SetActive(true);
                PrintStartText();
                break;
            case 2: // Инициализация объектов акта
                openCloseEyesCanvas.SetActive(true);
                startTextCanvas.SetActive(false);
                elders.SetActive(true);
                break;
                //..
            case 22: // Выходим на улицу
                break;
        }
    Time.timeScale = 1;
}

Результат:


Исправление неожиданных багов

На некоторых сценах есть движущиеся объекты, такие как машины. У них есть положение, откуда они начинают движение после загрузки сцены. Если объект переместится из данной точки, и затем в этом месте сохранится игрок, то после загрузки сцены координаты обоих объектов будут пересекаться и игрок провалится сквозь пол. Чтобы это предотвратить, я триггерами создал зоны, в которых изначально находятся движущиеся объекты. Если игрок сохранится внутри одного из таких триггеров, то после загрузки он появится не внутри него, а рядом с ним

Comments are closed.