Skip to content

Instantly share code, notes, and snippets.

@andrew-raphael-lukasik
Last active July 10, 2023 08:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrew-raphael-lukasik/e02f21cc507da9e5eb7569b8793e70f4 to your computer and use it in GitHub Desktop.
Save andrew-raphael-lukasik/e02f21cc507da9e5eb7569b8793e70f4 to your computer and use it in GitHub Desktop.
Skeleton structure for a save system.

DataInspector class gives you this window: image

How to use Persistence in practice:

// PersistenceController.cs
using UnityEngine;
public class PersistenceController : MonoBehaviour
{
    public string directory = "my_game";
    public string fileName = "save001";
    void OnGUI ()
    {
        if( GUI.Button(new Rect(10,10,50,20),"Load")) Load();
        if( GUI.Button(new Rect(10,40,50,20),"Save")) Save();
    }
    public void Load () { Persistence.Load( directory , fileName ); }
    public void Save () { Persistence.Save( directory , fileName ); }
}
// Enemy.cs
using UnityEngine;
public class Enemy : MonoBehaviour, Persistence.IMember
{
	[SerializeField] string ID = null;
	string Persistence.IMember.ID { get=>ID; set=>ID=value; }
	[SerializeField] bool _persistent = false;
	bool Persistence.IMember.persistent { get=>_persistent; set=>_persistent=value; }

	
	[SerializeField] int _numAmmo = 100;
	string _key_numAmmo => $"{ID}.ammo";

	[SerializeField] bool _isAlive = true;
	string _key_isAlive => $"{ID}.alive";

	[SerializeField] float _gold = 1.23456789f;
	string _key_gold => $"{ID}.gold";

	string _key_position => $"{ID}.position";
	
	
	
	#if UNITY_EDITOR
	void OnValidate () => Persistence.ValidateID( this );
	void OnDrawGizmosSelected ()
	{
		if( _persistent )
			UnityEditor.Handles.Label( transform.position , ID );
	}
	#endif

	void Awake ()
	{
		Persistence.ValidateID( this );

		Persistence.onAfterLoad += FromSaveData;
		Persistence.onBeforeSave += ToSaveData;
	}
	
	void OnDestroy ()
	{
		Persistence.onAfterLoad -= FromSaveData;
		Persistence.onBeforeSave -= ToSaveData;
	}

	/// <summary> Reads and applies save data </summary>
	/// <remarks> Save data -> game state </remarks>
	void FromSaveData ()
	{
		if( !_persistent ) return;


		if( Persistence.GetBoolean(_key_isAlive, out bool getIsAlive) )
		{
			if( getIsAlive==false )
				gameObject.SetActive(false);
		}
		
		if( Persistence.GetInteger(_key_numAmmo,out int getNumAmmo) )
			_numAmmo = getNumAmmo;

		if( Persistence.GetVector(_key_position, out Vector3 getPosition) )
			transform.position = getPosition;
		
		if( Persistence.GetFloat(_key_gold,out float getGold) )
			_gold = getGold;
	}
	/// <summary> Prepares save data for writing </summary>
	/// <remarks> Game state -> save data </remarks>
	void ToSaveData ()
	{
		if( !_persistent ) return;
		

		if( _isAlive )
		{
			Persistence.SetInteger( _key_numAmmo , _numAmmo );
			Persistence.SetVector( _key_position , transform.position );
			Persistence.SetFloat( _key_gold , _gold );
			Persistence.RemoveBoolean( _key_isAlive );// let's keep this file small and store booleans for dead enemies only
		}
		else
		{
			Persistence.SetBoolean( _key_isAlive , false );// flag as dead
			Persistence.RemoveInteger( _key_numAmmo );// remove old data
			Persistence.RemoveVector( _key_position );// remove old data
			Persistence.RemoveFloat( _key_gold );// remove old data
		}
	}
	
}
// src: https://gist.github.com/andrew-raphael-lukasik/e02f21cc507da9e5eb7569b8793e70f4
using System.Collections.Generic;
using System.Linq;
using IO = System.IO;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
#if UNITY_EDITOR
using UnityEditor.UIElements;
#endif
using DateTime = System.DateTime;
public class Persistence
{
static Dictionary<string,int> _integers;
static Dictionary<string,float> _floats;
static Dictionary<string,Vector3> _vectors;
static Dictionary<string,string> _strings;
public static DateTime lastChangeTime { get; private set; }
public static event System.Action onBeforeSave = ()=> {};
public static event System.Action onAfterLoad = ()=> {};
static Persistence () => Reset();// static constructor for initialization
/// IMPORTANT: value will be 0 when entry doesn't exist yet
/// hence use if(GetInteger(key,out int val)) { _value = val; }
public static bool GetInteger ( string key , out int value )
{
return _integers.TryGetValue( key, out value );
}
public static void SetInteger ( string key , int value )
{
if( _integers.ContainsKey(key) ) _integers[key] = value;
else _integers.Add( key , value );
lastChangeTime = DateTime.Now;
}
public static void RemoveInteger ( string key )
{
if( _integers.ContainsKey(key) ) _integers.Remove(key);
}
public static bool GetVector ( string key , out Vector3 value )
{
return _vectors.TryGetValue( key, out value );
}
public static void SetVector ( string key , Vector3 value )
{
if( _vectors.ContainsKey(key) ) _vectors[key] = value;
else _vectors.Add( key , value );
lastChangeTime = DateTime.Now;
}
public static void RemoveVector ( string key )
{
if( _vectors.ContainsKey(key) ) _vectors.Remove(key);
}
public static bool GetFloat ( string key , out float value )
{
return _floats.TryGetValue( key, out value );
}
public static void SetFloat ( string key , float value )
{
if( _floats.ContainsKey(key) ) _floats[key] = value;
else _floats.Add( key , value );
lastChangeTime = DateTime.Now;
}
public static void RemoveFloat ( string key )
{
if( _floats.ContainsKey(key) ) _floats.Remove(key);
}
public static bool GetBoolean ( string key , out bool value )
{
bool valueExists = GetInteger( key, out int i );
value = i==0 ? false : true;
return valueExists;
}
public static void SetBoolean ( string key , bool value ) => SetInteger( key , value ? 1 : 0 );
public static void RemoveBoolean ( string key ) => RemoveInteger( key );
public static bool GetString ( string key , out string value )
{
return _strings.TryGetValue( key, out value );
}
public static void SetString ( string key , string value )
{
if( _strings.ContainsKey(key) ) _strings[key] = value;
else _strings.Add( key , value );
lastChangeTime = DateTime.Now;
}
public static void RemoveString ( string key )
{
if( _strings.ContainsKey(key) ) _strings.Remove(key);
}
public static bool Load ( string directory , string fileName )
{
string filePath = GetSaveFilePath(directory,fileName);
if( !IO.File.Exists(filePath) )
return false;
var json = IO.File.ReadAllText( filePath );
var data = JsonUtility.FromJson<SerializableData>( json );
Load( data );
onAfterLoad();
return true;
}
public static void Load ( SerializableData data )
{
Reset();
lastChangeTime = DateTime.Now;
if( data.integer_keys!=null )
{
Assert.AreEqual( data.integer_keys.Length , data.integer_values.Length );
for( int i=0 ; i<data.integer_keys.Length ; i++ )
_integers.Add( data.integer_keys[i] , data.integer_values[i] );
}
if( data.vector_keys!=null )
{
Assert.AreEqual( data.vector_keys.Length , data.vector_values.Length );
for( int i=0 ; i<data.vector_keys.Length ; i++ )
_vectors.Add( data.vector_keys[i] , data.vector_values[i] );
}
if( data.float_keys!=null )
{
Assert.AreEqual( data.float_keys.Length , data.float_values.Length );
for( int i=0 ; i<data.float_keys.Length ; i++ )
_floats.Add( data.float_keys[i] , data.float_values[i] );
}
if( data.string_keys!=null )
{
Assert.AreEqual( data.string_keys.Length , data.string_values.Length );
for( int i=0 ; i<data.string_keys.Length ; i++ )
_strings.Add( data.string_keys[i] , data.string_values[i] );
}
}
public static void Save ( string directory , string fileName )
{
onBeforeSave();
Save( out var data );
string json = JsonUtility.ToJson( data );
string fileDir = GetSaveDirectoryPath(directory);
if( !IO.Directory.Exists(fileDir) ) IO.Directory.CreateDirectory(fileDir);
string filePath = GetSaveFilePath(directory,fileName);
IO.File.WriteAllText( path:filePath , contents:json );
}
public static void Save ( out SerializableData data )
{
data = new SerializableData{
integer_keys = _integers.Keys.ToArray() ,
integer_values = _integers.Values.ToArray() ,
vector_keys = _vectors.Keys.ToArray() ,
vector_values = _vectors.Values.ToArray() ,
float_keys = _floats.Keys.ToArray() ,
float_values = _floats.Values.ToArray() ,
string_keys = _strings.Keys.ToArray() ,
string_values = _strings.Values.ToArray() ,
};
}
static string GetSaveDirectoryPath ( string directory ) => IO.Path.Combine( Application.persistentDataPath , directory );
static string GetSaveFilePath ( string directory , string fileName ) => IO.Path.Combine( GetSaveDirectoryPath(directory) , fileName );
/// call when new game starts
public static void Reset ()
{
_integers = new Dictionary<string,int>();
_vectors = new Dictionary<string,Vector3>();
_floats = new Dictionary<string,float>();
_strings = new Dictionary<string,string>();
lastChangeTime = DateTime.Now;
}
/// call from OnValidate methods
public static void ValidateID ( IMember owner )
{
// validate prefabs:
#if UNITY_EDITOR
if( owner is Object && UnityEditor.PrefabUtility.IsPartOfPrefabAsset(owner as Object) )
{
// GameObject.Intantiate(thisPrefab) will duplicate this IDs likely causing mayhem as a result, so lets clear it
owner.ID = null;
owner.persistent = false;
return;
}
#endif
// validate scene instances:
if( owner.persistent && string.IsNullOrEmpty(owner.ID) )
{
if( Application.isPlaying )// runtime
{
// no persistence for prefab instances
owner.persistent = false;
owner.ID = $"TEMPORARY.{GenerateUniqueID()}";// mark it, so seeing this appear in data means a bug
}
else// editor
{
owner.ID = GenerateUniqueID();
#if UNITY_EDITOR
if( owner is Object )
UnityEditor.Undo.RecordObject( owner as Object , "ID generated" );
#endif
}
}
}
public static string GenerateUniqueID () => System.Guid.NewGuid().ToString();
[System.Serializable]
public class SerializableData
{
public string[] integer_keys;
public int[] integer_values;
public string[] vector_keys;
public Vector3[] vector_values;
public string[] float_keys;
public float[] float_values;
public string[] string_keys;
public string[] string_values;
}
public interface IMember
{
string ID { get; set; }
bool persistent { get; set; }
}
#if UNITY_EDITOR
public class DataInspector : UnityEditor.EditorWindow
{
ListView _integersView, _vectorsView, _floatsView, _stringsView;
DateTime _lastRebindTime;
void OnEnable () => Rebuild();
void Rebuild ()
{
rootVisualElement.Clear();
var FOLDOUT_INT = new Foldout{ text="Integers" };
_integersView = CreateListView( _integers , () => new IntegerField() , (e,i) => {
string key = (string) _integersView.itemsSource[i];
var field = e as IntegerField;
field.label = key;
field.value = _integers[key];
field.RegisterValueChangedCallback( (e)=> {
_integers[key] = e.newValue;
} );
} );
FOLDOUT_INT.style.flexGrow = 1;
FOLDOUT_INT.Add( _integersView );
var FOLDOUT_FLT = new Foldout{ text="Floats" };
_floatsView = CreateListView( _floats , ()=>new FloatField() , (e,i) => {
string key = (string) _floatsView.itemsSource[i];
var field = e as FloatField;
field.label = key;
field.value = _floats[key];
field.RegisterValueChangedCallback( (e)=> {
_floats[key] = e.newValue;
} );
} );
FOLDOUT_FLT.style.flexGrow = 1;
FOLDOUT_FLT.Add( _floatsView );
var FOLDOUT_VEC = new Foldout{ text="Vectors" };
_vectorsView = CreateListView( _vectors , ()=>new Vector3Field() , (e,i) => {
string key = (string) _vectorsView.itemsSource[i];
var field = e as Vector3Field;
field.label = $"{i} {key}";
field.value = _vectors[key];
field.RegisterValueChangedCallback( (e)=> {
_vectors[key] = e.newValue;
} );
} );
FOLDOUT_VEC.style.flexGrow = 1;
FOLDOUT_VEC.Add( _vectorsView );
var FOLDOUT_STR = new Foldout{ text="Strings" };
_stringsView = CreateListView( _strings , ()=>new TextField() , (e,i) => {
string key = (string) _stringsView.itemsSource[i];
var field = e as TextField;
field.label = $"{i} {key}";
field.value = _strings[key];
field.RegisterValueChangedCallback( (e)=> {
_strings[key] = e.newValue;
} );
} );
FOLDOUT_STR.style.flexGrow = 1;
FOLDOUT_STR.Add( _stringsView );
Rebind();
rootVisualElement.Add( FOLDOUT_INT );
rootVisualElement.Add( FOLDOUT_FLT );
rootVisualElement.Add( FOLDOUT_VEC );
rootVisualElement.Add( FOLDOUT_STR );
}
ListView CreateListView <K,V> ( Dictionary<K,V> dict , System.Func<VisualElement> makeItem , System.Action<VisualElement,int> bindItem )
{
var LIST = new ListView();
{
var style = LIST.style;
style.flexGrow = 1.0f;
style.minHeight = LIST.itemHeight * 3;
}
LIST.itemHeight = 16;
LIST.makeItem = makeItem;
LIST.bindItem = bindItem;
LIST.selectionType = SelectionType.Single;//SelectionType.None;
return LIST;
}
void Rebind ()
{
_integersView.itemsSource = _integers.Keys.ToArray();
_floatsView.itemsSource = _floats.Keys.ToArray();
_vectorsView.itemsSource = _vectors.Keys.ToArray();
_stringsView.itemsSource = _strings.Keys.ToArray();
_lastRebindTime = lastChangeTime;
}
public void Update ()
{
if( _lastRebindTime!=lastChangeTime )
Rebind();
}
[UnityEditor.MenuItem("Game/Persistence/Inspector",false,0)]
public static void CreateWindow () => UnityEditor.EditorWindow.GetWindow<DataInspector>();
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment