Chronicles of cyberpunk — architecture

Home  /  Programming  /  Chronicles of cyberpunk — architecture

Игровой цикл

В игре 9 игровых сцен и еще титры, главное меню, preload. Когда в Unity загружаем новую сцену, все объекты предыдущей удаляются из памяти и к ним нельзя обратиться. А для каждого акта нужно активировать разные группы объектов, поэтому нужно хранить где-то номер текущего акта

Для этого есть скрипт GameManager с функцией DontDestroyOnLoad(), которая инициализируется на первой сцене. Инициализация этой функции — единственное ее назначение, после нее загружается главное меню. Объект, который содержит скрипт, помечен тегом, поэтому после загрузки движок быстро его находит. Этот скрипт существует на протяжении всей игры на всех сценах в единственном экземпляре

    public string nameOfLastLoadedScene;

    private GameObject player;
    public int currentActNumber { get; set; } // инкремент в конце каждого акта

                         // Главное меню
                         // Preload
    private Act_0 act0;
    private Act_1 act1;
    private Act_2 act2;
    //..
    private Act_2 act28;

   void Start ()
    {
        DontDestroyOnLoad(this);
        SceneManager.LoadScene("Home");
    }

    void OnLevelWasLoaded()
    {
        player = GameObject.FindGameObjectWithTag("PlayerOnMainScene");
        PlacingPlayerNearHouse();
    }

    // При загрузке главной сцены размещаем игрока рядом с домом, откуда он выходил
    void PlacingPlayerNearHouse()
    {
        switch (nameOfLastLoadedScene)
        {            
            case "": player.transform.position = new Vector3(-4.42f, 0.65f, 49.26f); break;           
            case "": player.transform.position = new Vector3(-14.8f, 0.65f,  -7.2f); break;
            case "": player.transform.position = new Vector3( 0.44f, 0.65f,  6.89f); break;
            case "": player.transform.position = new Vector3(36.63f, 0.65f,   6.9f); break;
            case "": player.transform.position = new Vector3( 9.45f, 0.65f,-36.73f); break;
            case "": player.transform.position = new Vector3(-0.32f, 0.65f, -9.34f); break;
            default: break;
        }
    }

В зависимости от номера текущего акта загружаем определенное поведение

    void Update()
    {
        ActManager();
    }

    // Запускать определенный акт в зависимости от currentActNumber
    void ActManager()
    {
        Debug.Log("currentActNumber: " + currentActNumber);
        if (SceneManager.GetActiveScene().name != "PRELOAD" &&
            SceneManager.GetActiveScene().name != "STARTSCREEN")
        {
            GameObject objWithActScripts = GameObject.FindGameObjectWithTag("Acts");

            switch (currentActNumber)
            {
                case 0: act0 = objWithActScripts.GetComponent<Act_0>();
                        act0.StartAct(); break;
                case 1: act1 = objWithActScripts.GetComponent<Act_1>();
                        act1.StartAct(); break;
                //..
            }
        }
    }

Программируем последовательность шагов для каждого акта отдельно

    public void StartAct1(MonoBehaviour mb)
    {
        switch (stepNumber)
        { 
            case 0:
                // проигрываем анимацию открытия глаз
                mb.StartCoroutine(OpenCloseEyesAnimation());
                break;
            case 1:
                // отображаем подсказку "нажмите любую клавишу, чтобы проснуться"
                ShowTip(contentToPrint.tipsTasks[0]);
                stepNumber++;
                break;
            case 2:
                // при нажатии на любую кнопку очищаем текст подсказки,
                // анимация закрытия глаз, аудио зевания
                if (Input.anyKey)
                {
                    audioYawn.Play();
                    ShowTip("");
                    mb.StartCoroutine(OpenCloseEyesAnimation());
                }
                break;
            case 3:
                // отключаем игрока в кровати и включаем основного игрока,
                // анимация открытия глаз
                PlayerInBedDisable();
                mb.StartCoroutine(OpenCloseEyesAnimation());
                break;
            case 4:
                // блокируем перемещение, отображаем диалог с дроном
                ShowUIAndPrintMessage(0, 0);                
                stepNumber++;
                break;
            //..
        }
    }

Реализация сохранения/загрузки

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

При загрузке сцены нужно найти объекты со скриптами, чтобы к ним обратиться. И если есть метод со строчкой, загружающей новую сцену, то код, выполняющийся после этой строчки, выполняется не для новой загруженной сцены, а для предыдущей. Поэтому нужно проверять, чтобы все выполнялось на нужных сценах. Часть переменных сделал статическими, а часть кода загрузки вызывал в 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.