| | |
| | | |
| | | using System; |
| | | using System.Threading; |
| | | using Spine; |
| | | using Spine.Unity; |
| | | using UnityEngine; |
| | | using UnityEngine.UI; |
| | | using Cysharp.Threading.Tasks; |
| | | |
| | | public class UIHeroController : MonoBehaviour |
| | | { |
| | |
| | | private int skinID; |
| | | protected SkeletonGraphic skeletonGraphic; |
| | | |
| | | protected Spine.AnimationState spineAnimationState; |
| | | public Spine.AnimationState spineAnimationState; |
| | | private GameObject instanceGO; |
| | | private bool isInitializing = false; |
| | | private bool isInitialized = false; |
| | | private CancellationTokenSource loadCancellationToken; // 用于取消之前的加载任务 |
| | | |
| | | private Action onComplete; |
| | | // 使用 UniTask 提供的并发控制 - 支持 WebGL |
| | | // 限制同时加载的资源数量(默认为4,可根据设备性能调整) |
| | | private const int MAX_CONCURRENT_LOADS = 4; |
| | | private static int currentLoadingCount = 0; |
| | | private static readonly object loadLock = new object(); |
| | | private static int initializationOrder = 0; // 用于分帧延迟的序号 |
| | | |
| | | public Action onComplete; |
| | | public void Create(int _skinID, float scale = 0.8f, Action _onComplete = null, string motionName = "idle", bool isLh = false) |
| | | { |
| | | if (skinID == _skinID) |
| | | { |
| | | //避免重复创建 |
| | | |
| | | if (skeletonGraphic != null) |
| | | { |
| | | SetMaterialNone(); |
| | | if (isLh) |
| | | { |
| | | var skinConfigTmp = HeroSkinConfig.Get(skinID); |
| | | if (skinConfigTmp != null && skinConfigTmp.Tachie.IsSpine()) |
| | | { |
| | | skeletonGraphic.enabled = true; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | skeletonGraphic.enabled = true; |
| | | } |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (skeletonGraphic != null) |
| | | { |
| | | skeletonGraphic.enabled = false; |
| | | } |
| | | |
| | | skinID = _skinID; |
| | |
| | | |
| | | //立绘特殊处理,没有spine动画的改用图片 |
| | | var lhImg = this.AddMissingComponent<RawImage>(); |
| | | if (!skinConfig.Tachie.Contains("SkeletonData")) |
| | | if (!skinConfig.Tachie.IsSpine()) |
| | | { |
| | | //图片替换 |
| | | lhImg.SetTexture2DPNG(skinConfig.Tachie); |
| | |
| | | } |
| | | |
| | | onComplete = _onComplete; |
| | | pool = GameObjectPoolManager.Instance.RequestPool(UILoader.LoadPrefab("UIHero")); |
| | | |
| | | if (!transform.gameObject.activeSelf) |
| | | { |
| | | transform.SetActive(true); |
| | | } |
| | | if (instanceGO == null) |
| | | { |
| | | instanceGO = pool.Request(); |
| | | instanceGO.transform.SetParent(transform); |
| | | //transform 的Pivot Y是0,让instanceGO 居中 |
| | | instanceGO.transform.localPosition = new Vector3(0, instanceGO.GetComponent<RectTransform>().sizeDelta.y * 0.5f); |
| | | |
| | | //instanceGO.transform.localPosition = Vector3.zero; |
| | | instanceGO.transform.localScale = Vector3.one; |
| | | instanceGO.transform.localRotation = Quaternion.identity; |
| | | } |
| | | |
| | | skeletonGraphic = instanceGO.GetComponentInChildren<SkeletonGraphic>(true); |
| | | if (isLh) |
| | | { |
| | | skeletonGraphic.skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("Hero/SpineRes/", skinConfig.Tachie); |
| | | } |
| | | else |
| | | { |
| | | skeletonGraphic.skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("Hero/SpineRes/", skinConfig.SpineRes); |
| | | } |
| | | if (skeletonGraphic.skeletonDataAsset == null) |
| | | { |
| | | |
| | | transform.SetActive(false); |
| | | if (pool != null) |
| | | pool.Release(instanceGO); |
| | | skeletonGraphic = null; |
| | | Destroy(instanceGO); |
| | | Debug.LogError("未配置spine"); |
| | | return; |
| | | } |
| | | skeletonGraphic.Initialize(true); |
| | | |
| | | spineAnimationState = skeletonGraphic.AnimationState; |
| | | spineAnimationState.Data.DefaultMix = 0f; |
| | | if (motionName == "") |
| | | motionName = GetFistSpineAnim(); |
| | | PlayAnimation(motionName, true); |
| | | spineAnimationState.Complete -= OnAnimationComplete; |
| | | spineAnimationState.Complete += OnAnimationComplete; |
| | | // 取消之前的异步任务,避免多次调用导致错乱 |
| | | CancelLoadTask(); |
| | | // 创建新的取消令牌 |
| | | loadCancellationToken = new CancellationTokenSource(); |
| | | // 使用 UniTask 进行异步初始化,将instanceGO创建和资源加载都移到异步处理 |
| | | DelayedInitializeAsync(skinConfig, motionName, isLh, loadCancellationToken.Token).Forget(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 取消之前的加载任务 |
| | | /// </summary> |
| | | private void CancelLoadTask() |
| | | { |
| | | if (loadCancellationToken != null && !loadCancellationToken.IsCancellationRequested) |
| | | { |
| | | loadCancellationToken.Cancel(); |
| | | loadCancellationToken.Dispose(); |
| | | } |
| | | } |
| | | |
| | | public bool HasAnimation(string motionName) |
| | | { |
| | | if (skeletonGraphic == null || skeletonGraphic.Skeleton == null) |
| | | { |
| | | Debug.LogWarning("skeletonGraphic or Skeleton is null, cannot check animation: " + motionName); |
| | | return false; |
| | | } |
| | | return skeletonGraphic.Skeleton.ContainsMotion(motionName); |
| | | } |
| | | |
| | | |
| | | protected void OnDestroy() |
| | | { |
| | | // 取消正在进行的加载任务 |
| | | CancelLoadTask(); |
| | | |
| | | if (spineAnimationState != null) |
| | | { |
| | | spineAnimationState.Complete -= OnAnimationComplete; |
| | |
| | | pool = null; |
| | | } |
| | | |
| | | private string pendingAnimationName = null; |
| | | private bool pendingAnimationLoop = false; |
| | | private bool pendingAnimationReplay = true; |
| | | private float? pendingSpeed = null; |
| | | private bool pendingEnabled = true; // 改为普通bool,false表示未设置,true表示需要启用 |
| | | private bool pendingGray = false; // 改为普通bool,表示是否需要设置灰度 |
| | | |
| | | /// <summary> |
| | | /// 播放 Spine 动画 |
| | | /// </summary> |
| | | /// <param name="motionName">动作名</param> |
| | | /// <param name="loop">循环</param> |
| | | /// <param name="replay">如果相同动作是否再次重播,比如跑步重播就会跳帧不顺滑</param> |
| | | public virtual void PlayAnimation(string motionName, bool loop = false, bool replay=true) |
| | | public virtual TrackEntry PlayAnimation(string motionName, bool loop = false, bool replay = true) |
| | | { |
| | | if (spineAnimationState == null) return; |
| | | // 如果正在初始化中,保存动画参数,等待初始化完成后再播放 |
| | | if (isInitializing) |
| | | { |
| | | pendingAnimationName = motionName; |
| | | pendingAnimationLoop = loop; |
| | | pendingAnimationReplay = replay; |
| | | return null; |
| | | } |
| | | |
| | | if (spineAnimationState == null) |
| | | { |
| | | Debug.LogWarning("spineAnimationState is null, cannot play animation: " + motionName); |
| | | return null; |
| | | } |
| | | |
| | | if (GetCurrentAnimationName() == motionName && !replay) |
| | | return; |
| | | return null; |
| | | |
| | | // 直接使用 ToString() 而不是调用 GetAnimationName |
| | | spineAnimationState.SetAnimation(0, motionName.ToString(), loop); |
| | | try |
| | | { |
| | | return spineAnimationState.SetAnimation(0, motionName.ToString(), loop); |
| | | } |
| | | catch (System.Exception e) |
| | | { |
| | | Debug.LogError("播放动画失败: " + motionName + ", 错误: " + e.Message); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | // 播放第一个动画(作为默认动画) |
| | | string GetFistSpineAnim() |
| | | { |
| | | if (skeletonGraphic == null || skeletonGraphic.Skeleton == null) |
| | | { |
| | | Debug.LogWarning("skeletonGraphic or Skeleton is null, cannot get first animation"); |
| | | return "idle"; // 返回默认动画名称 |
| | | } |
| | | |
| | | var skeletonData = skeletonGraphic.Skeleton.Data; |
| | | if (skeletonData.Animations.Count > 0) |
| | | { |
| | |
| | | { |
| | | Debug.LogError("Spine 数据中没有找到任何动画!武将皮肤:" + skinID); |
| | | } |
| | | return ""; |
| | | return "idle"; // 返回默认动画名称 |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | //越大越快 |
| | | public void SetSpeed(float speed) |
| | | { |
| | | // 如果正在初始化中,保存速度参数,等待初始化完成后再设置 |
| | | if (isInitializing) |
| | | { |
| | | pendingSpeed = speed; |
| | | return; |
| | | } |
| | | |
| | | if (spineAnimationState == null) |
| | | { |
| | | Debug.LogWarning("spineAnimationState is null, cannot set speed"); |
| | | return; |
| | | } |
| | | spineAnimationState.TimeScale = speed; |
| | | } |
| | | |
| | | public void SetEnabled(bool isEnable) |
| | | { |
| | | { |
| | | // 如果正在初始化中,保存启用状态,等待初始化完成后再设置 |
| | | if (isInitializing) |
| | | { |
| | | pendingEnabled = isEnable; |
| | | return; |
| | | } |
| | | |
| | | if (skeletonGraphic == null) |
| | | { |
| | | Debug.LogWarning("skeletonGraphic is null, cannot set enabled state"); |
| | | return; |
| | | } |
| | | skeletonGraphic.enabled = isEnable; |
| | | } |
| | | |
| | | public void SetGray() |
| | | { |
| | | // 如果正在初始化中,标记需要设置灰度,等待初始化完成后再设置 |
| | | if (isInitializing) |
| | | { |
| | | pendingGray = true; |
| | | return; |
| | | } |
| | | |
| | | if (skeletonGraphic == null) |
| | | { |
| | | Debug.LogWarning("skeletonGraphic is null, cannot set gray material"); |
| | | return; |
| | | } |
| | | skeletonGraphic.material = MaterialUtility.GetDefaultSpriteGrayMaterial(); |
| | | } |
| | | public void SetMaterialNone() |
| | | { |
| | | // 如果正在初始化中,标记需要设置无材质,等待初始化完成后再设置 |
| | | if (isInitializing) |
| | | { |
| | | pendingGray = false; |
| | | return; |
| | | } |
| | | |
| | | if (skeletonGraphic == null) |
| | | { |
| | | Debug.LogWarning("skeletonGraphic is null, cannot set material to none"); |
| | | return; |
| | | } |
| | | skeletonGraphic.material = null; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 延迟初始化,结合并发控制和分帧延迟 |
| | | /// 1. 资源加载使用真正的异步(LoadAssetAsync) |
| | | /// 2. skeletonGraphic.Initialize() 前进行分帧延迟,避免主线程卡顿 |
| | | /// </summary> |
| | | private async UniTaskVoid DelayedInitializeAsync(HeroSkinConfig skinConfig, string motionName, bool isLh, CancellationToken cancellationToken) |
| | | { |
| | | isInitializing = true; |
| | | |
| | | try |
| | | { |
| | | // 检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | // 获取加载信号量 - 限制并发数,避免资源竞争 |
| | | await AcquireLoadSlotAsync(cancellationToken); |
| | | |
| | | // 异步创建instanceGO和加载资源(真正的异步,不阻塞) |
| | | await CreateInstanceAndLoadAssetsAsync(skinConfig, isLh, cancellationToken); |
| | | |
| | | // 获取当前序号用于分帧延迟 |
| | | int myOrder; |
| | | lock (loadLock) |
| | | { |
| | | myOrder = initializationOrder++; |
| | | } |
| | | |
| | | // 再次检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | // 在 skeletonGraphic.Initialize() 前进行分帧延迟 |
| | | // 根据 MAX_CONCURRENT_LOADS 调整延迟,避免所有对象同时执行 Initialize |
| | | int delayFrames = (myOrder % MAX_CONCURRENT_LOADS); |
| | | if (delayFrames > 0) |
| | | { |
| | | for (int i = 0; i < delayFrames; i++) |
| | | { |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | await UniTask.NextFrame(cancellationToken); |
| | | } |
| | | } |
| | | |
| | | // 再次检查是否已被取消(可能在延迟期间被取消) |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | if (skeletonGraphic == null || skeletonGraphic.skeletonDataAsset == null) |
| | | { |
| | | Debug.LogError("资源加载失败,无法初始化模型"); |
| | | return; |
| | | } |
| | | |
| | | skeletonGraphic.initialSkinName = skinConfig.InitialSkinName; |
| | | skeletonGraphic.Initialize(true); |
| | | |
| | | // 初始化完成后设置皮肤 |
| | | if (!string.IsNullOrEmpty(skinConfig.InitialSkinName)) |
| | | { |
| | | var skeleton = skeletonGraphic.Skeleton; |
| | | skeleton.SetSkin(skinConfig.InitialSkinName); |
| | | skeleton.SetSlotsToSetupPose(); |
| | | skeletonGraphic.Update(0); |
| | | } |
| | | |
| | | spineAnimationState = skeletonGraphic.AnimationState; |
| | | spineAnimationState.Data.DefaultMix = 0f; |
| | | |
| | | // 初始化完成后才显示模型 |
| | | skeletonGraphic.enabled = pendingEnabled; |
| | | |
| | | if (pendingGray) |
| | | { |
| | | skeletonGraphic.material = MaterialUtility.GetDefaultSpriteGrayMaterial(); |
| | | } |
| | | else |
| | | { |
| | | skeletonGraphic.material = null; |
| | | } |
| | | |
| | | // 检查是否有待设置的速度,如果有则设置 |
| | | if (pendingSpeed.HasValue) |
| | | { |
| | | spineAnimationState.TimeScale = pendingSpeed.Value; |
| | | pendingSpeed = null; |
| | | } |
| | | |
| | | // 检查是否有待播放的动画,如果有则优先播放外部调用的动画 |
| | | if (!string.IsNullOrEmpty(pendingAnimationName)) |
| | | { |
| | | isInitializing = false; |
| | | PlayAnimation(pendingAnimationName, pendingAnimationLoop, pendingAnimationReplay); |
| | | // 清除所有待播放动画参数 |
| | | pendingAnimationName = null; |
| | | pendingAnimationLoop = false; |
| | | pendingAnimationReplay = true; |
| | | } |
| | | else |
| | | { |
| | | // 如果没有外部调用的动画,播放默认动画 |
| | | if (motionName == "") |
| | | motionName = GetFistSpineAnim(); |
| | | |
| | | isInitializing = false; |
| | | PlayAnimation(motionName, true); |
| | | } |
| | | |
| | | spineAnimationState.Complete -= OnAnimationComplete; |
| | | spineAnimationState.Complete += OnAnimationComplete; |
| | | |
| | | isInitialized = true; |
| | | isInitializing = false; |
| | | } |
| | | catch (System.OperationCanceledException) |
| | | { |
| | | // 任务被取消,正常返回 |
| | | isInitializing = false; |
| | | } |
| | | catch (System.Exception e) |
| | | { |
| | | Debug.LogError($"英雄初始化异常: {e.Message}"); |
| | | isInitializing = false; |
| | | } |
| | | finally |
| | | { |
| | | // 释放加载槽位 |
| | | ReleaseLoadSlot(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取加载槽位(支持 WebGL 的并发控制) |
| | | /// </summary> |
| | | private async UniTask AcquireLoadSlotAsync(CancellationToken cancellationToken) |
| | | { |
| | | while (true) |
| | | { |
| | | // 检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | lock (loadLock) |
| | | { |
| | | if (currentLoadingCount < MAX_CONCURRENT_LOADS) |
| | | { |
| | | currentLoadingCount++; |
| | | return; |
| | | } |
| | | } |
| | | // 如果已达到最大并发数,等待下一帧再试 |
| | | await UniTask.NextFrame(cancellationToken); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 释放加载槽位 |
| | | /// </summary> |
| | | private void ReleaseLoadSlot() |
| | | { |
| | | lock (loadLock) |
| | | { |
| | | currentLoadingCount--; |
| | | } |
| | | } |
| | | |
| | | |
| | | /// <summary> |
| | | /// 异步创建instanceGO和加载资源(用于非立绘) |
| | | /// 使用真正的异步加载,不阻塞主线程 |
| | | /// </summary> |
| | | private async UniTask CreateInstanceAndLoadAssetsAsync(HeroSkinConfig skinConfig, bool isLh, CancellationToken cancellationToken) |
| | | { |
| | | // 确保transform处于激活状态 |
| | | if (!transform.gameObject.activeSelf) |
| | | { |
| | | transform.SetActive(true); |
| | | } |
| | | |
| | | // 检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | // 创建pool和instanceGO |
| | | pool = GameObjectPoolManager.Instance.GetPool(UILoader.LoadPrefab("UIHero")); |
| | | |
| | | if (instanceGO == null) |
| | | { |
| | | instanceGO = pool.Request(); |
| | | instanceGO.transform.SetParent(transform); |
| | | //transform 的Pivot Y是0,让instanceGO 居中 |
| | | instanceGO.transform.localPosition = new Vector3(0, instanceGO.GetComponent<RectTransform>().sizeDelta.y * 0.5f); |
| | | instanceGO.transform.localScale = Vector3.one; |
| | | instanceGO.transform.localRotation = Quaternion.identity; |
| | | } |
| | | |
| | | skeletonGraphic = instanceGO.GetComponentInChildren<SkeletonGraphic>(true); |
| | | |
| | | // 真正的异步加载资源 - 不阻塞主线程 |
| | | string assetName = isLh ? skinConfig.Tachie : skinConfig.SpineRes; |
| | | SkeletonDataAsset loadedAsset = await ResManager.Instance.LoadAssetAsync<SkeletonDataAsset>("Hero/SpineRes/", assetName); |
| | | |
| | | // 再次检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | if (loadedAsset != null) |
| | | { |
| | | skeletonGraphic.skeletonDataAsset = loadedAsset; |
| | | } |
| | | else |
| | | { |
| | | transform.SetActive(false); |
| | | if (pool != null) |
| | | pool.Release(instanceGO); |
| | | skeletonGraphic = null; |
| | | Destroy(instanceGO); |
| | | Debug.LogError("未配置spine"); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /// <summary> |
| | | /// 检查是否已完成初始化 |
| | | /// </summary> |
| | | public bool IsInitialized() |
| | | { |
| | | return isInitialized && !isInitializing; |
| | | } |
| | | } |