[unity] SkeletonDataAsset now allows creating AnimationReferenceAssets as nested assets. Closes #1940.

This commit is contained in:
Harald Csaszar 2026-03-03 18:06:01 +01:00
parent 2eb06b7544
commit 1dac7ed019
8 changed files with 155 additions and 15 deletions

View File

@ -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 `<skeletonname>_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**

View File

@ -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<AnimationReferenceAsset>(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<AnimationReferenceAsset>(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;

View File

@ -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<AnimationReferenceAsset>(assetPath);
if (existingAsset == null) {
AnimationReferenceAsset newAsset = ScriptableObject.CreateInstance<AnimationReferenceAsset>();
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<AnimationReferenceAssetContainer>(containerPath);
if (container == null) {
container = ScriptableObject.CreateInstance<AnimationReferenceAssetContainer>();
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<string> existingAnimationNames = new HashSet<string>();
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<AnimationReferenceAsset>();
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.

View File

@ -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<AnimationReferenceAsset>(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);
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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; }
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 5f30f83b4b98ce44cbae81286c0efac1
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 52b12ec801461494185a4d3dc66f3d1d, type: 3}
userData:
assetBundleName:
assetBundleVariant: