Last active
April 25, 2018 19:22
-
-
Save freddiebabord/83f5fe55f463dc2bdedc4546c59d5148 to your computer and use it in GitHub Desktop.
Auto-Save Modified Scenes and Assets with sudo version history in Unity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// =============================== | |
// 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