Сериализация игровых объектов

Сериализация игровых данных или проще говоря, сохранение игры. Представим такую ситуацию: у нас есть сцена с несколькими, допустим, кубиками, которые игрок может перемещать и после того, как игрок совершит манипуляции с этими объектами, нам бы хотелось сохранить текущее состояние сцены, чтобы после ее загрузки, всё вернулось на момент сохранения. Рассмотрим общую логику. В сцене есть предметы, с которыми можно взаимодействовать, все эти объекты находятся в массиве, после вызова метода сохранения, нам нужно сохранить данный массив, точнее параметры объектов: позицию, вращение и другие. Когда происходит загрузка сцены, мы удаляем дефолтные предметы массива/сцены, затем, загружаем их заново, из ресурсов, на основе полученных данных и присваиваем им сохраненные значения.

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

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

Альтернатива для Vector3:

using System.Collections;
using UnityEngine;

[System.Serializable]
public struct SurrogateVector3 {

	public float x, y, z;

	public SurrogateVector3(float rX, float rY, float rZ)
	{
		x = rX;
		y = rY;
		z = rZ;
	}

	public static implicit operator Vector3(SurrogateVector3 rValue)
	{
		return new Vector3(rValue.x, rValue.y, rValue.z);
	}

	public static implicit operator SurrogateVector3(Vector3 rValue)
	{
		return new SurrogateVector3(rValue.x, rValue.y, rValue.z);
	}
}

Альтернатива для Quaternion:

using System.Collections;
using UnityEngine;

[System.Serializable]
public struct SurrogateQuaternion {

	public float x, y, z, w;

	public SurrogateQuaternion(float rX, float rY, float rZ, float rW)
	{
		x = rX;
		y = rY;
		z = rZ;
		w = rW;
	}

	public static implicit operator Quaternion(SurrogateQuaternion rValue)
	{
		return new Quaternion(rValue.x, rValue.y, rValue.z, rValue.w);
	}

	public static implicit operator SurrogateQuaternion(Quaternion rValue)
	{
		return new SurrogateQuaternion(rValue.x, rValue.y, rValue.z, rValue.w);
	}
}

С этим всё. Теперь можно передавать значение от обычного вектора, нашему суррогату.

Теперь, создадим базовый класс для игровых предметов:

using System.Collections;
using UnityEngine;

[System.Serializable]
public class BaseData {

	[System.NonSerialized] private GameObject _inst; // ссылка на сам объект
	public GameObject Inst {set{_inst = value;}}
	public string Name {get;set;}
	public SurrogateVector3 Position {get;set;}
	public SurrogateQuaternion Rotation {get;set;}

	public BaseData(){}

	public BaseData(string name, Vector3 position, Quaternion rotation)
	{
		this.Name = name;
		this.Position = position;
		this.Rotation = rotation;
	}

	public BaseData(GameObject current, string name, Vector3 position, Quaternion rotation)
	{
		this.Inst = current;
		this.Name = name;
		this.Position = position;
		this.Rotation = rotation;
	}

	public virtual void Update()
	{
		if(_inst == null) // если объект был удален, он так-же не будет восстановлен после загрузки сцены
		{
			this.Name = null;
			return;
		}

		Position = _inst.transform.position;
		Rotation = _inst.transform.rotation;
	}
}

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

Если мы хотим, например, сохранить позицию игрока и еще его здоровье/энергию:

using System.Collections;
using UnityEngine;

[System.Serializable]
public class PlayerData : BaseData { // сохранить не только позицию и вращение, но и доп. параметры

	public float Health {get;set;}

	public PlayerData(){}

	public PlayerData(string name, Vector3 position, Quaternion rotation, float health) : base(name, position, rotation)
	{
		this.Health = health;
	}

	public PlayerData(GameObject current, string name, Vector3 position, Quaternion rotation, float health) : base(current, name, position, rotation)
	{
		this.Health = health;
	}

	public override void Update()
	{
		base.Update();

		// здесь нужно делать запрос на обновление переменных
		// например: this.Health = Manager.currentPlayerHealth;
	}
}

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

Следующий класс:

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

[System.Serializable]
public class SceneState {

	public List<BaseData> itemList = new List<BaseData>(); // список всех объектов для сериализации

	public SceneState(){}

	public void AddItem(BaseData item)
	{
		itemList.Add(item);
	}

	public void Update()
	{
		foreach(BaseData t in itemList)
			t.Update();
	}
}

Здесь мы из параметров объектов, формируем массив и взаимодействуем с ним.

Далее, метод сериализации и десериализации:

using System.Collections;
using UnityEngine;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public class Serializator {

	public static void SaveBinary(SceneState state, string dataPath)
	{
		BinaryFormatter binary = new BinaryFormatter();
		FileStream stream = new FileStream(dataPath, FileMode.Create);
		binary.Serialize(stream, state);
		stream.Close();
		Debug.Log("[Serializator] --> Сохранение по адресу: " + dataPath);
	}

	public static SceneState LoadBinary(string dataPath)
	{
		BinaryFormatter binary = new BinaryFormatter();
		FileStream stream = new FileStream(dataPath, FileMode.Open);
		SceneState state = (SceneState)binary.Deserialize(stream);
		stream.Close();
		Debug.Log("[Serializator] --> Загрузка данных из файла: " + dataPath);
		return state;
	}
}

Для удобства добавлен вывод в лог, чтобы легко можно было найти ссылку на файл.

Со вспомогательными инструментами закончили, двигаемся дальше.

Генератор объектов сцены:

using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine;
using System.IO;

public class Generator : MonoBehaviour {

	// здесь мы указываем объекты, для которых предусмотрена сериализация
	[SerializeField] private GeneratorComponent[] item;
	[SerializeField] private GeneratorComponent player;

	private SceneState state;
	private string dataPath;
	private static Generator _inst;

	void Awake()
	{
		_inst = this;

		dataPath = Application.persistentDataPath + "/" + SceneManager.GetActiveScene().name + ".dat";

		if(File.Exists(dataPath))
		{
			state = Serializator.LoadBinary(dataPath);
			Generate();
		}
		else SetDefault();
	}

	// добавить новый объект на сцену, он должен быть в Resources
	public static void InstantiateItem(string prefabName, Vector3 position, Quaternion rotation)
	{
		_inst.InstantiateItem_inst(prefabName, position, rotation);
	}

	void InstantiateItem_inst(string prefabName, Vector3 position, Quaternion rotation)
	{
		GameObject obj = Resources.Load<GameObject>(prefabName);

		if(obj != null)
		{
			Instantiate(obj, position, rotation);

			state.AddItem(new BaseData(obj, prefabName, position, rotation));
		}
	}

	void Clear() // очистка сцены
	{
		for(int i = 0; i < item.Length; i++)
		{
			item[i].gameObject.SetActive(false);
			Destroy(item[i].gameObject);
		}

		player.gameObject.SetActive(false);
		Destroy(player.gameObject);
	}
	
	void SetDefault() // если файла с сохранениями нет, заполняем массив дефолтными данными
	{
		state = new SceneState();

		for(int i = 0; i < item.Length; i++)
		{
			if(!string.IsNullOrEmpty(item[i].prefabName))
			{
				state.AddItem(new BaseData(item[i].gameObject, item[i].prefabName, item[i].transform.position, item[i].transform.rotation));
			}
		}

		if(player != null && !string.IsNullOrEmpty(player.prefabName))
		{
			state.AddItem(new PlayerData(player.gameObject, player.prefabName, player.transform.position, player.transform.rotation, 100));
		}
	}

	void Generate() // воссоздание объектов
	{
		Clear();

		foreach(BaseData t in state.itemList)
		{
			if(!string.IsNullOrEmpty(t.Name))
			{
				t.Inst = Instantiate(Resources.Load<GameObject>(t.Name), t.Position, t.Rotation) as GameObject;
			}
		}
	}

	public void SaveScene() // сохранение текущей сцены
	{
		state.Update(); // обновляем переменные, перед сохранением
		Serializator.SaveBinary(state, dataPath);
	}
}

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

На сохраняемые объекты, цепляем:

using System.Collections;
using UnityEngine;

public class GeneratorComponent : MonoBehaviour {

	[SerializeField] private string _prefabName; // имя префаба, данного объекта в Resources
	public string prefabName {get{return _prefabName;}}

}

Обратите внимание! Все предметы для сериализации должны находиться в папке Resources в виде префабов. Так как определенный объект мы можем дублировать несколько раз, изменить его имя. Оригинальное имя не должно меняться, чтобы делать запросы в ресурсах юнити, поэтому и нужен данный простенький скрипт.

Если в процессе игры, нам нужно добавить новый объект на сцену:

Generator.InstantiateItem("nane", Vector3, Quaternion);

Метод похож на обычный Instantiate, но тут учитывается сериализация данного объекта.

Скачать скрипты и демо:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.
Тестировалось на: Unity 2017.1.0

Комментариев 18

Офлайн
Disablak 19 июля 2017
На сколько безопасный даный способ сохранения?
Онлайн
Light 19 июля 2017
Disablak, в плане чего безопасность?

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

Если нужно хранить данные переменных (золото, инвентарь, жизни и т.п.), то рекомендую этот вариант https://null-code.ru/project/155-rasshirennaya-sistema-sohraneniya.html кроме сериализации, в нем использовано доп. шифрование, значений ключей.
Офлайн
Спасибо очень помогли!)))
Офлайн
Привет! Спасибо вам за ваши труды. Не могли бы рассказать как сделать движение игрового объекта (поезда в моем случае) по чекпоинтам,но с возможностью их менять.Допустим поезд переходит по стрелкам. Спасибо
Онлайн
Light 20 июля 2017
Денис Чёрный, была такая мысль, но пака не могу сказать буду делать подобный проект или нет, в любом случаи тут надо работать с кривыми Безье.
Офлайн
FunStrike 8 августа 2017
У меня началась такая проблема, после второго сохранение(Первый раз сораняю создается файл, перезапускаю сцену, загружает, и еще раз сохраняю), при загрузке каждый объект удваивается. Изменения в скрипте Generator, добавляю объекты в item внутрий функций SaveScene, так же добавил еще одну переменную float типа для передачи в BaseDate. Пробывал удалял файл и пересоздавать заного в том же SaveScene, обнулял массив item, не помогло.
Онлайн
Light 8 августа 2017
FunStrike, нужно смотреть изменения в скрипте.
Офлайн
FunStrike 8 августа 2017
Онлайн
Light 8 августа 2017
FunStrike, перед воссозданием сцены, ее нужно очистить от тех объектов, которые будут воссозданы.
Офлайн
FunStrike 8 августа 2017
Light,
Не совсем понял, если ты про Clear то он работает
Онлайн
Light 8 августа 2017
FunStrike, кинь мне в ЛС архив со скриптами, где были хоть какие-то изменения.
Офлайн
FunStrike 8 августа 2017
Я так понимаю нельзя просто взять и целый скрипт на объекте сохранить?) Надо его как то обработать?
Онлайн
Light 8 августа 2017
FunStrike, со скрипта, так же как и с объекта, можно внести данные в BaseData, а потом восстановить их, как это делается с параметрами позиции и вращения объекта.
Офлайн
evan 16 августа 2017
можно использовать ScriptableObject для хранения данных, создать класс для работы с ним и во время игры сразу записывать эти позиции, чтобы позже не морочится с меню паузы и т.п.
Офлайн
Prog-Maker 12 сентября 2017
А почему бы не использовать ScriptableObject?
Офлайн
TheFrankyDoll 13 марта 2018
Так, а если я хочу процедурно создать что-нибудь, что должно будет сохраниться, что делать?
Ведь насколько я понял, этот GameObj нужно записать в массив Generator-а, но массив по определению имеет фиксированный размер.
Офлайн
TheFrankyDoll 13 марта 2018
Цитата: TheFrankyDoll
Так, а если я хочу процедурно создать что-нибудь, что должно будет сохраниться, что делать?
Ведь насколько я понял, этот GameObj нужно записать в массив Generator-а, но массив по определению имеет фиксированный размер.

Итак, я понял что для этого создан Generator.InstantiateItem("nane", Vector3, Quaternion);
Но тут возник ещё один вопрос: как получать сохранённые данные для игрока, и где это лучше делать? Ну то есть, я сохранил данные в PlayerData, но как при загрузке указать откуда получить значение той же переменной "Health"?
Онлайн
Light 14 марта 2018
TheFrankyDoll, нужно выполнить обновление состояния значений объектов. Но вообще это просто пример, как делать доп. параметры. Такой способ сериализации предназначен для игр типа Subnautica или похожих, где игрок что-то строит и нужно это сохранять.

Что касается параметров персонажа и т.п., к чему нужен прямой доступ, то лучше использовать, нечто вроде https://null-code.ru/project/155-rasshirennaya-sistema-sohraneniya.html
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика