spine绑定骨骼运动工具spine纹理压缩自动设置工具

📅 2026/8/25 14:00:52
spine绑定骨骼运动工具spine纹理压缩自动设置工具
背景1美术给spine运动绑定骨骼名比较懒散众所周知骨骼路径是典型的树目录我提过几次每次给我们路径的时候需要给全路径但是美术那边不太配合还是习惯性的给结点名好吧给就给吧那我们只有写工具处理了背景2程序拖进美术的spine动效后打包就spine包都有10多甚至20M一看就是给的贴图太大了未做压缩因此需要改进spine的导入工具自动处理它的纹理压缩比如设置为Astc6X6,pc上DXT5绑骨工具改造之前官方的BoneFollowGraphic是没有绑定骨骼这个参数的也没有目录树你要绑定就只能从BoneName这个弹窗里面去选注意它是一个树结构找的你想死感觉就是被美术骗着浪费时间。对官方的BoneFollowerGraphicInspector类进行扩展新增2个变量绑定骨骼和目录树OnEnable的时候绑定属性显示成中文变量名方便日常开发有变化就去走深度优先查找最后完整代码如下using UnityEditor; using UnityEngine; namespace Spine.Unity.Editor { using Editor UnityEditor.Editor; using Event UnityEngine.Event; [CustomEditor(typeof(BoneFollowerGraphic)), CanEditMultipleObjects] public class BoneFollowerGraphicInspector : Editor { SerializedProperty boneName, CustomBoneName, bonePath, skeletonGraphic, followXYPosition, followZPosition, followBoneRotation, followLocalScale, followParentWorldScale, followSkeletonFlip, maintainedAxisOrientation; BoneFollowerGraphic targetBoneFollower; bool needsReset; #region Context Menu Item [MenuItem(CONTEXT/SkeletonGraphic/Add BoneFollower GameObject)] static void AddBoneFollowerGameObject (MenuCommand cmd) { var skeletonGraphic cmd.context as SkeletonGraphic; var go EditorInstantiation.NewGameObject(BoneFollower, true, typeof(RectTransform)); var t go.transform; t.SetParent(skeletonGraphic.transform); t.localPosition Vector3.zero; var f go.AddComponentBoneFollowerGraphic(); f.skeletonGraphic skeletonGraphic; f.SetBone(skeletonGraphic.Skeleton.RootBone.Data.Name); EditorGUIUtility.PingObject(t); Undo.RegisterCreatedObjectUndo(go, Add BoneFollowerGraphic); } // Validate [MenuItem(CONTEXT/SkeletonGraphic/Add BoneFollower GameObject, true)] static bool ValidateAddBoneFollowerGameObject (MenuCommand cmd) { var skeletonGraphic cmd.context as SkeletonGraphic; return skeletonGraphic.IsValid; } #endregion void OnEnable () { skeletonGraphic serializedObject.FindProperty(skeletonGraphic); boneName serializedObject.FindProperty(boneName); CustomBoneName serializedObject.FindProperty(CustomBoneName); bonePath serializedObject.FindProperty(bonePath); bonePath serializedObject.FindProperty(bonePath); followBoneRotation serializedObject.FindProperty(followBoneRotation); followXYPosition serializedObject.FindProperty(followXYPosition); followZPosition serializedObject.FindProperty(followZPosition); followLocalScale serializedObject.FindProperty(followLocalScale); followParentWorldScale serializedObject.FindProperty(followParentWorldScale); followSkeletonFlip serializedObject.FindProperty(followSkeletonFlip); maintainedAxisOrientation serializedObject.FindProperty(maintainedAxisOrientation); targetBoneFollower (BoneFollowerGraphic)target; if (targetBoneFollower.SkeletonGraphic ! null) targetBoneFollower.SkeletonGraphic.Initialize(false); if (!targetBoneFollower.valid || needsReset) { targetBoneFollower.Initialize(); targetBoneFollower.LateUpdate(); needsReset false; SceneView.RepaintAll(); } } Bone FindBoneDFS(Bone root, string targetName) { if (root.Data.Name targetName) return root; var children root.Children; for (int i 0; i children.Count; i) { var result FindBoneDFS(children.Items[i], targetName); if (result ! null) return result; } return null; } public void OnSceneGUI () { var tbf target as BoneFollowerGraphic; var skeletonGraphicComponent tbf.SkeletonGraphic; if (skeletonGraphicComponent null) return; var transform skeletonGraphicComponent.transform; var skeleton skeletonGraphicComponent.Skeleton; var canvas skeletonGraphicComponent.canvas; float positionScale canvas null ? 1f : skeletonGraphicComponent.canvas.referencePixelsPerUnit; if (string.IsNullOrEmpty(boneName.stringValue)) { SpineHandles.DrawBones(transform, skeleton, positionScale); SpineHandles.DrawBoneNames(transform, skeleton, positionScale); Handles.Label(tbf.transform.position, No bone selected, EditorStyles.helpBox); } else { var targetBone tbf.bone; if (targetBone null) return; SpineHandles.DrawBoneWireframe(transform, targetBone, SpineHandles.TransformContraintColor, positionScale); Handles.Label(targetBone.GetWorldPosition(transform, positionScale), targetBone.Data.Name, SpineHandles.BoneNameStyle); } } private string GetBonePath(Bone bone) { if (bone null) return string.Empty; // 从当前骨骼向上回溯到 RootBone var path bone.Data.Name; var parent bone.Parent; while (parent ! null) { path parent.Data.Name / path; parent parent.Parent; } return path; } override public void OnInspectorGUI () { if (serializedObject.isEditingMultipleObjects) { if (needsReset) { needsReset false; foreach (var o in targets) { var bf (BoneFollower)o; bf.Initialize(); bf.LateUpdate(); } SceneView.RepaintAll(); } EditorGUI.BeginChangeCheck(); DrawDefaultInspector(); needsReset | EditorGUI.EndChangeCheck(); return; } if (needsReset Event.current.type EventType.Layout) { targetBoneFollower.Initialize(); targetBoneFollower.LateUpdate(); needsReset false; SceneView.RepaintAll(); } serializedObject.Update(); // Find Renderer if (skeletonGraphic.objectReferenceValue null) { SkeletonGraphic parentRenderer targetBoneFollower.GetComponentInParentSkeletonGraphic(); if (parentRenderer ! null parentRenderer.gameObject ! targetBoneFollower.gameObject) { skeletonGraphic.objectReferenceValue parentRenderer; Debug.Log(Inspector automatically assigned BoneFollowerGraphic.SkeletonGraphic); } } EditorGUILayout.PropertyField(skeletonGraphic); var skeletonGraphicComponent skeletonGraphic.objectReferenceValue as SkeletonGraphic; if (skeletonGraphicComponent ! null) { if (skeletonGraphicComponent.gameObject targetBoneFollower.gameObject) { skeletonGraphic.objectReferenceValue null; EditorUtility.DisplayDialog(Invalid assignment., BoneFollowerGraphic can only follow a skeleton on a separate GameObject.\n\nCreate a new GameObject for your BoneFollower, or choose a SkeletonGraphic from a different GameObject., Ok); } } if (!targetBoneFollower.valid) { needsReset true; } if (targetBoneFollower.valid) { EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(CustomBoneName,new GUIContent(绑定骨骼)); EditorGUILayout.PropertyField(boneName); EditorGUILayout.PropertyField(bonePath,new GUIContent(目录树)); needsReset | EditorGUI.EndChangeCheck(); EditorGUILayout.PropertyField(followBoneRotation); EditorGUILayout.PropertyField(followXYPosition); EditorGUILayout.PropertyField(followZPosition); EditorGUILayout.PropertyField(followLocalScale); EditorGUILayout.PropertyField(followParentWorldScale); EditorGUILayout.PropertyField(followSkeletonFlip); if ((followSkeletonFlip.hasMultipleDifferentValues || followSkeletonFlip.boolValue false) (followBoneRotation.hasMultipleDifferentValues || followBoneRotation.boolValue true)) { using (new SpineInspectorUtility.IndentScope()) EditorGUILayout.PropertyField(maintainedAxisOrientation); } if (needsReset CustomBoneName.stringValue ! boneName.stringValue) { var tbf target as BoneFollowerGraphic; Skeleton tmpSkeleton tbf.skeletonGraphic.Skeleton; var bone FindBoneDFS(tbf.SkeletonGraphic.Skeleton.RootBone, CustomBoneName.stringValue); tbf.bone bone; boneName.stringValue CustomBoneName.stringValue; bonePath.stringValue GetBonePath(bone); } //BoneFollowerInspector.RecommendRigidbodyButton(targetBoneFollower); } else { var boneFollowerSkeletonGraphic targetBoneFollower.skeletonGraphic; if (boneFollowerSkeletonGraphic null) { EditorGUILayout.HelpBox(SkeletonGraphic is unassigned. Please assign a SkeletonRenderer (SkeletonAnimation or SkeletonMecanim)., MessageType.Warning); } else { boneFollowerSkeletonGraphic.Initialize(false); if (boneFollowerSkeletonGraphic.skeletonDataAsset null) EditorGUILayout.HelpBox(Assigned SkeletonGraphic does not have SkeletonData assigned to it., MessageType.Warning); if (!boneFollowerSkeletonGraphic.IsValid) EditorGUILayout.HelpBox(Assigned SkeletonGraphic is invalid. Check target SkeletonGraphic, its SkeletonData asset or the console for other errors., MessageType.Warning); } } var current Event.current; bool wasUndo (current.type EventType.ValidateCommand current.commandName UndoRedoPerformed); if (wasUndo) targetBoneFollower.Initialize(); serializedObject.ApplyModifiedProperties(); } } }最终效果展示spine纹理压缩格式Assets\3rdParty\Spine\Editor\spine-unity\Editor\Utility\AssetUtility.cs中新增的代码为//AssetDatabase.ImportAsset(texturePath); var android texImporter.GetPlatformTextureSettings(android); if(android ! null !android.overridden) { android.overridden true; android.format TextureImporterFormat.ASTC_6x6; texImporter.SetPlatformTextureSettings(android); } var ios texImporter.GetPlatformTextureSettings(ios); if (ios ! null !ios.overridden) { ios.overridden true; ios.format TextureImporterFormat.ASTC_6x6; texImporter.SetPlatformTextureSettings(ios); } var standalone texImporter.GetPlatformTextureSettings(Standalone); if (standalone ! null !standalone.overridden) { standalone.overridden true; standalone.format TextureImporterFormat.DXT5; texImporter.SetPlatformTextureSettings(standalone); } texImporter.SaveAndReimport(); AssetDatabase.SaveAssets();普通ui纹理压缩格式自动处理TextureAutoImportSession.cs 存放于unity工程 Assets Editor目录下另外新增了中文纹理资源不得导入到工程的设定美术经常那么制作。#if DebugMod UNITY_EDITOR_WIN using UnityEditor; using UnityEngine; using System.Collections.Generic; using System; using System.IO; /// summary /// 纹理自动导入会话管理 —— 检测首次打开工程 /// 首次打开时跳过所有纹理处理避免拉项目后首次导入耗时过长 /// 正常关闭编辑器后标记为已初始化下次打开才会自动处理新导入的纹理 /// /summary [InitializeOnLoad] public static class TextureAutoImportSession { private const string PrefKeyPrefix TexAutoImport_Initialized; private static bool _isFirstProjectOpen; static TextureAutoImportSession() { string key GetProjectKey(); _isFirstProjectOpen !EditorPrefs.GetBool(key, false); if (_isFirstProjectOpen) { Debug.Log([TextureAutoImport] 检测到首次打开此工程跳过所有纹理自动处理避免首次导入耗时过长。正常关闭编辑器后下次打开将自动处理新导入的纹理。); EditorApplication.wantsToQuit OnEditorQuitting; } } private static bool OnEditorQuitting() { EditorPrefs.SetBool(GetProjectKey(), true); return true; } private static string GetProjectKey() { return PrefKeyPrefix _ Application.dataPath.GetHashCode(); } /// summary 是否为首次打开此工程 /summary public static bool IsFirstProjectOpen _isFirstProjectOpen; /// summary 手动标记工程已初始化用于设置窗口的按钮 /summary public static void MarkProjectInitialized() { EditorPrefs.SetBool(GetProjectKey(), true); _isFirstProjectOpen false; } /// summary 重置工程初始化状态模拟首次打开用于测试 /summary public static void ResetProject() { EditorPrefs.DeleteKey(GetProjectKey()); _isFirstProjectOpen true; } } /// summary /// 纹理自动导入设置 —— 持久化存储于 EditorPrefs /// /summary public static class TextureAutoImportPrefs { private const string EnabledKey TexAutoImport_Enabled; private const string OnlyArtKey TexAutoImport_OnlyArt; private const string DisableRwKey TexAutoImport_DisableRW; private const string CustomIgnoreKey TexAutoImport_CustomIgnore; // ── 基础开关 ── public static bool Enabled { get EditorPrefs.GetBool(EnabledKey, true); set EditorPrefs.SetBool(EnabledKey, value); } /// summary 仅处理 Assets/Art 目录下的纹理 /summary public static bool OnlyProcessArtFolder { get EditorPrefs.GetBool(OnlyArtKey, true); set EditorPrefs.SetBool(OnlyArtKey, value); } /// summary 自动关闭 Read/Write节省内存 /summary public static bool DisableReadable { get EditorPrefs.GetBool(DisableRwKey, true); set EditorPrefs.SetBool(DisableRwKey, value); } // ── 用户自定义忽略关键字分号分隔存储于 EditorPrefs ── public static string CustomIgnoreRaw { get EditorPrefs.GetString(CustomIgnoreKey, ); set EditorPrefs.SetString(CustomIgnoreKey, value); } /// summary 获取自定义忽略关键字列表 /summary public static Liststring GetCustomIgnoreKeywords() { var result new Liststring(); if (string.IsNullOrEmpty(CustomIgnoreRaw)) return result; string[] parts CustomIgnoreRaw.Split(;); foreach (var p in parts) { string trimmed p.Trim(); if (!string.IsNullOrEmpty(trimmed)) result.Add(trimmed); } return result; } } /// summary /// 纹理自动导入处理器 /// 在纹理导入时自动设置压缩格式处理流程 /// 1. 功能未启用 → 跳过 /// 2. 批处理模式batmode → 跳过 /// 3. 首次打开工程 → 跳过 /// 4. 编译中 → 跳过 /// 5. 非 PNG/JPG/TGA → 跳过 /// 6. 非 Assets 目录 → 跳过 /// 7. 若仅处理 Art 目录且不在 Art 下 → 跳过 /// 8. 路径命中忽略关键字 → 跳过 /// 9. .meta 已存在重新导入 → 跳过 /// 10. 已勾选平台 Override → 跳过 /// 11. 其余纹理自动设置压缩格式Android/iOS: ASTC_8x8, Standalone: DXT5 /// /summary public class TextureAutoImportProcessor : AssetPostprocessor { // 内置忽略关键字与现有 TextureASTCViewer / SpineTextureCompress 保持一致 public static readonly Liststring BuiltinIgnoreKeywords new Liststring() { Editor/, 3rdParty/, Resources/, RTLTMPro/, Runtime/, Reporter/, spine/, // spine 不在此处理由 SpineTextureCompress 手动工具单独处理 icon.png // 游戏图标 忽略 }; void OnPreprocessTexture() { // 1. 功能开关 if (!TextureAutoImportPrefs.Enabled) return; // 2. 批处理模式不处理 if (Application.isBatchMode) return; // 3. 首次打开工程不处理 if (TextureAutoImportSession.IsFirstProjectOpen) return; // 4. 编译中不处理避免域重载引发的批量重新导入 if (EditorApplication.isCompiling) return; // 5. 检查文件扩展名仅处理 PNG / JPG / TGA string ext Path.GetExtension(assetPath)?.ToLower(); if (ext ! .png ext ! .jpg ext ! .jpeg ext ! .tga) return; // 6. 必须在 Assets 目录下 if (!assetPath.StartsWith(Assets/)) return; // 7. 若要求仅处理 Assets/Art 目录 if (TextureAutoImportPrefs.OnlyProcessArtFolder !assetPath.StartsWith(Assets/Art)) return; // 8. 检查路径是否包含中文中文资源路径会导致打包/热更问题直接屏蔽并报错 if (ContainsChinese(assetPath)) { AssetDatabase.DeleteAsset(assetPath); Debug.LogError($[TextureAutoImport] 检测到中文资源路径已屏蔽请使用纯英文路径: {assetPath}); return; } if (IsNonNormalImage(assetPath.ToLower())) return; // 12. 应用纹理压缩设置 ProcessNormalImage(assetImporter as TextureImporter); } void ProcessNormalImage(TextureImporter importer) { if (HasPlatformOverride(importer)) return; ApplyTextureSettings(importer); } void ProcessSpineTexture(TextureImporter importer) { if (HasPlatformOverride(importer)) return; ApplyTextureSettings(importer, TextureImporterFormat.ASTC_6x6); } /// summary 检查路径是否包含中文字符 /summary private bool ContainsChinese(string text) { foreach (char c in text) { if (c 0x4e00 c 0x9fff) return true; } return false; } /// summary 检查路径是否匹配忽略关键字大小写不敏感 /summary private bool IsNonNormalImage(string path) { string lowerPath path; // 内置关键字 foreach (var keyword in BuiltinIgnoreKeywords) { if (!string.IsNullOrEmpty(keyword) lowerPath.Contains(keyword.ToLower())) return true; } // 用户自定义关键字 foreach (var keyword in TextureAutoImportPrefs.GetCustomIgnoreKeywords()) { if (!string.IsNullOrEmpty(keyword) lowerPath.Contains(keyword.ToLower())) return true; } return false; } /// summary 检查是否已勾选任意平台的 Override /summary private bool HasPlatformOverride(TextureImporter importer) { if (importer null) return true; // 任意平台已勾选 Override 则认为用户已手动配置不干预 var android importer.GetPlatformTextureSettings(android); var iphone importer.GetPlatformTextureSettings(ios); var standalone importer.GetPlatformTextureSettings(Standalone); if (android.overridden) return true; if (iphone.overridden) return true; if (standalone.overridden) return true; return false; } /// summary 应用纹理压缩设置仅在未 Override 的平台上设置 /summary private void ApplyTextureSettings(TextureImporter importer, TextureImporterFormat formatTex TextureImporterFormat.ASTC_8x8) { if (importer null) { return; } bool changed false; // 关闭 Read/Write节省内存 if (TextureAutoImportPrefs.DisableReadable importer.isReadable) { importer.isReadable false; changed true; } // Android: ASTC_8x8 var android importer.GetPlatformTextureSettings(Android); if (!android.overridden) { android.overridden true; android.format formatTex; importer.SetPlatformTextureSettings(android); changed true; } // iOS: ASTC_8x8 var iphone importer.GetPlatformTextureSettings(ios); if (!iphone.overridden) { iphone.overridden true; iphone.format formatTex; importer.SetPlatformTextureSettings(iphone); changed true; } // Standalone: DXT5 var standalone importer.GetPlatformTextureSettings(Standalone); if (!standalone.overridden) { standalone.overridden true; standalone.format TextureImporterFormat.DXT5; importer.SetPlatformTextureSettings(standalone); changed true; } if (changed) { Debug.Log($[TextureAutoImport] 已处理纹理: {assetPath}); } } } #endif