Комментарии 28
как вариант можно с помощью атрибутов разметить поля которые требуется сохранять, с помощью T4 который поддерживается в MonoDevelop и VS сгенерить DTO классы и методы копирования этих полей в DTO. А потом нормальным сериализатором положить это в JSON.
И при этом не писать руками функции сохранения/загрузки.
btw. синглтоны с глобальным доступом это зло.
И при этом не писать руками функции сохранения/загрузки.
btw. синглтоны с глобальным доступом это зло.
+1
Насчет глобальных синглтонов не знаю. Но я у себя сделал самый закрытый менеджер игры.
Он у меня — такой мост между всеми подсистемами. Слушает события и вызывает нужные функции. А к самому нему никто доступ не имеет.
Все, что с ним можно сделать — проинициализировать (ну, чтобы он хоть как-то появился в памяти).
Он у меня — такой мост между всеми подсистемами. Слушает события и вызывает нужные функции. А к самому нему никто доступ не имеет.
Все, что с ним можно сделать — проинициализировать (ну, чтобы он хоть как-то появился в памяти).
Максимально закрытый GameManager
public class GameManager
{
static int EnemiesCount;
static int EnemiesCountCurrent;
static float MatchStartSeconds;
static PlayerController player;
public static void Init()
{
player = GameObject.FindObjectOfType<PlayerController>();
player.OnDamaged += OnPlayerDamagedHandler;
AbstractAliveController.OnDead += OnAliveDeadHandler;
EnemiesCount = GameObject.FindObjectsOfType<NPC.Enemy.EnemyController>().Length;
UI.UIController.OnRestart += RestartMatch;
InitMatch();
}
}
0
А потом вы добавили новое поле в класс и сохраненное значение развернётся как получится.
Где то переименовали свойство, где то ещё что-то…
Я пока не видел идеальных и хороших решений на все случаи жизни.
Несовместимость разных версий сохранений и приложений — полная печаль, на мой взгляд.
Где то переименовали свойство, где то ещё что-то…
Я пока не видел идеальных и хороших решений на все случаи жизни.
Несовместимость разных версий сохранений и приложений — полная печаль, на мой взгляд.
+2
Новое поле добавляется в модель. Копирование модели в модель пишешь сам. Хотя по идее должно же быть встроенное средство копирования объектов.
А каркас остается. Я сначала на геймсджем писал игру про котосминогов и сделал там эту систему сохранения. А потом для тестового задания про шутер просто использовал ее же, заменив модели. До самого последнего момента не знал, заработает ли. Вызвал в GameManager AbstractSaveLoad.LoadInit() для рестарта матча — и все заработало.
Конечно, лучший код — ненаписанный код и кнопочка «Сделать прекрасно». Но чем богаты.
Во всяком случае добавление новых сохраняемых сущностей происходит максимально безболезненно, проверено на двух разных проектах.
А каркас остается. Я сначала на геймсджем писал игру про котосминогов и сделал там эту систему сохранения. А потом для тестового задания про шутер просто использовал ее же, заменив модели. До самого последнего момента не знал, заработает ли. Вызвал в GameManager AbstractSaveLoad.LoadInit() для рестарта матча — и все заработало.
Конечно, лучший код — ненаписанный код и кнопочка «Сделать прекрасно». Но чем богаты.
Во всяком случае добавление новых сохраняемых сущностей происходит максимально безболезненно, проверено на двух разных проектах.
-2
А может быть просто сохранять в json непосредственно сам тип объекта при сохранении? И при загрузке создавать объект примерно таким образом (сам я правда так не пробовал):
System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(className);
+1
Выглядит очень похоже на правду.
Я тоже так не пробовал. Если добавить в мое решение автоматическое полное копирование объектов, автоматическую сериализацию и автоопределение типа, то будет полностью завершенное решение.
Я тоже так не пробовал. Если добавить в мое решение автоматическое полное копирование объектов, автоматическую сериализацию и автоопределение типа, то будет полностью завершенное решение.
0
Но вообще есть нюанс. Из разряда «нам бы ваши проблемы». Переносимость сохранений из МегаИгра1 в МегаИгра2: Воскрешение.
Мы не можем гарантировать, что какой-нибудь умник не переименует класс. Да может быть мы сами в процессе разработки что-то переименуем. И, предположим, у нас есть несколько сохранений, чтобы тестировать игру в разных местах (ну вдруг).
Короче, надо об этом очень сильно помнить.
Мы не можем гарантировать, что какой-нибудь умник не переименует класс. Да может быть мы сами в процессе разработки что-то переименуем. И, предположим, у нас есть несколько сохранений, чтобы тестировать игру в разных местах (ну вдруг).
Короче, надо об этом очень сильно помнить.
0
Самый быстрый способ сохранения/загрузки состояния который я знаю и использую примерно такой:
1. Создание оберток для MemoryStream по контрактам вида:
2. Делаем контракт
3. Пример использования
Минусы:
Плюсы:
Да, есть куча готовых мапперов, но каждый слой абстракции будет добавлять своё замедление.
1. Создание оберток для MemoryStream по контрактам вида:
public interface IBinaryWriter : IDisposable
{
void WriteBoolean(bool val);
void WriteByte(byte val);
void WriteBytes(byte[] val);
void WriteDouble(double val);
void WriteInt32(Int32 number);
void WriteLong(Int64 number);
void WriteString(string line);
void WriteGuid(Guid guid);
void WriteDateTime(DateTime datetime);
// Плюс такие же методы для коллекций
}
public interface IBinaryReader: IDisposable
{
bool ReadBoolean();
byte ReadByte();
byte[] ReadBytes();
Double ReadDouble();
Int32 ReadInt32();
Int64 ReadLong();
string ReadString();
Guid ReadGuid();
DateTime? ReadDateTime();
// Плюс такие же методы для коллекций
}
2. Делаем контракт
public interface IBinarySerializable
{
void Serialize(IBinaryWriter writer);
void Deserialize(IBinaryReader reader);
}
3. Пример использования
public class Location : IBinarySerializable
{
public Location() { }
public Location(IBinaryReader reader) { Deserialize(reader); }
public double X;
public double Y;
public void Deserialize(IBinaryReader reader)
{
this.X = reader.ReadDouble();
this.Y = reader.ReadDouble();
}
public void Serialize(IBinaryWriter writer)
{
writer.WriteDouble(this.X);
writer.WriteDouble(this.Y);
}
}
public class Player : IBinarySerializable
{
public string Name;
public double Health;
public Location Position;
public void Deserialize(IBinaryReader reader)
{
this.Name = reader.ReadString();
this.Health = reader.ReadDouble();
Position = new Location(reader);
}
public void Serialize(IBinaryWriter writer)
{
writer.WriteString(this.Name);
writer.WriteDouble(this.Health);
this.Position.Serialize(writer);
}
}
Минусы:
- нужно быть внимательным
- нет переносимости (т.к. не храним информацию о типах), но если используется только .net и нет динамической генерации типов, это не существенно
Плюсы:
- ~30% экономии по объему памяти для каждого объекта по сравнению с нативной сериализацией (BinaryFormatter)
- ~500% выигрыш по скорости сериализации (по сравнению с тем же BinaryFormatter)
Да, есть куча готовых мапперов, но каждый слой абстракции будет добавлять своё замедление.
+3
1) Спрячте под спойлер, пожалуйста.
2) Можно ли так сериализовать тип? Чтобы ридер сам знал, что читает из файла?
3) Можно ли так сериализовать всё в один файл (из коллекции) и потом прочитать это же всё и создать нужные объекты?
4) насколько удобно это для передачи по сети?
2-4 — потому что я никогда не работал с бинарной сериализацией, я во многих вопросах еще нуб.
2) Можно ли так сериализовать тип? Чтобы ридер сам знал, что читает из файла?
3) Можно ли так сериализовать всё в один файл (из коллекции) и потом прочитать это же всё и создать нужные объекты?
4) насколько удобно это для передачи по сети?
2-4 — потому что я никогда не работал с бинарной сериализацией, я во многих вопросах еще нуб.
0
- Не успел отредактировать, уже не спрячу.
- Можно, но зачем, проще знать порядок записи чтения и следовать ему. Как в примере.
- Конечно, по сути это линейная запись байт-массива, можно сохранить любое количество объектов. Кроме того можно унаследовать от IBinaryWriter и IBinaryReader класс который пишет сразу в файл, чтобы не хранить промежуточно байт-массив в оперативе.
- Именно для передачи по сети и делал изначально этот подход, т.к. в моем приложении объем данных и скорость сериализации критичны.
Пример как это выглядит (Unity не знаю и игры не пишу, поэтому что придумалось то и есть):
Показать
public class FileStreamWriter : IBinaryWriter
{
// ToDo
}
public class FileStreamReader : IBinaryReader
{
// ToDo
}
public class GameState
{
public Player Player;
public List<Enemy> Enemies;
public List<Bullet> Bullets;
public DateTime DayTime;
}
public class GameStateConservator
{
public void Save(string saveName, GameState state)
{
using (IBinaryWriter writer = new FileStreamWriter(saveName))
{
state.Player.Serialize(writer);
writer.WriteCollection<Enemy>(state.Enemies);
writer.WriteCollection<Bullet>(state.Bullets);
writer.WriteDateTime(state.DayTime);
writer.Complete();
}
}
public GameState Load(string saveName)
{
var state = new GameState();
using (IBinaryReader reader = new FileStreamReader(saveName))
{
state.Player = new Player(reader);
state.Enemies = reader.ReadCollection<Enemy>();
state.Bullets = reader.ReadCollection<Bullet>();
state.DayTime = reader.ReadDateTime();
}
return state;
}
}
0
Перечитал заголовок статьи, если для любого проекта, тогда лучше прикрутить маппер. Который будет использовать такой же механизм сериализации, но позволит не писать вручную код. Главное не делать чтение/запись полей через Reflection, лучше использовать Emit или ExpressionTree. Или взять что-то готовое из nuget'а.
0
В Юнити нельзя предсказать, в какой последовательности объекты сохранятся. И, следовательно, в какой последовательности будут храниться и загружаться.
Вообще, может, и можно: создать вручную начальное сохранение, а потом читать из него в заранее заданном порядке в начале игры, в этом же порядке хранить в коллекции и в этом же порядке записывать.
Но вообще бывает такая вещь как крафт: о) И расход ресурсов. И смерть (исчезновение) персонажей.
В общем случае набор сущностей в игре непостоянный, поэтому «сразу знать, что и где» — негибко.
Вообще, может, и можно: создать вручную начальное сохранение, а потом читать из него в заранее заданном порядке в начале игры, в этом же порядке хранить в коллекции и в этом же порядке записывать.
Но вообще бывает такая вещь как крафт: о) И расход ресурсов. И смерть (исчезновение) персонажей.
В общем случае набор сущностей в игре непостоянный, поэтому «сразу знать, что и где» — негибко.
0
1) Я лично слабо представляю, как в PlayerPrefs хранить весь мир Fallout4
2) Ну и переносимость сохранений.
Так что файлы наше всё
2) Ну и переносимость сохранений.
Так что файлы наше всё
0
Больше синглтонов богу синглтонов? Синглтоны и статичные переменные сложных типов — зло. Ищите решение, которое их не использует. Например пусть какой-нибудь менеджер находит все компоненты определенного типа и сохраняет их. Не забываем добавить уникальный идентификатор для каждого такого компонента.
Для сериализации есть прекрасная утилита, которая заслуживает упоминания в этой статье:
которая может сериализовать практически любой класс с приемлемой скоростью.
Вот накидал за полчаса примерчик:
Для сериализации есть прекрасная утилита, которая заслуживает упоминания в этой статье:
JsonUtility.ToJson();
JsonUtility.FromJson<Foo>();
которая может сериализовать практически любой класс с приемлемой скоростью.
Вот накидал за полчаса примерчик:
Примерная реализация без синглтонов
Пример менеджера сохраняющего нужные нам данные:
Остальные классы, нужные для работы сего безобразия:
public class TransformSaver : MonoBehaviour
{
[SerializeField]
private Transform[] _transforms;
private readonly SaveManager _saveManager = new SaveManager();
private void Start() {
for (var index = 0; index < _transforms.Length; index++)
_saveManager.Register(new TransformSave(index.ToString(), _transforms[index]));
}
[ContextMenu("Save")]
public void Save() {
_saveManager.Save();
}
[ContextMenu("Load")]
public void Load() {
_saveManager.Load();
}
}
Остальные классы, нужные для работы сего безобразия:
public class SaveManager
{
private readonly List<ISave> _saves = new List<ISave>();
public void Register(ISave element) { _saves.Add(element); }
public void Unregister(ISave element) { _saves.Remove(element); }
public void Save() {
var saves = new Saves();
foreach (var save in _saves)
saves.Add(save.Uid, save.Serialize());
PlayerPrefs.SetString("Save", JsonUtility.ToJson(saves));
PlayerPrefs.Save(); // Force save player prefs
}
public void Load() {
var json = PlayerPrefs.GetString("Save", "");
if (string.IsNullOrEmpty(json))
return;
var saves = JsonUtility.FromJson<Saves>(json);
for (var index = 0; index < saves.Uids.Count; index++) {
var element = _saves.Single(x => x.Uid == saves.Uids[index]);
element.Deserialize(saves.List[index]);
}
}
[Serializable]
private class Saves
{
public List<string> Uids = new List<string>();
public List<string> List = new List<string>();
public void Add(string uid, string value) {
if (Uids.Contains(uid))
throw new ArgumentException("Uids has already have \"" + uid + "\"");
Uids.Add(uid);
List.Add(value);
}
}
}
public interface ISave
{
string Uid { get; }
string Serialize();
void Deserialize(string json);
}
public class TransformSave : ISave
{
private readonly Transform _transform;
public string Uid { get; private set; }
public TransformSave(string uid, Transform transform) {
Uid = uid;
_transform = transform;
}
public string Serialize() {
return JsonUtility.ToJson(
new TransformData {
Position = _transform.position,
Rotation = _transform.rotation
}
);
}
public void Deserialize(string json) {
var deserialized = JsonUtility.FromJson<TransformData>(json);
_transform.SetPositionAndRotation(deserialized.Position, deserialized.Rotation);
D.Log("Json", json);
}
[Serializable]
private class TransformData
{
public Vector3 Position;
public Quaternion Rotation;
}
}
+2
А в чем проблема с синглтонами и статичными переменными сложных типов?
За утилиту спасибо: о)
За утилиту спасибо: о)
0
Синглтоны: Повышение связанности кода, невозможностью заменить\удалить и почти всегда применение синглтона говорит о том что с архитектурой что-то не то — участок кода попахивает.
http://rsdn.org/forum/design/2615563.flat#2615563
Статичные переменные — родственники глобальных переменных. Основная проблема — потеря контроля над значениями. Их может изменить кто угодно и откуда угодно. Я, похоже запутался, и сказал про сложный тип, но имел в виду константы (строки, числа). Т.е. константные статические переменные еще куда не шло, а вот изменяемые значения — зло стопроцентное.
Хотя в Unity — использовать константы не круто, потому что сам по себе движок помогает с сериализацией, и в 99% случаев лучше использовать ScriptableObject, а значения менять прямо из редактора.
Вообще почитайте про инверсию управления через Dependency Injection. Отличный фреймворк для этих целей под Unity — Zenject. В пару проектов втыкаешь и забываешь про синглтоны.
http://rsdn.org/forum/design/2615563.flat#2615563
Статичные переменные — родственники глобальных переменных. Основная проблема — потеря контроля над значениями. Их может изменить кто угодно и откуда угодно. Я, похоже запутался, и сказал про сложный тип, но имел в виду константы (строки, числа). Т.е. константные статические переменные еще куда не шло, а вот изменяемые значения — зло стопроцентное.
Хотя в Unity — использовать константы не круто, потому что сам по себе движок помогает с сериализацией, и в 99% случаев лучше использовать ScriptableObject, а значения менять прямо из редактора.
Вообще почитайте про инверсию управления через Dependency Injection. Отличный фреймворк для этих целей под Unity — Zenject. В пару проектов втыкаешь и забываешь про синглтоны.
+3
Я стараюсь использовать в Unity как можно меньше Unity. Ибо в реальных проектах, которые на работе за зарплат, логика часто shared или серверная.
А еще можно взять shared бизнес-логику, скомпилировать в dll и перенести на Unreal. Поэтому имхо чем меньше ScriptableObject, MonoBehaviour и прочего using UnityEngine, тем лучше.
А еще можно взять shared бизнес-логику, скомпилировать в dll и перенести на Unreal. Поэтому имхо чем меньше ScriptableObject, MonoBehaviour и прочего using UnityEngine, тем лучше.
-2
1) Чтобы кто угодно не изменял, делаешь public get protected set
2) Я делаю следующим образом: все поля в модели public, но сама модель видна только своему контроллеру
3) Статичные публичные — у меня обычно методы. Вот тот же SaveLoadBehaviour. У него
protected static List All
А вот методы SaveAll, LoadInitAll — публичные.
4) Мне надоело писать для синглтонов MyClass.Instance.DoMethod(), я делаю MyClass.DoMethod() и в нем уже на статический инстанс ссылаюсь.
Т.е. может дело не в синглтонах, а в том, чтобы правильно их готовить?
А повышение связности когда — опять же, помогут прямые руки и ООП. Для заменяемости/удаляемости пользуйся интерфейсами, и будет тебе счастье.
В моей статье предложен подход:
1) абстрактный класс со статик полями
2) от него наследуется обобщенный класс для работы с разными моделями
3) от обощенного — конкретная реализация
Тот же подход можно применить для синглтонов
1) Интерфейс, определяющий желаемое поведение синглтона IAdapter
2) Обобщенный синглтон GenericSingletone3) Абстрактный класс, наследующий от синглтона и интерфейса.
SingletoneOne: GenericSingletone, IAdapter
4) Если мы по примеру 3 сделаем SingletoneTwo, то у них буду разные static Instance
5) А вот если мы унаследуем от SingletoneOne, то у SingletoneOne_1 и SingletoneOne_2 статический инстанс будет общим!
6) Соответственно, во всем коде работаешь с SingletoneOne.Instance.AdapterMetod()
7) А конкретную реализацию меняй как вздумается.
2) Я делаю следующим образом: все поля в модели public, но сама модель видна только своему контроллеру
3) Статичные публичные — у меня обычно методы. Вот тот же SaveLoadBehaviour. У него
protected static List All
А вот методы SaveAll, LoadInitAll — публичные.
4) Мне надоело писать для синглтонов MyClass.Instance.DoMethod(), я делаю MyClass.DoMethod() и в нем уже на статический инстанс ссылаюсь.
Т.е. может дело не в синглтонах, а в том, чтобы правильно их готовить?
А повышение связности когда — опять же, помогут прямые руки и ООП. Для заменяемости/удаляемости пользуйся интерфейсами, и будет тебе счастье.
В моей статье предложен подход:
1) абстрактный класс со статик полями
2) от него наследуется обобщенный класс для работы с разными моделями
3) от обощенного — конкретная реализация
Тот же подход можно применить для синглтонов
1) Интерфейс, определяющий желаемое поведение синглтона IAdapter
2) Обобщенный синглтон GenericSingletone3) Абстрактный класс, наследующий от синглтона и интерфейса.
SingletoneOne: GenericSingletone, IAdapter
4) Если мы по примеру 3 сделаем SingletoneTwo, то у них буду разные static Instance
5) А вот если мы унаследуем от SingletoneOne, то у SingletoneOne_1 и SingletoneOne_2 статический инстанс будет общим!
6) Соответственно, во всем коде работаешь с SingletoneOne.Instance.AdapterMetod()
7) А конкретную реализацию меняй как вздумается.
0
Я стараюсь использовать в Unity как можно меньше Unity.
По статье как-то незаметно, компоненты поверх компонентов, компонентами погоняют :). ScriptableObject'ы легко меняются на обычный класс. Сравни с моей реализацией — там один MB — как точка входа алгоритма, а записывать можно любые данные практически.
1 — Есть еще readonly, но с MonoBehaviour такое не провернешь. А можно геттером спрятать.
3 и 4 — MyClass.Something уже плохо.
Правильно приготовленный синглтон — это синглтон который не был написан.
Обращаясь из одного класса к другому через MyClass.Something — ты создаешь зависимость, которую довольно сложно отследить, а потом, при рефакторинге заменить. Привязываешь класс А к классу Б стальными тросами. Тут даже нет речи об интерфейсах и т.д. Сплошное несчастье.
Это сложно объяснить, но когда ты столкнешься с этим на практике, то поймешь насколько синглтоны — зло.
+1
Блин, классы взаимодействуют, куда от этого деться? Я предполагаю, что если человек использовал синглтон, значит, ему реально позарез надо обращаться от одного класса к другому. Для примера можно взять игрока. Методы получения урона и гибели одинаковы у игрока и врага. Но на гибель игрока игра должна особым образом реагировать. Посылается событие person.ondead, а игра должна проверить, не игрок ли преставился.
Это для примера. Я пытался сказать, что если синглтон кажется разработчику подходящим решением, то недостатки сильной связности можно устранить интерфейсами и наследованиями
Ну и компоненты у меня — тоже тут никуда не денешься. Шутер, коллизии, физика, трансформы всякие. Тут единственный способ избавиться от юнити — писать свой движок
0
Json конечно же удобен до того момента, как вам придеться делать игры под консоли, которые налагают определенные ограничения на время сохранения. Кроме того, в случае когда в игре много сохранений — всё это дело начинает занимать много места не жестком диске, что для клаудсейвов очень даже критично.
0
Синглтоны-синглтоны…
У кого сколько было случаев попытки создания второй копии синглтон класса и чтобы вот эти предосторожности помогали? Типо, «Ошибочка, бро! Это же синглтон!!»
У меня пока ни разу…
У кого сколько было случаев попытки создания второй копии синглтон класса и чтобы вот эти предосторожности помогали? Типо, «Ошибочка, бро! Это же синглтон!!»
У меня пока ни разу…
+1
Завязываться на monoBehaviour-синглоны кажется не самой лучшей идеей. Все же лучше такие вещи выносить отдельно и иметь какой-то общий контроллер, который будет заниматься только загрузкой и сохранением.
Почитать как сделано в моем хобби-проекте можно тут. Есть единая точка входа, где мы и определяем что и в каком формате должно сохраняться (сейчас это сериализация data-классов в один json-файл). Есть куда это все развивать, но в целом такой подход кажется более подходящим.
Почитать как сделано в моем хобби-проекте можно тут. Есть единая точка входа, где мы и определяем что и в каком формате должно сохраняться (сейчас это сериализация data-классов в один json-файл). Есть куда это все развивать, но в целом такой подход кажется более подходящим.
0
Хм. Меня, похоже, не поняли. Синглтон я использую только как демонстрацию наследования статических полей. И попутно показываю, что есть такой способ делать синглтоне, если кому надо. Моя система сохранения не основана на синглтоне. Она основана на обобщённом классе поведения (контроллере), которому можно передать любую модель и таким образом легко получить сохраняемый объект, просто определив поля и не реализуя один и тот же функционал каждый раз. А потом все такие объекты можно сохранить вызовом одной функции.
0
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Unity: система сохранения для любого проекта