📜 ⬆️ ⬇️

Unity: save system for any project

Games need to be saved. There can be a great number of saved entities. For example, in recent releases of TES and Fallout, the game remembers the location of each rolled bottle. A decision is needed to:

1) Written once and use in any project for any entities. Well, as far as possible.
2) Created the essence - and it is saved by itself, with a minimum of additional efforts.

The decision came from the camp of singltons. Are you tired of writing the same singleton code? And yet there is a generic singleton.

Here's how it looks for 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"); } } 


Those. You can make a Generic class whose static fields will be unique for each input type.
')
And this is our case. Because the behavior of the saved object is completely identical, only the saved models differ. And the type of model just acts as an input.

Here is the model interface code. It is remarkable that the SetValues ​​method takes as its argument only a model of the same (or derived) type. Isn't it a miracle?

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); } } } 


The model also needs a generic controller, but the following nuance is associated with it, so for the time being we’ll omit it.

From these classes — the abstract model and the generalized controller — you can inherit everything that is saved and loaded. I wrote a model, inherited the controller - and forgot, everything works. Fine!

And what to do with saving and loading? After all, you need to save and load everything at once. And to write for each new entity the code to save and load in some SaveLoadManager is tedious and easy to forget.

And here come to the aid of statics.

1) Abstract class with protected save and load functions
2) He has a static collection All, where each instance of the descendant class is added during initialization.
3) And static public functions of preservation and loading, inside of which all instances from All are searched and specific methods of preservation and loading are called.

And so what code is obtained as a result.

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); } } } 


Examples of specific classes inherited:

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); } } } 


Disadvantages of the solution and how to fix them.

1) All is saved (overwritten). Even that which has not been changed.

Possible solution: check before saving the equality of the fields in the original and current models and save only when necessary.

2) Download from file. From json, for example. Here is a list of models. How can the loader know which class to create for this json text?

Possible solution: make the dictionary <System.Type, string> where to register types with hardcode. When loading from json, a string type identifier is taken and an object of the required class is instantiated. When saving, the object checks if there is a key of its type in the dictionary, and issues a message / error / exception. This will allow an outside programmer to remember to add a new type to the dictionary.

You can see my code with this and other good solutions here (projects in the initial stage):

FPSProject
Incredible cosmic adventures of quirky catminders

Comments, improvements, tips are welcome.
Suggestions for help and co-creation are welcome.
Job offers are highly welcome.

UPD:
I see, questions arise a la “What is the profit from your decision? It's the same to do models, to do serialization. ”
I answer:
You came to checkpoint or click save. A button or checkpoint told the class manager to save the game state. What does the manager do?

Bad option 1:
 void Save() { Entity1.Save; Entity2.Save; Entity3.Save; ... EntityInfinity.Save; } 

Bad version 2: Every SaveLoadBehaviour subscribes to the manager's OnSave event. Or registers itself in some kind of "container".
Bad, because SaveLoadBehaviour needs to know about the existence of a manager / container. I tried to make the classes as autonomous as possible, and all the knowledge about their connections was stored in the manager himself.

Bad option 3: the manager, during initialization, searches for all stored components.
1) The search function may differ between platforms. GameObject.FindObjectsOfType () is applicable only for MonoBehaviour, but what if we do shared logic? Implementation should be as flexible and cross-platform as possible.
2) If we decide to rewrite the manager from scratch (for another game, for example), then we must not forget to insert the search function.

My good option:
 class GameManager { void Save() { AbstractSaveLoadBehaviour.SaveAll(); } } 


I was also asked what to do if we want to put several saveloadbehaviour on one game object? How are they going to load in one gameobject?

Here is the solution that came to my mind:
  1. In each SaveloadBehaviour in the save and load function add an event call.
     void Save() { if (OnSaveSendToMainBehaviour == null) { //   } else OnSaveSendToMainBehaviour(savedModel) } 
  2. The main controller of the entity, which, when initialized, searches for all components of SaveLoadBehaviour and subscribes to their events.
  3. If it exists, it aggregates events from all storage controllers, collects their models and saves them to the file itself, individually.
  4. To check that all controllers have already sent everything, you can make a counter.
     voidOnSaveModelFromDependentController(model partModel) { currentSaveCount++; model.AddPArtModel(partModel); if (currentSaveCount == TotalSaveCount) Save(model); } 
  5. And even the addition of such a uber controller can be automated. Every saveloadbehaviour on Awake or Start seeks if there are others. If it is, it searches for the uber controller and adds it, if necessary.
    And the uber controller on Awake or Start subscribes to all.
    Double subscriptions will not happen, because the uber controller will be added only once, and its Awake / Start will also be called only once.

Source: https://habr.com/ru/post/330278/


All Articles