| | |
| | | |
| | | using System; |
| | | using System.Threading; |
| | | using Spine; |
| | | using Spine.Unity; |
| | | using UnityEngine; |
| | |
| | | private GameObject instanceGO; |
| | | private bool isInitializing = false; |
| | | private bool isInitialized = false; |
| | | private static int activeInitializationCount = 0; // 当前正在初始化的卡片数量 |
| | | private static readonly object initializationLock = new object(); |
| | | private CancellationTokenSource loadCancellationToken; // 用于取消之前的加载任务 |
| | | |
| | | // 使用 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) |
| | |
| | | } |
| | | |
| | | onComplete = _onComplete; |
| | | |
| | | |
| | | // 立绘需要立即显示,其他情况延迟初始化 |
| | | if (isLh) |
| | | { |
| | | // 取消之前的异步任务 |
| | | CancelLoadTask(); |
| | | // 立绘立即初始化 |
| | | ForceInitialize(motionName); |
| | | } |
| | | else |
| | | { |
| | | // 取消之前的异步任务,避免多次调用导致错乱 |
| | | CancelLoadTask(); |
| | | // 创建新的取消令牌 |
| | | loadCancellationToken = new CancellationTokenSource(); |
| | | // 使用 UniTask 进行异步初始化,将instanceGO创建和资源加载都移到异步处理 |
| | | DelayedInitializeAsync(skinConfig, motionName).Forget(); |
| | | DelayedInitializeAsync(skinConfig, motionName, loadCancellationToken.Token).Forget(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 取消之前的加载任务 |
| | | /// </summary> |
| | | private void CancelLoadTask() |
| | | { |
| | | if (loadCancellationToken != null && !loadCancellationToken.IsCancellationRequested) |
| | | { |
| | | loadCancellationToken.Cancel(); |
| | | loadCancellationToken.Dispose(); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | protected void OnDestroy() |
| | | { |
| | | // 取消正在进行的加载任务 |
| | | CancelLoadTask(); |
| | | |
| | | if (spineAnimationState != null) |
| | | { |
| | | spineAnimationState.Complete -= OnAnimationComplete; |
| | |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 延迟初始化,避免批量创建时卡顿 - 使用 UniTask |
| | | /// 延迟初始化,结合并发控制和分帧延迟 |
| | | /// 1. 资源加载使用真正的异步(LoadAssetAsync) |
| | | /// 2. skeletonGraphic.Initialize() 前进行分帧延迟,避免主线程卡顿 |
| | | /// </summary> |
| | | private async UniTaskVoid DelayedInitializeAsync(HeroSkinConfig skinConfig, string motionName) |
| | | private async UniTaskVoid DelayedInitializeAsync(HeroSkinConfig skinConfig, string motionName, CancellationToken cancellationToken) |
| | | { |
| | | isInitializing = true; |
| | | |
| | | try |
| | | { |
| | | // 增加当前初始化计数器 |
| | | int currentIndex; |
| | | lock (initializationLock) |
| | | // 检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | // 获取加载信号量 - 限制并发数,避免资源竞争 |
| | | await AcquireLoadSlotAsync(cancellationToken); |
| | | |
| | | // 异步创建instanceGO和加载资源(真正的异步,不阻塞) |
| | | await CreateInstanceAndLoadAssetsAsync(skinConfig, isLh: false, cancellationToken); |
| | | |
| | | // 获取当前序号用于分帧延迟 |
| | | int myOrder; |
| | | lock (loadLock) |
| | | { |
| | | currentIndex = activeInitializationCount++; |
| | | myOrder = initializationOrder++; |
| | | } |
| | | |
| | | // 根据当前初始化序号计算延迟时间 |
| | | int delayFrames = Mathf.Min(currentIndex*2, 60); |
| | | // 再次检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | for (int i = 0; i < delayFrames; i++) |
| | | { |
| | | await UniTask.NextFrame(); |
| | | } |
| | | // 在 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); |
| | | } |
| | | } |
| | | |
| | | // 异步创建instanceGO和加载资源 |
| | | await CreateInstanceAndLoadAssetsAsync(skinConfig, isLh: false); |
| | | // 再次检查是否已被取消(可能在延迟期间被取消) |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | if (skeletonGraphic == null || skeletonGraphic.skeletonDataAsset == null) |
| | | { |
| | | Debug.LogError("资源加载失败,无法初始化模型"); |
| | | return; |
| | | } |
| | | if (skeletonGraphic == null || skeletonGraphic.skeletonDataAsset == null) |
| | | { |
| | | Debug.LogError("资源加载失败,无法初始化模型"); |
| | | return; |
| | | } |
| | | |
| | | skeletonGraphic.initialSkinName = skinConfig.InitialSkinName; |
| | | skeletonGraphic.Initialize(true); |
| | | skeletonGraphic.initialSkinName = skinConfig.InitialSkinName; |
| | | skeletonGraphic.Initialize(true); |
| | | |
| | | // 初始化完成后设置皮肤 |
| | | if (!string.IsNullOrEmpty(skinConfig.InitialSkinName)) |
| | |
| | | skeletonGraphic.Update(0); |
| | | } |
| | | |
| | | |
| | | spineAnimationState = skeletonGraphic.AnimationState; |
| | | spineAnimationState.Data.DefaultMix = 0f; |
| | | |
| | | |
| | | // 初始化完成后才显示模型 |
| | | skeletonGraphic.enabled = pendingEnabled; |
| | | |
| | | |
| | | if (pendingGray) |
| | | { |
| | | skeletonGraphic.material = MaterialUtility.GetDefaultSpriteGrayMaterial(); |
| | |
| | | { |
| | | skeletonGraphic.material = null; |
| | | } |
| | | |
| | | |
| | | // 检查是否有待设置的速度,如果有则设置 |
| | | if (pendingSpeed.HasValue) |
| | | { |
| | | spineAnimationState.TimeScale = pendingSpeed.Value; |
| | | pendingSpeed = null; // 清除待设置速度 |
| | | pendingSpeed = null; |
| | | } |
| | | |
| | | |
| | | // 检查是否有待播放的动画,如果有则优先播放外部调用的动画 |
| | | if (!string.IsNullOrEmpty(pendingAnimationName)) |
| | | { |
| | | // 临时设置isInitializing为false,以便PlayAnimation能正常播放 |
| | | isInitializing = false; |
| | | PlayAnimation(pendingAnimationName, pendingAnimationLoop, pendingAnimationReplay); |
| | | // 清除所有待播放动画参数 |
| | |
| | | // 如果没有外部调用的动画,播放默认动画 |
| | | if (motionName == "") |
| | | motionName = GetFistSpineAnim(); |
| | | |
| | | // 临时设置isInitializing为false,以便PlayAnimation能正常播放 |
| | | |
| | | isInitializing = false; |
| | | PlayAnimation(motionName, true); |
| | | } |
| | | |
| | | |
| | | spineAnimationState.Complete -= OnAnimationComplete; |
| | | spineAnimationState.Complete += OnAnimationComplete; |
| | | |
| | |
| | | } |
| | | catch (System.OperationCanceledException) |
| | | { |
| | | // 任务被取消,正常处理 |
| | | // 任务被取消,正常返回 |
| | | isInitializing = false; |
| | | } |
| | | catch (System.Exception e) |
| | | { |
| | | Debug.LogError($"英雄初始化异常: {e.Message}"); |
| | | isInitializing = false; |
| | | } |
| | | finally |
| | | { |
| | | // 减少当前初始化计数器 |
| | | lock (initializationLock) |
| | | // 释放加载槽位 |
| | | ReleaseLoadSlot(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取加载槽位(支持 WebGL 的并发控制) |
| | | /// </summary> |
| | | private async UniTask AcquireLoadSlotAsync(CancellationToken cancellationToken) |
| | | { |
| | | while (true) |
| | | { |
| | | // 检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | lock (loadLock) |
| | | { |
| | | activeInitializationCount--; |
| | | 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) |
| | | 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(); |
| | |
| | | } |
| | | |
| | | skeletonGraphic = instanceGO.GetComponentInChildren<SkeletonGraphic>(true); |
| | | |
| | | // 在主线程中加载资源,避免Unity API线程安全问题 |
| | | // 使用UniTask.Yield()来确保在主线程中执行 |
| | | await UniTask.Yield(); |
| | | |
| | | if (isLh) |
| | | |
| | | // 真正的异步加载资源 - 不阻塞主线程 |
| | | string assetName = isLh ? skinConfig.Tachie : skinConfig.SpineRes; |
| | | SkeletonDataAsset loadedAsset = await ResManager.Instance.LoadAssetAsync<SkeletonDataAsset>("Hero/SpineRes/", assetName); |
| | | |
| | | // 再次检查是否已被取消 |
| | | cancellationToken.ThrowIfCancellationRequested(); |
| | | |
| | | if (loadedAsset != null) |
| | | { |
| | | skeletonGraphic.skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("Hero/SpineRes/", skinConfig.Tachie); |
| | | skeletonGraphic.skeletonDataAsset = loadedAsset; |
| | | } |
| | | else |
| | | { |
| | | skeletonGraphic.skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("Hero/SpineRes/", skinConfig.SpineRes); |
| | | } |
| | | |
| | | if (skeletonGraphic.skeletonDataAsset == null) |
| | | { |
| | | transform.SetActive(false); |
| | | if (pool != null) |