SkeletonDataReader.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. using Spine.Unity;
  4. using System.Collections.Generic;
  5. using UnityEditor;
  6. using System.Linq;
  7. using System.IO;
  8. using System;
  9. using Spine;
  10. using UnityEditor.Animations;
  11. [InitializeOnLoad]
  12. public class SkeletonDataMonitor
  13. {
  14. private static Dictionary<string, CachedPrefabData> _prefabCache = new Dictionary<string, CachedPrefabData>();
  15. private static Dictionary<SkeletonDataAsset, List<string>> _skeletonToPrefabs = new Dictionary<SkeletonDataAsset, List<string>>();
  16. private struct CachedPrefabData
  17. {
  18. public SkeletonDataAsset skeletonDataAsset;
  19. public DateTime lastModified;
  20. }
  21. static SkeletonDataMonitor()
  22. {
  23. if (!EditorApplication.isPlayingOrWillChangePlaymode)
  24. {
  25. Initialize();
  26. }
  27. EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
  28. }
  29. private static void OnPlayModeStateChanged(PlayModeStateChange state)
  30. {
  31. if (state == PlayModeStateChange.ExitingEditMode)
  32. {
  33. // 进入运行模式前清理
  34. Cleanup();
  35. }
  36. else if (state == PlayModeStateChange.EnteredEditMode)
  37. {
  38. // 返回编辑模式时重新初始化
  39. Initialize();
  40. }
  41. }
  42. private static void Initialize()
  43. {
  44. EditorApplication.hierarchyChanged += OnHierarchyChanged;
  45. Selection.selectionChanged += OnSelectionChanged;
  46. AssemblyReloadEvents.afterAssemblyReload += InitializeCache;
  47. InitializeCache();
  48. }
  49. private static void Cleanup()
  50. {
  51. EditorApplication.hierarchyChanged -= OnHierarchyChanged;
  52. Selection.selectionChanged -= OnSelectionChanged;
  53. AssemblyReloadEvents.afterAssemblyReload -= InitializeCache;
  54. }
  55. private static void InitializeCache()
  56. {
  57. _prefabCache.Clear();
  58. _skeletonToPrefabs.Clear();
  59. var allPrefabs = AssetDatabase.FindAssets("t:Prefab")
  60. .Select(AssetDatabase.GUIDToAssetPath)
  61. .Where(p => p.EndsWith(".prefab"));
  62. foreach (var path in allPrefabs)
  63. {
  64. var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
  65. var skeleton = prefab.GetComponentInChildren<SkeletonRenderer>(true);
  66. if (skeleton != null && skeleton.skeletonDataAsset != null)
  67. {
  68. CachePrefab(path, skeleton.skeletonDataAsset);
  69. }
  70. }
  71. }
  72. private static void CachePrefab(string path, SkeletonDataAsset skeletonData)
  73. {
  74. _prefabCache[path] = new CachedPrefabData
  75. {
  76. skeletonDataAsset = skeletonData,
  77. lastModified = File.GetLastWriteTime(path)
  78. };
  79. if (!_skeletonToPrefabs.ContainsKey(skeletonData))
  80. {
  81. _skeletonToPrefabs[skeletonData] = new List<string>();
  82. }
  83. if (!_skeletonToPrefabs[skeletonData].Contains(path))
  84. {
  85. _skeletonToPrefabs[skeletonData].Add(path);
  86. }
  87. }
  88. private static void OnHierarchyChanged()
  89. {
  90. if (EditorApplication.isPlayingOrWillChangePlaymode)
  91. return;
  92. var newSkeletons = GameObject.FindObjectsOfType<SkeletonRenderer>(true)
  93. .Where(s => PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(s.gameObject) != null);
  94. foreach (var skeleton in newSkeletons)
  95. {
  96. if (skeleton.skeletonDataAsset != null)
  97. {
  98. var path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(skeleton.gameObject);
  99. if (!_prefabCache.ContainsKey(path) ||
  100. _prefabCache[path].skeletonDataAsset != skeleton.skeletonDataAsset)
  101. {
  102. ProcessSkeletonChange(path, skeleton.skeletonDataAsset);
  103. }
  104. }
  105. }
  106. }
  107. private static void OnSelectionChanged()
  108. {
  109. if (EditorApplication.isPlayingOrWillChangePlaymode)
  110. return;
  111. if (Selection.activeGameObject != null)
  112. {
  113. var skeleton = Selection.activeGameObject.GetComponentInChildren<SkeletonRenderer>(true);
  114. if (skeleton != null && skeleton.skeletonDataAsset != null)
  115. {
  116. var path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(skeleton.gameObject);
  117. if (!string.IsNullOrEmpty(path) &&
  118. (!_prefabCache.TryGetValue(path, out var cache) ||
  119. cache.skeletonDataAsset != skeleton.skeletonDataAsset))
  120. {
  121. ProcessSkeletonChange(path, skeleton.skeletonDataAsset);
  122. }
  123. }
  124. }
  125. }
  126. private static void ProcessSkeletonChange(string prefabPath, SkeletonDataAsset skeletonData, bool forceRefresh = false)
  127. {
  128. var currentModTime = File.GetLastWriteTime(prefabPath);
  129. // 如果是强制刷新或者文件有修改
  130. if (forceRefresh || !_prefabCache.TryGetValue(prefabPath, out var cache) ||
  131. cache.skeletonDataAsset != skeletonData ||
  132. cache.lastModified != currentModTime)
  133. {
  134. WriteData(prefabPath);
  135. CachePrefab(prefabPath, skeletonData);
  136. }
  137. }
  138. static Character cha;
  139. static SkeletonMecanim skeletonMecanim;
  140. static AttackController attack;
  141. private static void WriteData(string path)
  142. {
  143. GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
  144. GameObject prefabInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
  145. try
  146. {
  147. cha = prefabInstance.GetComponent<Character>();
  148. attack = prefabInstance.GetComponent<AttackController>();
  149. ReadData(prefabInstance);
  150. PrefabUtility.SaveAsPrefabAsset(prefabInstance, path);
  151. UnityEngine.Object.DestroyImmediate(prefabInstance);
  152. }
  153. catch (Exception ex)
  154. {
  155. Debug.LogError($"处理Prefab时出错({path}): {ex.Message}");
  156. }
  157. finally
  158. {
  159. UnityEngine.Object.DestroyImmediate(prefabInstance);
  160. }
  161. }
  162. static void ReadData(GameObject prefab)
  163. {
  164. skeletonMecanim = prefab.GetComponentInChildren<SkeletonMecanim>();
  165. if (skeletonMecanim == null)
  166. {
  167. Debug.LogError("SkeletonMecanim component not found!");
  168. return;
  169. }
  170. // 获取 SkeletonDataAsset
  171. SkeletonDataAsset skeletonDataAsset = skeletonMecanim.SkeletonDataAsset;
  172. if (skeletonDataAsset == null)
  173. {
  174. Debug.LogError("SkeletonDataAsset not found!");
  175. return;
  176. }
  177. // 获取 SkeletonData
  178. SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(true);
  179. if (skeletonData == null)
  180. {
  181. Debug.LogError("SkeletonData not found!");
  182. return;
  183. }
  184. attack.attackKeys = new List<AttackController.SpineAniKey>();
  185. // 遍历所有动画
  186. foreach (var animation in skeletonData.Animations)
  187. {
  188. string animationName = animation.Name;
  189. float animationDuration = animation.Duration;
  190. Debug.Log($"动画名称: {animationName}, 动画时长: {animationDuration}");
  191. if (animationName == "die")
  192. {
  193. cha.totalDieKeepTime = animationDuration;
  194. Debug.Log(animationDuration);
  195. }
  196. else if (animationName == "attack_summon")
  197. {
  198. cha.totalAttack_summonTime = animationDuration;
  199. }
  200. else if (animationName == "attack_march")
  201. {
  202. cha.totalAttack_marchTime = animationDuration;
  203. }
  204. AttackController.SpineAniKey sak;
  205. sak.aniName = animationName;
  206. sak.keys = new List<AttackController.AttackKeyType>();
  207. AttackController.AttackKeyType akt = new AttackController.AttackKeyType();
  208. bool isStarKey = true;
  209. int canWrite = 0;
  210. foreach(var timeline in animation.Timelines)
  211. {
  212. if(timeline is EventTimeline eventTimeline)
  213. {
  214. foreach(var eventFrame in eventTimeline.Events)
  215. {
  216. string eventName = eventFrame.ToString();
  217. float eventTime = eventFrame.Time;
  218. if (isStarKey)
  219. {
  220. akt.startKeyName = eventName;
  221. akt.startKeyTime = eventTime;
  222. akt.attackType = AttackController.KeyType.AttackStart;
  223. canWrite = 1;
  224. }
  225. else if(canWrite == 1)
  226. {
  227. akt.endKeyName = eventName;
  228. akt.endKeyTime = eventTime;
  229. akt.endType = AttackController.KeyType.AttackEnd;
  230. canWrite = 2;
  231. }
  232. isStarKey = !isStarKey;
  233. }
  234. }
  235. }
  236. if (canWrite == 2)
  237. {
  238. sak.keys.Add(akt);
  239. attack.attackKeys.Add(sak);
  240. }
  241. }
  242. }
  243. // 监听SkeletonDataAsset文件变化
  244. public class SkeletonDataPostprocessor : AssetPostprocessor
  245. {
  246. static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets,
  247. string[] movedAssets, string[] movedFromAssetPaths)
  248. {
  249. if (EditorApplication.isPlayingOrWillChangePlaymode)
  250. return;
  251. foreach (string path in importedAssets)
  252. {
  253. if (path.EndsWith(".asset"))
  254. {
  255. var asset = AssetDatabase.LoadAssetAtPath<SkeletonDataAsset>(path);
  256. if (asset != null && _skeletonToPrefabs.ContainsKey(asset))
  257. {
  258. foreach (var prefabPath in _skeletonToPrefabs[asset])
  259. {
  260. ProcessSkeletonChange(prefabPath, asset);
  261. }
  262. }
  263. }
  264. }
  265. }
  266. }
  267. [MenuItem("Tools/Spine/强制更新选中SkeletonData")]
  268. private static void ForceRefreshSelectedSkeletonData()
  269. {
  270. if (EditorApplication.isPlayingOrWillChangePlaymode)
  271. {
  272. EditorUtility.DisplayDialog("提示", "该功能只能在编辑模式下使用", "确定");
  273. return;
  274. }
  275. var selected = Selection.activeObject as SkeletonDataAsset;
  276. if (selected == null)
  277. {
  278. EditorUtility.DisplayDialog("提示", "请先选择一个SkeletonDataAsset", "确定");
  279. return;
  280. }
  281. // 强制更新所有使用此SkeletonData的预制体
  282. if (_skeletonToPrefabs.TryGetValue(selected, out var prefabPaths))
  283. {
  284. foreach (var path in prefabPaths)
  285. {
  286. ForceRefreshPrefab(path, selected);
  287. }
  288. AssetDatabase.SaveAssets();
  289. Debug.Log($"已强制更新 {prefabPaths.Count} 个使用 {selected.name} 的预制体");
  290. }
  291. else
  292. {
  293. Debug.Log($"没有找到使用 {selected.name} 的预制体");
  294. }
  295. }
  296. private static void ForceRefreshPrefab(string prefabPath, SkeletonDataAsset skeletonData)
  297. {
  298. GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
  299. GameObject prefabInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
  300. try
  301. {
  302. // 强制重新读取数据
  303. WriteData(prefabPath);
  304. PrefabUtility.SaveAsPrefabAsset(prefabInstance, prefabPath);
  305. // 更新缓存
  306. CachePrefab(prefabPath, skeletonData);
  307. }
  308. catch (Exception ex)
  309. {
  310. Debug.LogError($"强制更新预制体 {prefabPath} 时出错: {ex.Message}");
  311. }
  312. finally
  313. {
  314. UnityEngine.Object.DestroyImmediate(prefabInstance);
  315. }
  316. }
  317. [MenuItem("Tools/Spine/手动刷新变更")]
  318. private static void ForceRefreshModified()
  319. {
  320. if (EditorApplication.isPlayingOrWillChangePlaymode)
  321. {
  322. EditorUtility.DisplayDialog("提示", "该功能只能在编辑模式下使用", "确定");
  323. return;
  324. }
  325. foreach (var kvp in _prefabCache.ToList())
  326. {
  327. var currentModTime = File.GetLastWriteTime(kvp.Key);
  328. if (currentModTime != kvp.Value.lastModified)
  329. {
  330. ProcessSkeletonChange(kvp.Key, kvp.Value.skeletonDataAsset);
  331. }
  332. }
  333. }
  334. [MenuItem("Tools/Spine/Generate Simple Animator")]
  335. public static void GenerateSimpleAnimator()
  336. {
  337. SkeletonDataAsset skeletonData = Selection.activeObject as SkeletonDataAsset;
  338. if (skeletonData == null)
  339. {
  340. Debug.LogWarning("请先选择一个SkeletonDataAsset");
  341. return;
  342. }
  343. // 1. 创建Animator Controller
  344. string skeletonFolder = Path.GetDirectoryName(AssetDatabase.GetAssetPath(skeletonData));
  345. string controllerName = $"{skeletonData.name}_Controller.controller";
  346. string path = $"{skeletonFolder}/{controllerName}";
  347. if (File.Exists(path))
  348. {
  349. AssetDatabase.DeleteAsset(path);
  350. }
  351. var controller = AnimatorController.CreateAnimatorControllerAtPath(path);
  352. // 2. 获取Spine动画数据
  353. var skeleton = skeletonData.GetSkeletonData(true);
  354. // 3. 为每个动画创建空Clip并设置正确时长
  355. foreach (var spineAnim in skeleton.Animations)
  356. {
  357. AnimationClip clip = new AnimationClip();
  358. clip.name = spineAnim.Name;
  359. // 设置Clip时长与Spine动画一致
  360. SetClipLength(clip, spineAnim.Duration);
  361. // 添加到Controller
  362. AssetDatabase.AddObjectToAsset(clip, controller);
  363. var state = controller.layers[0].stateMachine.AddState(clip.name);
  364. state.motion = clip;
  365. }
  366. AssetDatabase.SaveAssets();
  367. Debug.Log($"已生成包含 {skeleton.Animations.Count} 个动画的控制器");
  368. }
  369. static void SetClipLength(AnimationClip clip, float duration)
  370. {
  371. // 创建一个空曲线来设置时长
  372. AnimationCurve curve = new AnimationCurve();
  373. curve.AddKey(0, 0);
  374. curve.AddKey(duration, 0);
  375. // 绑定到任意属性(这里使用假属性)
  376. AnimationUtility.SetEditorCurves(clip, new[] {
  377. new EditorCurveBinding {
  378. path = "",
  379. type = typeof(GameObject),
  380. propertyName = "m_IsActive"
  381. }
  382. }, new[] { curve });
  383. }
  384. [MenuItem("Tools/Spine/Generate Simple Animator", true)]
  385. static bool ValidateGenerate()
  386. {
  387. return Selection.activeObject is SkeletonDataAsset;
  388. }
  389. }
  390. #endif