动画模块:性能优化方案
using UnityEngine;
using UnityEditor;
using Spine.Unity;
using System.IO;
using System.Collections.Generic;
public class SpineAssetTools
{
// -----------------------------------------------------------
// 菜单入口 1:针对单个文件(保留这个功能,偶尔修补用)
// -----------------------------------------------------------
[MenuItem( "Assets/Spine/生成动作引用 (选中文件)", false, 10 )]
public static void CreateFromSingleFile( )
{
var selectedObj = Selection.activeObject as SkeletonDataAsset;
if ( selectedObj == null ) return;
GenerateForSkeleton( selectedObj );
AssetDatabase.SaveAssets( );
AssetDatabase.Refresh( );
Debug.Log( $"[Spine工具] 单个文件处理完成: {selectedObj.name}" );
}
[MenuItem( "Assets/Spine/生成动作引用 (选中文件)", true )]
public static bool ValidateSingleFile( )
{
return Selection.activeObject is SkeletonDataAsset;
}
// -----------------------------------------------------------
// 菜单入口 2:针对文件夹(递归批量处理)
// -----------------------------------------------------------
[MenuItem( "Assets/Spine/批量生成动作引用 (选中文件夹)", false, 11 )]
public static void CreateFromFolder( )
{
string selectedPath = AssetDatabase.GetAssetPath( Selection.activeObject );
// 1. 查找该目录下所有 SkeletonDataAsset
// "t:SkeletonDataAsset" 是 Unity 搜索语法,效率极高
string[] guids = AssetDatabase.FindAssets( "t:SkeletonDataAsset", new[] { selectedPath } );
if ( guids.Length == 0 )
{
EditorUtility.DisplayDialog( "提示", "该文件夹下没有找到任何 Spine 骨骼数据!", "知道啦" );
return;
}
// 2. 开启进度条,防止卡死焦虑
try
{
int count = 0;
for ( int i = 0; i < guids.Length; i++ )
{
string assetPath = AssetDatabase.GUIDToAssetPath( guids[ i ] );
var skeletonDataAsset = AssetDatabase.LoadAssetAtPath<SkeletonDataAsset>( assetPath );
// 更新进度条
EditorUtility.DisplayProgressBar( "Spine 批量生成中...",
$"正在处理 ({i + 1}/{guids.Length}): {skeletonDataAsset.name}",
( float ) i / guids.Length );
if ( skeletonDataAsset != null )
{
GenerateForSkeleton( skeletonDataAsset );
count++;
}
}
Debug.Log( $"[Spine工具] 批量处理完成!共处理了 {count} 个骨骼文件。" );
}
catch ( System.Exception e )
{
Debug.LogError( $"[Spine工具] 发生错误: {e.Message}" );
}
finally
{
// 3. 无论成功失败,都要清除进度条并保存
EditorUtility.ClearProgressBar( );
AssetDatabase.SaveAssets( );
AssetDatabase.Refresh( );
}
}
[MenuItem( "Assets/Spine/批量生成动作引用 (选中文件夹)", true )]
public static bool ValidateFolder( )
{
// 只有选中文件夹时才显示此菜单
return Selection.activeObject is DefaultAsset;
}
// -----------------------------------------------------------
// 核心修复:使用 SerializedObject 写入 protected 字段
// -----------------------------------------------------------
private static void GenerateForSkeleton( SkeletonDataAsset skeletonDataAsset )
{
if ( skeletonDataAsset == null ) return;
var skeletonData = skeletonDataAsset.GetSkeletonData( true );
if ( skeletonData == null ) return;
string assetPath = AssetDatabase.GetAssetPath( skeletonDataAsset );
string directory = Path.GetDirectoryName( assetPath );
string targetFolder = Path.Combine( directory, skeletonDataAsset.name + "_Anims" );
if ( !Directory.Exists( targetFolder ) ) Directory.CreateDirectory( targetFolder );
foreach ( var anim in skeletonData.Animations )
{
string animName = anim.Name;
// 处理非法字符
string safeAnimName = animName.Replace( "/", "_" ).Replace( "\\", "_" );
string fileName = $"Ref_{skeletonDataAsset.name}_{safeAnimName}.asset";
string fullPath = Path.Combine( targetFolder, fileName );
// 如果文件已存在,跳过
if ( File.Exists( fullPath ) ) continue;
// 1. 创建空实例
AnimationReferenceAsset refAsset = ScriptableObject.CreateInstance<AnimationReferenceAsset>( );
// 2. 【关键】使用 SerializedObject 包装它,绕过 protected 限制
SerializedObject so = new SerializedObject( refAsset );
// 3. 查找 protected 属性 (对应 Spine 源码中的字段名)
SerializedProperty skeletonProp = so.FindProperty( "skeletonDataAsset" );
SerializedProperty animNameProp = so.FindProperty( "animationName" );
// 4. 赋值
if ( skeletonProp != null ) skeletonProp.objectReferenceValue = skeletonDataAsset;
if ( animNameProp != null ) animNameProp.stringValue = animName;
// 5. 应用修改
so.ApplyModifiedPropertiesWithoutUndo( );
// 6. 保存到磁盘
AssetDatabase.CreateAsset( refAsset, fullPath );
}
}
}
3.1 问题痛点
传统写法 SetAnimation(0, "Run", true) 存在隐患:
- 性能消耗:Spine 内部需遍历 List 并进行字符串 Hash 对比,高频调用(如 Update)会产生显著 CPU 开销。
- 维护风险:美术修改动作名会导致代码失效,且由字符串硬编码引起,编译器无法报错。
3.2 解决方案:AnimationReferenceAsset
废弃字符串调用,全面采用 Spine-Unity 提供的 AnimationReferenceAsset 资源引用流程。
代码规范示例:
using Spine.Unity;
public class UnitController : MonoBehaviour
{
public SkeletonAnimation skeletonAnim;
[Header("动作资源配置")]
public AnimationReferenceAsset animIdle; // 在 Inspector 拖入 Asset
public AnimationReferenceAsset animRun;
public void PlayRun()
{
// 直接传递 Asset,内部自动解包 Animation 对象,无字符串查找开销
if (animRun != null)
skeletonAnim.AnimationState.SetAnimation(0, animRun, true);
}
}
4. 工具链:自动化工作流
为避免手动创建数以千计的 AnimationReferenceAsset,项目组提供了自动化生成工具。
4.1 工具脚本
脚本路径:Assets/Editor/SpineAssetTools.cs
(脚本代码已在版本库中归档,此处略,核心功能见下文)
4.2 操作指南
- 选中目标:在 Project 窗口选中某个怪物的文件夹(如
Assets/Art/Characters/Boss_01)或整个角色总目录。 - 执行生成:右键点击 -> 选择菜单
Assets/Spine/批量生成动作引用 (选中文件夹)。 - 结果:工具会自动扫描目录下所有
SkeletonDataAsset,并在其同级目录创建[SkeletonName]_Anims文件夹,生成所有动作的 Asset 引用。 - 配置:程序在 Inspector 面板直接搜索生成的 Asset 名称进行赋值。
