From d47c0a06d858c027a3dd88e4697dfa25da35ade7 Mon Sep 17 00:00:00 2001 From: quyong Date: Wed, 8 Apr 2026 12:29:33 +0800 Subject: [PATCH] feat: add option to remove from preloadedAssets when build player --- Packages/src/CHANGELOG.md | 6 ++ .../Editor/UISoftMaskProjectSettingsEditor.cs | 42 ++++++++- Packages/src/README.md | 7 ++ .../PreloadedProjectSettings.cs | 78 +++++++++++++++- .../Utilities/ShaderVariantRegistry.cs | 89 +++++++++++++++++-- .../MaskingShape/TerminalMaskingShape.cs | 3 +- .../src/Runtime/UISoftMaskProjectSettings.cs | 9 ++ .../src/Runtime/Utilities/SoftMaskUtils.cs | 3 +- 8 files changed, 226 insertions(+), 11 deletions(-) diff --git a/Packages/src/CHANGELOG.md b/Packages/src/CHANGELOG.md index ff6a9cc6..8edf0a2e 100644 --- a/Packages/src/CHANGELOG.md +++ b/Packages/src/CHANGELOG.md @@ -1,3 +1,9 @@ +## [Unreleased] + +### Features + +* add `Exclude From Preloaded Assets When Build Player` option: temporarily removes settings from `PlayerSettings.preloadedAssets` during Player Build only, enabling AssetBundle / Addressables hot-update workflows while keeping normal Editor behavior unchanged + # [3.5.0](https://github.com/mob-sakai/SoftMaskForUGUI/compare/3.4.1...3.5.0) (2025-11-08) diff --git a/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs b/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs index 9589ab45..e110502a 100644 --- a/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs +++ b/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs @@ -11,8 +11,20 @@ internal class UISoftMaskProjectSettingsEditor : Editor public override void OnInspectorGUI() { + serializedObject.Update(); EditorGUIUtility.labelWidth = 180; - base.OnInspectorGUI(); + + // Setting + EditorGUILayout.LabelField("Setting", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_SoftMaskEnabled")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_StereoEnabled")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_TransformSensitivity")); + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_SoftMaskable")); + + // Editor + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Editor", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_HideGeneratedComponents")); // Shader registry. EditorGUILayout.Space(); @@ -29,6 +41,34 @@ public override void OnInspectorGUI() } _shaderVariantRegistryEditor.Draw(); + + // Advanced + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Advanced", EditorStyles.boldLabel); + var excludeProp = serializedObject.FindProperty("m_ExcludeFromPreloadedAssetsWhenBuildPlayer"); + EditorGUILayout.PropertyField(excludeProp); + + if (excludeProp.boolValue) + { + var shadersProp = serializedObject.FindProperty("m_ShaderVariantRegistry") + .FindPropertyRelative("m_RegisteredShaders"); + EditorGUI.BeginDisabledGroup(true); + EditorGUI.indentLevel++; + for (var i = 0; i < shadersProp.arraySize; i++) + { + EditorGUILayout.PropertyField(shadersProp.GetArrayElementAtIndex(i), GUIContent.none); + } + EditorGUI.indentLevel--; + EditorGUI.EndDisabledGroup(); + + if (shadersProp.arraySize == 0) + { + EditorGUILayout.HelpBox( + "No shaders registered. Re-import or modify the settings asset to trigger sync.", + MessageType.Warning); + } + } + serializedObject.ApplyModifiedProperties(); // Upgrade All Assets For V3. diff --git a/Packages/src/README.md b/Packages/src/README.md index 3dcf259f..f172ac62 100644 --- a/Packages/src/README.md +++ b/Packages/src/README.md @@ -415,6 +415,13 @@ You can adjust the project-wide settings for SoftMaskForUGUI. (`Edit > Project S > - The setting file is usually saved in `Assets/ProjectSettings/UISoftMaskProjectSettings.asset`. Include this file in your version control system. > - The setting file is automatically added as a preloaded asset in `ProjectSettings/ProjectSettings.asset`. +#### Advanced + +- **Exclude From Preloaded Assets When Build Player**: When enabled, the settings asset will be **temporarily removed** from `PlayerSettings.preloadedAssets` **during Player Build only**, so it is NOT included in the built player. The asset remains in PreloadedAssets during normal Editor operation. + - Use this for AssetBundle / Addressables based hot-update workflows where settings and shaders are delivered externally instead of being built into the player. + - Shader references are serialized from the `ShaderVariantCollection` at edit-time, so `Shader.Find()` is not required at runtime. + - When toggled off, serialized shader references are automatically cleared. +

### Usage with Scripts diff --git a/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs b/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs index 1908b11c..31300070 100644 --- a/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs +++ b/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs @@ -14,6 +14,37 @@ namespace Coffee.UISoftMaskInternal public abstract class PreloadedProjectSettings : ScriptableObject #if UNITY_EDITOR { + [Tooltip("When enabled, this settings asset will be temporarily removed from " + + "PlayerSettings.preloadedAssets during Player Build, so it is NOT included " + + "in the built player. The asset remains in PreloadedAssets during normal " + + "Editor operation.\n\n" + + "Enable this when you deliver settings and shaders via AssetBundles or " + + "Addressables for hot-update support. Shaders are then resolved via direct " + + "serialized references instead of Shader.Find().")] + [SerializeField] + private bool m_ExcludeFromPreloadedAssetsWhenBuildPlayer; + + /// + /// When enabled, this settings asset will be temporarily removed from + /// only during Player Build, + /// so it is NOT included in the built player. The asset remains in + /// PreloadedAssets during normal Editor operation. + /// + /// Enable this when you deliver settings and shaders via AssetBundles or + /// Addressables for hot-update support. Shaders are resolved via direct + /// serialized references in the shader variant registry instead of + /// , since AB-loaded shaders are not registered + /// in the global shader name table. + /// + /// + public bool excludeFromPreloadedAssetsWhenBuildPlayer + { + get => m_ExcludeFromPreloadedAssetsWhenBuildPlayer; + set => m_ExcludeFromPreloadedAssetsWhenBuildPlayer = value; + } + + protected static bool s_BuildingPlayer; + private class Postprocessor : AssetPostprocessor { private static void OnPostprocessAllAssets(string[] _, string[] __, string[] ___, string[] ____) @@ -22,6 +53,47 @@ private static void OnPostprocessAllAssets(string[] _, string[] __, string[] ___ } } + private class ExcludeFromBuild : IPreprocessBuildWithReport, IPostprocessBuildWithReport + { + int IOrderedCallback.callbackOrder => -1; + + void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report) + { + s_BuildingPlayer = true; + + foreach (var t in TypeCache.GetTypesDerivedFrom(typeof(PreloadedProjectSettings<>))) + { + var settings = GetDefaultSettings(t); + if (!settings || !settings.m_ExcludeFromPreloadedAssetsWhenBuildPlayer) continue; + + PlayerSettings.SetPreloadedAssets( + PlayerSettings.GetPreloadedAssets() + .Where(x => x && x.GetType() != t) + .ToArray()); + + Debug.Log($"[PreloadedProjectSettings] Build started: removed '{settings.name}' " + + $"({t.Name}) from PreloadedAssets. " + + $"It will be restored after build completes."); + } + } + + void IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report) + { + s_BuildingPlayer = false; + + Initialize(); + + foreach (var t in TypeCache.GetTypesDerivedFrom(typeof(PreloadedProjectSettings<>))) + { + var settings = GetDefaultSettings(t); + if (!settings || !settings.m_ExcludeFromPreloadedAssetsWhenBuildPlayer) continue; + + Debug.Log($"[PreloadedProjectSettings] Build finished: restored '{settings.name}' " + + $"({t.Name}) to PreloadedAssets."); + } + } + } + private class PreprocessBuildWithReport : IPreprocessBuildWithReport { int IOrderedCallback.callbackOrder => 0; @@ -41,11 +113,11 @@ private static void Initialize() { // When create a new instance, automatically set it as default settings. defaultSettings = CreateInstance(t) as PreloadedProjectSettings; - SetDefaultSettings(defaultSettings); + if (!s_BuildingPlayer) SetDefaultSettings(defaultSettings); } else if (GetPreloadedSettings(t).Length != 1) { - SetDefaultSettings(defaultSettings); + if (!s_BuildingPlayer) SetDefaultSettings(defaultSettings); } if (defaultSettings) @@ -151,7 +223,7 @@ public static T instance return s_Instance; } - SetDefaultSettings(s_Instance); + if (!s_BuildingPlayer) SetDefaultSettings(s_Instance); return s_Instance; } } diff --git a/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs b/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs index 4a2d14e2..2bd79c2b 100644 --- a/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs +++ b/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs @@ -46,6 +46,7 @@ public override int GetHashCode() } private Dictionary _cachedOptionalShaders = new Dictionary(); + private Dictionary _shaderByName; [SerializeField] private List m_OptionalShaders = new List(); @@ -53,6 +54,9 @@ public override int GetHashCode() [SerializeField] internal ShaderVariantCollection m_Asset; + [SerializeField] + private List m_RegisteredShaders = new List(); + #if UNITY_EDITOR [SerializeField] private bool m_ErrorOnUnregisteredVariant = false; @@ -64,6 +68,43 @@ public override int GetHashCode() public ShaderVariantCollection shaderVariantCollection => m_Asset; public Func onShaderRequested; + /// + /// Build the runtime name-to-shader lookup from . + /// When shaders are delivered via AssetBundles (i.e. not in the player build), + /// cannot locate them by name. This lookup provides + /// direct references serialized at edit-time as a reliable alternative. + /// + public void InitializeShaderLookup() + { + var count = m_RegisteredShaders.Count; + if (count == 0) return; + + if (_shaderByName == null) + _shaderByName = new Dictionary(count); + else + _shaderByName.Clear(); + + for (var i = 0; i < count; i++) + { + var s = m_RegisteredShaders[i]; + if (s) + _shaderByName[s.name] = s; + } + } + + /// + /// Find a shader by name. Prefers the registered direct reference from + /// when available, falls back to + /// otherwise. This ensures correct behavior in both + /// built-in delivery (PreloadedAssets) and external delivery (AssetBundle) modes. + /// + public Shader FindShaderByName(string name) + { + if (_shaderByName != null && _shaderByName.TryGetValue(name, out var shader) && shader) + return shader; + return Shader.Find(name); + } + public Shader FindOptionalShader(Shader shader, string requiredName, string format, @@ -75,7 +116,7 @@ public Shader FindOptionalShader(Shader shader, var id = shader.GetInstanceID(); if (_cachedOptionalShaders.TryGetValue(id, out var optionalShaderName)) { - return Shader.Find(optionalShaderName); + return FindShaderByName(optionalShaderName); } // The shader has required name. @@ -91,7 +132,7 @@ public Shader FindOptionalShader(Shader shader, foreach (var pair in m_OptionalShaders) { if (pair.key != shaderName) continue; - optionalShader = Shader.Find(pair.value); + optionalShader = FindShaderByName(pair.value); if (optionalShader) { _cachedOptionalShaders[id] = pair.value; @@ -101,7 +142,7 @@ public Shader FindOptionalShader(Shader shader, // Find optional shader by format. optionalShaderName = string.Format(format, shaderName); - optionalShader = Shader.Find(optionalShaderName); + optionalShader = FindShaderByName(optionalShaderName); if (optionalShader) { _cachedOptionalShaders[id] = optionalShaderName; @@ -111,13 +152,13 @@ public Shader FindOptionalShader(Shader shader, #if UNITY_EDITOR if (onShaderRequested?.Invoke(optionalShaderName) ?? false) { - return Shader.Find(defaultOptionalShaderName); + return FindShaderByName(defaultOptionalShaderName); } #endif // Find default optional shader. _cachedOptionalShaders[id] = defaultOptionalShaderName; - return Shader.Find(defaultOptionalShaderName); + return FindShaderByName(defaultOptionalShaderName); } #if UNITY_EDITOR @@ -204,10 +245,48 @@ public void InitializeIfNeeded(Object owner) AssetDatabase.SaveAssets(); } + SyncRegisteredShaders(owner); ClearCache(); Profiler.EndSample(); } + /// + /// Sync based on the current delivery mode. + /// When is enabled, + /// extracts shader references from the SVC so they can be resolved at runtime + /// without . Otherwise clears the list to avoid + /// stale references and unnecessary asset dependencies. + /// + private void SyncRegisteredShaders(Object owner) + { + var settings = owner as PreloadedProjectSettings; + var needsDirectRef = settings && settings.excludeFromPreloadedAssetsWhenBuildPlayer; + + if (!needsDirectRef || !m_Asset) + { + if (m_RegisteredShaders.Count > 0) + { + m_RegisteredShaders.Clear(); + EditorUtility.SetDirty(owner); + } + return; + } + + var so = new SerializedObject(m_Asset); + var shaders = so.FindProperty("m_Shaders"); + m_RegisteredShaders.Clear(); + for (var i = 0; i < shaders.arraySize; i++) + { + var shaderRef = shaders.GetArrayElementAtIndex(i) + .FindPropertyRelative("first") + .objectReferenceValue as Shader; + if (shaderRef && !m_RegisteredShaders.Contains(shaderRef)) + m_RegisteredShaders.Add(shaderRef); + } + + EditorUtility.SetDirty(owner); + } + internal void RegisterVariant(Material material, string path) { if (!material || !material.shader || !m_Asset) return; diff --git a/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs b/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs index 8b934acc..289230c1 100644 --- a/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs +++ b/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs @@ -31,7 +31,8 @@ protected override void OnEnable() { if (!s_SharedTerminalMaterial) { - s_SharedTerminalMaterial = new Material(Shader.Find("Hidden/UI/TerminalMaskingShape")) + s_SharedTerminalMaterial = new Material(UISoftMaskProjectSettings.shaderRegistry + .FindShaderByName("Hidden/UI/TerminalMaskingShape")) { hideFlags = HideFlags.DontSave | HideFlags.NotEditable }; diff --git a/Packages/src/Runtime/UISoftMaskProjectSettings.cs b/Packages/src/Runtime/UISoftMaskProjectSettings.cs index fdcfeee6..ca522e4e 100644 --- a/Packages/src/Runtime/UISoftMaskProjectSettings.cs +++ b/Packages/src/Runtime/UISoftMaskProjectSettings.cs @@ -118,6 +118,14 @@ private static void ResetAllSoftMasks() InternalListPool.Return(ref softMasks); } +#if !UNITY_EDITOR + protected override void OnEnable() + { + base.OnEnable(); + m_ShaderVariantRegistry.InitializeShaderLookup(); + } +#endif + #if UNITY_EDITOR [InitializeOnLoadMethod] private static void InitializeOnLoadMethod() @@ -172,6 +180,7 @@ private static void InitializeOnLoadMethod() protected override void OnEnable() { base.OnEnable(); + m_ShaderVariantRegistry.InitializeShaderLookup(); m_ShaderVariantRegistry.onShaderRequested = ShaderSampleImporter.ImportShaderIfSelected; m_ShaderVariantRegistry.ClearCache(); } diff --git a/Packages/src/Runtime/Utilities/SoftMaskUtils.cs b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs index 8babd9b6..69f5f878 100644 --- a/Packages/src/Runtime/Utilities/SoftMaskUtils.cs +++ b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs @@ -152,7 +152,8 @@ private static Material GetSoftMaskingMaterial(ref Material mat, BlendOp op) { if (mat) return mat; - mat = new Material(Shader.Find("Hidden/UI/SoftMask")) + mat = new Material(UISoftMaskProjectSettings.shaderRegistry + .FindShaderByName("Hidden/UI/SoftMask")) { hideFlags = HideFlags.DontSave | HideFlags.NotEditable };