diff --git a/CHANGELOG.md b/CHANGELOG.md index a5437c8a7..957524e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -373,6 +373,7 @@ - Added new variants of `GetRepackedSkin` and `GetRepackedAttachments` supporting blend modes. These new variants take a packing configuration input struct `RepackAttachmentsSettings` which provides optional `additiveMaterialSource`, `multiplyMaterialSource` and `screenMaterialSource` properties, enabling blend mode repacking when any is non-null. Create your `RepackAttachmentsSettings` from default settings via `RepackAttachmentsSettings.Default` and then customize settings as needed. Blend mode materials can be set at once using `UseSourceMaterialsFrom(SkeletonDataAsset)` or `UseBlendModeMaterialsFrom(SkeletonDataAsset)`. Uses new `RepackAttachmentsOutput` struct providing `DestroyGeneratedAssets` to easily destroy any previously generated assets. - Updated example scenes to demonstrate new `GetRepackedSkin` variant usage. - AnimationReferenceAsset: Added animation selector drop-down popup next to object assignment field for easy initial assignment and animation switching. Shows red warning color if the assigned AnimationReferenceAsset's SkeletonDataAsset does not match the one at the GameObjects `SkeletonAnimation` component. A mismatch may be intentional for a special split SkeletonDataAsset setup. + - AnimationReferenceAssets: The `SkeletonDataAsset` Inspector now allows to generate the set of AnimationReferenceAssets as nested assets below a single asset `_AnimationReferences.asset` to avoid cluttering the project with hundreds of asset files. This also makes searching the project for a suitable `AnimationReferenceAsset` of a given `SkeletonDataAsset` much faster. There are now two buttons available in the `SkeletonDataAsset` Inspector: `Create Nested` to generate assets in the new nested way, and `Create Individual`, creating individual asset files as before. - **Deprecated** diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/AnimationReferenceAssetDrawer.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/AnimationReferenceAssetDrawer.cs index 61bb76eca..2af007b8d 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/AnimationReferenceAssetDrawer.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/AnimationReferenceAssetDrawer.cs @@ -171,18 +171,38 @@ namespace Spine.Unity.Editor { string skeletonDataAssetPath = AssetDatabase.GetAssetPath(targetSkeletonDataAsset); string parentFolder = Path.GetDirectoryName(skeletonDataAssetPath); + + // Search AnimationReferenceAssetContainer sub-assets + string skeletonDataAssetName = Path.GetFileNameWithoutExtension(skeletonDataAssetPath); + string baseName = skeletonDataAssetName.Replace(AssetUtility.SkeletonDataSuffix, ""); + string containerPath = string.Format("{0}/{1}{2}.asset", parentFolder, baseName, + SpineEditorUtilities.AnimationReferenceContainerSuffix); + AnimationReferenceAsset foundAsset = FindAnimationReferenceInSubAssets(containerPath, targetSkeletonDataAsset, targetAnimationName); + if (foundAsset != null) return foundAsset; + + // Search standalone files in same asset directory string dataPath = parentFolder + "/" + ReferenceAssetsFolderName; string safeName = AssetUtility.GetPathSafeName(targetAnimationName); string assetPath = string.Format("{0}/{1}.asset", dataPath, safeName); - AnimationReferenceAsset existingAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (existingAsset != null) return existingAsset; - // Search the project for matching AnimationReferenceAsset + // Global fallback: search the project for matching AnimationReferenceAsset including sub-assets string[] guids = AssetDatabase.FindAssets("t:AnimationReferenceAsset"); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); - AnimationReferenceAsset asset = AssetDatabase.LoadAssetAtPath(path); + foundAsset = FindAnimationReferenceInSubAssets(path, targetSkeletonDataAsset, targetAnimationName); + if (foundAsset != null) return foundAsset; + } + return null; + } + + static AnimationReferenceAsset FindAnimationReferenceInSubAssets (string assetPath, + SkeletonDataAsset targetSkeletonDataAsset, string targetAnimationName) { + + UnityEngine.Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath); + foreach (UnityEngine.Object obj in allAssets) { + AnimationReferenceAsset asset = obj as AnimationReferenceAsset; if (asset == null) continue; if (asset.SkeletonDataAsset == targetSkeletonDataAsset && asset.AnimationName == targetAnimationName) return asset; diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SkeletonDataAssetInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SkeletonDataAssetInspector.cs index b6da180f1..2de2b629f 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SkeletonDataAssetInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Asset Types/SkeletonDataAssetInspector.cs @@ -220,10 +220,31 @@ namespace Spine.Unity.Editor { EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); DrawAnimationList(); if (targetSkeletonData.Animations.Count > 0) { - const string AnimationReferenceButtonText = "Create Animation Reference Assets"; - const string AnimationReferenceTooltipText = "AnimationReferenceAsset acts as Unity asset for a reference to a Spine.Animation. This can be used in inspectors.\n\nIt serializes a reference to a SkeletonData asset and an animationName.\n\nAt runtime, a reference to its Spine.Animation is loaded and cached into the object to be used as needed. This skips the need to find and cache animation references in individual MonoBehaviours."; - if (GUILayout.Button(SpineInspectorUtility.TempContent(AnimationReferenceButtonText, Icons.animationRoot, AnimationReferenceTooltipText), GUILayout.Width(250), GUILayout.Height(26))) { - CreateAnimationReferenceAssets(); + const string AnimationReferenceTooltipText = + "AnimationReferenceAsset acts as Unity asset for a reference to a Spine.Animation. This can " + + "be used in inspectors." + + "\n\n" + + "It serializes a reference to a SkeletonData asset and an animationName." + + "\n\n" + + "At runtime, a reference to its Spine.Animation is loaded and cached into the object to be " + + "used as needed. This skips the need to find and cache animation references in individual " + + "MonoBehaviours."; + EditorGUILayout.Space(); + EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Create Animation Reference Assets", + Icons.animationRoot, AnimationReferenceTooltipText)); + using (new GUILayout.HorizontalScope()) { + if (GUILayout.Button(SpineInspectorUtility.TempContent("Create Individual", + EditorGUIUtility.IconContent("Folder Icon").image as Texture2D, + "Legacy method, no longer recommended. Creates individual .asset files in a " + + "ReferenceAssets subfolder."), GUILayout.Height(20), GUILayout.MaxWidth(130))) { + CreateAnimationReferenceAssets(); + } + if (GUILayout.Button(SpineInspectorUtility.TempContent("Create Nested", + EditorGUIUtility.IconContent("AnimatorController Icon").image as Texture2D, + "Recommended. Creates AnimationReferenceAssets as sub-assets nested under a single " + + "container asset."), GUILayout.Height(20), GUILayout.MaxWidth(130))) { + CreateAnimationReferenceAssetsNested(); + } } } EditorGUILayout.Space(); @@ -258,15 +279,13 @@ namespace Spine.Unity.Editor { AssetDatabase.CreateFolder(parentFolder, AssetFolderName); } - FieldInfo nameField = typeof(AnimationReferenceAsset).GetField("animationName", BindingFlags.NonPublic | BindingFlags.Instance); - FieldInfo skeletonDataAssetField = typeof(AnimationReferenceAsset).GetField("skeletonDataAsset", BindingFlags.NonPublic | BindingFlags.Instance); foreach (Animation animation in targetSkeletonData.Animations) { string assetPath = string.Format("{0}/{1}.asset", dataPath, AssetUtility.GetPathSafeName(animation.Name)); AnimationReferenceAsset existingAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (existingAsset == null) { AnimationReferenceAsset newAsset = ScriptableObject.CreateInstance(); - skeletonDataAssetField.SetValue(newAsset, targetSkeletonDataAsset); - nameField.SetValue(newAsset, animation.Name); + newAsset.SkeletonDataAsset = targetSkeletonDataAsset; + newAsset.AnimationName = animation.Name; AssetDatabase.CreateAsset(newAsset, assetPath); } } @@ -278,6 +297,50 @@ namespace Spine.Unity.Editor { } } + void CreateAnimationReferenceAssetsNested () { + string skeletonDataAssetPath = AssetDatabase.GetAssetPath(targetSkeletonDataAsset); + string parentFolder = System.IO.Path.GetDirectoryName(skeletonDataAssetPath); + string skeletonDataAssetName = System.IO.Path.GetFileNameWithoutExtension(skeletonDataAssetPath); + string baseName = skeletonDataAssetName.Replace(AssetUtility.SkeletonDataSuffix, ""); + string containerPath = string.Format("{0}/{1}{2}.asset", parentFolder, baseName, + SpineEditorUtilities.AnimationReferenceContainerSuffix); + + AnimationReferenceAssetContainer container = AssetDatabase.LoadAssetAtPath(containerPath); + if (container == null) { + container = ScriptableObject.CreateInstance(); + container.SkeletonDataAsset = targetSkeletonDataAsset; + AssetDatabase.CreateAsset(container, containerPath); + } else { + container.SkeletonDataAsset = targetSkeletonDataAsset; + EditorUtility.SetDirty(container); + } + + // Collect existing sub-assets to avoid duplicates + UnityEngine.Object[] existingSubAssets = AssetDatabase.LoadAllAssetsAtPath(containerPath); + HashSet existingAnimationNames = new HashSet(); + foreach (UnityEngine.Object subAsset in existingSubAssets) { + AnimationReferenceAsset existingRef = subAsset as AnimationReferenceAsset; + if (existingRef != null) + existingAnimationNames.Add(existingRef.AnimationName); + } + + foreach (Animation animation in targetSkeletonData.Animations) { + if (existingAnimationNames.Contains(animation.Name)) + continue; + + AnimationReferenceAsset newAsset = ScriptableObject.CreateInstance(); + newAsset.name = AssetUtility.GetPathSafeName(animation.Name); + newAsset.SkeletonDataAsset = targetSkeletonDataAsset; + newAsset.AnimationName = animation.Name; + AssetDatabase.AddObjectToAsset(newAsset, container); + } + + AssetDatabase.SaveAssets(); + AssetDatabase.ImportAsset(containerPath); + Selection.activeObject = container; + EditorGUIUtility.PingObject(container); + } + void OnInspectorGUIMulti () { // Skeleton data file field. diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/DataReloadHandler.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/DataReloadHandler.cs index ff5627924..c2ce43105 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/DataReloadHandler.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/DataReloadHandler.cs @@ -152,9 +152,11 @@ namespace Spine.Unity.Editor { string[] guids = UnityEditor.AssetDatabase.FindAssets("t:AnimationReferenceAsset"); foreach (string guid in guids) { string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); - if (!string.IsNullOrEmpty(path)) { - AnimationReferenceAsset referenceAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); - if (referenceAsset.SkeletonDataAsset == skeletonDataAsset) + if (string.IsNullOrEmpty(path)) continue; + UnityEngine.Object[] allAssets = UnityEditor.AssetDatabase.LoadAllAssetsAtPath(path); + foreach (UnityEngine.Object obj in allAssets) { + AnimationReferenceAsset referenceAsset = obj as AnimationReferenceAsset; + if (referenceAsset != null && referenceAsset.SkeletonDataAsset == skeletonDataAsset) func(referenceAsset); } } diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineEditorUtilities.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineEditorUtilities.cs index a6e8185c9..b26b53476 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineEditorUtilities.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineEditorUtilities.cs @@ -79,6 +79,7 @@ namespace Spine.Unity.Editor { [InitializeOnLoad] public partial class SpineEditorUtilities : AssetPostprocessor { public const string ReferenceAssetsFolderName = "ReferenceAssets"; + public const string AnimationReferenceContainerSuffix = "_AnimationReferences"; public static string editorPath = ""; public static string editorGUIPath = ""; public static bool initialized; diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAsset.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAsset.cs index 5f1376571..86fdbd5d3 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAsset.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAsset.cs @@ -40,7 +40,10 @@ namespace Spine.Unity { [SerializeField, SpineAnimation] protected string animationName; private Animation animation; - public SkeletonDataAsset SkeletonDataAsset { get { return skeletonDataAsset; } } + public SkeletonDataAsset SkeletonDataAsset { + get { return skeletonDataAsset; } + set { skeletonDataAsset = value; } + } public string AnimationName { get { diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAssetContainer.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAssetContainer.cs new file mode 100644 index 000000000..fba436e84 --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAssetContainer.cs @@ -0,0 +1,40 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2026, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using UnityEngine; + +namespace Spine.Unity { + public class AnimationReferenceAssetContainer : ScriptableObject { + [SerializeField] protected SkeletonDataAsset skeletonDataAsset; + public SkeletonDataAsset SkeletonDataAsset { + get { return skeletonDataAsset; } + set { skeletonDataAsset = value; } + } + } +} diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAssetContainer.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAssetContainer.cs.meta new file mode 100644 index 000000000..384be5a8c --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/AnimationReferenceAssetContainer.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 5f30f83b4b98ce44cbae81286c0efac1 +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 52b12ec801461494185a4d3dc66f3d1d, type: 3} + userData: + assetBundleName: + assetBundleVariant: