Система сохранения и загрузки, в игре

Возвращаемся к теме сохранения игровых данных в файл. Материалов на данную тематику в сети не мало, да и на нашем сайте тоже есть. В основном, такие примеры представлены в виде кусков кода, которые необходимо редактировать под каждый проект индивидуально. Но прогресс не стоит на месте, и мы тоже не будем отставать. Поэтому в этот раз, мы напишем своего рода систему, для сохранения и загрузки из файла. Преимуществ тут сразу несколько: удобство в использовании; контроль ошибок чтения; шифрование данных; просто настроить и т.п.

Стоит отметить, что скрипт не нуждается в подгонке под конкретную игру. Вы просто делаете запрос на сохранение, а при загрузке игры - запрос загрузки нужных вам переменных. По сути, скрипт можно использовать почти в любых играх.

Чтож, приступим к написанию, а потом рассмотрим как его использовать.
Создаем новый C# скрипт SaveSystem, подключаем необходимые библиотеки:

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

Добавим несколько переменных:

// блок настроек, в случае его изменения - удалите старый файл сохранений
private static bool isCrypt = false;
private static string fileName = "Profile.bin";
private static int elementCount = 15;
private static char element_sep = ' ';
private static char value_sep = ':';

private static List<string> Array_Settings;
private static bool loading;

isCrypt - шифровать данные или нет.

fileName - имя и разрешение файла для сохранения. Если нужно держать файл в отдельной папке, то надо заранее создать ее, где именно создавать папку зависит от платформы, подробнее см. здесь. Конечный вид может быть примерно таким: "Settings/Profile.bin".

elementCount - разбитие на строки, сколько элементов будет в строке, если 0 - отключено.

element_sep - символ разделитель между элементами.
Элемент имеет вид: name + value_sep + value (имя/ключ + разделитель + значение).

value_sep - разделитель между ключом и значением.

Array_Settings - массив, в который будут загружены все элементы.

Теперь, добавим функции:

public static bool InitializeLoading()
{
	loading = true;
	Array_Settings = new List<string>();
	if(File.Exists(Application.dataPath + "/" + fileName)) Load(); else {Default(); Save();}
	return loading;
}

public static void ResetToDefault()
{
	Default();
	Save();
	Debug.Log("Восстановление настроек по умолчанию -> " + fileName);
}

static void Error(string info)
{
	Array_Settings = new List<string>();
	Debug.Log(info);
	loading = false;
}

Инициализация загрузки, сброс и контроль ошибок. Плюс запись в лог информацию об ошибке.

Еще нужно будет фильтровать текст, разделять элементы между собой и отделять значение от ключа:

static string SplitElement(string text, char index)
{
	string result = string.Empty;
	if(text.Trim() != string.Empty)
	{
		result = text.Split(new char[] {index}, StringSplitOptions.RemoveEmptyEntries)[0];
	}
	return result;
}

static string RemoveElement(string text, char index)
{
	string result = string.Empty;
	result = text.Remove(0, text.IndexOf(index)+1);
	return result;
}

Первая функция обрезает всё, что следует после указанного символа. Вторая - вырезает всё, что находится до указанного символа.

Чтение файла и загрузка данных в массив:

static void Load()
{
	StreamReader streamReader = new StreamReader(Application.dataPath + "/" + fileName);
	
	string txt = string.Empty;
	while(!streamReader.EndOfStream)
	{
		string tmp = Crypt(streamReader.ReadLine());
		if(txt == string.Empty) txt += tmp;
		else txt += element_sep + tmp;
	}
	streamReader.Close();
	
	if(txt.Trim() == string.Empty)
	{
		Error("Файл " + fileName + " -> Пуст!");
	}
	else
	{
		string tmp = string.Empty;
		int err = 0;
		int count = 0;
		if(int.TryParse(SplitElement(txt, element_sep), out count)) 
		{
			txt = RemoveElement(txt, element_sep);
			for(int i = 0; i < count; i++)
			{
				tmp = txt;
				string t = SplitElement(txt, element_sep);
				Array_Settings.Add(t);
				if(t == tmp)
				{
					if(err > 0)
					{
						Error("Файл " + fileName + " -> Ошибка чтения элементов!");
						return;
					}
					err++;
				}
				else
				{
					txt = RemoveElement(txt, element_sep);
				}
			}
		}
		else
		{
			Error("Файл " + fileName + " -> Ошибка загрузки данных!");
		}
	}
	txt = string.Empty;
}

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

Блок данных по умолчанию:

static void Default()
{
	SetFloat("myFloat", 1.5f);
	SetString("myString", "MyLevel");
	SetBool("myBool", false);
	SetInt("myInt", 10);
}

Если файла с сохранениями не существует, то обращаемся к этой функции. Здесь надо установить дефолтные значения настроек.

Добавление новых элементов:

static void ReplaceItem(string name, string item)
{
	int i = 0;
	bool j = false;
	foreach(string line in Array_Settings)
	{
		if(name == SplitElement(line, value_sep))
		{
			Array_Settings[i] = item;
			j = true;
		}
		i++;
	}
	if(!j) Array_Settings.Add(item);
}

public static void SetBool(string name, bool value)
{
	string tmp = string.Empty;
	if(value) tmp = name + value_sep + "1"; else tmp = name + value_sep + "0";
	if(Array_Settings.Count > 0) ReplaceItem(name, tmp); else Array_Settings.Add(tmp);
}

public static void SetFloat(string name, float value)
{
	string tmp = name + value_sep + value;
	if(Array_Settings.Count > 0) ReplaceItem(name, tmp); else Array_Settings.Add(tmp);
}

public static void SetInt(string name, int value)
{
	string tmp = name + value_sep + value;
	if(Array_Settings.Count > 0) ReplaceItem(name, tmp); else Array_Settings.Add(tmp);
}

public static void SetString(string name, string value)
{
	string tmp = name + value_sep + value;
	if(Array_Settings.Count > 0) ReplaceItem(name, tmp); else Array_Settings.Add(tmp);
}

Стоит отметить, что элементы добавляются только в массив. Сохранение делается в отдельной функции.

Собственно вот и сохранение:

public static void Save()
{
	if(Array_Settings.Count > 0)
	{
		StreamWriter sw = new StreamWriter(Application.dataPath + "/" + fileName);
		int j = 0;
		string txt = Array_Settings.Count.ToString();
		for(int i = 0; i < Array_Settings.Count; i++)
		{
			if(txt == string.Empty) txt += Array_Settings[i]; else txt += element_sep + Array_Settings[i];
			if(elementCount > 0 && j > elementCount)
			{
				sw.WriteLine(Crypt(txt));
				txt = string.Empty;
				j = 0;
			}
			j++;
		}
		if(txt != string.Empty) sw.WriteLine(Crypt(txt));
		sw.Close();
		Debug.Log("Сохранение игровых данных -> " + fileName);
	}
}

Ничего особенного, просто запись файла из данных массива.

Получение конкретных значений:

public static bool GetBool(string name)
{
	bool value = false;
	if(Array_Settings.Count > 0)
	{
		foreach(string line in Array_Settings)
		{
			string txt = line;
			if(name == SplitElement(line, value_sep))
			{
				string	t = RemoveElement(txt, value_sep);
				if(t == "1") value = true;
			}
		}
	}
	return value;
}

public static float GetFloat(string name)
{
	float value = 0;
	if(Array_Settings.Count > 0)
	{
		foreach(string line in Array_Settings)
		{
			string txt = line;
			if(name == SplitElement(line, value_sep))
			{
				string	tmp = RemoveElement(txt, value_sep);
				float result;
				if(float.TryParse(tmp, out result)) value = result;
			}
		}
	}
	return value;
}

public static int GetInt(string name)
{
	int value = 0;
	if(Array_Settings.Count > 0)
	{
		foreach(string line in Array_Settings)
		{
			string txt = line;
			if(name == SplitElement(line, value_sep))
			{
				string	tmp = RemoveElement(txt, value_sep);
				int result;
				if(int.TryParse(tmp, out result)) value = result;
			}
		}
	}
	return value;
}

public static string GetString(string name)
{
	string value = string.Empty;
	if(Array_Settings.Count > 0)
	{
		foreach(string line in Array_Settings)
		{
			string txt = line;
			if(name == SplitElement(line, value_sep))
			{
				string	t = RemoveElement(txt, value_sep);
				value = t;
			}
		}
	}
	return value;
}

Чтобы получить какое-либо значение, достаточно указать имя/ключ, затем происходит поиск соответствующего ключа в массиве и возвращается значение, связанное с этим ключом.

Ну и на подлесок, функция шифрования и дешифрования:

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


Итак, теперь о том как использовать скрипт.

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

void Awake ()
{
	if(SaveSystem.InitializeLoading())
	{
		Debug.Log("Успешная загрузка файла сохранений.");
	}
	else
	{
		SaveSystem.ResetToDefault();
	}
}

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

На старте сцены, когда нам нужны данные сохранений, мы делаем запросы в массив, допустим так:

bool myBool;
int myInt;
float myFloat;
string myString;

myBool = SaveSystem.GetBool("myBool");
myFloat = SaveSystem.GetFloat("myFloat");
myInt = SaveSystem.GetInt("myInt");
myString = SaveSystem.GetString("myString");

В случае, если элемент массива имеет значительные повреждения или его просто не существует, то тогда будет возращено дефолтное значение относительно типа переменной, то есть, для bool -> false, для int -> 0 и т.д. Имейте это ввиду.

Сохранение данных делается примерно вот так:

SaveSystem.SetBool("myBool", true);
SaveSystem.SetFloat("myFloat", 3.45f);
SaveSystem.SetString("myString", "string");
SaveSystem.SetInt("myInt", 1);

SaveSystem.Save();

В начале заполняем массив и в конце вызываем функцию, которая запишет всё в файл.

Скачать скрипт можно тут:

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

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

Офлайн
Light 14 января 2017
KapralFoster, рекомендую обновленную версию скрипта https://null-code.ru/project/155-rasshirennaya-sistema-sohraneniya.html
Офлайн
KapralFoster 13 января 2017
Как установить скрипт скаченый?
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика