[Bootstrap] First go at Addressables bootsrapped objects

This commit is contained in:
Michal Pikulski
2025-09-07 12:36:35 +02:00
parent d3c6b838b4
commit d20004238d
65 changed files with 1748 additions and 3 deletions

View File

@@ -0,0 +1,216 @@
using Bootstrap;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Editor.Bootstrap
{
/// <summary>
/// Edit-mode utilities.
///
/// This class provides a handy mechanism to enable editor bootstrapping.
/// When enabled, the bootstrap system will initialise during edit-mode, allowing
/// developers to preview the effects of the bootstrap system.
///
/// There are several caveats here:
/// 1. DontDestroyOnLoad doesn't work in edit-mode. Any objects loaded by the bootstrap system
/// will be added to the current scene.
/// 2. Entering playmode will cause this script to de-initialise the bootstrapper, if initialised.
/// This means that playmode priority is always given to the boostrapper.
/// 3. If editor bootstrapping is enabled, then the bootstrapper will de-init and re-init
/// whenever the current scene changes. This may not be appropriate for all workflows!
/// 4. If bootstrapping is enabled, and the current scene is saved, then the bootstrapper will be
/// de-initialised prior to the scene being saved to disk, and then re-initialised, thereby
/// avoiding scene pollution.
/// </summary>
[InitializeOnLoad]
public static class CustomBootEditorUtils
{
/// <summary>
/// Editor prefs key for the edit-mode bootstrapper.
/// </summary>
private const string INITIALISE_IN_EDITOR = "bootstrap.editor_init_enabled";
/// <summary>
/// Menu path for the edit-mode bootstrapper
/// </summary>
private const string EDITOR_INIT_MENU = "Bootstrap/Editor Initialise";
static CustomBootEditorUtils()
{
InitPlayModeListener();
if (Application.isPlaying) return;
//Don't initialise if we're about to change playmode!
if (EditorInitialisationEnabled && !EditorApplication.isPlayingOrWillChangePlaymode)
{
DoInit();
}
}
/// <summary>
/// Initialise the playmode change listener.
/// This also sets up a listener for scene saving, so that we can
/// de-init and reinit the bootstrapper when the user saves changes, thereby
/// avoiding bootstrapped objects polluting the saved scene.
/// </summary>
private static void InitPlayModeListener()
{
EditorApplication.playModeStateChanged += EditorApplicationOnplayModeStateChanged;
EditorSceneManager.sceneSaving += OnSceneSaving;
}
/// <summary>
/// Handle the <see cref="EditorSceneManager.sceneSaving"/> event.
/// This de-initialises the bootstrapper, removing objects from the scene,
/// and then listens for the <see cref="EditorSceneManager.sceneSaved"/> event
/// in order to re-initialise the bootstrapper.
/// </summary>
/// <param name="scene"></param>
/// <param name="path"></param>
private static void OnSceneSaving(Scene scene, string path)
{
if (CustomBoot.Initialised)
{
DoDeInit();
EditorSceneManager.sceneSaved += EditorSceneManagerOnsceneSaved;
void EditorSceneManagerOnsceneSaved(Scene scene1)
{
EditorSceneManager.sceneSaved -= EditorSceneManagerOnsceneSaved;
CheckInit();
}
}
}
/// <summary>
/// Deinitialise the playmode change listener
/// </summary>
private static void DeInitPlayModeListener()
{
EditorApplication.playModeStateChanged -= EditorApplicationOnplayModeStateChanged;
}
/// <summary>
/// Is play-mode bootstrapping enabled?
/// </summary>
private static bool EditorInitialisationEnabled
{
get => EditorPrefs.GetBool(INITIALISE_IN_EDITOR, false);
set => EditorPrefs.SetBool(INITIALISE_IN_EDITOR, value);
}
/// <summary>
/// Menu handler for the edit-mode bootstrapper
/// </summary>
[MenuItem(EDITOR_INIT_MENU)]
private static void EditorInitialise()
{
EditorInitialisationEnabled = !EditorInitialisationEnabled;
CheckInit();
}
/// <summary>
/// Menu validator for the edit-mode bootstrapper
/// </summary>
/// <returns></returns>
[MenuItem(EDITOR_INIT_MENU, true)]
private static bool EditorInitialiseValidate()
{
Menu.SetChecked(EDITOR_INIT_MENU, EditorPrefs.GetBool(INITIALISE_IN_EDITOR, false));
return true;
}
/// <summary>
/// Perform initialisation or de-initialisation depending on the current context
/// </summary>
private static void CheckInit()
{
if (EditorInitialisationEnabled && !CustomBoot.Initialised && !Application.isPlaying)
{
DoInit();
}
else if (CustomBoot.Initialised)
{
DoDeInit();
}
}
/// <summary>
/// Perform the initialisation process.
/// This adds a listener for the <see cref="EditorSceneManager.sceneClosing"/> event so that we
/// can handle scene changes once we're initialised
/// </summary>
private static void DoInit()
{
EditorSceneManager.sceneClosing += OnSceneClosing;
CustomBoot.PerformInitialisation();
}
/// <summary>
/// Handle the <see cref="EditorSceneManager.sceneClosing"/> event
/// This de-initialises the bootstrapper and then sets up a listener for the
/// <see cref="EditorSceneManager.activeSceneChangedInEditMode"/> event so that
/// we can safely re-initialise the bootstrapper
/// </summary>
/// <param name="scene"></param>
/// <param name="removingscene"></param>
private static void OnSceneClosing(Scene scene, bool removingscene)
{
if (!Application.isPlaying && CustomBoot.Initialised)
{
DoDeInit();
EditorSceneManager.activeSceneChangedInEditMode += OnSceneLoaded;
}
}
/// <summary>
/// Handles the <see cref="EditorSceneManager.activeSceneChangedInEditMode"/> event,
/// allowing the boostrapper to reinitialise if required
/// </summary>
/// <param name="arg0"></param>
/// <param name="scene"></param>
private static void OnSceneLoaded(Scene arg0, Scene scene)
{
if (!Application.isPlaying)
{
EditorSceneManager.activeSceneChangedInEditMode -= OnSceneLoaded;
CheckInit();
}
}
/// <summary>
/// De-Initialises the bootstrapper
/// </summary>
private static void DoDeInit()
{
CustomBoot.PerformDeInitialisation();
EditorSceneManager.sceneClosing -= OnSceneClosing;
}
/// <summary>
/// Handles playmode change events. This will de-initialise the bootstrapper
/// when exiting edit-mode, and call <see cref="CheckInit"/> when entering edit-mode
/// </summary>
/// <param name="obj"></param>
private static void EditorApplicationOnplayModeStateChanged(PlayModeStateChange obj)
{
switch (obj)
{
case PlayModeStateChange.ExitingEditMode:
if (CustomBoot.Initialised)
{
DoDeInit();
}
break;
case PlayModeStateChange.EnteredEditMode:
CheckInit();
break;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f2e29f8987f24903ac5e99d8c1b4f003
timeCreated: 1731265444

View File

@@ -0,0 +1,21 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Editor.Bootstrap
{
/// <summary>
/// A scriptable object used to store references to CustomBoot settings for both Runtime and Editor.
/// </summary>
public class CustomBootProjectSettings : ScriptableObject
{
/// <summary>
/// The Addressables reference for the runtime settings
/// </summary>
public AssetReference RuntimeSettings;
/// <summary>
/// The Addressables reference for the editor settings
/// </summary>
public AssetReference EditorSettings;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 96f303d5dfe74b3ea131e7c7de54901a
timeCreated: 1724178591

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Bootstrap;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UIElements;
namespace Editor.Bootstrap
{
/// <summary>
/// Settings provider for the custom boot behaviour
/// </summary>
public class CustomBootSettingsProvider : SettingsProvider
{
/// <summary>
/// Internal reference to the serialized boot settings object
/// </summary>
private SerializedObject customBootSettings;
private CustomBootSettingsProvider(string path, SettingsScope scopes = SettingsScope.Project, IEnumerable<string> keywords = null) : base(path, scopes, keywords)
{
}
/// <summary>
/// Initialise the UI for the settings provider
/// </summary>
/// <param name="searchContext"></param>
/// <param name="rootElement"></param>
public override void OnActivate(string searchContext, VisualElement rootElement)
{
customBootSettings = CustomBootSettingsUtil.GetSerializedSettings();
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/StyleSheets/CustomBootStyles.uss");
rootElement.styleSheets.Add(styleSheet);
rootElement.AddToClassList("settings");
var title = new Label()
{
text = "Custom Boot"
};
title.AddToClassList("title");
rootElement.Add(title);
var properties = new VisualElement()
{
style =
{
flexDirection = FlexDirection.Column
}
};
properties.AddToClassList("property-list");
var runtimeProp = customBootSettings.FindProperty(nameof(CustomBootProjectSettings.RuntimeSettings));
var editorProp = customBootSettings.FindProperty(nameof(CustomBootProjectSettings.EditorSettings));
properties.Add(CreateBootSettingsEditor(runtimeProp));
properties.Add(CreateBootSettingsEditor(editorProp));
rootElement.Add(properties);
rootElement.Bind(customBootSettings);
}
/// <summary>
/// Draw an editor for the CustomBootSettings object associated with the given property
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
private static VisualElement CreateBootSettingsEditor(SerializedProperty property)
{
var bootSettingsObject = new SerializedObject(AssetDatabase.LoadAssetAtPath<CustomBootSettings>(AssetDatabase.GUIDToAssetPath((property.boxedValue as AssetReference).AssetGUID)));
var propertyEditorContainer = new VisualElement();
DrawObject(propertyEditorContainer, bootSettingsObject);
return propertyEditorContainer;
}
/// <summary>
/// Generic SerializedObject property editor
/// </summary>
/// <param name="container"></param>
/// <param name="o"></param>
private static void DrawObject(VisualElement container, SerializedObject o)
{
var l = new Label(o.targetObject.name);
container.Add(l);
var f = GetVisibleSerializedFields(o.targetObject.GetType());
foreach (var field in f)
{
var prop = o.FindProperty(field.Name);
var pField = new PropertyField(prop);
container.Add(pField);
}
container.Bind(o);
}
/// <summary>
/// Retrieve all accessible serialised fields for the given type
/// </summary>
/// <param name="T"></param>
/// <returns></returns>
private static FieldInfo[] GetVisibleSerializedFields(Type T)
{
var publicFields = T.GetFields(BindingFlags.Instance | BindingFlags.Public);
var infoFields = publicFields.Where(t => t.GetCustomAttribute<HideInInspector>() == null).ToList();
var privateFields = T.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
infoFields.AddRange(privateFields.Where(t => t.GetCustomAttribute<SerializeField>() != null));
return infoFields.ToArray();
}
/// <summary>
/// Create the Settings Provider. Internally, this will ensure the settings object is created.
/// </summary>
/// <returns></returns>
[SettingsProvider]
public static SettingsProvider CreateCustomBootSettingsProvider()
{
if (!CustomBootSettingsUtil.IsSettingsAvailable())
{
CustomBootSettingsUtil.GetOrCreateSettings();
}
var provider = new CustomBootSettingsProvider("Project/Custom Boot", SettingsScope.Project);
return provider;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 41cc0ad6a6e04eb4b9ebe5107f00f8b3
timeCreated: 1724007531

View File

@@ -0,0 +1,218 @@
using System.IO;
using System.Linq;
using Bootstrap;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Editor.Bootstrap
{
/// <summary>
/// Helper methods for CustomBoot configuration
/// </summary>
public class CustomBootSettingsUtil
{
/// <summary>
/// Path to the ProjectSettings file
/// </summary>
private const string PROJECT_SETTINGS_PATH = "ProjectSettings/CustomBoot.asset";
/// <summary>
/// Path to the runtime custom boot settings file
/// </summary>
private const string RUNTIME_CUSTOM_BOOT_SETTINGS_PATH =
"Assets/Data/Bootstrap/Runtime/CustomBootSettings_Runtime.asset";
/// <summary>
/// Path to the editor custom boot settings file
/// </summary>
private const string EDITOR_CUSTOM_BOOT_SETTINGS_PATH =
"Assets/Data/Bootstrap/Editor/CustomBootSettings_Editor.asset";
/// <summary>
/// Determine whether the settings asset file is available
/// </summary>
/// <returns></returns>
internal static bool IsSettingsAvailable()
{
return File.Exists(PROJECT_SETTINGS_PATH);
}
/// <summary>
/// Retrieve the settings object if it exists, otherwise create and return it.
/// </summary>
/// <returns></returns>
internal static CustomBootProjectSettings GetOrCreateSettings()
{
CustomBootProjectSettings projectSettings;
//Check whether the settings file already exists
if (IsSettingsAvailable())
{
//If it exists, load it
projectSettings = InternalEditorUtility.LoadSerializedFileAndForget(PROJECT_SETTINGS_PATH).First() as
CustomBootProjectSettings;
}
else
{
//If it doesn't exist, create a new ScriptableObject
projectSettings = ScriptableObject.CreateInstance<CustomBootProjectSettings>();
//Configure the settings file
CreateBootSettingsAssets(out var runtimeEntry, out var editorEntry);
projectSettings.RuntimeSettings = new AssetReference(runtimeEntry.guid);
projectSettings.EditorSettings = new AssetReference(editorEntry.guid);
//And save it!
InternalEditorUtility.SaveToSerializedFileAndForget(new Object[] { projectSettings },
PROJECT_SETTINGS_PATH, true);
}
//Finally, return our settings object
return projectSettings;
}
/// <summary>
/// Create the Runtime and Editor CustomBootSettings assets.
/// </summary>
/// <param name="runtimeEntry"></param>
/// <param name="editorEntry"></param>
private static void CreateBootSettingsAssets(out AddressableAssetEntry runtimeEntry,
out AddressableAssetEntry editorEntry)
{
//Create two assets representing our boot configurations
var runtimeSettings =
GetOrCreateBootSettingsAsset(RUNTIME_CUSTOM_BOOT_SETTINGS_PATH, out var runtimeCreated);
var editorSettings = GetOrCreateBootSettingsAsset(EDITOR_CUSTOM_BOOT_SETTINGS_PATH, out var editorCreated);
//Save the AssetDatabase state if either asset is new
if (runtimeCreated || editorCreated)
{
AssetDatabase.SaveAssets();
}
//Configure the Addressables system with the new assets.
AddSettingsToAddressables(runtimeSettings, editorSettings, out runtimeEntry, out editorEntry);
}
/// <summary>
/// Load, or create, a CustomBootSettings asset at the given path
/// </summary>
/// <param name="path"></param>
/// <param name="wasCreated"></param>
/// <returns></returns>
private static CustomBootSettings GetOrCreateBootSettingsAsset(string path, out bool wasCreated)
{
var settings = AssetDatabase.LoadAssetAtPath<CustomBootSettings>(path);
if (!settings)
{
//Make sure full path is created
var dirPath = Path.GetDirectoryName(path);
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
}
settings = ScriptableObject.CreateInstance<CustomBootSettings>();
AssetDatabase.CreateAsset(settings, path);
wasCreated = true;
}
else
{
wasCreated = false;
}
return settings;
}
/// <summary>
/// Add the CustomBootSettings asset to the relevant Addressables groups.
/// </summary>
/// <param name="runtimeSettings"></param>
/// <param name="editorSettings"></param>
/// <param name="runtimeEntry"></param>
/// <param name="editorEntry"></param>
private static void AddSettingsToAddressables(CustomBootSettings runtimeSettings,
CustomBootSettings editorSettings, out AddressableAssetEntry runtimeEntry,
out AddressableAssetEntry editorEntry)
{
InitialiseAddressableGroups(out var runtimeGroup, out var editorGroup);
runtimeEntry =
CreateCustomBootSettingsEntry(runtimeSettings, runtimeGroup, $"{nameof(CustomBootSettings)}_Runtime");
editorEntry =
CreateCustomBootSettingsEntry(editorSettings, editorGroup, $"{nameof(CustomBootSettings)}_Editor");
AssetDatabase.SaveAssets();
}
/// <summary>
/// Create an Addressables entry for the given CustomBootSettings object, and add it to the given group.
/// </summary>
/// <param name="bootSettings"></param>
/// <param name="group"></param>
/// <param name="key"></param>
/// <returns></returns>
private static AddressableAssetEntry CreateCustomBootSettingsEntry(CustomBootSettings bootSettings,
AddressableAssetGroup group, string key)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
var entry = settings.CreateOrMoveEntry(
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(bootSettings)),
group);
entry.address = key;
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true);
return entry;
}
/// <summary>
/// Ensure the Runtime and Editor Addressables groups exist
/// </summary>
/// <param name="runtimeGroup"></param>
/// <param name="editorGroup"></param>
private static void InitialiseAddressableGroups(out AddressableAssetGroup runtimeGroup,
out AddressableAssetGroup editorGroup)
{
runtimeGroup = GetOrCreateGroup($"{nameof(CustomBoot)}_Runtime", true);
editorGroup = GetOrCreateGroup($"{nameof(CustomBoot)}_Editor", false);
}
/// <summary>
/// Retrieve or create an Addressables group.
/// </summary>
/// <param name="name"></param>
/// <param name="includeInBuild"></param>
/// <returns></returns>
private static AddressableAssetGroup GetOrCreateGroup(string name, bool includeInBuild)
{
// Use GetSettings(true) to ensure the settings asset is created if missing
var settings = AddressableAssetSettingsDefaultObject.GetSettings(true);
if (settings == null)
{
Debug.LogError("AddressableAssetSettings could not be found or created. Please ensure Addressables are set up in your project.");
return null;
}
var group = settings.FindGroup(name);
if (group == null)
{
group = settings.CreateGroup(name, false, false, true, settings.DefaultGroup.Schemas);
group.GetSchema<BundledAssetGroupSchema>().IncludeInBuild = includeInBuild;
}
return group;
}
/// <summary>
/// Retrieve the serialised representation of the settings object
/// </summary>
/// <returns></returns>
internal static SerializedObject GetSerializedSettings()
{
return new SerializedObject(GetOrCreateSettings());
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 05fc3698d2694614882942c0c41db065
timeCreated: 1724010512