Игры надо сохранять. Сохраняемых сущностей может быть великое множество. Например, в последних выпусках TES и Fallout игра помнит расположение каждой закатившейся склянки. Необходимо решение, чтобы:
1) Написал один раз и используй в любом проекте для любых сущностей. Ну, насколько возможно.
2) Создал сущность — и она сохраняется сама собою, с минимумом дополнительных усилий.
Решение пришло из стана синглтонов. Не надоело ли вам писать один и тот же синглтон-код? А меж тем есть generic singleton.
Т.е. можно сделать Generic класс, статические поля которого будут уникальны для каждого входного типа.
И это как раз наш случай. Потому что поведение сохраняемого объекта полностью идентично, различаются только сохраняемые модели. И тип модели как раз и выступает в качестве входного.
Вот код интерфейса модели. Он примечателен тем, что метод SetValues примет в качестве аргумента только модель такого же (или производного) типа. Не чудо ли?
Для модели также нужен обобщенный контроллер, но с ним связан нижеследующий нюанс, поэтому пока что опустим.
От этих классов — абстрактной модели и обобщенного контроллера можно наследовать всё, что сохраняется и загружается. Написал модель, унаследовал контроллер — и забыл, всё работает. Отлично!
А что делать с сохранением и загрузкой? Ведь нужно сохранять и загружать сразу всё. А писать для каждой новой сущности код для сохранения и загрузки в каком-нибудь SaveLoadManager — утомительно и легкозабываемо.
И тут на помощь приходят статики.
1) Абстрактный класс с protected функциями сохранения и загрузки
2) У него — статичная коллекция All, куда каждый экземпляр класса-потомка добавляется при инициализации
3) И статичные публичные функции сохранения и загрузки, внутри которых перебираются все экземпляры из All и вызываются конкретные методы сохранения и загрузки.
И вот какой код получается в результате.
Примеры унаследованных конкретных классов:
Недостатки решения и способы их исправления.
1) Сохраняется (перезаписывается) всё. Даже то, что не было изменено.
Возможное решение: проверять перед сохранением равенство полей у исходной и текущей моделей и сохранять только при необходимости.
2) Загрузка из файла. Из json, например. Вот есть список моделей. Как загрузчику узнать, какой класс надо создать для этого json-текста?
Возможное решение: сделать словарь <System.Type, string> где регистрировать типы хардкодом. При загрузке из json берется строковой идентификатор типа и инстанцируется объект нужного класса. При сохранении объект проверяет, есть ли в словаре ключ его типа, и выдает сообщение/ошибку/исключение. Это позволит стороннему программисту не забыть добавить новый тип в словарь.
Посмотреть мой код с этим и другими хорошими решениями можно здесь (проекты в начальной стадии):
> FPSProject
> Невероятные космические похождения изворотливых котосминогов
Замечания, улучшения, советы — приветствуются.
Предложения помощи и совместного творчества приветствуются.
Предложения о работе крайне приветствуются.
UPD:
Вижу, возникают вопросы а ля «Каков профит от твоего решения? Все равно же делать модели, делать сериализацию.»
Отвечаю:
Вы пришли на чекпоинт или нажали кнопку сохранить. Кнопка или чекпоинт сообщили классу-менеджеру, что нужно сохранить состояние игры. Что делает менеджер?
Плохой вариант 1:
Плохой вариант 2: Каждый SaveLoadBehaviour подписывается на событие OnSave менеджера. Или регистрирует себя в каком-то «контейнере».
Плохо, потому что SaveLoadBehaviour должен знать о существовании менеджера/контейнера. Я же пытался сделать так, чтобы классы были максимально автономны, а все знания об их связях хранились в самом менеджере.
Плохой вариант 3: менеджер при инициализации ищет все сохраняемые компоненты.
1) Функция поиска может отличаться между платформами. GameObject.FindObjectsOfType() применима только для MonoBehaviour, а что если мы делаем shared-логику? Реализация должна быть максимально гибкой и кроссплатформенной.
2) Если мы решим переписать менеджер с нуля (для другой игры, например), то надо обязательно не забыть вставить функцию поиска.
Мой хороший вариант:
Еще мне задали вопрос, что делать, если мы хотим положить на один геймобжект несколько saveloadbehaviour? Как они при загрузке соберутся в один геймобжект?
Вот решение, которое пришло мне на ум:
1) Написал один раз и используй в любом проекте для любых сущностей. Ну, насколько возможно.
2) Создал сущность — и она сохраняется сама собою, с минимумом дополнительных усилий.
Решение пришло из стана синглтонов. Не надоело ли вам писать один и тот же синглтон-код? А меж тем есть generic singleton.
Вот как он выглядит для MonoBehaviour
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenericSingleton<T> : MonoBehaviour {
static GenericSingleton<T> instance;
public static GenericSingleton<T> Instance { get { return instance; } }
void Awake () {
if (instance && instance != this)
{
Destroy(this);
return;
}
instance = this;
}
}
public class TestSingletoneA : GenericSingleton<TestSingletoneA> {
// Use this for initialization
void Start () {
Debug.Log("A");
}
}
Т.е. можно сделать Generic класс, статические поля которого будут уникальны для каждого входного типа.
И это как раз наш случай. Потому что поведение сохраняемого объекта полностью идентично, различаются только сохраняемые модели. И тип модели как раз и выступает в качестве входного.
Вот код интерфейса модели. Он примечателен тем, что метод SetValues примет в качестве аргумента только модель такого же (или производного) типа. Не чудо ли?
AbstractModel
/// <summary>
/// Voloshin Game Framework: basic scripts supposed to be reusable
/// </summary>
namespace VGF
{
//[System.Serializable]
public interface AbstractModel<T> where T : AbstractModel<T>, new()
{
/// <summary>
/// Copy fields from target
/// </summary>
/// <param name="model">Source model</param>
void SetValues(T model);
}
public static class AbstratModelMethods
{
/// <summary>
/// Initialize model with source, even if model is null
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="model">Target model, can be null</param>
/// <param name="source">Source model</param>
public static void InitializeWith<T>(this T model, T source) where T: AbstractModel<T>, new ()
{
//model = new T();
if (source == null)
return;
model.SetValues(source);
}
}
}
Для модели также нужен обобщенный контроллер, но с ним связан нижеследующий нюанс, поэтому пока что опустим.
От этих классов — абстрактной модели и обобщенного контроллера можно наследовать всё, что сохраняется и загружается. Написал модель, унаследовал контроллер — и забыл, всё работает. Отлично!
А что делать с сохранением и загрузкой? Ведь нужно сохранять и загружать сразу всё. А писать для каждой новой сущности код для сохранения и загрузки в каком-нибудь SaveLoadManager — утомительно и легкозабываемо.
И тут на помощь приходят статики.
1) Абстрактный класс с protected функциями сохранения и загрузки
2) У него — статичная коллекция All, куда каждый экземпляр класса-потомка добавляется при инициализации
3) И статичные публичные функции сохранения и загрузки, внутри которых перебираются все экземпляры из All и вызываются конкретные методы сохранения и загрузки.
И вот какой код получается в результате.
SaveLoadBehaviour
using System.Collections.Generic;
using UnityEngine;
namespace VGF
{
/* Why abstract class instead of interface?
* 1) Incapsulate all save, load, init, loadinit functions inside class, make them protected, mnot public
* 2) Create static ALL collection and static ALL methods
* */
//TODO: create a similar abstract class for non-mono classes. For example, PlayerController needs not to be a MonoBehaviour
/// <summary>
/// Abstract class for all MonoBehaiour classes that support save and load
/// </summary>
public abstract class SaveLoadBehaviour : CachedBehaviour
{
/// <summary>
/// Collection that stores all SaveLoad classes in purpose of providing auto registration and collective save and load
/// </summary>
static List<SaveLoadBehaviour> AllSaveLoadObjects = new List<SaveLoadBehaviour>();
protected override void Awake()
{
base.Awake();
Add(this);
}
static void Add(SaveLoadBehaviour item)
{
if (AllSaveLoadObjects.Contains(item))
{
Debug.LogError(item + " element is already in All list");
}
else
AllSaveLoadObjects.Add(item);
}
public static void LoadAll()
{
foreach (var item in AllSaveLoadObjects)
{
if (item == null)
{
Debug.LogError("empty element in All list");
continue;
}
else
item.Load();
}
}
public static void SaveAll()
{
Debug.Log(AllSaveLoadObjects.Count);
foreach (var item in AllSaveLoadObjects)
{
if (item == null)
{
Debug.LogError("empty element in All list");
continue;
}
else
item.Save();
}
}
public static void LoadInitAll()
{
foreach (var item in AllSaveLoadObjects)
{
if (item == null)
{
Debug.LogError("empty element in All list");
continue;
}
else
item.LoadInit();
}
}
protected abstract void Save();
protected abstract void Load();
protected abstract void Init();
protected abstract void LoadInit();
}
}
GenericModelBehaviour<T>
using UnityEngine;
namespace VGF
{
/// <summary>
/// Controller for abstract models, providing save, load, reset model
/// </summary>
/// <typeparam name="T">AbstractModel child type</typeparam>
public class GenericModelBehaviour<T> : SaveLoadBehaviour where T: AbstractModel<T>, new()
{
[SerializeField]
protected T InitModel;
//[SerializeField]
protected T CurrentModel, SavedModel;
protected override void Awake()
{
base.Awake();
//Init();
}
void Start()
{
Init();
}
protected override void Init()
{
//Debug.Log(InitModel);
if (InitModel == null)
return;
//Debug.Log(gameObject.name + " : Init current model");
if (CurrentModel == null)
CurrentModel = new T();
CurrentModel.InitializeWith(InitModel);
//Debug.Log(CurrentModel);
//Debug.Log("Init saved model");
SavedModel = new T();
SavedModel.InitializeWith(InitModel);
}
protected override void Load()
{
//Debug.Log(gameObject.name + " saved");
LoadFrom(SavedModel);
}
protected override void LoadInit()
{
LoadFrom(InitModel);
}
void LoadFrom(T source)
{
if (source == null)
return;
CurrentModel.SetValues(source);
}
protected override void Save()
{
//Debug.Log(gameObject.name + " saved");
if (CurrentModel == null)
return;
if (SavedModel == null)
SavedModel.InitializeWith(CurrentModel);
else
SavedModel.SetValues(CurrentModel);
}
}
}
Примеры унаследованных конкретных классов:
AbstractAliveController : GenericModelBehaviour
public abstract class AbstractAliveController : GenericModelBehaviour<AliveModelTransform>, IAlive
{
//TODO: create separate unity implementation where put all the [SerializeField] attributes
[SerializeField]
bool Immortal;
static Dictionary<Transform, AbstractAliveController> All = new Dictionary<Transform, AbstractAliveController>();
public static bool GetAliveControllerForTransform(Transform tr, out AbstractAliveController aliveController)
{
return All.TryGetValue(tr, out aliveController);
}
DamageableController[] BodyParts;
public bool IsAlive { get { return Immortal || CurrentModel.HealthCurrent > 0; } }
public bool IsAvailable { get { return IsAlive && myGO.activeSelf; } }
public virtual Vector3 Position { get { return myTransform.position; } }
public static event Action<AbstractAliveController> OnDead;
/// <summary>
/// Sends the current health of this alive controller
/// </summary>
public event Action<int> OnDamaged;
//TODO: create 2 inits
protected override void Awake()
{
base.Awake();
All.Add(myTransform, this);
}
protected override void Init()
{
InitModel.Position = myTransform.position;
InitModel.Rotation = myTransform.rotation;
base.Init();
BodyParts = GetComponentsInChildren<DamageableController>();
foreach (var bp in BodyParts)
bp.OnDamageTaken += TakeDamage;
}
protected override void Save()
{
CurrentModel.Position = myTransform.position;
CurrentModel.Rotation = myTransform.rotation;
base.Save();
}
protected override void Load()
{
base.Load();
LoadTransform();
}
protected override void LoadInit()
{
base.LoadInit();
LoadTransform();
}
void LoadTransform()
{
myTransform.position = CurrentModel.Position;
myTransform.rotation = CurrentModel.Rotation;
myGO.SetActive(true);
}
public void Respawn()
{
LoadInit();
}
public void TakeDamage(int damage)
{
if (Immortal)
return;
CurrentModel.HealthCurrent -= damage;
OnDamaged.CallEventIfNotNull(CurrentModel.HealthCurrent);
if (CurrentModel.HealthCurrent <= 0)
{
OnDead.CallEventIfNotNull(this);
Die();
}
}
public int CurrentHealth
{
get { return CurrentModel == null? InitModel.HealthCurrent: CurrentModel.HealthCurrent; }
}
protected abstract void Die();
}
AliveModelTransform : AbstractModel
namespace VGF.Action3d
{
[System.Serializable]
public class AliveModelTransform : AliveModelBasic, AbstractModel<AliveModelTransform>
{
[HideInInspector]
public Vector3 Position;
[HideInInspector]
public Quaternion Rotation;
public void SetValues(AliveModelTransform model)
{
Position = model.Position;
Rotation = model.Rotation;
base.SetValues(model);
}
}
}
Недостатки решения и способы их исправления.
1) Сохраняется (перезаписывается) всё. Даже то, что не было изменено.
Возможное решение: проверять перед сохранением равенство полей у исходной и текущей моделей и сохранять только при необходимости.
2) Загрузка из файла. Из json, например. Вот есть список моделей. Как загрузчику узнать, какой класс надо создать для этого json-текста?
Возможное решение: сделать словарь <System.Type, string> где регистрировать типы хардкодом. При загрузке из json берется строковой идентификатор типа и инстанцируется объект нужного класса. При сохранении объект проверяет, есть ли в словаре ключ его типа, и выдает сообщение/ошибку/исключение. Это позволит стороннему программисту не забыть добавить новый тип в словарь.
Посмотреть мой код с этим и другими хорошими решениями можно здесь (проекты в начальной стадии):
> FPSProject
> Невероятные космические похождения изворотливых котосминогов
Замечания, улучшения, советы — приветствуются.
Предложения помощи и совместного творчества приветствуются.
Предложения о работе крайне приветствуются.
UPD:
Вижу, возникают вопросы а ля «Каков профит от твоего решения? Все равно же делать модели, делать сериализацию.»
Отвечаю:
Вы пришли на чекпоинт или нажали кнопку сохранить. Кнопка или чекпоинт сообщили классу-менеджеру, что нужно сохранить состояние игры. Что делает менеджер?
Плохой вариант 1:
void Save()
{
Entity1.Save;
Entity2.Save;
Entity3.Save;
...
EntityInfinity.Save;
}
Плохой вариант 2: Каждый SaveLoadBehaviour подписывается на событие OnSave менеджера. Или регистрирует себя в каком-то «контейнере».
Плохо, потому что SaveLoadBehaviour должен знать о существовании менеджера/контейнера. Я же пытался сделать так, чтобы классы были максимально автономны, а все знания об их связях хранились в самом менеджере.
Плохой вариант 3: менеджер при инициализации ищет все сохраняемые компоненты.
1) Функция поиска может отличаться между платформами. GameObject.FindObjectsOfType() применима только для MonoBehaviour, а что если мы делаем shared-логику? Реализация должна быть максимально гибкой и кроссплатформенной.
2) Если мы решим переписать менеджер с нуля (для другой игры, например), то надо обязательно не забыть вставить функцию поиска.
Мой хороший вариант:
class GameManager
{
void Save()
{
AbstractSaveLoadBehaviour.SaveAll();
}
}
Еще мне задали вопрос, что делать, если мы хотим положить на один геймобжект несколько saveloadbehaviour? Как они при загрузке соберутся в один геймобжект?
Вот решение, которое пришло мне на ум:
- В каждый SaveloadBehaviour в функции сохранения и загрузки добавить вызов события.
void Save() { if (OnSaveSendToMainBehaviour == null) { //сохраняем в файл } else OnSaveSendToMainBehaviour(savedModel) }
- ГЛАВНЫЙ контроллер сущности, который при инициализации ищет все компоненты SaveLoadBehaviour и подписывается на их события.
- Если он есть, то он агрегирует события от всех контроллеров сохранения, собирает их модели и сохраняет их в файл сам, единолично.
- Чтобы проверить, что все контроллеры уже всё отправили, можно сделать счетчик.
voidOnSaveModelFromDependentController(model partModel) { currentSaveCount++; model.AddPArtModel(partModel); if (currentSaveCount == TotalSaveCount) Save(model); }
- И даже добавление такого убер-контроллера можно автоматизировать. Каждый saveloadbehaviour на Awake или Start ищет, есть ли другие. Если есть, то ищет убер-контроллер и по необходимости добавляет.
А убер-контроллер на Awake или Start подписывается на всех.
Двойной подписки не произойдет, т.к. убер-контроллер будет добавлен лишь единожды, и его Awake/Start тоже лишь единожды будет вызван.
Поделиться с друзьями
shai_hulud
как вариант можно с помощью атрибутов разметить поля которые требуется сохранять, с помощью T4 который поддерживается в MonoDevelop и VS сгенерить DTO классы и методы копирования этих полей в DTO. А потом нормальным сериализатором положить это в JSON.
И при этом не писать руками функции сохранения/загрузки.
btw. синглтоны с глобальным доступом это зло.
Neongrey
Насчет глобальных синглтонов не знаю. Но я у себя сделал самый закрытый менеджер игры.
Он у меня — такой мост между всеми подсистемами. Слушает события и вызывает нужные функции. А к самому нему никто доступ не имеет.
Все, что с ним можно сделать — проинициализировать (ну, чтобы он хоть как-то появился в памяти).
MonkAlex
А потом вы добавили новое поле в класс и сохраненное значение развернётся как получится.
Где то переименовали свойство, где то ещё что-то…
Я пока не видел идеальных и хороших решений на все случаи жизни.
Несовместимость разных версий сохранений и приложений — полная печаль, на мой взгляд.
Neongrey
Новое поле добавляется в модель. Копирование модели в модель пишешь сам. Хотя по идее должно же быть встроенное средство копирования объектов.
А каркас остается. Я сначала на геймсджем писал игру про котосминогов и сделал там эту систему сохранения. А потом для тестового задания про шутер просто использовал ее же, заменив модели. До самого последнего момента не знал, заработает ли. Вызвал в GameManager AbstractSaveLoad.LoadInit() для рестарта матча — и все заработало.
Конечно, лучший код — ненаписанный код и кнопочка «Сделать прекрасно». Но чем богаты.
Во всяком случае добавление новых сохраняемых сущностей происходит максимально безболезненно, проверено на двух разных проектах.
igormich88
А может быть просто сохранять в json непосредственно сам тип объекта при сохранении? И при загрузке создавать объект примерно таким образом (сам я правда так не пробовал):
Neongrey
Выглядит очень похоже на правду.
Я тоже так не пробовал. Если добавить в мое решение автоматическое полное копирование объектов, автоматическую сериализацию и автоопределение типа, то будет полностью завершенное решение.
Neongrey
Но вообще есть нюанс. Из разряда «нам бы ваши проблемы». Переносимость сохранений из МегаИгра1 в МегаИгра2: Воскрешение.
Мы не можем гарантировать, что какой-нибудь умник не переименует класс. Да может быть мы сами в процессе разработки что-то переименуем. И, предположим, у нас есть несколько сохранений, чтобы тестировать игру в разных местах (ну вдруг).
Короче, надо об этом очень сильно помнить.
Ogoun
Самый быстрый способ сохранения/загрузки состояния который я знаю и использую примерно такой:
1. Создание оберток для MemoryStream по контрактам вида:
2. Делаем контракт
3. Пример использования
Минусы:
Плюсы:
Да, есть куча готовых мапперов, но каждый слой абстракции будет добавлять своё замедление.
Neongrey
1) Спрячте под спойлер, пожалуйста.
2) Можно ли так сериализовать тип? Чтобы ридер сам знал, что читает из файла?
3) Можно ли так сериализовать всё в один файл (из коллекции) и потом прочитать это же всё и создать нужные объекты?
4) насколько удобно это для передачи по сети?
2-4 — потому что я никогда не работал с бинарной сериализацией, я во многих вопросах еще нуб.
Ogoun
Пример как это выглядит (Unity не знаю и игры не пишу, поэтому что придумалось то и есть):
Ogoun
Перечитал заголовок статьи, если для любого проекта, тогда лучше прикрутить маппер. Который будет использовать такой же механизм сериализации, но позволит не писать вручную код. Главное не делать чтение/запись полей через Reflection, лучше использовать Emit или ExpressionTree. Или взять что-то готовое из nuget'а.
Neongrey
Суть-то как раз в том, чтобы сделать решение, которое ложится на любой проект. Чтобы одно и то же по сто раз не писать.
И у меня пока что нет сохранения в файл, всё хранится в оперативке — и в этом все равно есть смысл, ибо загрузка последнего сохранения (чекпоинта) и рестарт уровня.
Neongrey
В Юнити нельзя предсказать, в какой последовательности объекты сохранятся. И, следовательно, в какой последовательности будут храниться и загружаться.
Вообще, может, и можно: создать вручную начальное сохранение, а потом читать из него в заранее заданном порядке в начале игры, в этом же порядке хранить в коллекции и в этом же порядке записывать.
Но вообще бывает такая вещь как крафт: о) И расход ресурсов. И смерть (исчезновение) персонажей.
В общем случае набор сущностей в игре непостоянный, поэтому «сразу знать, что и где» — негибко.
Ogoun
Посмотрел как делают люди (тут или тут или тут) и везде механизм аналогичный описанному.
Neongrey
1) Я лично слабо представляю, как в PlayerPrefs хранить весь мир Fallout4
2) Ну и переносимость сохранений.
Так что файлы наше всё
Suvitruf
1) Потому что это глупость. В PlayerPrefs надо хранить небольшие данные: настройки и т.п. Никто в здравом уме туда весь мир пихать не станет.
Как по мне, самый оптимальный вариант либо Binary Serizlization, либо в json. Разве что ручками всё это писать придётся.
WeslomPo
Больше синглтонов богу синглтонов? Синглтоны и статичные переменные сложных типов — зло. Ищите решение, которое их не использует. Например пусть какой-нибудь менеджер находит все компоненты определенного типа и сохраняет их. Не забываем добавить уникальный идентификатор для каждого такого компонента.
Для сериализации есть прекрасная утилита, которая заслуживает упоминания в этой статье:
которая может сериализовать практически любой класс с приемлемой скоростью.
Вот накидал за полчаса примерчик:
Остальные классы, нужные для работы сего безобразия:
Neongrey
А в чем проблема с синглтонами и статичными переменными сложных типов?
За утилиту спасибо: о)
WeslomPo
Синглтоны: Повышение связанности кода, невозможностью заменить\удалить и почти всегда применение синглтона говорит о том что с архитектурой что-то не то — участок кода попахивает.
http://rsdn.org/forum/design/2615563.flat#2615563
Статичные переменные — родственники глобальных переменных. Основная проблема — потеря контроля над значениями. Их может изменить кто угодно и откуда угодно. Я, похоже запутался, и сказал про сложный тип, но имел в виду константы (строки, числа). Т.е. константные статические переменные еще куда не шло, а вот изменяемые значения — зло стопроцентное.
Хотя в Unity — использовать константы не круто, потому что сам по себе движок помогает с сериализацией, и в 99% случаев лучше использовать ScriptableObject, а значения менять прямо из редактора.
Вообще почитайте про инверсию управления через Dependency Injection. Отличный фреймворк для этих целей под Unity — Zenject. В пару проектов втыкаешь и забываешь про синглтоны.
Neongrey
Я стараюсь использовать в Unity как можно меньше Unity. Ибо в реальных проектах, которые на работе за зарплат, логика часто shared или серверная.
А еще можно взять shared бизнес-логику, скомпилировать в dll и перенести на Unreal. Поэтому имхо чем меньше ScriptableObject, MonoBehaviour и прочего using UnityEngine, тем лучше.
Neongrey
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) А конкретную реализацию меняй как вздумается.
WeslomPo
По статье как-то незаметно, компоненты поверх компонентов, компонентами погоняют :). ScriptableObject'ы легко меняются на обычный класс. Сравни с моей реализацией — там один MB — как точка входа алгоритма, а записывать можно любые данные практически.
1 — Есть еще readonly, но с MonoBehaviour такое не провернешь. А можно геттером спрятать.
3 и 4 — MyClass.Something уже плохо.
Правильно приготовленный синглтон — это синглтон который не был написан.
Обращаясь из одного класса к другому через MyClass.Something — ты создаешь зависимость, которую довольно сложно отследить, а потом, при рефакторинге заменить. Привязываешь класс А к классу Б стальными тросами. Тут даже нет речи об интерфейсах и т.д. Сплошное несчастье.
Это сложно объяснить, но когда ты столкнешься с этим на практике, то поймешь насколько синглтоны — зло.
Neongrey
Блин, классы взаимодействуют, куда от этого деться? Я предполагаю, что если человек использовал синглтон, значит, ему реально позарез надо обращаться от одного класса к другому. Для примера можно взять игрока. Методы получения урона и гибели одинаковы у игрока и врага. Но на гибель игрока игра должна особым образом реагировать. Посылается событие person.ondead, а игра должна проверить, не игрок ли преставился.
Это для примера. Я пытался сказать, что если синглтон кажется разработчику подходящим решением, то недостатки сильной связности можно устранить интерфейсами и наследованиями
Ну и компоненты у меня — тоже тут никуда не денешься. Шутер, коллизии, физика, трансформы всякие. Тут единственный способ избавиться от юнити — писать свой движок
Anger22
А в основном так и происходит, пишут огромные фреймворки над юнити, т.к. стандартные компоненты жутко тормозят и их никак не оптимизировать.
Anger22
Json конечно же удобен до того момента, как вам придеться делать игры под консоли, которые налагают определенные ограничения на время сохранения. Кроме того, в случае когда в игре много сохранений — всё это дело начинает занимать много места не жестком диске, что для клаудсейвов очень даже критично.
2morrowMan
Синглтоны-синглтоны…
У кого сколько было случаев попытки создания второй копии синглтон класса и чтобы вот эти предосторожности помогали? Типо, «Ошибочка, бро! Это же синглтон!!»
У меня пока ни разу…
KonH
Завязываться на monoBehaviour-синглоны кажется не самой лучшей идеей. Все же лучше такие вещи выносить отдельно и иметь какой-то общий контроллер, который будет заниматься только загрузкой и сохранением.
Почитать как сделано в моем хобби-проекте можно тут. Есть единая точка входа, где мы и определяем что и в каком формате должно сохраняться (сейчас это сериализация data-классов в один json-файл). Есть куда это все развивать, но в целом такой подход кажется более подходящим.
Neongrey
Хм. Меня, похоже, не поняли. Синглтон я использую только как демонстрацию наследования статических полей. И попутно показываю, что есть такой способ делать синглтоне, если кому надо. Моя система сохранения не основана на синглтоне. Она основана на обобщённом классе поведения (контроллере), которому можно передать любую модель и таким образом легко получить сохраняемый объект, просто определив поля и не реализуя один и тот же функционал каждый раз. А потом все такие объекты можно сохранить вызовом одной функции.