Этот учебник предполагает базовые знания Unity Engine. Если у вас нет собственного проекта, вы можете свободно взять пример проекта, связанный здесь. Вы также найдете загрузку завершенного проекта в конце этой статьи.
Если вы боретесь с сохранением данных между двумя сценами, это руководство для вас.
Отправная точка
Загрузите пример проекта:
[Ссылка на репозиторий GitHub]
[ZIP Скачать]
Логика
Unity — игровой движок со своей философией. Несмотря на то, что существует довольно много альтернатив, он довольно уникален в том, как он обрабатывает строительные блоки любой игры — игровые объекты, сцены, код, граф сцены. И под уникальным я подразумеваю, как легко это понять.
Если вы пытались запустить Unity для тестирования, поскольку его можно бесплатно загрузить, вы, вероятно, познакомились с реализацией его сценариев. «Скрипты», написанные на C #, JavaScript (или, начиная с Unity 5, UnityScript) или на языке Boo, являются компонентами, которые прикрепляются к любому игровому объекту. Внутри скрипта вы можете получить доступ к объекту, к которому он прикреплен, или к любым связанным объектам. Он довольно интуитивно понятен, прост в использовании и с ним интересно строить.
Возможно, тогда вы пытались построить второй уровень; скажем, ваш первый уровень был интерьер дома, а затем ваш второй уровень находится за пределами дома. Переход использует дверь, чтобы загрузить следующую сцену.
Вот основная проблема, которую мы будем решать сегодня. Каждый уровень в движке Unity называется «Сцена». Вы можете редактировать сцену, используя графический редактор, как вам нравится. Вы можете перемещать сцену, используя одну строку кода (которая запускается, возможно, при прикосновении игрока к двери или при помощи объекта и т. Д.). Каждая сцена имеет объекты, которые имеют «компоненты».
Общие «Объекты» представляют то, что еще составляет ваш уровень (сетки, триггеры, частицы и т. Д.)
Каждая сцена построена в своем начальном состоянии. Переход сцены в другую означает, что новая сцена будет загружена в своем первоначальном состоянии (естественно). Но как насчет статистики игрока, например, количества боеприпасов, опыта или инвентаря?
Как мы сохраняем данные, когда мы можем писать код только в «Scripts» — компонентах игровых объектов — которые просто разрушаются при переходах между сценами?
Вот где мы становимся немного хитрыми. Существует способ сохранить игровые объекты посредством переходов между сценами, эффективно создавая не объект всей сцены, а объект всей игры , который будет хранить наши данные.
Вот основной рабочий процесс игры с использованием такого объекта:
Нам нужно сохранить данные, перенести сцену, а затем загрузить данные обратно.
Вот основная разбивка логики, которую мы будем использовать:
- Независимо от сцены, в которой мы находимся (даже если это сцена 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;
}
//[...]
С этим кодом наш поток данных будет выглядеть примерно так:
Это касается любого перехода сцены, повсеместно. Даже переход от Сцены 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 Скачать]
Вопросов? Комментарии? Дайте нам знать в области ниже!