Расширенная система сохранения

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

Это очень похоже на встроенную в Unity систему PlayerPrefs, разница в том, что тут мы работает с файлом и у нас есть возможность использовать шифрование данных. Ну и плюс добавлено пару новых функций.

Чтобы всё работало как надо. Например, в стартовой сцене, цепляем на пустой объект:

using UnityEngine;
using System.Collections;

public class SaveSystemSetup : MonoBehaviour {

	[SerializeField] private string fileName = "Profile.bin";
	[SerializeField] private bool dontDestroyOnLoad;

	void Awake()
	{
		SaveSystem.Initialize(fileName);
		if(dontDestroyOnLoad) DontDestroyOnLoad(transform.gameObject);
	}

	void OnApplicationQuit()
	{
		SaveSystem.SaveToDisk();
	}
}

Здесь мы определяем некоторые базовые параметры.

Далее, нужно создать классы для хранения и обработки данных.
Создайте скрипт с именем, например, SaveSystemData очищаем его и пишем:

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

[System.Serializable]
public class SaveData {

	public string Key {get;set;}
	public string Value {get;set;}

	public SaveData(){}

	public SaveData(string key, string value)
	{
		this.Key = key;
		this.Value = value;
	}
}

[System.Serializable]
public class DataState {

	public List<SaveData> items = new List<SaveData>();

	public DataState(){}

	public void AddItem(SaveData item)
	{
		items.Add(item);
	}
}

public class SerializatorBinary {

	public static void SaveBinary(DataState state, string dataPath)
	{
		BinaryFormatter binary = new BinaryFormatter();
		FileStream stream = new FileStream(dataPath, FileMode.Create);
		binary.Serialize(stream, state);
		stream.Close();
	}

	public static DataState LoadBinary(string dataPath)
	{
		BinaryFormatter binary = new BinaryFormatter();
		FileStream stream = new FileStream(dataPath, FileMode.Open);
		DataState state = (DataState)binary.Deserialize(stream);
		stream.Close();
		return state;
	}
}

Как видно, тут три класса. SaveData и DataState используются для создания массива ключей и значений. А класс SerializatorBinary нужен для сериализации и десериализации данных.

Продолжим. В папку со скриптами проекта кидаем:

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

public static class SaveSystem {
	
	private static string file;
	private static bool loaded;
	private static DataState data;

	public static void Initialize(string fileName) // инициализация (используется один раз, после запуска приложения)
	{
		if(!loaded)
		{
			file = fileName;
			if(File.Exists(GetPath())) Load(); else data = new DataState();
			loaded = true;
		}
	}

	static string GetPath()
	{
		return Application.persistentDataPath + "/" + file;
	}

	static void Load()
	{
		data = SerializatorBinary.LoadBinary(GetPath());
		Debug.Log("[SaveSystem] --> Загрузка файла сохранений: " + GetPath());
	}

	static void ReplaceItem(string name, string item)
	{
		bool j = false;
		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				data.items[i].Value = Crypt(item);
				j = true;
				break;
			}
		}

		if(!j) data.AddItem(new SaveData(name, Crypt(item)));
	}

	public static bool HasKey(string name) // проверка, на наличие ключа
	{
		if(string.IsNullOrEmpty(name)) return false;

		foreach(SaveData k in data.items)
		{
			if(string.Compare(name, k.Key) == 0)
			{
				return true;
			}
		}

		return false;
	}

	public static void SetVector3(string name, Vector3 val)
	{
		if(string.IsNullOrEmpty(name)) return;
		SetString(name, val.x + "|" + val.y + "|" + val.z);
	}

	public static void SetVector2(string name, Vector2 val)
	{
		if(string.IsNullOrEmpty(name)) return;
		SetString(name, val.x + "|" + val.y);
	}

	public static void SetColor(string name, Color val)
	{
		if(string.IsNullOrEmpty(name)) return;
		SetString(name, val.r + "|" + val.g + "|" + val.b + "|" + val.a);
	}

	public static void SetBool(string name, bool val) // установка ключа и значения
	{
		if(string.IsNullOrEmpty(name)) return;
		string tmp = string.Empty;
		if(val) tmp = "1"; else tmp = "0";
		ReplaceItem(name, tmp);
	}

	public static void SetFloat(string name, float val)
	{
		if(string.IsNullOrEmpty(name)) return;
		ReplaceItem(name, val.ToString());
	}

	public static void SetInt(string name, int val)
	{
		if(string.IsNullOrEmpty(name)) return;
		ReplaceItem(name, val.ToString());
	}

	public static void SetString(string name, string val)
	{
		if(string.IsNullOrEmpty(name)) return;
		ReplaceItem(name, val);
	}

	public static void SaveToDisk() // запись данных в файл
	{
		if(data.items.Count == 0) return;
		SerializatorBinary.SaveBinary(data, GetPath());
		Debug.Log("[SaveSystem] --> Сохранение игровых данных: " + GetPath());
	}

	public static Vector3 GetVector3(string name)
	{
		if(string.IsNullOrEmpty(name)) return Vector3.zero;
		return iVector3(name, Vector3.zero);
	}

	public static Vector3 GetVector3(string name, Vector3 defaultValue)
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iVector3(name, defaultValue);
	}

	static Vector3 iVector3(string name, Vector3 defaultValue)
	{
		Vector3 vector = Vector3.zero;

		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				string[] t = Crypt(data.items[i].Value).Split(new char[]{'|'});
				if(t.Length == 3)
				{
					vector.x = floatParse(t[0]);
					vector.y = floatParse(t[1]);
					vector.z = floatParse(t[2]);
					return vector;
				}
				break;
			}
		}

		return defaultValue;
	}

	public static Vector2 GetVector2(string name)
	{
		if(string.IsNullOrEmpty(name)) return Vector2.zero;
		return iVector2(name, Vector2.zero);
	}

	public static Vector2 GetVector2(string name, Vector2 defaultValue)
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iVector2(name, defaultValue);
	}

	static Vector2 iVector2(string name, Vector2 defaultValue)
	{
		Vector2 vector = Vector2.zero;

		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				string[] t = Crypt(data.items[i].Value).Split(new char[]{'|'});
				if(t.Length == 2)
				{
					vector.x = floatParse(t[0]);
					vector.y = floatParse(t[1]);
					return vector;
				}
				break;
			}
		}

		return defaultValue;
	}

	public static Color GetColor(string name)
	{
		if(string.IsNullOrEmpty(name)) return Color.white;
		return iColor(name, Color.white);
	}

	public static Color GetColor(string name, Color defaultValue)
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iColor(name, defaultValue);
	}

	static Color iColor(string name, Color defaultValue)
	{
		Color color = Color.clear;

		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				string[] t = Crypt(data.items[i].Value).Split(new char[]{'|'});
				if(t.Length == 4)
				{
					color.r = floatParse(t[0]);
					color.g = floatParse(t[1]);
					color.b = floatParse(t[2]);
					color.a = floatParse(t[2]);
					return color;
				}
				break;
			}
		}

		return defaultValue;
	}

	public static bool GetBool(string name) // получить значение по ключу
	{
		if(string.IsNullOrEmpty(name)) return false;
		return iBool(name, false);
	}

	public static bool GetBool(string name, bool defaultValue) // с установкой значения по умолчанию
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iBool(name, defaultValue);
	}

	static bool iBool(string name, bool defaultValue)
	{
		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				if(string.Compare(Crypt(data.items[i].Value), "1") == 0) return true; else return false;
			}
		}

		return defaultValue;
	}

	public static float GetFloat(string name)
	{
		if(string.IsNullOrEmpty(name)) return 0;
		return iFloat(name, 0);
	}

	public static float GetFloat(string name, float defaultValue)
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iFloat(name, defaultValue);
	}

	static float iFloat(string name, float defaultValue)
	{
		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				return floatParse(Crypt(data.items[i].Value));
			}
		}

		return defaultValue;
	}

	public static int GetInt(string name)
	{
		if(string.IsNullOrEmpty(name)) return 0;
		return iInt(name, 0);
	}

	public static int GetInt(string name, int defaultValue)
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iInt(name, defaultValue);
	}

	static int iInt(string name, int defaultValue)
	{
		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				return intParse(Crypt(data.items[i].Value));
			}
		}

		return defaultValue;
	}

	public static string GetString(string name)
	{
		if(string.IsNullOrEmpty(name)) return string.Empty;
		return iString(name, string.Empty);
	}

	public static string GetString(string name, string defaultValue)
	{
		if(string.IsNullOrEmpty(name)) return defaultValue;
		return iString(name, defaultValue);
	}

	static string iString(string name, string defaultValue)
	{
		for(int i = 0; i < data.items.Count; i++)
		{
			if(string.Compare(name, data.items[i].Key) == 0)
			{
				return Crypt(data.items[i].Value);
			}
		}

		return defaultValue;
	}

	static int intParse(string val)
	{
		int value;
		if(int.TryParse(val, out value)) return value;
		return 0;
	}

	static float floatParse(string val)
	{
		float value;
		if(float.TryParse(val, out value)) return value;
		return 0;
	}

	static string Crypt(string text)
	{
		string result = string.Empty;
		foreach(char j in text) result += (char)((int)j ^ 42);
		return result;
	}
}

Отметим одну важную деталь. Для сохранения в файл мы используем BinaryFormatter, когда мы сериализуем данные, в конечном файле можно найти значения, то есть, среди текстовой "абры-кадабры" можно обнаружить количество золота персонажа, допустим. Чтобы решить эту проблему, мы храним значения ключей в шифрованном виде. Они шифруются XOR'ом, перед записью в массив и дешифруются только после запроса.

Примеры использования:

SaveSystem.SetFloat("key", value);

Получить значение по ключу:

float f = SaveSystem.GetFloat("key");

Второй вариант, с установкой значения по умолчанию:

float f = SaveSystem.GetFloat("key", 10F);

То есть, если файл не загружен или такого ключа нет, то будет присвоено значение по умолчанию.

Скачать проект:

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

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

Офлайн
Крутой урок , спс! Я раньше ваще не знал как это делать!
smile
Офлайн
xcube 5 февраля 2017
Спасибо за полезные примеры!
Интересно как этот скрипт будет работать на мобильных устройствах (ios, android), понадобятся ли какие нибудь дополнительные настройки в проекте?
Офлайн
шикарная система, красивый код sunglasses

только вот можешь подсказать, как завести это всё на андройде?
(под виндой шикарно работает, под ведром чего то не хочет:с )
Офлайн
Light 16 марта 2017
Марк Никулин, пробовал на юньке 5.5.2 делать сборку под андройд, вроде работает.
Офлайн
Light,
сейчас перекачал ассет из статьи и залил его на чистый проект (юнити 5.5.1), добавил скрипт который при старте сцены сохраняет цифру "1" в файл, а потом в другую переменную выгружает и з файла эту цифру и выводит её на экран, скомпилил, запустил на телефоне - не выводит ничего =\

Может быть при компиляции нужно указать где, что бы были права на создание файлов на телефоне? Или при обыкновенном билде без каких либо доп настроек должно заводиться?
Офлайн
сохранил ассетом свой тест: https://cloud.mail.ru/public/2sNN/Vo5TFpPu3
Офлайн
Light 16 марта 2017
Марк Никулин, попробуй версию 5.5.2, в ней пофиксен Application.persistentDataPath
Офлайн
Light,
не, всё равно не юзает сохранения.

мб дело в версии андройда? у меня MIUI8 (5.1.1) андройд, и я замечаю что очень много приложух страдают от "обрезания прав" (допустим всем без исключения приложениям нужно принудительно давать доступ "не выгружаться", когда телефон с выключенным экраном)

Ты на какой версии андройда тестил? И можно ли как то принудительно дать разрешение на "запись на диск"?
Офлайн
Light 17 марта 2017
Марк Никулин, может тогда проще юзать стандартный метод PlayerPrefs?
Офлайн
Light,
когда будет время, можешь скинуть тестовый скомпилированный apk (который при установке у тебя на телефоне работает как надо) в котором можно протестировать функцию сохранения данных (я этот апк себе поставлю, что бы понять в чём проблема - в процессе компила или в телефоне)?
Офлайн
Light,
спасибо за наводку, попробую PlayerPrefs
Офлайн
Vladsvs90 22 июля 2017
Марк Никулин,
void OnApplicationQuit()
{
SaveSystem.SaveToDisk();
}

Vladsvs90,
void OnApplicationpause()
{
SaveSystem.SaveToDisk();
}
Офлайн
Здравствуйте! А как сделать, чтобы этот SaveSystem возвращал ссылку на определенный GameObject?

Точнее, сохранял и загружал
Офлайн
Light 2 декабря 2017
Владимир Дебиэн, если на сцене весит SaveSystemSetup, то он автоматом будет загружать.
Офлайн
Light,
Блин, наверное неправильно выразился. Нужно что-то типа
GameObject g = SaveSystem.GetObject("key");
Была мысля создать массив всех доступных таким образом объектов, а в SaveSystem будет сохраняться переменная типа int, которая будет индексом искомого объекта в массиве.
Плюс, создать метод который будет принимать эту переменную аргументом, а возвращать ссылку. Что-то типа
GameObject GetObject(int num){
GameObject result;
result = массивОбъектов[num];
return result;
}
Тогда
int i = SaveSystem.GetInt("key");
GameObject g = GetObject(i);
Но мне такое решение показалось громоздким и неудобным
Офлайн
Light 2 декабря 2017
Владимир Дебиэн, конечный результат? Ты хочешь объекты сохранять на сцене?
Офлайн
Light,
Здравствуйте! Извиняюсь да долгий перерыв - на работе (на сутках) был.
Я делаю игру, в которой игроку предстоит управлять футуристичными танками. Танки настраиваемые и если различное навесное оборудование, двигатель, силовую установку и т. д. можно просто переменными записать, то у оружия слишком много параметров. Я его сделал отдельными объектами (порядка 20-ти наименований). Мне нужно чтобы игрок мог в отдельной сцене (ангар) собрать себе танк, сохранить результат, а в боевых сценах этот результат будет загружаться. Как-то так
Офлайн
Light 3 декабря 2017
Владимир Дебиэн, много это сколько? Сколько переменных? В любом случаи, нужно делать шаблон сохранения/загрузки для оружия. Т.е. прописать один раз переменные для сохранения, а потом, когда игрок выходит из ангара, по этому шаблону сохранялось текущее оружие и т.д.
Офлайн
Light,
А переменные ссылочного типа внутри скрипта оружия (например, префаб снаряда) как сохранять? На данный момент 3 enum, один bool, 1 int, 2 float. Вообще, использую видоизмененные скрипты из вот этой статьи: https://null-code.ru/scripts/150-upravlenie-2d-tankom-vid-sverhu.html
Офлайн
Light 3 декабря 2017
Владимир Дебиэн, ссылки не нужно сохранять, просто айди/имя префаба, а потом его подгружать из Resources.
Офлайн
Light,
Примерно ухватил суть. А саму Resoucers можно внутри структурировать? И вызывать типа
Resources.Load("Dir/Dir/needObject")? Или там все только кучей должно валяться?
Офлайн
Light 3 декабря 2017
Владимир Дебиэн, в проекте создаешь папку Resoucers, а в ней любые другие с файлами/ассетами. Запрос ассета делается в зависимости от его типа, для префаба это:

GameObject prefab = Resources.Load<GameObject>("folder/asset");

folder - папка внутри Resoucers, если она есть.

asset - имя ассета.
Офлайн
Light,
Ага, именно это спрашивал.
Спасибо!
Офлайн
TheFrankyDoll 15 марта 2018
Отлично, всё работает, однако в вашем SaveSystem не хватает поддержки Quaternion-ов. Однако там хватает примеров того, как записывать такие структуры, поэтому проблем не возникло.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика