Instantly share code, notes, and snippets.

Embed
What would you like to do?
Auto-Save Modified Scenes and Assets with sudo version history in Unity
// ===============================
// AUTHOR : Frederic Babord
// CREATE DATE : 24th April 2018
// PURPOSE : Auto-Save Modified Scenes and Assets in Unity
// SPECIAL NOTES : This script requires Asyncronous functions which REQUIRES C# 6.0 and .NET 4.6 which is
// experimental in Unity 2017 and stable in Unity 2018.
// MINIMUM Unity Version: 2017
// Needs to be placed in an Editor folder
// ===============================
// Change History:
// 24th April 2018
// - Initial Creation
// - Select Timestamp format
// - Autosave frequency
// - Max autosaves
// - What to autosave
// ==================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
namespace FreddieBabord
{
namespace AutoSave
{
[InitializeOnLoad]
public class AutoSaveEditor
{
private static bool _autoSave;
private static int _autoSaveDurationMins, _autoSaveDurationSecs, _maxAutosaves;
private static bool _autoSaveScenes;
private static bool _autoSaveAssets;
private static bool _debugMessages;
private static readonly List<string> AutosaveTimeFormat =
new List<string>() {"yyyyMMddTHHmmss", "yyMMddTHHmmss", "ddMMyyyyTHHmmss", "ddMMyyTHHmmss"};
private static bool _prefsLoaded;
private static DateTime _startingTimestamp;
private static int _timeFormatIndex;
private static bool _editorAdded;
public static bool TriggeredFromAutoSave;
public static List<string> AutoSavePaths = new List<string>();
public static List<string> Timestamps = new List<string>();
// Default constructor to get the current time when the system
// was initialised
static AutoSaveEditor()
{
_startingTimestamp = DateTime.Now;
}
// Add the GUI to Unitys preference pane > Unity / Preferences
[PreferenceItem("Auto Save")]
private static void CustomPreferencesGUI()
{
// If we have yet to load in the preferences from a previous session
// do so now.
if (!_prefsLoaded)
{
_autoSave = EditorPrefs.GetBool("BoolPreferenceKey", false);
_autoSaveDurationMins = EditorPrefs.GetInt("AutoSaveMins", 0);
_autoSaveDurationSecs = EditorPrefs.GetInt("AutoSaveSecs", 30);
_autoSaveAssets = EditorPrefs.GetBool("autoSaveAssets", true);
_autoSaveScenes = EditorPrefs.GetBool("autoSaveScenes", true);
_timeFormatIndex = EditorPrefs.GetInt("timeFormatIndex", 2);
_debugMessages = EditorPrefs.GetBool("debugMessages", true);
var str = EditorPrefs.GetString("asps");
var paths = str.Split(new [] {"|SF|"}, StringSplitOptions.RemoveEmptyEntries);
AutoSavePaths.AddRange(paths);
var tsps = EditorPrefs.GetString("tsps");
var times = tsps.Split(new [] {"|TS|"}, StringSplitOptions.RemoveEmptyEntries);
Timestamps.AddRange(times);
if (_autoSave)
{
_startingTimestamp = DateTime.Now;
EditorApplication.update += Update;
_editorAdded = true;
}
_prefsLoaded = true;
}
// GUI
EditorGUILayout.LabelField("Version 1.0.0 - 24th April 2018");
EditorGUILayout.LabelField("by Frederic Babord", EditorStyles.miniLabel);
GUILayout.Space(20);
// We need to know if a user has changed any settings
EditorGUI.BeginChangeCheck();
_autoSave = EditorGUILayout.Toggle("Enabled: ", _autoSave);
GUILayout.Space(20);
// Basic functional properties
EditorGUILayout.LabelField("Basic Configuration", EditorStyles.boldLabel);
_autoSaveDurationMins = EditorGUILayout.IntField("Minutes", _autoSaveDurationMins);
_autoSaveDurationSecs = EditorGUILayout.IntField("Secconds", _autoSaveDurationSecs);
GUILayout.Space(20);
// Advanced config properties
EditorGUILayout.LabelField("Advanced Configuration", EditorStyles.boldLabel);
// Timestamp format
_timeFormatIndex = EditorGUILayout.Popup("Timestamp Format", _timeFormatIndex, AutosaveTimeFormat.ToArray());
// Do some int capping in relation to time
while (_autoSaveDurationSecs >= 60)
{
_autoSaveDurationMins++;
_autoSaveDurationSecs -= 60;
}
if (_autoSaveDurationSecs < 0)
{
_autoSaveDurationSecs = 0;
}
// Max auto saves
EditorGUILayout.HelpBox("If Max Auto-Saves is 0, there will be no cap in autosaves!", MessageType.Info);
_maxAutosaves = EditorGUILayout.IntField("Max Auto-Saves", _maxAutosaves);
if (_maxAutosaves < 0)
_maxAutosaves = 0;
// Auto save assets of type
_autoSaveAssets = EditorGUILayout.Toggle("Save Assets To Disk", _autoSaveAssets);
_autoSaveScenes = EditorGUILayout.Toggle("Save The Current ACTIVE Scene", _autoSaveScenes);
// Should we log messages to the console?
_debugMessages = EditorGUILayout.Toggle("Debug To Console", _debugMessages);
GUILayout.Space(10);
// So dev clean up tools
GUILayout.Label("Cache Memory", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("Note: You need to clear the cache memory of this system if you delete and autosaved scene.", MessageType.Warning);
if (GUILayout.Button("Clear Cache Memory"))
{
Timestamps.Clear();
AutoSavePaths.Clear();
}
// If any of the properties have been changed, we should save them for future use if the editor is closed (or chashes!)
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool("BoolPreferenceKey", _autoSave);
EditorPrefs.SetInt("AutoSaveMins", _autoSaveDurationMins);
EditorPrefs.SetInt("AutoSaveSecs", _autoSaveDurationSecs);
EditorPrefs.SetBool("autoSaveAssets", _autoSaveAssets);
EditorPrefs.SetBool("autoSaveScenes", _autoSaveScenes);
EditorPrefs.SetInt("timeFormatIndex", _timeFormatIndex);
EditorPrefs.SetBool("debugMessages", _debugMessages);
// We can also decide if we should add or remove ourself from the Editors Update loop
if (_autoSave && !_editorAdded)
{
_startingTimestamp = DateTime.Now;
Update();
EditorApplication.update += Update;
_editorAdded = true;
}
else if (_editorAdded)
{
EditorApplication.update -= Update;
_editorAdded = false;
}
}
}
private static void Update()
{
// If the system is disabled and we have magically gotten into the update loop, don't continue.
// ABORT!!!!
if (!_autoSave) return;
// Get the current timestamp and work out the difference from either the previous saved timestamp or the starting timestamp.
var difference = DateTime.Now.Subtract(_startingTimestamp);
if (difference.Minutes > _autoSaveDurationMins)
{
if (difference.Seconds > _autoSaveDurationSecs)
{
// Reset the timestamp clock
_startingTimestamp = DateTime.Now;
if (_autoSaveScenes)
{
// Get the active scene. If its untitled, we have a new blank scene. Ignore it.
// The scene needs to be saved with an actual name first.
TriggeredFromAutoSave = true;
var scene = SceneManager.GetActiveScene();
if (!scene.name.ToLower().Contains("untitled"))
{
string autosavedpath = SceneManager.GetActiveScene().path;
// If we came from a previous auto-save, remove the previous autosave tag from the scene name
if (Timestamps.Count > 0)
{
var originalPath =
scene.path.Substring(0, scene.path.IndexOf("_AS_" + Timestamps.Last(), StringComparison.CurrentCulture));
originalPath += ".unity";
autosavedpath = originalPath;
}
// Add the current timestamp to the auto saved path
autosavedpath = autosavedpath.Insert(autosavedpath.IndexOf(".unity", StringComparison.CurrentCulture),
"_AS_" + _startingTimestamp.ToString(AutosaveTimeFormat[_timeFormatIndex]));
AutoSavePaths.Add(autosavedpath);
// If we have reached the max amount of autosaves and we dont want to have infinite,
// Delete the autosaved scene and emove their refs from cache
if (AutoSavePaths.Count > _maxAutosaves && _maxAutosaves > 0)
{
AssetDatabase.DeleteAsset(AutoSavePaths[0]);
AutoSavePaths.RemoveAt(0);
Timestamps.RemoveAt(0);
}
// Save the updated chache to the editors preferences
string asps = "";
foreach (var path in AutoSavePaths)
asps += path + "|SF|";
EditorPrefs.SetString("asps", asps);
if (!Timestamps.Contains(_startingTimestamp.ToString(AutosaveTimeFormat[_timeFormatIndex])))
Timestamps.Add(_startingTimestamp.ToString(AutosaveTimeFormat[_timeFormatIndex]));
string tsps = "";
foreach (var times in Timestamps)
tsps += times + "|TS|";
EditorPrefs.SetString("tsps", tsps);
// Save the scene as an autosaved scene
if (EditorSceneManager.SaveScene(scene, autosavedpath) && _debugMessages)
Debug.Log("AUTO SAVE :: The scene: " + SceneManager.GetActiveScene().name +
" has been saved at TimeStamp: " +
_startingTimestamp.ToLocalTime().ToString(AutosaveTimeFormat[_timeFormatIndex]) + " to: " +
autosavedpath);
}
}
// If we should save project assets
if (_autoSaveAssets)
{
TriggeredFromAutoSave = true;
// Save them
AssetDatabase.SaveAssets();
if (_debugMessages)
Debug.Log("AUTO SAVE :: Any unsaved changes to assets from the project have been saved at Timestamo: " +
_startingTimestamp.ToLocalTime().ToString(AutosaveTimeFormat[_timeFormatIndex]));
// If the current timestamp doesn't exist in the save system, add it
if (!Timestamps.Contains(_startingTimestamp.ToString(AutosaveTimeFormat[_timeFormatIndex])))
Timestamps.Add(_startingTimestamp.ToString(AutosaveTimeFormat[_timeFormatIndex]));
}
}
}
}
/// <summary>
/// Resaves the current autosaved scene without the autosave information in the scene name.
/// Then revers teh autosave system back to fresh.
/// </summary>
/// <param name="scene">The target scene it needs to save</param>
/// <param name="path">The original path to the autosaved scene</param>
public static async void ResaveScene(Scene scene, string path)
{
// Wait for the save to complete
await Task.Delay(TimeSpan.FromSeconds(3));
// Trim the path of the autosaved scene to remove autosaved information
path = path.Substring(0, path.IndexOf("_AS_", StringComparison.CurrentCulture)) + ".unity";
// Save the scene back to the original path and name
if(!EditorSceneManager.SaveScene(scene, path))
Debug.LogError("AUTO SAVE :: For some reason, we couldn't save the scene. Please try again later.");
// Delete all of the autosaved scenes
foreach (var t in AutoSavePaths)
AssetDatabase.DeleteAsset(t);
// Clear the cache of the auto saved data
AutoSavePaths.Clear();
Timestamps.Clear();
EditorPrefs.SetString("asps", "");
EditorPrefs.SetString("tsps", "");
}
}
// Hook into Unitys Asset Modifation Processor to determine if a scene is being saved.
public class AutoSavePreprocessor : UnityEditor.AssetModificationProcessor
{
public static string[] OnWillSaveAssets(string[] paths)
{
// If the save processor has not been triggered by the autosave system and has the last timestamp from the autosave system,
// then we should resave the current scene and clean up the autosave cache.
if (!AutoSaveEditor.TriggeredFromAutoSave && AutoSaveEditor.AutoSavePaths.Count > 0)
{
foreach (var p in paths)
{
if (p.Contains(AutoSaveEditor.Timestamps.Last()))
{
AutoSaveEditor.ResaveScene(SceneManager.GetActiveScene(), p);
}
}
}
AutoSaveEditor.TriggeredFromAutoSave = false;
return paths;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment