Статьи

Сохранение данных между сценами в Unity

Этот учебник предполагает базовые знания Unity Engine. Если у вас нет собственного проекта, вы можете свободно взять пример проекта, связанный здесь. Вы также найдете загрузку завершенного проекта в конце этой статьи.

Если вы боретесь с сохранением данных между двумя сценами, это руководство для вас.

единичный логотип


Отправная точка

Загрузите пример проекта:

[Ссылка на репозиторий GitHub]
[ZIP Скачать]

Логика

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

Если вы пытались запустить Unity для тестирования, поскольку его можно бесплатно загрузить, вы, вероятно, познакомились с реализацией его сценариев. «Скрипты», написанные на C #, JavaScript (или, начиная с Unity 5, UnityScript) или на языке Boo, являются компонентами, которые прикрепляются к любому игровому объекту. Внутри скрипта вы можете получить доступ к объекту, к которому он прикреплен, или к любым связанным объектам. Он довольно интуитивно понятен, прост в использовании и с ним интересно строить.

Возможно, тогда вы пытались построить второй уровень; скажем, ваш первый уровень был интерьер дома, а затем ваш второй уровень находится за пределами дома. Переход использует дверь, чтобы загрузить следующую сцену.

Вот основная проблема, которую мы будем решать сегодня. Каждый уровень в движке Unity называется «Сцена». Вы можете редактировать сцену, используя графический редактор, как вам нравится. Вы можете перемещать сцену, используя одну строку кода (которая запускается, возможно, при прикосновении игрока к двери или при помощи объекта и т. Д.). Каждая сцена имеет объекты, которые имеют «компоненты».

1

Общие «Объекты» представляют то, что еще составляет ваш уровень (сетки, триггеры, частицы и т. Д.)

Каждая сцена построена в своем начальном состоянии. Переход сцены в другую означает, что новая сцена будет загружена в своем первоначальном состоянии (естественно). Но как насчет статистики игрока, например, количества боеприпасов, опыта или инвентаря?

Как мы сохраняем данные, когда мы можем писать код только в «Scripts» — компонентах игровых объектов — которые просто разрушаются при переходах между сценами?

Вот где мы становимся немного хитрыми. Существует способ сохранить игровые объекты посредством переходов между сценами, эффективно создавая не объект всей сцены, а объект всей игры , который будет хранить наши данные.

Вот основной рабочий процесс игры с использованием такого объекта:

2

Нам нужно сохранить данные, перенести сцену, а затем загрузить данные обратно.

Вот основная разбивка логики, которую мы будем использовать:

  • Независимо от сцены, в которой мы находимся (даже если это сцена 1), ПЕРВЫЙ инициализирует игрока начальными данными.
  • Затем скопируйте данные из Глобального объекта.

Логика инициализации глобального объекта:

  • Инициализируйте начальными данными и сохраните наш экземпляр с помощью переходов между сценами.

Этот поток гарантирует, что сцена 1 всегда инициализирует игрока начальными данными. Затем, если вы сохраняете данные в глобальном объекте перед любым переходом сцены, вы гарантируете, что эти данные всегда будут загружаться в объект Player следующего уровня.

Это предполагает, что вы поместили один и тот же объект Player (предпочтительно объект Prefabbed) в каждую сцену. Обратите внимание, что «логика инициализации объекта Player» применима к любому объекту, который нуждается в иллюзии «сохранения через сцены»; мы используем Player только в качестве наиболее очевидного примера.

Код

Хорошо, достаточно графиков и абстрактного мышления на данный момент. Давайте перейдем к кодированию.

Я предполагаю, что к этому моменту у вас есть две сцены и реализация для перехода между ними — обратитесь к стартовому проекту в верхней части статьи. Вы играете с одним и тем же аватаром игрока, и вам просто нужно сохранить данные игрока между ними, чтобы создать иллюзию одного и того же «объекта игрока».

Давайте сначала создадим глобальный объект для всей игры. Важно, чтобы мы поняли это правильно, поэтому давайте выясним, что нам нужно для этого:

  • Нам нужно иметь доступ к нему из любого другого скрипта, из любой части игры. Логичным выбором для этого была бы концепция дизайна Singleton. Если вы не знаете, что это такое, круто, вы собираетесь узнать что-то новое.
  • Нам нужно, чтобы он был инициализирован только один раз и перенесен через переходы сцены.
  • Нам нужно хранить любые данные, которые нам могут понадобиться перенести. Мы знаем переменные, которые нам нужно сохранить, поэтому просто введем их.

Сначала перейдите к своей первой сцене и создайте новый пустой игровой объект. Переименуйте его в нечто более подходящее, например, «GameMaster» или «GlobalObject».

Затем создайте новый сценарий C # (желательно в новой папке — не забывайте, чтобы все было организовано). Дайте ему подходящее имя. Мой сценарий называется «GlobalControl».

Прикрепите новый пустой скрипт C # к новому игровому объекту и откройте скрипт в выбранном вами редакторе. MonoDevelop, который поставляется с Unity, хорош, но вы также можете использовать Visual Studio.

Поместите этот код в скрипт GlobalControl:

public class GlobalControl : MonoBehaviour 
{
    public static GlobalControl Instance;

    void Awake ()   
       {
        if (Instance == null)
        {
            DontDestroyOnLoad(gameObject);
            Instance = this;
        }
        else if (Instance != this)
        {
            Destroy (gameObject);
        }
      }
}

Основная предпосылка шаблона проектирования Singleton состоит в том, что существует один публичный статический экземпляр одного класса. В методе пробуждения (тот, который вызывается, когда предполагается, что объект загружен), мы проверяем это, говоря: «Если есть другой экземпляр, уничтожьте его и убедитесь, что этот экземпляр является этим».

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

Если вы хотите, вы можете проверить это сейчас. Поместите GameMaster или GlobalObject (или что-либо еще с этим сценарием) в каждую имеющуюся у вас сцену и попробуйте перемещать сцены во время выполнения. Вы заметите, что в данный момент в сцене есть только один такой глобальный объект.

Если мы сейчас запишем в него данные, они будут сохранены!

Теперь перейдем к другой части проблемы:
Что нам нужно сохранить?

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

  • HP , с начальным значением 100,
  • Боеприпасы , с начальным значением 0,
  • XP , с начальным значением 0.

Они сохраняются где-то внутри вашего объекта Player . Какой именно сценарий, на самом деле не имеет значения. Нам нужно, чтобы в нашем GlobalObject были одинаковые переменные, поэтому добавьте их в свой код:

 public class GlobalControl : MonoBehaviour 
{
//[...]
    public float HP;
    public float Ammo;
    public float XP;
//[...]

Теперь мы готовы сохранить данные. Нам нужно сохранять данные только при переходе сцены и загружать их, когда мы начинаем сцену.

Вот как сохранить данные из скрипта, в котором вы храните переменные вашего игрока:

 public class PlayerState : MonoBehaviour 
{
//[...]

    public float HP;
    public float Ammo;
    public float XP;

//[...]

//Save data to global control   
    public void SavePlayer()
    {
        GlobalControl.Instance.HP = HP;
        GlobalControl.Instance.Ammo = Ammo;
        GlobalControl.Instance.XP = XP;
        }
//[...]

Целесообразно создать специальную функцию для сохранения данных игрока в экземпляр.
Теперь пропущен еще один шаг: загрузка из GlobalControl. Вы можете легко вызвать это в функции Start скрипта State вашего игрока:

 public class PlayerState : MonoBehaviour 
{
//[...]

    public float HP;
    public float Ammo;
    public float XP;

//[...]


    //At start, load data from GlobalControl.
    void Start () 
    {   
        HP = GlobalControl.Instance.HP;
        Ammo = GlobalControl.Instance.Ammo;
        XP = GlobalControl.Instance.XP;
    }
//[...]

С этим кодом наш поток данных будет выглядеть примерно так:

3

Это касается любого перехода сцены, повсеместно. Даже переход от Сцены 2 к Сцене 1 теперь будет сохранять статистику вашего игрока!

Есть, конечно, несколько изломов, чтобы работать. Например, если вы выходите в главное меню и запускаете новую игру (не выходя из игры вообще), вам необходимо сбросить сохраненные данные, в противном случае вы начнете новую игру со статистикой игрока из предыдущего сеанса!

Почему не публичный статический класс?

На данный момент, если вы знакомы с программированием на C # и .NET, вам может быть интересно, почему мы просто не используем что-то подобное:

 public static class GlobalObject

Вопреки тому, что вы можете интуитивно подумать, публичные статические классы на самом деле не сохраняются в игре. Поскольку любой класс (то есть любой скрипт) присоединен к игровому объекту, он будет уничтожен при загрузке новой сцены. Даже если в другой сцене есть новый открытый статический класс, данные внутри будут сброшены, то есть статический класс будет инициализирован заново при загрузке сцены.

Вот почему мы должны использовать метод DontDestroyOnLoad и немного больше кода, чтобы убедиться, что у нас есть только один экземпляр класса, который мы намерены переносить по уровням.

Полировка и подготовка к следующему уроку

Вы, наверное, заметили, что в этом примере нетрудно вручную ввести три необходимых значения в GlobalData и обратно. Но что, если у нас будет большая, более сложная игра с десятками, если не сотнями переменных игрока, за которыми нужно следить?

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

Во-первых, давайте создадим новый скрипт в нашем проекте. Это будет немного другой тип сценария, потому что он не будет расширять класс MonoBehavior и не будет привязан ни к какому объекту.

Мы назовем его «Сериализуемые», и это будет выглядеть так:

 using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;

public class PlayerStatistics
{
    public float HP;
    public float Ammo;
    public float XP;
}

Как видите, здесь нет функций, нет пространств имен, есть только один класс без конструктора, содержащий наши три известные переменные. Зачем мы это делаем?

Таким образом, в нашем глобальном объекте мы можем превратить отдельные статистические данные в один класс для их хранения:

 public class GlobalControl : MonoBehaviour 
{
//[...]
    public float HP;
    public float Ammo;
    public float XP;
//[...]

…как это:

 public class GlobalControl : MonoBehaviour 
{
//[...]
    public PlayerStatistics savedPlayerData = new PlayerStatistics();
//[...]

И то же самое в переменных игрока:

 public class PlayerState : MonoBehaviour 
{
//[...]

    public float HP;
    public float Ammo;
    public float XP;

//[...]

Это дает нам дополнительный уровень безопасности. Мы не можем случайно записать неверные переменные проигрывателя в сохраненные переменные (например, XP = HP):

 public class PlayerState : MonoBehaviour 
{
//[...]
    public PlayerStatistics localPlayerData = new PlayerStatistics();
//[...]

Теперь, когда мы хотим сохранить данные, мы просто удаляем это:

 //Save data to global control   
    public void SavePlayer()
    {
        GlobalControl.Instance.HP = HP;
        GlobalControl.Instance.Ammo = Ammo;
        GlobalControl.Instance.XP = XP;
    }

… и вместо этого скопируйте ссылку на класс, который содержит наши данные. Все значения внутри останутся там, где они должны:

 //Save data to global control   
    public void SavePlayer()
    {
                GlobalControl.Instance.savedPlayerData = localPlayerData;        
    }

То же самое для загрузки данных игрока в функцию запуска игрока!

Теперь у нас есть вся статистика нашего игрока в классе, который представляет только данные игрока, и ничего лишнего. Во время разработки вашей игры, когда вам нужно сохранить и загрузить больше переменных игрока, просто добавьте их в класс — сохранение и извлечение данных игрока в / из Global Object останется прежним.

Вывод

В следующей статье мы рассмотрим сохранение и загрузку всего класса на жесткий диск (а не только глобальный объект) с помощью сериализации.

Если вы застряли в какой-то момент или просто хотите посмотреть, как выглядит готовый проект, вы можете скачать его здесь:

[GitHub Repository]
[ZIP Скачать]

Вопросов? Комментарии? Дайте нам знать в области ниже!