3 способа сохранения данных игры

Любой игрок чтит как минимум три заповеди: «Уклоняйся!», «Перекатывайся!», «Сохраняйся!». Ибо их нарушение сулит – боль. Ну, и раз такое дело, мы рассмотрим три способа, как сохранять игровые данные. Конкретно на примере, переменных, типа: bool, float, int, string и массив. Первый вариант, с использованием стандартных функций Unity. Второй, сохранение в файл, формат которого можно выбрать на свое усмотрение, кроме того, этот вариант дает возможность шифровать данные. Третий способ, работа с файлом формата XML.

Приступим к делу. Создаем C# с любым именем и редактируем его.

Подключаем необходимые библиотеки, для всех нижеприведенных примеров:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System;

Также, общие переменные:

То, что будем сохранять:

public float saveFloat = 0.25f;
public int saveInt = 7;
public bool saveBool = true;
public string[] saveArray = {"Элемент_0","Элемент_1","Элемент_2"};

Куда будем загружать:

public float loadFloat = 0;
public int loadInt = 0;
public bool loadBool = false;
public string[] loadArray;


Имя нашего файла:

public string fileName = "SaveData";

Стандартные функции

Сохранение в реестр (если Windows):

void SavePlayerPrefs()
{
	PlayerPrefs.SetFloat("saveFloat", saveFloat);
	PlayerPrefs.SetInt("saveInt", saveInt);
	PlayerPrefs.SetString("saveBool", saveBool.ToString());
	
	for(int i = 0; i < saveArray.Length; i++)
	{
		PlayerPrefs.SetString("elementArray_" + i, saveArray[i]);
	}
}

Читаем:

void LoadPlayerPrefs()
{
	if(PlayerPrefs.HasKey("saveFloat")) loadFloat = PlayerPrefs.GetFloat("saveFloat");
	if(PlayerPrefs.HasKey("saveInt")) loadInt = PlayerPrefs.GetInt("saveInt");
	if(PlayerPrefs.HasKey("saveBool")) loadBool = bool.Parse(PlayerPrefs.GetString("saveBool"));
	
	int j = 0;
	List<string> tmp = new List<string>();
	while(PlayerPrefs.HasKey("elementArray_" + j))
	{
		tmp.Add(PlayerPrefs.GetString("elementArray_" + j));
		j++;
	}
	
	loadArray = new string[tmp.Count];
	for(int i = 0; i < tmp.Count; i++)
	{
		loadArray[i] = tmp[i];
	}
}

Чтение массива здесь сделано с расчетом на то, что неизвестно сколько там элементов записано, поэтому перебираем все, какие есть с соответствующими ключами. Допустим при первом сохранение массив содержал три строки, с добавленным номером 0,1,2. А в процессе игры, могут произойти изменения и будут элементы с номерами 0,1,2,3,4,5 и т.д. Если есть номер 0 - то добавляется +1 и снова проверка. Однако, надо иметь ввиду, что поскольку проверка осуществляется по конкретному ключу, то нельзя просто удалить один элемент, иначе проверка прервется на первом отсутствующем ключе. Обновлять массив надо весь, сохраняя отсчет с 0.

Сохранение в файл

Добавим переменную для этого способа, она понадобится для выбора - шифровать данные или нет.

public bool isCrypt;



Итак, сохранение:

void SaveFile()
{
	StreamWriter sw = new StreamWriter(Application.dataPath + "/" + fileName + ".ncs");
	string sp = " ";
	sw.WriteLine(Crypt("key_float"+ sp + saveFloat));
	sw.WriteLine(Crypt("key_int"+ sp + saveInt));
	sw.WriteLine(Crypt("key_bool"+ sp + saveBool));
	
	for(int i = 0; i < saveArray.Length; i++)
	{
		sw.WriteLine(Crypt(i + "key_array"+ sp + saveArray[i]));
	}
	
	sw.Close();
}

Внимание! Массив тут тоже идет с конкретным ключом, ситуация как и в первом случаи. И еще одна важная деталь - к каждому ключу добавляется переменная, содержащая пробел, а потом уже данные. Сам ключ должен быть без пробела! Потому как это разделитель, логика такая: то что идет до первого пробела - считается ключом, то что идет после первого пробела - считается данными, которые надо загрузить. Вообще, можно просто читать строки без заморочек с ключами, но в таком случаи необходимо соблюдать порядок записи/чтения, т.е. в каком порядке шла запись - в таком же нужно и читать данные. Не очень-то удобно, при учете того, что редактировать код и переставлять его куски с места на место, придется часто в процессе разработки. Поэтому мы пишем строку допустим: energy 68 - затем разделяем и получаем нужное нам число, на запрос energy.

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

Загрузка файла:

void LoadFile()
{
	if(File.Exists(Application.dataPath + "/" + fileName + ".ncs"))
	{
		string[] rows = File.ReadAllLines(Application.dataPath + "/" + fileName + ".ncs");
		
		bool _b;
		if(bool.TryParse(GetValue(rows, "key_bool"), out _b)) loadBool = _b;
		
		float _f;
		if(float.TryParse(GetValue(rows, "key_float"), out _f)) loadFloat = _f;
		
		int _i;
		if(int.TryParse(GetValue(rows, "key_int"), out _i)) loadInt = _i;
		
		int j = 0;
		List<string> tmp = new List<string>();
		while(GetValue(rows, j + "key_array") != string.Empty)
		{
			tmp.Add(GetValue(rows, j + "key_array"));
			j++;
		}
		
		loadArray = new string[tmp.Count];
		for(int i = 0; i < tmp.Count; i++)
		{
			loadArray[i] = tmp[i];
		}
		
		rows = new string[0];
	}
}


Функция шифрования и дешифрирования:

string Crypt(string text)
{
	if(!isCrypt) return text;
	
	string result = string.Empty;
	foreach (char j in text)
	{
		// ((int) j ^ 49) - применение XOR к номеру символа
		// (char)((int) j ^ 49) - получаем символ из измененного номера
		// Число, которым мы XORим можете поставить любое. Эксперементируйте.
		result += (char)((int)j ^ 49);
	}
	
	return result;
}


Функция поиска строки по заданному ключу:

string GetValue(string[] line, string pattern)
{
	string result = "";
	foreach(string key in line)
	{
		if(key.Trim() != string.Empty)
		{
			string value = "";
			if(isCrypt) value = Crypt(key); else value = key;
			
			if(pattern == value.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries)[0])
			{
				result = value.Remove(0, value.IndexOf(' ')+1);
			}
		}
	}
	return result;
}

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

Кстати строка:

value.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries)[0]

Значит, что в value будет удалено всё, после первого пробела.

Строка:

value.Remove(0, value.IndexOf(' ')+1)

Означает, что в value будет вырезано первое слово.

Результат, который будет записан в файл, если не зашифровано:

key_float 0.25
key_int 7
key_bool True
0key_array Элемент_0
1key_array Элемент_1
2key_array Элемент_2


Если зашифровано:

3 способа сохранения данных игры

Вариант с XML файлом


Создание файла:

void SaveXML()
{
	XmlNode userNode;
	XmlAttribute attribute;
	XmlElement element;
	
	XmlDocument xmlDoc = new XmlDocument();
	XmlNode rootNode = xmlDoc.CreateElement("GameSettings");
	xmlDoc.AppendChild(rootNode);
	
	element = xmlDoc.CreateElement("key_bool");
	element.SetAttribute("value", saveBool.ToString());
	rootNode.AppendChild(element);
	
	element = xmlDoc.CreateElement("key_float");
	element.SetAttribute("value", saveFloat.ToString());
	rootNode.AppendChild(element);
	
	element = xmlDoc.CreateElement("key_int");
	element.SetAttribute("value", saveInt.ToString());
	rootNode.AppendChild(element);
	
	userNode = xmlDoc.CreateElement("KeysArray");
	for(int i = 0; i < saveArray.Length; i++)
	{
		element = xmlDoc.CreateElement("key");
		element.SetAttribute("value", saveArray[i].ToString());
		userNode.AppendChild(element);
	}
	rootNode.AppendChild(userNode);
	
	userNode = xmlDoc.CreateElement("Info");
	attribute = xmlDoc.CreateAttribute("Unity");
	attribute.Value = Application.unityVersion;
	userNode.Attributes.Append(attribute);
	userNode.InnerText = "Company Name: " + Application.companyName + " :: Product Name: " + Application.productName;
	rootNode.AppendChild(userNode);
	
	xmlDoc.Save(Application.dataPath + "/" + fileName + ".xml");
}


Чтение:

void LoadXML()
{
	try
	{
		List<string> tmp = new List<string>();
		XmlTextReader reader = new XmlTextReader(Application.dataPath + "/" + fileName + ".xml");
		while (reader.Read())
		{
			if(reader.IsStartElement("key_int"))
			{
				int k;
				if(int.TryParse(reader.GetAttribute("value"), out k)) loadInt = k;
			}
			if(reader.IsStartElement("key_float"))
			{
				float k;
				if(float.TryParse(reader.GetAttribute("value"), out k)) loadFloat = k;
			}
			if(reader.IsStartElement("key_bool"))
			{
				bool k;
				if(bool.TryParse(reader.GetAttribute("value"), out k)) loadBool = k;
			}
			if(reader.IsStartElement("KeysArray"))
			{
				while (reader.ReadToFollowing("key"))
				{
					tmp.Add(reader.GetAttribute("value"));
				}
			}
		}
		
		loadArray = new string[tmp.Count];
		for(int i = 0; i < tmp.Count; i++)
		{
			loadArray[i] = tmp[i];
		}
		
		reader.Close();
	}
	
	catch(System.Exception)
	{
		Debug.Log("Ошибка чтения файла!");
	}
}

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

Финальный результат:

<GameSettings>
  <key_bool value="True" />
  <key_float value="0.25" />
  <key_int value="7" />
  <KeysArray>
    <key value="Элемент_0" />
    <key value="Элемент_1" />
    <key value="Элемент_2" />
  </KeysArray>
  <Info Unity="5.0.1f1">Company Name: DefaultCompany :: Product Name: Demo</Info>
</GameSettings>

Последняя строка Info не особо то и нужна, но для примера сойдет.)

Путь сохранения.
Файл будет создан в папке Assets, если запускать в Unity. Либо в папке, где хранятся архивы игры, если запускать после сборки. Более подробную информацию по функции Application.dataPath читайте в официальном мануале.

Скачать скрипт:

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

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

Офлайн
dpman 14 июля 2016
спасибо за инф
Офлайн
SkyAngel 2 августа 2016
Спасибо за информацию, выручил!
Офлайн
ABredin 9 мая 2017
Всё качественно и понятно) Спасибо)
Офлайн
Crysis001 16 июля 2017
Подскажите пожалуйста, а как защитить игру от взлома?
Офлайн
Light 16 июля 2017
Цитата: Crysis001
Подскажите пожалуйста, а как защитить игру от взлома?

Смотря что имеется ввиду.
Офлайн
whs00 10 ноября 2017
Добрый вечер. Не до конца понятно в третьем способе:

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


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

Т.е. например задача такая: считываем в начале xml файл, передаём всё в массив (как этот массив с данными увидеть в другом классе?), затем работаем с данными, и при каком то условии нужно в обратиться в определенное место xml файла и именно туда записать изменения. Вот как примерно в таком случае должна выглядеть логика кода? Подскажите пожалуйста!
Офлайн
Light 11 ноября 2017
whs00, здесь скрипты для примера, поэтому и сделано, массивы и отдельные переменные. Если нужно работать с единой базой данных, то рекомендую эту систему https://null-code.ru/project/155-rasshirennaya-sistema-sohraneniya.html
Офлайн
Dimon 8 декабря 2017
У меня в этой строчке sw.WriteLine(Crypt("key_float"+ sp + saveFloat)); слово Crypt горит красным цветом, в чём проблема?
Офлайн
Dimon 8 декабря 2017
Цитата: Dimon
У меня в этой строчке sw.WriteLine(Crypt("key_float"+ sp + saveFloat)); слово Crypt горит красным цветом, в чём проблема?

Я нашёл в чём проблема
Офлайн
Viracocha 27 февраля 2020
В некоторых случаях файлы удобно сохранят в папке / StreamingAssets (в корневом каталоге). В этом случае файлы будут доступны как есть в папке созданного приложения.
Офлайн
Light 27 февраля 2020
Viracocha, да, парой может быть удобно Application.streamingAssetsPath, но в случае WebGL или Android будет возвращаться URL, поэтому придется использовать UnityWebRequest.

Но вообще для мобильных платформ проще использовать Application.persistentDataPath.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика