Игры надо сохранять. Сохраняемых сущностей может быть великое множество. Например, в последних выпусках TES и Fallout игра помнит расположение каждой закатившейся склянки. Необходимо решение, чтобы:
1) Написал один раз и используй в любом проекте для любых сущностей. Ну, насколько возможно.
2) Создал сущность — и она сохраняется сама собою, с минимумом дополнительных усилий.
Решение пришло из стана синглтонов. Не надоело ли вам писать один и тот же синглтон-код? А меж тем есть generic singleton.
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 примет в качестве аргумента только модель такого же (или производного) типа. Не чудо ли?
/// <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 и вызываются конкретные методы сохранения и загрузки.
И вот какой код получается в результате.
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();
}
}
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);
}
}
}
Примеры унаследованных конкретных классов:
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();
}
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
Невероятные космические похождения изворотливых котосминогов
Замечания, улучшения, советы — приветствуются.
Предложения помощи и совместного творчества приветствуются.
Предложения о работе крайне приветствуются.
Автор: Neongrey