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

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

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

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

Офлайн
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?
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика