|
using System.Collections.Generic; |
|
using System.Linq; |
|
using UnityEngine; |
|
|
|
#if UNITY_EDITOR |
|
using UnityEditor; |
|
#endif |
|
|
|
// Author: JohannesMP (2018-08-12) |
|
// |
|
// A wrapper that provides the means to safely serialize Scene Asset References. |
|
// |
|
// Internally we serialize an Object to the SceneAsset which only exists at editor time. |
|
// Any time the object is serialized, we store the path provided by this Asset (assuming it was valid). |
|
// |
|
// This means that, come build time, the string path of the scene asset is always already stored, which if |
|
// the scene was added to the build settings means it can be loaded. |
|
// |
|
// It is up to the user to ensure the scene exists in the build settings so it is loadable at runtime. |
|
// To help with this, a custom PropertyDrawer displays the scene build settings state. |
|
// |
|
// Known issues: |
|
// - When reverting back to a prefab which has the asset stored as null, Unity will show the property |
|
// as modified despite having just reverted. This only happens the fist time, and reverting again |
|
// fixes it. Under the hood the state is still always valid, and serialized correctly regardless. |
|
|
|
|
|
/// <summary> |
|
/// A wrapper that provides the means to safely serialize Scene Asset References. |
|
/// </summary> |
|
[System.Serializable] |
|
public class SceneReference : ISerializationCallbackReceiver |
|
{ |
|
#if UNITY_EDITOR |
|
// What we use in editor to select the scene |
|
[SerializeField] private Object sceneAsset = null; |
|
bool IsValidSceneAsset |
|
{ |
|
get |
|
{ |
|
if (sceneAsset == null) |
|
return false; |
|
return sceneAsset.GetType().Equals(typeof(SceneAsset)); |
|
} |
|
} |
|
#endif |
|
|
|
// This should only ever be set during serialization/deserialization! |
|
[SerializeField] |
|
private string scenePath = string.Empty; |
|
|
|
// Use this when you want to actually have the scene path |
|
public string ScenePath |
|
{ |
|
get |
|
{ |
|
#if UNITY_EDITOR |
|
// In editor we always use the asset's path |
|
return GetScenePathFromAsset(); |
|
#else |
|
// At runtime we rely on the stored path value which we assume was serialized correctly at build time. |
|
// See OnBeforeSerialize and OnAfterDeserialize |
|
return scenePath; |
|
#endif |
|
} |
|
set |
|
{ |
|
scenePath = value; |
|
#if UNITY_EDITOR |
|
sceneAsset = GetSceneAssetFromPath(); |
|
#endif |
|
} |
|
} |
|
|
|
public static implicit operator string(SceneReference sceneReference) |
|
{ |
|
return sceneReference.ScenePath; |
|
} |
|
|
|
// Called to prepare this data for serialization. Stubbed out when not in editor. |
|
public void OnBeforeSerialize() |
|
{ |
|
#if UNITY_EDITOR |
|
HandleBeforeSerialize(); |
|
#endif |
|
} |
|
|
|
// Called to set up data for deserialization. Stubbed out when not in editor. |
|
public void OnAfterDeserialize() |
|
{ |
|
#if UNITY_EDITOR |
|
// We sadly cannot touch assetdatabase during serialization, so defer by a bit. |
|
EditorApplication.update += HandleAfterDeserialize; |
|
#endif |
|
} |
|
|
|
|
|
|
|
#if UNITY_EDITOR |
|
private SceneAsset GetSceneAssetFromPath() |
|
{ |
|
if (string.IsNullOrEmpty(scenePath)) |
|
return null; |
|
return AssetDatabase.LoadAssetAtPath<SceneAsset>(scenePath); |
|
} |
|
|
|
private string GetScenePathFromAsset() |
|
{ |
|
if (sceneAsset == null) |
|
return string.Empty; |
|
return AssetDatabase.GetAssetPath(sceneAsset); |
|
} |
|
|
|
private void HandleBeforeSerialize() |
|
{ |
|
// Asset is invalid but have Path to try and recover from |
|
if (IsValidSceneAsset == false && string.IsNullOrEmpty(scenePath) == false) |
|
{ |
|
sceneAsset = GetSceneAssetFromPath(); |
|
if (sceneAsset == null) |
|
scenePath = string.Empty; |
|
|
|
UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty(); |
|
} |
|
// Asset takes precendence and overwrites Path |
|
else |
|
{ |
|
scenePath = GetScenePathFromAsset(); |
|
} |
|
} |
|
|
|
private void HandleAfterDeserialize() |
|
{ |
|
EditorApplication.update -= HandleAfterDeserialize; |
|
// Asset is valid, don't do anything - Path will always be set based on it when it matters |
|
if (IsValidSceneAsset) |
|
return; |
|
|
|
// Asset is invalid but have path to try and recover from |
|
if (string.IsNullOrEmpty(scenePath) == false) |
|
{ |
|
sceneAsset = GetSceneAssetFromPath(); |
|
// No asset found, path was invalid. Make sure we don't carry over the old invalid path |
|
if (sceneAsset == null) |
|
scenePath = string.Empty; |
|
|
|
if (Application.isPlaying == false) |
|
UnityEditor.SceneManagement.EditorSceneManager.MarkAllScenesDirty(); |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
|
|
#if UNITY_EDITOR |
|
/// <summary> |
|
/// Display a Scene Reference object in the editor. |
|
/// If scene is valid, provides basic buttons to interact with the scene's role in Build Settings. |
|
/// </summary> |
|
[CustomPropertyDrawer(typeof(SceneReference))] |
|
public class SceneReferencePropertyDrawer : PropertyDrawer |
|
{ |
|
// The exact name of the asset Object variable in the SceneReference object |
|
const string sceneAssetPropertyString = "sceneAsset"; |
|
// The exact name of the scene Path variable in the SceneReference object |
|
const string scenePathPropertyString = "scenePath"; |
|
|
|
static readonly RectOffset boxPadding = EditorStyles.helpBox.padding; |
|
static readonly float padSize = 2f; |
|
static readonly float lineHeight = EditorGUIUtility.singleLineHeight; |
|
static readonly float paddedLine = lineHeight + padSize; |
|
static readonly float footerHeight = 10f; |
|
|
|
/// <summary> |
|
/// Drawing the 'SceneReference' property |
|
/// </summary> |
|
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) |
|
{ |
|
var sceneAssetProperty = GetSceneAssetProperty(property); |
|
|
|
// Draw the Box Background |
|
position.height -= footerHeight; |
|
GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, EditorStyles.helpBox); |
|
position = boxPadding.Remove(position); |
|
position.height = lineHeight; |
|
|
|
// Draw the main Object field |
|
label.tooltip = "The actual Scene Asset reference.\nOn serialize this is also stored as the asset's path."; |
|
|
|
EditorGUI.BeginProperty(position, GUIContent.none, property); |
|
EditorGUI.BeginChangeCheck(); |
|
int sceneControlID = GUIUtility.GetControlID(FocusType.Passive); |
|
var selectedObject = EditorGUI.ObjectField(position, label, sceneAssetProperty.objectReferenceValue, typeof(SceneAsset), false); |
|
BuildUtils.BuildScene buildScene = BuildUtils.GetBuildScene(selectedObject); |
|
|
|
if (EditorGUI.EndChangeCheck()) |
|
{ |
|
sceneAssetProperty.objectReferenceValue = selectedObject; |
|
|
|
// If no valid scene asset was selected, reset the stored path accordingly |
|
if (buildScene.scene == null) |
|
GetScenePathProperty(property).stringValue = string.Empty; |
|
} |
|
position.y += paddedLine; |
|
|
|
if (buildScene.assetGUID.Empty() == false) |
|
{ |
|
// Draw the Build Settings Info of the selected Scene |
|
DrawSceneInfoGUI(position, buildScene, sceneControlID + 1); |
|
} |
|
|
|
EditorGUI.EndProperty(); |
|
} |
|
|
|
/// <summary> |
|
/// Ensure that what we draw in OnGUI always has the room it needs |
|
/// </summary> |
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) |
|
{ |
|
int lines = 2; |
|
SerializedProperty sceneAssetProperty = GetSceneAssetProperty(property); |
|
if (sceneAssetProperty.objectReferenceValue == null) |
|
lines = 1; |
|
|
|
return boxPadding.vertical + lineHeight * lines + padSize * (lines - 1) + footerHeight; |
|
} |
|
|
|
/// <summary> |
|
/// Draws info box of the provided scene |
|
/// </summary> |
|
private void DrawSceneInfoGUI(Rect position, BuildUtils.BuildScene buildScene, int sceneControlID) |
|
{ |
|
bool readOnly = BuildUtils.IsReadOnly(); |
|
string readOnlyWarning = readOnly ? "\n\nWARNING: Build Settings is not checked out and so cannot be modified." : ""; |
|
|
|
// Label Prefix |
|
GUIContent iconContent = new GUIContent(); |
|
GUIContent labelContent = new GUIContent(); |
|
|
|
// Missing from build scenes |
|
if (buildScene.buildIndex == -1) |
|
{ |
|
iconContent = EditorGUIUtility.IconContent("d_winbtn_mac_close"); |
|
labelContent.text = "NOT In Build"; |
|
labelContent.tooltip = "This scene is NOT in build settings.\nIt will be NOT included in builds."; |
|
} |
|
// In build scenes and enabled |
|
else if (buildScene.scene.enabled) |
|
{ |
|
iconContent = EditorGUIUtility.IconContent("d_winbtn_mac_max"); |
|
labelContent.text = "BuildIndex: " + buildScene.buildIndex; |
|
labelContent.tooltip = "This scene is in build settings and ENABLED.\nIt will be included in builds." + readOnlyWarning; |
|
} |
|
// In build scenes and disabled |
|
else |
|
{ |
|
iconContent = EditorGUIUtility.IconContent("d_winbtn_mac_min"); |
|
labelContent.text = "BuildIndex: " + buildScene.buildIndex; |
|
labelContent.tooltip = "This scene is in build settings and DISABLED.\nIt will be NOT included in builds."; |
|
} |
|
|
|
// Left status label |
|
using (new EditorGUI.DisabledScope(readOnly)) |
|
{ |
|
Rect labelRect = DrawUtils.GetLabelRect(position); |
|
Rect iconRect = labelRect; |
|
iconRect.width = iconContent.image.width + padSize; |
|
labelRect.width -= iconRect.width; |
|
labelRect.x += iconRect.width; |
|
EditorGUI.PrefixLabel(iconRect, sceneControlID, iconContent); |
|
EditorGUI.PrefixLabel(labelRect, sceneControlID, labelContent); |
|
} |
|
|
|
// Right context buttons |
|
Rect buttonRect = DrawUtils.GetFieldRect(position); |
|
buttonRect.width = (buttonRect.width) / 3; |
|
|
|
string tooltipMsg = ""; |
|
using (new EditorGUI.DisabledScope(readOnly)) |
|
{ |
|
// NOT in build settings |
|
if (buildScene.buildIndex == -1) |
|
{ |
|
buttonRect.width *= 2; |
|
int addIndex = EditorBuildSettings.scenes.Length; |
|
tooltipMsg = "Add this scene to build settings. It will be appended to the end of the build scenes as buildIndex: " + addIndex + "." + readOnlyWarning; |
|
if (DrawUtils.ButtonHelper(buttonRect, "Add...", "Add (buildIndex " + addIndex + ")", EditorStyles.miniButtonLeft, tooltipMsg)) |
|
BuildUtils.AddBuildScene(buildScene); |
|
buttonRect.width /= 2; |
|
buttonRect.x += buttonRect.width; |
|
} |
|
// In build settings |
|
else |
|
{ |
|
bool isEnabled = buildScene.scene.enabled; |
|
string stateString = isEnabled ? "Disable" : "Enable"; |
|
tooltipMsg = stateString + " this scene in build settings.\n" + (isEnabled ? "It will no longer be included in builds" : "It will be included in builds") + "." + readOnlyWarning; |
|
|
|
if (DrawUtils.ButtonHelper(buttonRect, stateString, stateString + " In Build", EditorStyles.miniButtonLeft, tooltipMsg)) |
|
BuildUtils.SetBuildSceneState(buildScene, !isEnabled); |
|
buttonRect.x += buttonRect.width; |
|
|
|
tooltipMsg = "Completely remove this scene from build settings.\nYou will need to add it again for it to be included in builds!" + readOnlyWarning; |
|
if (DrawUtils.ButtonHelper(buttonRect, "Remove...", "Remove from Build", EditorStyles.miniButtonMid, tooltipMsg)) |
|
BuildUtils.RemoveBuildScene(buildScene); |
|
} |
|
} |
|
|
|
buttonRect.x += buttonRect.width; |
|
|
|
tooltipMsg = "Open the 'Build Settings' Window for managing scenes." + readOnlyWarning; |
|
if (DrawUtils.ButtonHelper(buttonRect, "Settings", "Build Settings", EditorStyles.miniButtonRight, tooltipMsg)) |
|
{ |
|
BuildUtils.OpenBuildSettings(); |
|
} |
|
|
|
} |
|
|
|
static SerializedProperty GetSceneAssetProperty(SerializedProperty property) |
|
{ |
|
return property.FindPropertyRelative(sceneAssetPropertyString); |
|
} |
|
|
|
static SerializedProperty GetScenePathProperty(SerializedProperty property) |
|
{ |
|
return property.FindPropertyRelative(scenePathPropertyString); |
|
} |
|
|
|
private static class DrawUtils |
|
{ |
|
/// <summary> |
|
/// Draw a GUI button, choosing between a short and a long button text based on if it fits |
|
/// </summary> |
|
static public bool ButtonHelper(Rect position, string msgShort, string msgLong, GUIStyle style, string tooltip = null) |
|
{ |
|
GUIContent content = new GUIContent(msgLong); |
|
content.tooltip = tooltip; |
|
|
|
float longWidth = style.CalcSize(content).x; |
|
if (longWidth > position.width) |
|
content.text = msgShort; |
|
|
|
return GUI.Button(position, content, style); |
|
} |
|
|
|
/// <summary> |
|
/// Given a position rect, get its field portion |
|
/// </summary> |
|
static public Rect GetFieldRect(Rect position) |
|
{ |
|
position.width -= EditorGUIUtility.labelWidth; |
|
position.x += EditorGUIUtility.labelWidth; |
|
return position; |
|
} |
|
/// <summary> |
|
/// Given a position rect, get its label portion |
|
/// </summary> |
|
static public Rect GetLabelRect(Rect position) |
|
{ |
|
position.width = EditorGUIUtility.labelWidth - padSize; |
|
return position; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Various BuildSettings interactions |
|
/// </summary> |
|
static private class BuildUtils |
|
{ |
|
// time in seconds that we have to wait before we query again when IsReadOnly() is called. |
|
public static float minCheckWait = 3; |
|
|
|
static float lastTimeChecked = 0; |
|
static bool cachedReadonlyVal = true; |
|
|
|
/// <summary> |
|
/// A small container for tracking scene data BuildSettings |
|
/// </summary> |
|
public struct BuildScene |
|
{ |
|
public int buildIndex; |
|
public GUID assetGUID; |
|
public string assetPath; |
|
public EditorBuildSettingsScene scene; |
|
} |
|
|
|
/// <summary> |
|
/// Check if the build settings asset is readonly. |
|
/// Caches value and only queries state a max of every 'minCheckWait' seconds. |
|
/// </summary> |
|
static public bool IsReadOnly() |
|
{ |
|
float curTime = Time.realtimeSinceStartup; |
|
float timeSinceLastCheck = curTime - lastTimeChecked; |
|
|
|
if (timeSinceLastCheck > minCheckWait) |
|
{ |
|
lastTimeChecked = curTime; |
|
cachedReadonlyVal = QueryBuildSettingsStatus(); |
|
} |
|
|
|
return cachedReadonlyVal; |
|
} |
|
|
|
/// <summary> |
|
/// A blocking call to the Version Control system to see if the build settings asset is readonly. |
|
/// Use BuildSettingsIsReadOnly for version that caches the value for better responsivenes. |
|
/// </summary> |
|
static private bool QueryBuildSettingsStatus() |
|
{ |
|
// If no version control provider, assume not readonly |
|
if (UnityEditor.VersionControl.Provider.enabled == false) |
|
return false; |
|
|
|
// If we cannot checkout, then assume we are not readonly |
|
if (UnityEditor.VersionControl.Provider.hasCheckoutSupport == false) |
|
return false; |
|
|
|
//// If offline (and are using a version control provider that requires checkout) we cannot edit. |
|
//if (UnityEditor.VersionControl.Provider.onlineState == UnityEditor.VersionControl.OnlineState.Offline) |
|
// return true; |
|
|
|
// Try to get status for file |
|
var status = UnityEditor.VersionControl.Provider.Status("ProjectSettings/EditorBuildSettings.asset", false); |
|
status.Wait(); |
|
|
|
// If no status listed we can edit |
|
if (status.assetList == null || status.assetList.Count != 1) |
|
return true; |
|
|
|
// If is checked out, we can edit |
|
if (status.assetList[0].IsState(UnityEditor.VersionControl.Asset.States.CheckedOutLocal)) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
/// <summary> |
|
/// For a given Scene Asset object reference, extract its build settings data, including buildIndex. |
|
/// </summary> |
|
static public BuildScene GetBuildScene(Object sceneObject) |
|
{ |
|
BuildScene entry = new BuildScene() |
|
{ |
|
buildIndex = -1, |
|
assetGUID = new GUID(string.Empty) |
|
}; |
|
|
|
if (sceneObject as SceneAsset == null) |
|
return entry; |
|
|
|
entry.assetPath = AssetDatabase.GetAssetPath(sceneObject); |
|
entry.assetGUID = new GUID(AssetDatabase.AssetPathToGUID(entry.assetPath)); |
|
|
|
for (int index = 0; index < EditorBuildSettings.scenes.Length; ++index) |
|
{ |
|
if (entry.assetGUID.Equals(EditorBuildSettings.scenes[index].guid)) |
|
{ |
|
entry.scene = EditorBuildSettings.scenes[index]; |
|
entry.buildIndex = index; |
|
return entry; |
|
} |
|
} |
|
|
|
return entry; |
|
} |
|
|
|
/// <summary> |
|
/// Enable/Disable a given scene in the buildSettings |
|
/// </summary> |
|
static public void SetBuildSceneState(BuildScene buildScene, bool enabled) |
|
{ |
|
bool modified = false; |
|
EditorBuildSettingsScene[] scenesToModify = EditorBuildSettings.scenes; |
|
foreach (var curScene in scenesToModify) |
|
{ |
|
if (curScene.guid.Equals(buildScene.assetGUID)) |
|
{ |
|
curScene.enabled = enabled; |
|
modified = true; |
|
break; |
|
} |
|
} |
|
if (modified) |
|
EditorBuildSettings.scenes = scenesToModify; |
|
} |
|
|
|
/// <summary> |
|
/// Display Dialog to add a scene to build settings |
|
/// </summary> |
|
static public void AddBuildScene(BuildScene buildScene, bool force = false, bool enabled = true) |
|
{ |
|
if (force == false) |
|
{ |
|
int selection = EditorUtility.DisplayDialogComplex( |
|
"Add Scene To Build", |
|
"You are about to add scene at " + buildScene.assetPath + " To the Build Settings.", |
|
"Add as Enabled", // option 0 |
|
"Add as Disabled", // option 1 |
|
"Cancel (do nothing)"); // option 2 |
|
|
|
switch (selection) |
|
{ |
|
case 0: // enabled |
|
enabled = true; |
|
break; |
|
case 1: // disabled |
|
enabled = false; |
|
break; |
|
default: |
|
case 2: // cancel |
|
return; |
|
} |
|
} |
|
|
|
EditorBuildSettingsScene newScene = new EditorBuildSettingsScene(buildScene.assetGUID, enabled); |
|
List<EditorBuildSettingsScene> tempScenes = EditorBuildSettings.scenes.ToList(); |
|
tempScenes.Add(newScene); |
|
EditorBuildSettings.scenes = tempScenes.ToArray(); |
|
} |
|
|
|
/// <summary> |
|
/// Display Dialog to remove a scene from build settings (or just disable it) |
|
/// </summary> |
|
static public void RemoveBuildScene(BuildScene buildScene, bool force = false) |
|
{ |
|
bool onlyDisable = false; |
|
if (force == false) |
|
{ |
|
int selection = -1; |
|
|
|
string title = "Remove Scene From Build"; |
|
string details = string.Format("You are about to remove the following scene from build settings:\n {0}\n buildIndex: {1}\n\n{2}", |
|
buildScene.assetPath, buildScene.buildIndex, |
|
"This will modify build settings, but the scene asset will remain untouched."); |
|
string confirm = "Remove From Build"; |
|
string alt = "Just Disable"; |
|
string cancel = "Cancel (do nothing)"; |
|
|
|
if (buildScene.scene.enabled) |
|
{ |
|
details += "\n\nIf you want, you can also just disable it instead."; |
|
selection = EditorUtility.DisplayDialogComplex(title, details, confirm, alt, cancel); |
|
} |
|
else |
|
{ |
|
selection = EditorUtility.DisplayDialog(title, details, confirm, cancel) ? 0 : 2; |
|
} |
|
|
|
switch (selection) |
|
{ |
|
case 0: // remove |
|
break; |
|
case 1: // disable |
|
onlyDisable = true; |
|
break; |
|
default: |
|
case 2: // cancel |
|
return; |
|
} |
|
} |
|
|
|
// User chose to not remove, only disable the scene |
|
if (onlyDisable) |
|
{ |
|
SetBuildSceneState(buildScene, false); |
|
} |
|
// User chose to fully remove the scene from build settings |
|
else |
|
{ |
|
List<EditorBuildSettingsScene> tempScenes = EditorBuildSettings.scenes.ToList(); |
|
tempScenes.RemoveAll(scene => scene.guid.Equals(buildScene.assetGUID)); |
|
EditorBuildSettings.scenes = tempScenes.ToArray(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Open the default Unity Build Settings window |
|
/// </summary> |
|
static public void OpenBuildSettings() |
|
{ |
|
EditorWindow.GetWindow(typeof(BuildPlayerWindow)); |
|
} |
|
} |
|
} |
|
|
|
#endif |