yyl
2026-02-11 3f2cd27c5dfb3b450245bf1a37fc1b3414031c7c
小游戏适配 资源系统改造
79个文件已修改
36个文件已添加
5235 ■■■■■ 已修改文件
Main/Config/ConfigManager.cs 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/GameEngine/Launch/AssetBundleInitTask.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/GameEngine/Launch/BuiltInAssetCopyTask.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/GameEngine/Launch/LaunchInHot.cs 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/GameEngine/Launch/YooAssetInitTask.cs 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/GameEngine/Launch/YooAssetInitTask.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/ResModule/ScriptableObjectLoader.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Editor.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Editor/WebGLBuildOptimizer.cs 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Editor/WebGLBuildOptimizer.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Main.asmdef 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Manager/StageManager.cs 119 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Manager/UIManager.cs 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/AssetBundle/AssetBundleUtility.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/AudioLoader.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/BuiltInLoader.cs 110 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/IResourceCache.cs 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/IResourceCache.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/IResourcePreloader.cs 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/IResourcePreloader.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/IYooAssetService.cs 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/IYooAssetService.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/PlatformCacheHelper.cs 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/PlatformCacheHelper.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/RemoteServicesImpl.cs 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/RemoteServicesImpl.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/ResManager.cs 206 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/ResourceCacheManager.cs 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/ResourceCacheManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/ResourcePreloader.cs 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/ResourcePreloader.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/UILoader.cs 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/YooAssetPackageConfig.cs 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/YooAssetPackageConfig.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/YooAssetService.cs 677 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/ResModule/YooAssetService.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Arena/ArenaHeroHead.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/AssetVersion/DownLoadAndDiscompressHotTask.cs 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Attribute/TotalAttributeWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/AsyncResourceGuard.cs 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/AsyncResourceGuard.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/BattleField.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/StoryBattleField.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleHUDWin.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleLoadingWin.cs 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleLoadingWin.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleManager.cs 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleObject/BattleObjectFactory.cs 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/SkillEffect/SkillEffectFactory.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Sound/BattleSoundManager.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/UIComp/BossHeadCell.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/UIComp/SkillTips.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/UIComp/TotalDamageDisplayer.cs 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/BattleDetail/BattleDetailHeroInfoItem.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Debug/DebugUtility.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Equip/EquipExchangeCell.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Equip/EquipTipWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Equip/FloorItemCell.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Gubao/GubaoCallCell.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Gubao/GubaoCallWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Guild/GuildBaseWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Guild/GuildBossWin.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Guild/GuildManager.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HappyXB/HeroCallResultCell.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HappyXB/HeroCallResultWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Hero/UIHeroController.cs 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroBestWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroConnectionHeadCell.cs 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroGiftWashWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroHeadBaseCell.cs 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroHeadBaseNoTrainCell.cs 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroPosWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroScenePosCell.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroSkillWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroUI/HeroTrainWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Horse/HorseController.cs 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/InternalAffairs/AffairBaseWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/InternalAffairs/GoldRushLeader.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/InternalAffairs/GoldRushTentCell.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ItemTip/SmallTipWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/KnapSack/Logic/CommonGetItemWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/LineupRecommend/LineupRecommendItem.cs 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Login/LoginWin.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Main/EquipOnMainUI.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Main/HeroFightingCardCell.cs 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Main/HomeWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Message/ImgAnalysis.cs 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Message/RichText.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/NewBieGuidance/NewBieWin.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/OtherPlayerDetail/OtherEquipTipWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/OtherPlayerDetail/OtherHeroDetailWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/OtherPlayerDetail/OtherHeroFightingCardItem.cs 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/OtherPlayerDetail/OtherNPCDetailWin.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/PhantasmPavilion/PhantasmPavilionManager.cs 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/PhantasmPavilion/PhantasmPavilionModelItem.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Recharge/PrivilegeActiveCardWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Sound/SoundPlayer.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/TianziBillborad/TianziBillboradBossHead.cs 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/ScrollTip.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Tip/ScrollTipWin.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/UIBase/UIBase.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/Main.Tests.asmdef 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/Main.Tests.asmdef.meta 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/ResourceCacheManagerTests.cs 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/ResourceCacheManagerTests.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/ResourcePreloaderTests.cs 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/ResourcePreloaderTests.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/YooAssetServiceTests.cs 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Tests/YooAssetServiceTests.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/ComponentExtersion.cs 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/FontUtility.cs 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/MaterialUtility.cs 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/ShaderUtility.cs 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/UIUtility.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Config/ConfigManager.cs
@@ -22,7 +22,7 @@
    public override void Init()
    {
        base.Init();
        InitConfigs();
        InitConfigs().Forget();
    }
    public virtual async UniTask InitConfigs()
@@ -181,7 +181,9 @@
        {
            configName = configName.Substring(0, configName.Length - 6);
        }
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback, use LoadConfigByTypeAsync
        string[] texts = ResManager.Instance.LoadConfig(configName);
        #pragma warning restore CS0618
        if (texts != null)
        {
            string[] lines = texts;
@@ -208,11 +210,47 @@
        }
    }
    /// <summary>
    /// US2: Async variant of LoadConfigByType. Uses UniTask-based config loading.
    /// </summary>
    public async UniTask LoadConfigByTypeAsync(Type configType)
    {
        string configName = configType.Name;
        if (configName.EndsWith("Config"))
        {
            configName = configName.Substring(0, configName.Length - 6);
        }
        string[] texts = await ResManager.Instance.LoadConfigAsync(configName);
        if (texts != null)
        {
            var methodInfo = configType.GetMethod("Init", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.FlattenHierarchy);
            if (methodInfo != null)
            {
                methodInfo.Invoke(null, new object[] { texts });
                var isInitField = configType.GetField("isInit", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
                if (isInitField != null)
                {
                    isInitField.SetValue(null, true);
                }
            }
            else
            {
                Debug.LogError($"配置类 {configType.Name} 没有静态Init方法");
            }
        }
        else
        {
            Debug.LogError($"找不到配置文件: {configName}");
        }
    }
    private async UniTask LoadConfig<T>() where T : class
    {
        string configName = typeof(T).Name;
        #pragma warning disable CS0618
        string[] texts = ResManager.Instance.LoadConfig(configName);
        #pragma warning restore CS0618
        if (texts != null)
        {
            string[] lines = texts;
@@ -453,7 +491,9 @@
            if (configName.EndsWith("Config"))
                configName = configName.Substring(0, configName.Length - 6);
            #pragma warning disable CS0618
            string[] texts = ResManager.Instance.LoadConfig(configName);
            #pragma warning restore CS0618
            if (texts != null)
            {
                string[] lines = texts;
Main/Core/GameEngine/Launch/AssetBundleInitTask.cs
@@ -2,6 +2,12 @@
using System.Collections.Generic;
using System.IO;
using UnityEngine;
/// <summary>
/// [OBSOLETE] 已被 YooAssetInitTask 替代。
/// 此类不再在启动流水线中使用。保留仅供历史参考。
/// </summary>
[Obsolete("Replaced by YooAssetInitTask. This class is no longer in the startup pipeline.")]
public class AssetBundleInitTask : LaunchTask
{
    public override float expectTime
Main/Core/GameEngine/Launch/BuiltInAssetCopyTask.cs
@@ -54,7 +54,8 @@
    {
        if (AssetSource.isUseAssetBundle)
        { 
            AssetBundleUtility.Instance.InitBuiltInAsset();
            // YooAsset 已在 Launch 阶段初始化内置资源,不再需要 AssetBundleUtility.InitBuiltInAsset()
            // YooAssetInitializer.Instance.DefaultPackage 已包含内置资源
            LaunchInHot.Instance.InitSystemMgr();
Main/Core/GameEngine/Launch/LaunchInHot.cs
@@ -48,7 +48,7 @@
        var getVersionInfoTask = new GetVersionInfoTask();
        var checkAssetValidTask = new CheckAssetValidTask();
        var downLoadAssetTask = new DownLoadAssetTask();
        var assetBundleInitTask = new AssetBundleInitTask();
        // AssetBundleInitTask removed — replaced by YooAssetInitTask
        var configInitTask = new ConfigInitTask();
        var launchFadeOutTask = new LaunchFadeOutTask();
@@ -85,7 +85,10 @@
        tasks.Enqueue(checkAssetValidTask);
        tasks.Enqueue(downLoadAssetTask);
        tasks.Enqueue(assetBundleInitTask);
        // US1: Add YooAsset initialization task — replaces AssetBundleInitTask
        var yooAssetInitTask = new YooAssetInitTask();
        tasks.Enqueue(yooAssetInitTask);
        // AssetBundleInitTask removed — YooAssetInitTask handles all resource system initialization
        
        tasks.Enqueue(configInitTask);
        tasks.Enqueue(launchFadeOutTask);
Main/Core/GameEngine/Launch/YooAssetInitTask.cs
New file
@@ -0,0 +1,96 @@
// ============================================================================
// YooAssetInitTask.cs — YooAsset 初始化启动任务
// 在 LaunchInHot 的启动流水线中初始化 YooAsset,与 AssetBundleInitTask 并行/替代
// T013: Register YooAssetService as IYooAssetBridge
// ============================================================================
using Cysharp.Threading.Tasks;
using ProjSG.Resource;
using UnityEngine;
using YooAsset;
public class YooAssetInitTask : LaunchTask
{
    private bool _initStarted = false;
    private bool _initCompleted = false;
    public override float expectTime
    {
        get { return LocalSave.GetFloat("YooAssetInitTask_ExpectTime", 1f); }
        protected set { LocalSave.SetFloat("YooAssetInitTask_ExpectTime", value); }
    }
    public override void Begin()
    {
        LaunchInHot.m_CurrentStage = LaunchStage.AssetBundleInit;
        duration = Mathf.Max(0.5f, expectTime);
        if (!_initStarted)
        {
            _initStarted = true;
            RunInitAsync().Forget();
        }
    }
    private async UniTaskVoid RunInitAsync()
    {
        try
        {
            // Determine play mode based on AssetSource setting
            EPlayMode playMode;
            if (!AssetSource.isUseAssetBundle)
            {
#if UNITY_EDITOR
                playMode = EPlayMode.EditorSimulateMode;
#else
                playMode = EPlayMode.OfflinePlayMode;
#endif
            }
            else
            {
                playMode = EPlayMode.HostPlayMode;
            }
            // Initialize YooAssetService
            await YooAssetService.Instance.InitializeAsync(playMode);
            // Register as IYooAssetBridge for Launch assembly cross-assembly access
            YooAssetBridgeHolder.Register(YooAssetService.Instance);
            // US4 T042: Register default preload configs and execute StartupEssential preload
            ResourcePreloader.Instance.RegisterDefaultConfigs();
            await ResourcePreloader.Instance.PreloadAsync("StartupEssential");
            Debug.Log("[YooAssetInitTask] YooAssetService initialized, bridge registered, StartupEssential preloaded.");
            _initCompleted = true;
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"[YooAssetInitTask] Failed: {ex}");
            _initCompleted = true; // Mark done even on failure to avoid blocking pipeline
        }
    }
    public override void Update()
    {
        timer += Time.deltaTime;
        if (_initCompleted)
        {
            done = true;
            progress = 1f;
        }
        else
        {
            progress = Mathf.Clamp01(timer / duration);
        }
        ExceptionReport();
    }
    public override void End()
    {
        expectTime = timer;
        Debug.LogFormat("{0}执行时长:{1};", GetType().Name, timer);
    }
}
Main/Core/GameEngine/Launch/YooAssetInitTask.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 05b7ea8c552919b4fa4463756adbd74c
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Core/ResModule/ScriptableObjectLoader.cs
@@ -1,5 +1,7 @@
using UnityEngine;
using System;
using Cysharp.Threading.Tasks;
using ProjSG.Resource;
#if UNITY_EDITOR
using UnityEditor;
@@ -32,8 +34,8 @@
        else
        {
            var assetName = StringUtility.Concat(SoNewBieGuide_Suffix, _id.ToString());
            var assetInfo = new AssetInfo(bundleName, assetName);
            config = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo) as NewBieGuideScriptableObject;
            var assetPath = StringUtility.Concat("Assets/ResourcesOut/ScriptableObject/NewBieGuide/", assetName);
            config = YooAssetService.Instance.LoadAssetSync<NewBieGuideScriptableObject>(assetPath);
        }
        if (config == null)
@@ -44,5 +46,35 @@
        return config;
    }
    public static async UniTask<NewBieGuideScriptableObject> LoadSoNewBieGuideStepAsync(int _id)
    {
        NewBieGuideScriptableObject config = null;
        if (!AssetSource.isUseAssetBundle)
        {
#if UNITY_EDITOR
            var resourcePath = StringUtility.Concat(ResourcesPath.ResourcesOutAssetPath,
                                                   "ScriptableObject/NewBieGuide/",
                                                   SoNewBieGuide_Suffix,
                                                   _id.ToString(),
                                                   ".asset");
            config = AssetDatabase.LoadAssetAtPath<NewBieGuideScriptableObject>(resourcePath);
#endif
        }
        else
        {
            var assetName = StringUtility.Concat(SoNewBieGuide_Suffix, _id.ToString());
            var assetPath = StringUtility.Concat("Assets/ResourcesOut/ScriptableObject/NewBieGuide/", assetName);
            config = await YooAssetService.Instance.LoadAssetAsync<NewBieGuideScriptableObject>(assetPath);
        }
        if (config == null)
        {
            Debug.LogErrorFormat("ScriptableObjectLoader.LoadSoNewBieGuideStepAsync() => 加载不到资源: {0}.", _id);
        }
        return config;
    }
    
}
Main/Editor.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7424e48c56909d94b813a858d3dc9091
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Editor/WebGLBuildOptimizer.cs
New file
@@ -0,0 +1,78 @@
#if UNITY_EDITOR
// ============================================================================
// WebGLBuildOptimizer.cs — WebGL 构建优化设置
// US5 T048: 配置 IL2CPP 裁剪、压缩等以确保首包 ≤ 4MB
// ============================================================================
using UnityEditor;
using UnityEngine;
namespace ProjSG.Resource.Editor
{
    /// <summary>
    /// WebGL 构建优化配置工具。
    /// 通过菜单 "ProjSG/Build/Apply WebGL Optimizations" 应用。
    /// </summary>
    public static class WebGLBuildOptimizer
    {
        [MenuItem("ProjSG/Build/Apply WebGL Optimizations")]
        public static void ApplyOptimizations()
        {
            // IL2CPP code stripping
            PlayerSettings.stripEngineCode = true;
            // Managed stripping level — High for smallest build
            PlayerSettings.SetManagedStrippingLevel(BuildTargetGroup.WebGL, ManagedStrippingLevel.High);
            // WebGL compression — Brotli for smallest size
            PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli;
            // Decompression fallback — enable for broader server compatibility
            PlayerSettings.WebGL.decompressionFallback = true;
            // Data caching — enable for faster subsequent loads
            PlayerSettings.WebGL.dataCaching = true;
            // Exception support — minimal for smaller build
            PlayerSettings.WebGL.exceptionSupport = WebGLExceptionSupport.None;
            // WebGL template — Minimal
            PlayerSettings.WebGL.template = "APPLICATION:Minimal";
            // Memory size — reasonable default  (MB)
            PlayerSettings.WebGL.memorySize = 256;
            Debug.Log("[WebGLBuildOptimizer] Applied WebGL build optimizations for ≤ 4MB first package.");
        }
        [MenuItem("ProjSG/Build/Validate First Package Size")]
        public static void ValidateFirstPackageSize()
        {
            string buildPath = "Builds/WebGL";
            if (!System.IO.Directory.Exists(buildPath))
            {
                Debug.LogWarning($"[WebGLBuildOptimizer] Build directory not found: {buildPath}. Build first, then validate.");
                return;
            }
            long totalBytes = 0;
            var files = System.IO.Directory.GetFiles(buildPath, "*", System.IO.SearchOption.AllDirectories);
            foreach (var file in files)
            {
                var info = new System.IO.FileInfo(file);
                totalBytes += info.Length;
                // Log large files
                if (info.Length > 500 * 1024) // > 500KB
                {
                    Debug.Log($"  Large file: {info.Name} = {info.Length / 1024f / 1024f:F2} MB");
                }
            }
            float totalMB = totalBytes / 1024f / 1024f;
            string status = totalMB <= 4f ? "PASS" : "FAIL";
            Debug.Log($"[WebGLBuildOptimizer] First package total: {totalMB:F2} MB — {status} (target ≤ 4MB)");
        }
    }
}
#endif
Main/Editor/WebGLBuildOptimizer.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 468b960b2f11c3e4e9fc575cd3653289
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Main.asmdef
@@ -11,7 +11,9 @@
        "GUID:05d41852e29aa5141a64e3d2d5339981",
        "GUID:9ad05b610be6c974590152128a8b5b6e",
        "GUID:d51b17ee17bf72443860693b4f9c20af",
        "GUID:04376767bc1f3b428aefa3d20743e819"
        "GUID:04376767bc1f3b428aefa3d20743e819",
        "GUID:e34a5702dd353724aa315fb8011f08c3",
        "GUID:1278a46ce459c5a46b4eaeda148684ef"
    ],
    "includePlatforms": [],
    "excludePlatforms": [],
Main/Manager/StageManager.cs
@@ -3,6 +3,7 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
using ProjSG.Resource;
public enum StageName
{
@@ -37,14 +38,39 @@
    {
        UIManager.Instance.DestroyAllUI();
        // US3: Show loading screen FIRST, then load resources with progress
        LoadingWin loadingWin = UIManager.Instance.OpenWindow<LoadingWin>();
        InitLoadingWinData(loadingWin);
        // Phase 1 (0% ~ 30%): YooAsset resource preload
        if (AssetSource.isUseAssetBundle)
        {
            AssetBundleUtility.Instance.Sync_LoadAll("maps/Login");
            loadingWin.SetProgress(0.05f);
            await YooAssetService.Instance.LoadAllAssetsAsync<UnityEngine.Object>("Assets/ResourcesOut/maps/Login");
            loadingWin.SetProgress(0.3f);
        }
        // Phase 2 (30% ~ 60%): Scene loading
        AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("Login");
        asyncOperation.allowSceneActivation = false;
        await OnLoading(asyncOperation, ConfigManager.Instance.GetLoadingProgress, Main.InitManagers);
        while (!asyncOperation.isDone)
        {
            if (asyncOperation.progress >= 0.9f)
            {
                asyncOperation.allowSceneActivation = true;
            }
            loadingWin.SetProgress(0.3f + asyncOperation.progress * 0.3f);
            await UniTask.Yield();
        }
        // Phase 3 (60% ~ 100%): Manager initialization
        await WaitForManagerProgress(loadingWin, 0.6f, 1.0f,
            ConfigManager.Instance.GetLoadingProgress, Main.InitManagers);
        loadingWin.SetProgress(1f, true);
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        loadingWin.CloseWindow();
        Main.OnSwitchToLoginScene();
@@ -52,7 +78,6 @@
        UIManager.Instance.OpenWindow<LaunchBackGroundWin>();
        UIManager.Instance.OpenWindow<LoginWin>();
        // SoundPlayer.Instance.StopBackGroundMusic();
        if (VersionUtility.Instance.NeedDownAsset() && !AssetVersionUtility.hasDownLoadFullAsset)
        {
@@ -111,15 +136,41 @@
        
        BeforeLoadingGameScene?.Invoke();
        // ResManager.Instance.PrewarmResources();
        // US3: Show loading screen FIRST, then load resources with progress
        LoadingWin loadingWin = UIManager.Instance.OpenWindow<LoadingWin>();
        InitLoadingWinData(loadingWin);
        // Phase 1 (0% ~ 30%): YooAsset resource preload
        if (AssetSource.isUseAssetBundle)
        {
            AssetBundleUtility.Instance.Sync_LoadAll("maps/Game");
            loadingWin.SetProgress(0.05f);
            await YooAssetService.Instance.LoadAllAssetsAsync<UnityEngine.Object>("Assets/ResourcesOut/maps/Game");
            loadingWin.SetProgress(0.3f);
        }
        SoundPlayer.Instance.StopBackGroundMusic();
        AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("Game");
        await OnLoading(asyncOperation, () => (DTC0403_tagPlayerLoginLoadOK.finishedLogin ? .5f : 0f) + GetManagerRequestDataProgress() * .5f);
        SoundPlayer.Instance.StopBackGroundMusic();
        // Phase 2 (30% ~ 60%): Scene loading
        AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("Game");
        asyncOperation.allowSceneActivation = false;
        while (!asyncOperation.isDone)
        {
            if (asyncOperation.progress >= 0.9f)
            {
                asyncOperation.allowSceneActivation = true;
            }
            loadingWin.SetProgress(0.3f + asyncOperation.progress * 0.3f);
            await UniTask.Yield();
        }
        // Phase 3 (60% ~ 100%): Manager data ready
        await WaitForManagerProgress(loadingWin, 0.6f, 1.0f,
            () => (DTC0403_tagPlayerLoginLoadOK.finishedLogin ? .5f : 0f) + GetManagerRequestDataProgress() * .5f);
        loadingWin.SetProgress(1f, true);
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        loadingWin.CloseWindow();
        //  加载初始化数据完成
        currentStage = StageName.Game;
@@ -150,18 +201,7 @@
        asyncOperation.allowSceneActivation = false;
        LoadingWin loadingWin = UIManager.Instance.OpenWindow<LoadingWin>();
        LaunchWin launchWin = UIManager.Instance.GetUI<LaunchWin>();
        if (null != launchWin && launchWin.IsActive() && launchWinData == null)
        {
            launchWinData = launchWin.GetData();
        }
        if (null != launchWinData)
        {
            loadingWin.SetData(launchWinData);
            launchWinData = null;
        }
        InitLoadingWinData(loadingWin);
        while (!asyncOperation.isDone)
        {
@@ -198,6 +238,45 @@
        loadingWin.CloseWindow();
    }
    /// <summary>
    /// US3: 等待Manager初始化进度并更新LoadingWin。
    /// </summary>
    private async UniTask WaitForManagerProgress(LoadingWin loadingWin, float startPct, float endPct,
        Func<float> getProgress, Func<UniTask> extraTask = null)
    {
        float managerProgress = getProgress();
        while (managerProgress < 1f)
        {
            loadingWin.SetProgress(startPct + managerProgress * (endPct - startPct));
            await UniTask.Yield();
            managerProgress = getProgress();
        }
        if (extraTask != null)
        {
            await extraTask();
        }
    }
    /// <summary>
    /// US3: 初始化 LoadingWin 数据(从 LaunchWin 继承背景等)。
    /// </summary>
    private void InitLoadingWinData(LoadingWin loadingWin)
    {
        LaunchWin launchWin = UIManager.Instance.GetUI<LaunchWin>();
        if (launchWin != null && launchWin.IsActive() && launchWinData == null)
        {
            launchWinData = launchWin.GetData();
        }
        if (launchWinData != null)
        {
            loadingWin.SetData(launchWinData);
            launchWinData = null;
        }
    }
    private void OnCloseWindow(UIBase closeUI)
    {
        if (closeUI is LaunchWin)
Main/Manager/UIManager.cs
@@ -4,6 +4,7 @@
using UnityEngine;
using System.Linq;
using DG.Tweening;
using Cysharp.Threading.Tasks;
/// <summary>
/// UI管理器 - 负责管理所有UI界面的显示、隐藏和层级
@@ -562,7 +563,9 @@
        }
        else
        {
            #pragma warning disable CS0618 // Obsolete — sync legacy fallback, use LoadUIResourceAsync
            prefab = ResManager.Instance.LoadAsset<GameObject>("UI", uiName);
            #pragma warning restore CS0618
        }
        // 检查预制体是否加载成功
@@ -617,6 +620,246 @@
    {
        return LoadUIResource(uiName) as T;
    }
    // ====================================================================
    // US2: Async variants — InitUIRootAsync, LoadUIResourceAsync, OpenWindowAsync
    // ====================================================================
    /// <summary>
    /// US2: 异步初始化 UI 根节点。
    /// </summary>
    public async UniTask InitUIRootAsync()
    {
        GameObject root = GameObject.Find("UIRoot");
        if (root == null)
        {
            var prefab = await BuiltInLoader.LoadPrefabAsync("UIRoot");
            root = GameObject.Instantiate(prefab);
            root.name = "UIRoot";
            if (root == null)
            {
                Debug.LogError("无法加载UI根节点");
                return;
            }
            GameObject.DontDestroyOnLoad(root);
        }
    }
    /// <summary>
    /// US2: 异步加载 UI 资源。
    /// </summary>
    private async UniTask<UIBase> LoadUIResourceAsync(string uiName)
    {
        GameObject prefab;
        if (uiName == "LaunchWin" || uiName == "DownLoadWin" || uiName == "RequestSecretWin" || uiName == "GameAgeWarnWin")
        {
            prefab = await BuiltInLoader.LoadPrefabAsync(uiName);
        }
        else
        {
            prefab = await ResManager.Instance.LoadAssetAsync<GameObject>("UI", uiName);
        }
        if (prefab == null)
        {
            Debug.LogError($"加载UI预制体失败: {uiName}");
            return null;
        }
        GameObject uiObject = GameObject.Instantiate(prefab);
        uiObject.name = uiName;
        Type uiType = Type.GetType(uiName);
        if (uiType == null)
        {
            Debug.LogError($"找不到UI类型: {uiName}");
            return null;
        }
        UIBase uiBase = uiObject.GetComponent(uiType) as UIBase;
        if (uiBase == null)
        {
            Debug.LogError($"UI预制体 {uiName} 没有 UIBase 组件或类型不匹配");
            return null;
        }
        uiBase.uiName = uiName;
        Transform parentTrans = GetTransForLayer(uiBase.uiLayer);
        uiObject.transform.SetParent(parentTrans, false);
        int baseSortingOrder = GetBaseSortingOrderForLayer(uiBase.uiLayer);
        uiBase.SetSortingOrder(baseSortingOrder);
        return uiBase;
    }
    /// <summary>
    /// US2: 异步打开窗口。
    /// </summary>
    public async UniTask<UIBase> OpenWindowAsync(string uiName, int functionOrder = 0)
    {
        UIBase returnValue = null;
        UIBase parentUI = null;
        // Check closed cache
        if (closedUIDict.TryGetValue(uiName, out var closedUIList) && closedUIList.Count > 0)
        {
            returnValue = closedUIList[0] as UIBase;
            closedUIList.RemoveAt(0);
            if (closedUIList.Count == 0)
            {
                closedUIDict.Remove(uiName);
            }
        }
        else
        {
            // US3: Show loading indicator while loading UI prefab (auto-hide after load)
            ShowLoadingIndicator();
            try
            {
                returnValue = await LoadUIResourceAsync(uiName);
            }
            finally
            {
                HideLoadingIndicator();
            }
            if (returnValue == null)
            {
                Debug.LogError($"打开UI失败: {uiName}");
                return null;
            }
        }
        returnValue.gameObject.SetActive(true);
        if (returnValue.supportParentChildRelation && uiStack.Count > 0 && !returnValue.isMainUI)
        {
            parentUI = GetLastSupportParentChildRelationUI();
        }
        if (parentUI != null)
        {
            returnValue.parentUI = parentUI;
            if (parentUI.childrenUI == null)
            {
                parentUI.childrenUI = new List<UIBase>();
            }
            parentUI.childrenUI.Add(returnValue);
        }
        currentRound++;
        returnValue.lastUsedRound = currentRound;
        UpdateParentUIRounds(returnValue);
        if (!uiDict.ContainsKey(uiName))
        {
            uiDict[uiName] = new List<UIBase>();
        }
        uiDict[uiName].Add(returnValue);
        uiStack.Push(returnValue);
        UpdateUISortingOrder();
        returnValue.functionOrder = functionOrder;
        returnValue.HandleOpen();
        OnOpenWindow?.Invoke(returnValue);
        CheckAndCloseIdleUI();
        return returnValue;
    }
    /// <summary>
    /// US2: 泛型 异步打开窗口。
    /// </summary>
    public async UniTask<T> OpenWindowAsync<T>(int functionOrder = 0) where T : UIBase
    {
        string uiName = typeof(T).Name;
        var result = await OpenWindowAsync(uiName, functionOrder);
        return result as T;
    }
    // ====================================================================
    // US3: Loading indicator for async UI loading
    // ====================================================================
    private GameObject _loadingIndicatorGO;
    private int _loadingRefCount;
    /// <summary>
    /// US3: 显示加载指示器(引用计数,支持重入)。
    /// </summary>
    public void ShowLoadingIndicator()
    {
        _loadingRefCount++;
        if (_loadingRefCount == 1)
        {
            EnsureLoadingIndicator();
            if (_loadingIndicatorGO != null)
            {
                _loadingIndicatorGO.SetActive(true);
            }
        }
    }
    /// <summary>
    /// US3: 隐藏加载指示器。
    /// </summary>
    public void HideLoadingIndicator()
    {
        _loadingRefCount = Mathf.Max(0, _loadingRefCount - 1);
        if (_loadingRefCount == 0 && _loadingIndicatorGO != null)
        {
            _loadingIndicatorGO.SetActive(false);
        }
    }
    private void EnsureLoadingIndicator()
    {
        if (_loadingIndicatorGO != null) return;
        // 创建简易加载指示器: 半透明遮罩 + 旋转图标
        var canvas = uiRoot != null ? uiRoot.GetComponentInChildren<Canvas>() : null;
        if (canvas == null) return;
        _loadingIndicatorGO = new GameObject("UILoadingIndicator");
        _loadingIndicatorGO.transform.SetParent(canvas.transform, false);
        // 全屏半透明遮罩
        var maskRT = _loadingIndicatorGO.AddComponent<RectTransform>();
        maskRT.anchorMin = Vector2.zero;
        maskRT.anchorMax = Vector2.one;
        maskRT.offsetMin = Vector2.zero;
        maskRT.offsetMax = Vector2.zero;
        var maskImage = _loadingIndicatorGO.AddComponent<UnityEngine.UI.Image>();
        maskImage.color = new Color(0, 0, 0, 0.3f);
        maskImage.raycastTarget = true; // 拦截点击
        // 加载提示文字
        var textGO = new GameObject("LoadingText");
        textGO.transform.SetParent(_loadingIndicatorGO.transform, false);
        var textRT = textGO.AddComponent<RectTransform>();
        textRT.anchorMin = new Vector2(0.5f, 0.5f);
        textRT.anchorMax = new Vector2(0.5f, 0.5f);
        textRT.sizeDelta = new Vector2(300, 60);
        var text = textGO.AddComponent<UnityEngine.UI.Text>();
        text.text = "Loading...";
        text.alignment = TextAnchor.MiddleCenter;
        text.fontSize = 28;
        text.color = Color.white;
        text.font = UnityEngine.Font.CreateDynamicFontFromOSFont("Arial", 28);
        // 确保在最上层
        var sortCanvas = _loadingIndicatorGO.AddComponent<Canvas>();
        sortCanvas.overrideSorting = true;
        sortCanvas.sortingOrder = 30000;
        _loadingIndicatorGO.AddComponent<UnityEngine.UI.GraphicRaycaster>();
        _loadingIndicatorGO.SetActive(false);
    }
    
    #endregion
Main/ResModule/AssetBundle/AssetBundleUtility.cs
@@ -5,6 +5,11 @@
using UnityEngine;
using Cysharp.Threading.Tasks;
/// <summary>
/// [Obsolete] US1: 已被 YooAssetService 替代。将在 Phase 10 (T060) 物理删除。
/// 当前仍保留以支持 AssetBundleInitTask 的启动兼容性。
/// </summary>
[System.Obsolete("Use ProjSG.Resource.YooAssetService instead. This class will be removed in Phase 10 (T060).")]
public class AssetBundleUtility : SingletonMonobehaviour<AssetBundleUtility>
{
    private List<AssetBundleInfo> m_AssetBundleInfoList = new List<AssetBundleInfo>();
Main/ResModule/AudioLoader.cs
@@ -1,6 +1,8 @@
using UnityEngine;
using System.Collections;
using System;
using Cysharp.Threading.Tasks;
using System.Threading;
public class AudioLoader
{
@@ -14,5 +16,10 @@
        ResManager.Instance.LoadAssetAsync<AudioClip>("Audio/" + _folderName, _clipName, _callBack, false);
    }
    // US2: Async UniTask variant
    public static UniTask<AudioClip> LoadAudioAsync(string _folderName, string _clipName, CancellationToken ct = default)
    {
        return ResManager.Instance.LoadAssetAsync<AudioClip>("Audio/" + _folderName, _clipName, false, ct);
    }
}
Main/ResModule/BuiltInLoader.cs
@@ -2,6 +2,9 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.U2D;
using Cysharp.Threading.Tasks;
using System.Threading;
using ProjSG.Resource;
public class BuiltInLoader
{
@@ -32,13 +35,13 @@
        }
        else
        {
            //var assetInfo = new AssetInfo("builtin/sprites", "sprites");
            //var spriteAtlas = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo, typeof(SpriteAtlas)) as SpriteAtlas;
            //sprite = spriteAtlas?.GetSprite(name);
            //if (sprite == null)
            {
                var assetInfo = new AssetInfo("builtin/sprites", name);
                sprite = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo, typeof(Sprite)) as Sprite;
                // US1: Route through YooAssetService sync wrapper
                var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Sprites/", name, SPRITE_EXTENSION);
                #pragma warning disable CS0612
                sprite = YooAssetService.Instance.LoadAssetSync<Sprite>(path);
                #pragma warning restore CS0612
            }
        }
@@ -62,8 +65,11 @@
        }
        else
        {
            var assetInfo = new AssetInfo("builtin/prefabs", name);
            prefab = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo) as GameObject;
            // US1: Route through YooAssetService sync wrapper
            var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Prefabs/", name, PREFAB_EXTENSION);
            #pragma warning disable CS0612
            prefab = YooAssetService.Instance.LoadAssetSync<GameObject>(path);
            #pragma warning restore CS0612
        }
        if (prefab == null)
@@ -76,10 +82,7 @@
    public static void UnLoadPrefab(string name)
    {
        if (AssetSource.isUseAssetBundle)
        {
            AssetBundleUtility.Instance.UnloadAsset("builtin/prefabs", name);
        }
        // US1: No-op. YooAsset manages asset lifecycle via handle-based release.
    }
    public static AudioClip LoadMusic(string name)
@@ -94,8 +97,11 @@
        }
        else
        {
            var assetInfo = new AssetInfo("builtin/musics", name);
            audioClip = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo) as AudioClip;
            // US1: Route through YooAssetService sync wrapper
            var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Musics/", name, ".mp3");
            #pragma warning disable CS0612
            audioClip = YooAssetService.Instance.LoadAssetSync<AudioClip>(path);
            #pragma warning restore CS0612
        }
        if (audioClip == null)
@@ -118,8 +124,11 @@
        }
        else
        {
            var assetInfo = new AssetInfo("builtin/animationclips", name);
            clip = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo) as AnimationClip;
            // US1: Route through YooAssetService sync wrapper
            var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/AnimationClips/", name, ".anim");
            #pragma warning disable CS0612
            clip = YooAssetService.Instance.LoadAssetSync<AnimationClip>(path);
            #pragma warning restore CS0612
        }
        if (clip == null)
@@ -142,8 +151,11 @@
        }
        else
        {
            var assetInfo = new AssetInfo("builtin/materials", name);
            material = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo) as Material;
            // US1: Route through YooAssetService sync wrapper
            var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Materials/", name, ".mat");
            #pragma warning disable CS0612
            material = YooAssetService.Instance.LoadAssetSync<Material>(path);
            #pragma warning restore CS0612
        }
        if (material == null)
@@ -169,8 +181,12 @@
        }
        else
        {
            var assetInfo = new AssetInfo("builtin/scriptableobjects", name);
            config = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo) as T;
            // US1: Route through YooAssetService sync wrapper
            var path = StringUtility.Concat(ResourcesPath.ResourcesOutAssetPath,
                                           "BuiltIn/ScriptableObjects/", name, ".asset");
            #pragma warning disable CS0612
            config = YooAssetService.Instance.LoadAssetSync<T>(path);
            #pragma warning restore CS0612
        }
        if (config == null)
@@ -194,8 +210,12 @@
        }
        else
        {
            var assetInfo = new AssetInfo("builtin/font", fontName);
            font = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo, typeof(Font)) as Font;
            // US1: Route through YooAssetService sync wrapper
            var path = StringUtility.Concat(ResourcesPath.ResourcesOutAssetPath,
                                       "BuiltIn/Font/", fontName, ".ttf");
            #pragma warning disable CS0612
            font = YooAssetService.Instance.LoadAssetSync<Font>(path);
            #pragma warning restore CS0612
        }
        if (font == null)
@@ -206,5 +226,53 @@
        return font;
    }
    // ====================================================================
    // US2: Async UniTask variants
    // ====================================================================
    public static async UniTask<Sprite> LoadSpriteAsync(string name, CancellationToken ct = default)
    {
        var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Sprites/", name, SPRITE_EXTENSION);
        return await YooAssetService.Instance.LoadAssetAsync<Sprite>(path, ct: ct);
    }
    public static async UniTask<GameObject> LoadPrefabAsync(string name, CancellationToken ct = default)
    {
        var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Prefabs/", name, PREFAB_EXTENSION);
        return await YooAssetService.Instance.LoadAssetAsync<GameObject>(path, ct: ct);
    }
    public static async UniTask<AudioClip> LoadMusicAsync(string name, CancellationToken ct = default)
    {
        var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Musics/", name, ".mp3");
        return await YooAssetService.Instance.LoadAssetAsync<AudioClip>(path, ct: ct);
    }
    public static async UniTask<AnimationClip> LoadAnimationClipAsync(string name, CancellationToken ct = default)
    {
        var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/AnimationClips/", name, ".anim");
        return await YooAssetService.Instance.LoadAssetAsync<AnimationClip>(path, ct: ct);
    }
    public static async UniTask<Material> LoadMaterialAsync(string name, CancellationToken ct = default)
    {
        var path = StringUtility.Concat("Assets/ResourcesOut/BuiltIn/Materials/", name, ".mat");
        return await YooAssetService.Instance.LoadAssetAsync<Material>(path, ct: ct);
    }
    public static async UniTask<T> LoadScriptableObjectAsync<T>(string name, CancellationToken ct = default) where T : ScriptableObject
    {
        var path = StringUtility.Concat(ResourcesPath.ResourcesOutAssetPath,
                                       "BuiltIn/ScriptableObjects/", name, ".asset");
        return await YooAssetService.Instance.LoadAssetAsync<T>(path, ct: ct);
    }
    public static async UniTask<Font> LoadFontAsync(string fontName, CancellationToken ct = default)
    {
        var path = StringUtility.Concat(ResourcesPath.ResourcesOutAssetPath,
                                       "BuiltIn/Font/", fontName, ".ttf");
        return await YooAssetService.Instance.LoadAssetAsync<Font>(path, ct: ct);
    }
}
Main/ResModule/IResourceCache.cs
New file
@@ -0,0 +1,60 @@
// ============================================================================
// IResourceCache.cs — 资源缓存服务接口
// Feature: 001-async-resource-loading
// ============================================================================
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace ProjSG.Resource
{
    /// <summary>
    /// 全局资源缓存服务接口。
    /// 提供预加载后的同步缓存获取能力,以及异步加载+自动缓存能力。
    /// </summary>
    public interface IResourceCache
    {
        /// <summary>
        /// 缓存中的资源数量
        /// </summary>
        int CachedCount { get; }
        /// <summary>
        /// 同步获取已缓存的资源。
        /// 如果资源未在缓存中,返回 null(不会触发加载)。
        /// </summary>
        T GetCached<T>(string location) where T : UnityEngine.Object;
        /// <summary>
        /// 检查资源是否已在缓存中。
        /// </summary>
        bool IsCached(string location);
        /// <summary>
        /// 异步获取资源。缓存命中直接返回,未命中则加载并缓存。
        /// 同一资源的并发请求自动去重。
        /// </summary>
        UniTask<T> GetOrLoadAsync<T>(string location) where T : UnityEngine.Object;
        /// <summary>
        /// 批量预加载资源到缓存。
        /// </summary>
        UniTask PreloadAsync(string[] locations, bool permanent = false, IProgress<float> progress = null);
        /// <summary>
        /// 释放指定资源的缓存。常驻资源需 forceRelease=true。
        /// </summary>
        void Release(string location, bool forceRelease = false);
        /// <summary>
        /// 释放所有非常驻缓存资源。
        /// </summary>
        void ReleaseAll();
        /// <summary>
        /// 释放所有资源(含常驻)。
        /// </summary>
        void ForceReleaseAll();
    }
}
Main/ResModule/IResourceCache.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ce8cec774664004e85ea08f6557275c
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/IResourcePreloader.cs
New file
@@ -0,0 +1,60 @@
// ============================================================================
// IResourcePreloader.cs — 资源预加载服务接口
// Feature: 001-async-resource-loading
// ============================================================================
using System;
using Cysharp.Threading.Tasks;
namespace ProjSG.Resource
{
    /// <summary>
    /// 资源预加载服务接口。
    /// 按场景/流程组织批量资源预加载。
    /// </summary>
    public interface IResourcePreloader
    {
        /// <summary>
        /// 注册预加载配置。
        /// </summary>
        void RegisterConfig(PreloadConfig config);
        /// <summary>
        /// 执行指定预加载配置。
        /// </summary>
        UniTask PreloadAsync(string configName, IProgress<float> progress = null);
        /// <summary>
        /// 按资源标签批量预加载。
        /// </summary>
        UniTask PreloadByTagAsync(string tag, IProgress<float> progress = null);
        /// <summary>
        /// 卸载指定配置的资源(常驻配置不卸载)。
        /// </summary>
        void UnloadConfig(string configName);
        /// <summary>
        /// 检查配置是否已预加载完成。
        /// </summary>
        bool IsConfigLoaded(string configName);
    }
    /// <summary>
    /// 预加载配置数据。
    /// </summary>
    public class PreloadConfig
    {
        /// <summary>配置名称</summary>
        public string ConfigName { get; set; }
        /// <summary>需要预加载的资源地址列表</summary>
        public string[] Locations { get; set; }
        /// <summary>需要预加载的资源标签列表</summary>
        public string[] Tags { get; set; }
        /// <summary>是否为常驻资源(不允许卸载)</summary>
        public bool IsPermanent { get; set; }
    }
}
Main/ResModule/IResourcePreloader.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2307c27da18d094469bdbb03f7be2734
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/IYooAssetService.cs
New file
@@ -0,0 +1,164 @@
// ============================================================================
// IYooAssetService.cs — YooAsset 资源加载服务接口
// 定义在 Main 程序集中,供所有业务系统使用
// ============================================================================
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
namespace ProjSG.Resource
{
    /// <summary>
    /// YooAsset 资源加载服务接口。
    /// 封装 YooAsset ResourcePackage 的核心加载能力,提供 UniTask 异步 API。
    /// </summary>
    public interface IYooAssetService
    {
        /// <summary>
        /// 服务是否已初始化完成
        /// </summary>
        bool IsInitialized { get; }
        /// <summary>
        /// 当前运行模式
        /// </summary>
        EPlayMode PlayMode { get; }
        // ====================================================================
        // 初始化
        // ====================================================================
        /// <summary>
        /// 初始化 YooAsset 资源服务。
        /// 在 Launch 阶段由 YooAssetInitializer 调用。
        /// </summary>
        /// <param name="playMode">运行模式</param>
        /// <param name="remoteServices">CDN 远程服务(HostPlayMode/WebPlayMode 必需)</param>
        /// <returns>初始化完成</returns>
        UniTask InitializeAsync(EPlayMode playMode, IRemoteServices remoteServices = null);
        // ====================================================================
        // 资源加载 — Asset
        // ====================================================================
        /// <summary>
        /// 异步加载指定类型的资源。
        /// </summary>
        /// <typeparam name="T">资源类型(GameObject, Sprite, AudioClip 等)</typeparam>
        /// <param name="location">YooAsset 资源地址</param>
        /// <param name="priority">加载优先级(0 = 默认)</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>加载完成的资源对象,加载失败返回 null</returns>
        UniTask<T> LoadAssetAsync<T>(string location, uint priority = 0,
            CancellationToken ct = default) where T : UnityEngine.Object;
        /// <summary>
        /// 异步加载指定类型的资源(Type 参数版本)。
        /// </summary>
        UniTask<UnityEngine.Object> LoadAssetAsync(string location, Type type, uint priority = 0,
            CancellationToken ct = default);
        /// <summary>
        /// 异步加载子资源(如 SpriteAtlas 中的 Sprite)。
        /// </summary>
        UniTask<SubAssetsHandle> LoadSubAssetsAsync<T>(string location, uint priority = 0,
            CancellationToken ct = default) where T : UnityEngine.Object;
        /// <summary>
        /// 异步加载同一 Bundle 下的所有同类型资源。
        /// </summary>
        UniTask<AllAssetsHandle> LoadAllAssetsAsync<T>(string location, uint priority = 0,
            CancellationToken ct = default) where T : UnityEngine.Object;
        // ====================================================================
        // 资源加载 — RawFile
        // ====================================================================
        /// <summary>
        /// 异步加载原始文件并返回文本内容。
        /// 用于配置文件(.txt, .json, .csv 等)加载。
        /// </summary>
        UniTask<string> LoadRawFileTextAsync(string location, CancellationToken ct = default);
        /// <summary>
        /// 异步加载原始文件并返回字节数组。
        /// </summary>
        UniTask<byte[]> LoadRawFileBytesAsync(string location, CancellationToken ct = default);
        // ====================================================================
        // 场景加载
        // ====================================================================
        /// <summary>
        /// 异步加载场景。
        /// </summary>
        UniTask<SceneHandle> LoadSceneAsync(string location, LoadSceneMode sceneMode = LoadSceneMode.Single,
            LocalPhysicsMode physicsMode = LocalPhysicsMode.None, bool suspendLoad = false, uint priority = 0, CancellationToken ct = default);
        // ====================================================================
        // 资源信息查询
        // ====================================================================
        /// <summary>
        /// 检查资源地址是否有效。
        /// </summary>
        bool CheckLocationValid(string location);
        /// <summary>
        /// 获取指定标签的所有资源信息。
        /// </summary>
        YooAsset.AssetInfo[] GetAssetInfosByTag(string tag);
        /// <summary>
        /// 检查资源是否需要从远程下载。
        /// </summary>
        bool IsNeedDownloadFromRemote(string location);
        // ====================================================================
        // 资源下载
        // ====================================================================
        /// <summary>
        /// 创建资源下载器并开始下载。
        /// </summary>
        UniTask DownloadByTagsAsync(string[] tags, int downloadingMaxNumber = 10,
            int failedTryAgain = 3, IProgress<float> progress = null, CancellationToken ct = default);
        // ====================================================================
        // 版本管理
        // ====================================================================
        /// <summary>
        /// 请求最新包裹版本。
        /// </summary>
        UniTask<string> RequestPackageVersionAsync(CancellationToken ct = default);
        /// <summary>
        /// 更新包裹 Manifest 到指定版本。
        /// </summary>
        UniTask UpdatePackageManifestAsync(string packageVersion, CancellationToken ct = default);
        // ====================================================================
        // 资源释放
        // ====================================================================
        /// <summary>
        /// 释放资源句柄(引用计数 -1)。
        /// </summary>
        void ReleaseHandle(HandleBase handle);
        /// <summary>
        /// 卸载所有引用计数为零的资源。
        /// </summary>
        UniTask UnloadUnusedAssetsAsync();
        /// <summary>
        /// 强制卸载所有资源。
        /// </summary>
        UniTask UnloadAllAssetsAsync();
    }
}
Main/ResModule/IYooAssetService.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3361511557042814db2923ee333359a2
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/PlatformCacheHelper.cs
New file
@@ -0,0 +1,86 @@
// ============================================================================
// PlatformCacheHelper.cs — 平台文件系统缓存辅助
// 为各小游戏平台提供资源缓存路径和文件系统参数创建
// ============================================================================
using UnityEngine;
using YooAsset;
namespace ProjSG.Resource
{
    /// <summary>
    /// 平台资源缓存辅助工具。
    /// 为不同小游戏平台提供 YooAsset 文件系统参数创建和缓存路径获取。
    /// </summary>
    public static class PlatformCacheHelper
    {
        private const string CACHE_DIR_NAME = "__GAME_FILE_CACHE";
        /// <summary>
        /// 获取当前平台的资源缓存根路径。
        /// </summary>
        public static string GetCacheRootPath()
        {
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
            return $"{WeChatWASM.WX.env.USER_DATA_PATH}/{CACHE_DIR_NAME}";
#elif UNITY_WEBGL && DOUYINMINIGAME && !UNITY_EDITOR
            return $"{TTSDK.TTFileSystem.USER_DATA_PATH}/{CACHE_DIR_NAME}";
#else
            // Standalone / Editor / 其他移动端:使用 persistentDataPath
            return $"{Application.persistentDataPath}/{CACHE_DIR_NAME}";
#endif
        }
        /// <summary>
        /// 为当前平台创建 WebPlayMode 的 WebServerFileSystemParameters。
        /// 自动选择微信/抖音/默认 Web 文件系统。
        /// </summary>
        /// <param name="remoteServices">远程服务配置</param>
        /// <returns>文件系统参数</returns>
        public static FileSystemParameters CreateWebFileSystemParameters(IRemoteServices remoteServices)
        {
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
            string packageRoot = GetCacheRootPath();
            return WechatFileSystemCreater.CreateFileSystemParameters(packageRoot, remoteServices);
#elif UNITY_WEBGL && DOUYINMINIGAME && !UNITY_EDITOR
            string packageRoot = GetCacheRootPath();
            return TiktokFileSystemCreater.CreateFileSystemParameters(packageRoot, remoteServices);
#else
            // 默认 WebGL 文件系统
            return FileSystemParameters.CreateDefaultWebServerFileSystemParameters();
#endif
        }
        /// <summary>
        /// 获取缓存大小限制(MB)。
        /// 各平台有不同的存储限制。
        /// </summary>
        public static int GetCacheSizeLimitMB()
        {
            var platform = PlatformFactory.GetCurrent();
            switch (platform.GetPlatformType())
            {
                case PlatformType.WeChat:
                    return 200; // 微信小游戏用户数据上限约 200MB
                case PlatformType.Douyin:
                    return 200; // 抖音小游戏类似限制
                case PlatformType.Vivo:
                    return 100; // Vivo 限制较小
                default:
                    return 1024; // Standalone 无严格限制
            }
        }
        /// <summary>
        /// 检查当前平台是否支持文件系统缓存。
        /// </summary>
        public static bool IsCacheSupported()
        {
#if UNITY_WEBGL
            return true; // 所有小游戏平台都支持缓存
#else
            return true; // 移动端/桌面端也支持
#endif
        }
    }
}
Main/ResModule/PlatformCacheHelper.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 469fcba458bd2684681de1a15ba21655
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/RemoteServicesImpl.cs
New file
@@ -0,0 +1,90 @@
// ============================================================================
// RemoteServicesImpl.cs — IRemoteServices 实现
// 配合平台抽象层提供 CDN 地址,支持多平台 CDN 路由
// ============================================================================
using YooAsset;
namespace ProjSG.Resource
{
    /// <summary>
    /// YooAsset 远程服务实现。
    /// 提供主 CDN 和备用 CDN 的资源下载地址拼接。
    /// </summary>
    public class RemoteServicesImpl : IRemoteServices
    {
        private readonly string _mainUrl;
        private readonly string _fallbackUrl;
        /// <summary>
        /// 创建远程服务配置。
        /// </summary>
        /// <param name="mainUrl">主 CDN 根地址(如 https://cdn.example.com/bundles/)</param>
        /// <param name="fallbackUrl">备用 CDN 根地址</param>
        public RemoteServicesImpl(string mainUrl, string fallbackUrl)
        {
            _mainUrl = mainUrl;
            _fallbackUrl = fallbackUrl;
        }
        /// <summary>
        /// 获取主 CDN 下载地址。
        /// </summary>
        public string GetRemoteMainURL(string fileName)
        {
            return $"{_mainUrl}/{fileName}";
        }
        /// <summary>
        /// 获取备用 CDN 下载地址。
        /// </summary>
        public string GetRemoteFallbackURL(string fileName)
        {
            return $"{_fallbackUrl}/{fileName}";
        }
        /// <summary>
        /// 根据当前平台创建 RemoteServicesImpl。
        /// 通过 IPlatformService + PlatformFactory 自动路由到对应平台的 CDN。
        /// </summary>
        /// <param name="baseCdnUrl">基础 CDN 地址(如 https://cdn.example.com)</param>
        /// <param name="fallbackCdnUrl">备用 CDN 地址(可选,默认同 baseCdnUrl)</param>
        /// <returns>平台对应的 RemoteServicesImpl 实例</returns>
        public static RemoteServicesImpl CreateForCurrentPlatform(string baseCdnUrl, string fallbackCdnUrl = null)
        {
            fallbackCdnUrl = fallbackCdnUrl ?? baseCdnUrl;
            var platform = PlatformFactory.GetCurrent();
            var platformType = platform.GetPlatformType();
            // 按平台类型路由子路径
            string platformPath = GetPlatformCdnPath(platformType);
            string mainUrl = $"{baseCdnUrl}/{platformPath}";
            string fallbackUrl = $"{fallbackCdnUrl}/{platformPath}";
            UnityEngine.Debug.Log($"[RemoteServicesImpl] Platform={platformType}, CDN={mainUrl}");
            return new RemoteServicesImpl(mainUrl, fallbackUrl);
        }
        /// <summary>
        /// 获取平台对应的 CDN 子路径。
        /// </summary>
        private static string GetPlatformCdnPath(PlatformType platformType)
        {
            switch (platformType)
            {
                case PlatformType.WeChat:
                    return "wechat/bundles";
                case PlatformType.Douyin:
                    return "douyin/bundles";
                case PlatformType.Vivo:
                    return "vivo/bundles";
                // OPPO / Kuaishou 等可在此扩展
                case PlatformType.Standalone:
                default:
                    return "standalone/bundles";
            }
        }
    }
}
Main/ResModule/RemoteServicesImpl.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb5c16e6b126a2045866004b0e7eacc6
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/ResManager.cs
@@ -6,6 +6,9 @@
using UnityEngine.Video;
using Spine.Unity;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;
using ProjSG.Resource;
@@ -127,11 +130,11 @@
#endif
    //needExt 是否需要函数内部添加后缀
    [System.Obsolete("US2: Use LoadAssetAsync<T>(directory, name, needExt) returning UniTask<T> instead.")]
    public T LoadAsset<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
        name = name.Replace("\\", "/");
        T asset = null;
        //  特殊处理 因为有一层图集的关系 directory要传入的应该是atlas的名字
        if (typeof(T) == typeof(Sprite))
        {
@@ -165,34 +168,10 @@
        }
        else
        {
            if (!needExt)
            {
                //外部用到的自己加后缀,内部统一去除后缀名
                name = name.Substring(0, name.LastIndexOf("."));
            }
            //TODO: 临时特殊处理打包后的路径读取
            if (directory == "UI" || directory == "UIComp" || directory.StartsWith("Sprite")
            || directory == "Battle/Prefabs" || directory == "Materials")
            {
                directory = "UI/" + directory;
            }
            else if (name == "Hero_001")
            {
                directory = "UI/Hero/SpineRes";
            }
            else if (directory.Contains("Texture"))
            {
                directory = "maps/" + name;
            }
            else if (directory.Contains("Shader"))
            {
                directory = "graphic/shader";
            }
            var assetInfo = new AssetInfo(directory.ToLower(), name.ToLower());
            asset = AssetBundleUtility.Instance.Sync_LoadAsset(assetInfo, typeof(T)) as T;
            // US1: Route through YooAssetService sync wrapper (transitional)
            #pragma warning disable CS0612, CS0618
            asset = YooAssetService.Instance.LoadAssetSync<T>(path);
            #pragma warning restore CS0612, CS0618
        }
        if (asset == null)
@@ -203,6 +182,7 @@
        return asset;
    }
    [System.Obsolete("US2: Use LoadConfigAsync returning UniTask<string[]> instead.")]
    public string[] LoadConfig(string name)
    {
        string path = string.Empty;
@@ -224,7 +204,9 @@
    {
        if (!AssetSource.isUseAssetBundle)
        {
            #pragma warning disable CS0618 // Obsolete — legacy sync fallback
            SpriteAtlas atlas = LoadAsset<SpriteAtlas>("Sprite", atlasName.Replace("Sprite/", ""));
            #pragma warning restore CS0618
            if (null == atlas)
            {
                return null;
@@ -236,6 +218,7 @@
    }
    //needExt 是否需要函数内部添加后缀
    [System.Obsolete("US2: Use LoadAssetAsync<T>(directory, name, needExt) returning UniTask<T> instead.")]
    public void LoadAssetAsync<T>(string directory, string name, Action<bool, UnityEngine.Object> callBack, bool needExt = true) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
@@ -253,22 +236,19 @@
    private void LoadSpriteAsync<T>(string atlasName, string spriteName, Action<bool, UnityEngine.Object> callBack) where T : UnityEngine.Object
    {
#if !UNITY_EDITOR
        LoadAssetAsync<SpriteAtlas>(atlasName, spriteName, (isLoaded, atlas) => {
            if (isLoaded)
        if (!AssetSource.isUseAssetBundle)
        {
            // Editor 模式下可直接加载 sprite
            LoadAssetAsyncInternal<T>(atlasName, spriteName, callBack);
        }
        else
        {
            // AB 模式下直接加载单独的 Sprite 文件(YooAsset 自动处理 SpriteAtlas 依赖)
            LoadAssetAsyncInternal<Sprite>(atlasName, spriteName, (isLoaded, sprite) =>
            {
                SpriteAtlas _atlas = atlas as SpriteAtlas;
                callBack?.Invoke(isLoaded, _atlas.GetSprite(spriteName));
            }
            else
            {
                callBack?.Invoke(false, null);
            }
        });
#else
        //  编辑器下可以直接加载没啥问题
        LoadAssetAsyncInternal<T>(atlasName, spriteName, callBack);
#endif
                callBack?.Invoke(isLoaded, sprite);
            });
        }
    }
    private void LoadAssetAsyncInternal<T>(string directory, string name, Action<bool, UnityEngine.Object> callBack, bool needExt = true) where T : UnityEngine.Object
@@ -284,24 +264,36 @@
        }
        else
        {
            var assetInfo = new AssetInfo(directory.ToLower(), name.ToLower());
            AssetBundleUtility.Instance.Co_LoadAsset(assetInfo, callBack);
            // US1: Route through YooAssetService async
            CoLoadViaYooAsset<T>(path, callBack).Forget();
        }
    }
    public void UnloadAsset(string assetBundleName, string assetName)
    private async UniTaskVoid CoLoadViaYooAsset<T>(string path, Action<bool, UnityEngine.Object> callBack, CancellationToken ct = default) where T : UnityEngine.Object
    {
        if (!AssetSource.isUseAssetBundle)
            return;
        AssetBundleUtility.Instance.UnloadAsset(assetBundleName, assetName);
        try
        {
            var asset = await YooAssetService.Instance.LoadAssetAsync<T>(path, ct: ct);
            callBack?.Invoke(asset != null, asset);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[ResManager] Async load via YooAsset failed: {ex.Message}");
            callBack?.Invoke(false, null);
        }
    }
    [System.Obsolete("US1: Use YooAssetService.ReleaseHandle or UnloadUnusedAssetsAsync instead.")]
    public void UnloadAsset(string assetBundleName, string assetName)
    {
        // US1: AssetBundleUtility unload no longer effective since assets loaded via YooAsset.
        // Proper unload handled via YooAssetService handle-based release.
    }
    [System.Obsolete("US1: Use YooAssetService.UnloadUnusedAssetsAsync instead.")]
    public void UnloadAssetBundle(string assetBundleName, bool unloadAllLoadedObjects, bool includeDependenice)
    {
        if (!AssetSource.isUseAssetBundle)
            return;
        AssetBundleUtility.Instance.UnloadAssetBundle(assetBundleName, unloadAllLoadedObjects, includeDependenice);
        // US1: AssetBundleUtility unload no longer effective since assets loaded via YooAsset.
    }
    public string GetAssetFilePath(string _assetKey)
@@ -315,5 +307,111 @@
        return path;
    }
    // ====================================================================
    // US1: New UniTask-based async variants
    // ====================================================================
    /// <summary>
    /// 异步加载资源(UniTask 版本,US1 新增)。
    /// </summary>
    public async UniTask<T> LoadAssetAsync<T>(string directory, string name, bool needExt = true, CancellationToken ct = default) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
        name = name.Replace("\\", "/");
        if (typeof(T) == typeof(Sprite))
        {
            return await LoadSpriteAsyncUniTask(directory, name, ct) as T;
        }
        if (typeof(T) == typeof(SkeletonDataAsset))
        {
            if (name.Contains("/"))
            {
                directory += name.Substring(0, name.LastIndexOf("/"));
                name = name.Substring(name.LastIndexOf("/") + 1);
            }
        }
        var path = ($"Assets/ResourcesOut/{directory}/{name}" + (needExt ? GetExtension(typeof(T)) : ""))
            .Replace("//", "/").Trim().Replace("\\", "/");
        if (!AssetSource.isUseAssetBundle)
        {
#if UNITY_EDITOR
            return UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
#else
            return null;
#endif
        }
        return await YooAssetService.Instance.LoadAssetAsync<T>(path, ct: ct);
    }
    /// <summary>
    /// US4: 异步加载资源并走缓存层(缓存命中直接返回,未命中则加载并缓存)。
    /// </summary>
    public async UniTask<T> LoadAssetCachedAsync<T>(string directory, string name, bool needExt = true, CancellationToken ct = default) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
        name = name.Replace("\\", "/");
        var path = ($"Assets/ResourcesOut/{directory}/{name}" + (needExt ? GetExtension(typeof(T)) : ""))
            .Replace("//", "/").Trim().Replace("\\", "/");
        if (!AssetSource.isUseAssetBundle)
        {
#if UNITY_EDITOR
            return UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
#else
            return null;
#endif
        }
        return await ResourceCacheManager.Instance.GetOrLoadAsync<T>(path);
    }
    private async UniTask<Sprite> LoadSpriteAsyncUniTask(string atlasName, string spriteName, CancellationToken ct = default)
    {
        if (!AssetSource.isUseAssetBundle)
        {
            var atlas = await LoadAssetAsync<SpriteAtlas>("Sprite", atlasName.Replace("Sprite/", ""), ct: ct);
            return atlas?.GetSprite(spriteName);
        }
        else
        {
            var path = $"Assets/ResourcesOut/{atlasName}/{spriteName}.png"
                .Replace("//", "/").Trim().Replace("\\", "/");
            return await YooAssetService.Instance.LoadAssetAsync<Sprite>(path, ct: ct);
        }
    }
    /// <summary>
    /// 异步加载配置文件(UniTask 版本)。
    /// WebGL 平台使用 YooAsset RawFile 异步加载,其他平台使用线程池。
    /// </summary>
    public async UniTask<string[]> LoadConfigAsync(string name, CancellationToken ct = default)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        // WebGL 不支持多线程和 File.ReadAllLines,使用 YooAsset RawFile
        try
        {
            var text = await ProjSG.Resource.YooAssetService.Instance.LoadRawFileTextAsync($"config/{name}", ct);
            if (!string.IsNullOrEmpty(text))
            {
                return text.Split(new[] { "\r\n", "\n" }, System.StringSplitOptions.None);
            }
        }
        catch (System.Exception ex)
        {
            UnityEngine.Debug.LogError($"[ResManager] LoadConfigAsync WebGL fallback failed for '{name}': {ex.Message}");
        }
        return System.Array.Empty<string>();
#else
        #pragma warning disable CS0618 // LoadConfig is obsolete — used here as thread-pool fallback for non-WebGL
        return await UniTask.RunOnThreadPool(() => LoadConfig(name));
        #pragma warning restore CS0618
#endif
    }
}
Main/ResModule/ResourceCacheManager.cs
New file
@@ -0,0 +1,292 @@
// ============================================================================
// ResourceCacheManager.cs — 全局资源缓存管理器
// Feature: 001-async-resource-loading
// ============================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Cysharp.Threading.Tasks;
using YooAsset;
namespace ProjSG.Resource
{
    /// <summary>
    /// 全局资源缓存管理器。
    /// 提供同步缓存获取、异步加载+去重、LRU 淘汰等能力。
    /// </summary>
    public class ResourceCacheManager : Singleton<ResourceCacheManager>, IResourceCache
    {
        /// <summary>
        /// 缓存条目
        /// </summary>
        private class CachedResource
        {
            public UnityEngine.Object Asset;
            public AssetHandle Handle;
            public bool IsPermanent;
            public float LastAccessTime;
        }
        /// <summary>已缓存的资源</summary>
        private readonly Dictionary<string, CachedResource> _syncCache = new Dictionary<string, CachedResource>();
        /// <summary>正在加载中的任务(用于去重)</summary>
        private readonly Dictionary<string, UniTask<UnityEngine.Object>> _loadingTasks = new Dictionary<string, UniTask<UnityEngine.Object>>();
        /// <summary>LRU 淘汰阈值(非常驻资源数量超过此值时触发淘汰)</summary>
        private const int LRU_THRESHOLD = 200;
        /// <summary>淘汰后保留的非常驻资源数量</summary>
        private const int LRU_KEEP_COUNT = 150;
        /// <inheritdoc/>
        public int CachedCount => _syncCache.Count;
        // ====================================================================
        // 同步获取
        // ====================================================================
        /// <inheritdoc/>
        public T GetCached<T>(string location) where T : UnityEngine.Object
        {
            if (string.IsNullOrEmpty(location)) return null;
            if (_syncCache.TryGetValue(location, out var cached))
            {
                cached.LastAccessTime = Time.unscaledTime;
                return cached.Asset as T;
            }
            return null;
        }
        /// <inheritdoc/>
        public bool IsCached(string location)
        {
            return !string.IsNullOrEmpty(location) && _syncCache.ContainsKey(location);
        }
        // ====================================================================
        // 异步获取(缓存穿透 + 去重)
        // ====================================================================
        /// <inheritdoc/>
        public async UniTask<T> GetOrLoadAsync<T>(string location) where T : UnityEngine.Object
        {
            if (string.IsNullOrEmpty(location))
            {
                Debug.LogError("ResourceCacheManager.GetOrLoadAsync: location is null or empty");
                return null;
            }
            // 1. 缓存命中
            if (_syncCache.TryGetValue(location, out var cached))
            {
                cached.LastAccessTime = Time.unscaledTime;
                return cached.Asset as T;
            }
            // 2. 正在加载中 → 等待已有任务(去重)
            if (_loadingTasks.TryGetValue(location, out var loadingTask))
            {
                var result = await loadingTask;
                return result as T;
            }
            // 3. 发起新加载
            var task = LoadAndCacheAsync(location, false);
            _loadingTasks[location] = task;
            try
            {
                var asset = await task;
                return asset as T;
            }
            finally
            {
                _loadingTasks.Remove(location);
            }
        }
        // ====================================================================
        // 批量预加载
        // ====================================================================
        /// <inheritdoc/>
        public async UniTask PreloadAsync(string[] locations, bool permanent = false, IProgress<float> progress = null)
        {
            if (locations == null || locations.Length == 0)
            {
                progress?.Report(1f);
                return;
            }
            int completed = 0;
            int total = locations.Length;
            // 过滤已缓存的
            var toLoad = new List<string>();
            foreach (var loc in locations)
            {
                if (_syncCache.ContainsKey(loc))
                {
                    // 已缓存,更新常驻标记
                    if (permanent)
                    {
                        _syncCache[loc].IsPermanent = true;
                    }
                    completed++;
                }
                else
                {
                    toLoad.Add(loc);
                }
            }
            progress?.Report((float)completed / total);
            // 并行加载(限制并发数)
            const int maxConcurrency = 8;
            for (int i = 0; i < toLoad.Count; i += maxConcurrency)
            {
                var batch = new List<UniTask>();
                int batchEnd = Mathf.Min(i + maxConcurrency, toLoad.Count);
                for (int j = i; j < batchEnd; j++)
                {
                    var loc = toLoad[j];
                    batch.Add(LoadAndCacheAsync(loc, permanent).ContinueWith(_ =>
                    {
                        completed++;
                        progress?.Report((float)completed / total);
                    }));
                }
                await UniTask.WhenAll(batch);
            }
            progress?.Report(1f);
            TryLRUEviction();
        }
        // ====================================================================
        // 释放
        // ====================================================================
        /// <inheritdoc/>
        public void Release(string location, bool forceRelease = false)
        {
            if (string.IsNullOrEmpty(location)) return;
            if (_syncCache.TryGetValue(location, out var cached))
            {
                if (cached.IsPermanent && !forceRelease) return;
                if (cached.Handle != null && cached.Handle.IsValid)
                {
                    cached.Handle.Release();
                }
                _syncCache.Remove(location);
            }
        }
        /// <inheritdoc/>
        public void ReleaseAll()
        {
            var toRemove = new List<string>();
            foreach (var kvp in _syncCache)
            {
                if (!kvp.Value.IsPermanent)
                {
                    if (kvp.Value.Handle != null && kvp.Value.Handle.IsValid)
                    {
                        kvp.Value.Handle.Release();
                    }
                    toRemove.Add(kvp.Key);
                }
            }
            foreach (var key in toRemove)
            {
                _syncCache.Remove(key);
            }
        }
        /// <inheritdoc/>
        public void ForceReleaseAll()
        {
            foreach (var kvp in _syncCache)
            {
                if (kvp.Value.Handle != null && kvp.Value.Handle.IsValid)
                {
                    kvp.Value.Handle.Release();
                }
            }
            _syncCache.Clear();
            _loadingTasks.Clear();
        }
        // ====================================================================
        // 内部方法
        // ====================================================================
        private async UniTask<UnityEngine.Object> LoadAndCacheAsync(string location, bool permanent)
        {
            try
            {
                var asset = await YooAssetService.Instance.LoadAssetAsync<UnityEngine.Object>(location);
                if (asset != null && !_syncCache.ContainsKey(location))
                {
                    _syncCache[location] = new CachedResource
                    {
                        Asset = asset,
                        Handle = null, // Handle 由 YooAssetService 管理
                        IsPermanent = permanent,
                        LastAccessTime = Time.unscaledTime,
                    };
                }
                return asset;
            }
            catch (Exception e)
            {
                Debug.LogError($"ResourceCacheManager.LoadAndCacheAsync failed for '{location}': {e.Message}");
                return null;
            }
        }
        /// <summary>
        /// LRU 淘汰:非常驻资源超过阈值时,按访问时间淘汰最旧的资源。
        /// </summary>
        private void TryLRUEviction()
        {
            var nonPermanent = _syncCache
                .Where(kvp => !kvp.Value.IsPermanent)
                .ToList();
            if (nonPermanent.Count <= LRU_THRESHOLD) return;
            // 按访问时间排序,移除最旧的
            var sorted = nonPermanent
                .OrderBy(kvp => kvp.Value.LastAccessTime)
                .ToList();
            int toRemove = sorted.Count - LRU_KEEP_COUNT;
            for (int i = 0; i < toRemove; i++)
            {
                var kvp = sorted[i];
                if (kvp.Value.Handle != null && kvp.Value.Handle.IsValid)
                {
                    kvp.Value.Handle.Release();
                }
                _syncCache.Remove(kvp.Key);
            }
        }
    }
}
Main/ResModule/ResourceCacheManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 940d389e2ddcbe94ab0f851f56e988c9
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/ResourcePreloader.cs
New file
@@ -0,0 +1,196 @@
// ============================================================================
// ResourcePreloader.cs — 资源预加载管理器
// Feature: 001-async-resource-loading
// ============================================================================
using System;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
namespace ProjSG.Resource
{
    /// <summary>
    /// 资源预加载管理器。
    /// 按场景/流程组织预加载配置,通过 ResourceCacheManager 执行实际缓存。
    /// </summary>
    public class ResourcePreloader : Singleton<ResourcePreloader>, IResourcePreloader
    {
        private readonly Dictionary<string, PreloadConfig> _configs = new Dictionary<string, PreloadConfig>();
        private readonly HashSet<string> _loadedConfigs = new HashSet<string>();
        // ====================================================================
        // 预设配置
        // ====================================================================
        /// <summary>
        /// 注册默认预加载配置。应在 YooAsset 初始化后调用。
        /// </summary>
        public void RegisterDefaultConfigs()
        {
            // 启动必需资源(常驻)
            RegisterConfig(new PreloadConfig
            {
                ConfigName = "StartupEssential",
                Locations = new[]
                {
                    "Assets/ResourcesOut/Shader",                // Shader 全部
                    "Assets/ResourcesOut/Materials",             // 通用 Material
                    "Assets/ResourcesOut/BuiltIn/Font",          // 常用字体
                    "Assets/ResourcesOut/BuiltIn/UIRoot",        // UIRoot 预制体
                    "Assets/ResourcesOut/BuiltIn/SoundPlayer",   // 音频播放器
                },
                Tags = null,
                IsPermanent = true,
            });
            // 战斗场景资源(非常驻,战斗结束后可释放)
            RegisterConfig(new PreloadConfig
            {
                ConfigName = "BattleScene",
                Locations = null,
                Tags = new[] { "tag_battle_spine", "tag_battle_effect", "tag_battle_sound" },
                IsPermanent = false,
            });
        }
        // ====================================================================
        // IResourcePreloader 实现
        // ====================================================================
        /// <inheritdoc/>
        public void RegisterConfig(PreloadConfig config)
        {
            if (config == null || string.IsNullOrEmpty(config.ConfigName))
            {
                Debug.LogError("ResourcePreloader.RegisterConfig: config or ConfigName is null");
                return;
            }
            _configs[config.ConfigName] = config;
        }
        /// <inheritdoc/>
        public async UniTask PreloadAsync(string configName, IProgress<float> progress = null)
        {
            if (string.IsNullOrEmpty(configName))
            {
                Debug.LogError("ResourcePreloader.PreloadAsync: configName is null or empty");
                progress?.Report(1f);
                return;
            }
            if (!_configs.TryGetValue(configName, out var config))
            {
                Debug.LogWarning($"ResourcePreloader.PreloadAsync: config '{configName}' not found");
                progress?.Report(1f);
                return;
            }
            if (_loadedConfigs.Contains(configName))
            {
                progress?.Report(1f);
                return;
            }
            var cacheManager = ResourceCacheManager.Instance;
            int totalSteps = 0;
            int completedSteps = 0;
            // 计算总步骤
            if (config.Locations != null) totalSteps += config.Locations.Length;
            if (config.Tags != null) totalSteps += config.Tags.Length;
            if (totalSteps == 0)
            {
                progress?.Report(1f);
                _loadedConfigs.Add(configName);
                return;
            }
            // 加载 Locations
            if (config.Locations != null && config.Locations.Length > 0)
            {
                var locationProgress = new Progress<float>(p =>
                {
                    float locationWeight = (float)config.Locations.Length / totalSteps;
                    progress?.Report(p * locationWeight);
                });
                await cacheManager.PreloadAsync(config.Locations, config.IsPermanent, locationProgress);
                completedSteps += config.Locations.Length;
            }
            // 按 Tag 加载
            if (config.Tags != null)
            {
                foreach (var tag in config.Tags)
                {
                    await PreloadByTagAsync(tag);
                    completedSteps++;
                    progress?.Report((float)completedSteps / totalSteps);
                }
            }
            _loadedConfigs.Add(configName);
            progress?.Report(1f);
        }
        /// <inheritdoc/>
        public async UniTask PreloadByTagAsync(string tag, IProgress<float> progress = null)
        {
            if (string.IsNullOrEmpty(tag))
            {
                progress?.Report(1f);
                return;
            }
            try
            {
                // 使用 YooAssetService 按标签下载/缓存
                await YooAssetService.Instance.DownloadByTagsAsync(
                    new[] { tag },
                    progress: progress);
                progress?.Report(1f);
            }
            catch (Exception e)
            {
                Debug.LogError($"ResourcePreloader.PreloadByTagAsync failed for tag '{tag}': {e.Message}");
                progress?.Report(1f);
            }
        }
        /// <inheritdoc/>
        public void UnloadConfig(string configName)
        {
            if (string.IsNullOrEmpty(configName)) return;
            if (!_configs.TryGetValue(configName, out var config)) return;
            // 常驻配置不卸载
            if (config.IsPermanent)
            {
                Debug.LogWarning($"ResourcePreloader.UnloadConfig: config '{configName}' is permanent, skipping unload");
                return;
            }
            if (config.Locations != null)
            {
                var cacheManager = ResourceCacheManager.Instance;
                foreach (var loc in config.Locations)
                {
                    cacheManager.Release(loc);
                }
            }
            _loadedConfigs.Remove(configName);
        }
        /// <inheritdoc/>
        public bool IsConfigLoaded(string configName)
        {
            return _loadedConfigs.Contains(configName);
        }
    }
}
Main/ResModule/ResourcePreloader.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da2fba186b20495479f006115e8cc8d4
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/UILoader.cs
@@ -2,29 +2,40 @@
using System.Collections.Generic;
using UnityEngine;
using System;
using Cysharp.Threading.Tasks;
using System.Threading;
using System.IO;
using UnityEngine.U2D;
public class UILoader
{
    [System.Obsolete("US2: Use LoadWindowAsync instead.")]
    public static GameObject LoadWindow(string name)
    {
        #pragma warning disable CS0618
        return ResManager.Instance.LoadAsset<GameObject>(ResourcesPath.UI_WINDOW_SUFFIX, name);
        #pragma warning restore CS0618
    }
    [System.Obsolete("US2: Use LoadPrefabAsync instead.")]
    public static GameObject LoadPrefab(string _name)
    {
        #pragma warning disable CS0618
        return ResManager.Instance.LoadAsset<GameObject>(ResourcesPath.UI_PREFAB_SUFFIX, _name);
        #pragma warning restore CS0618
    }
    [System.Obsolete("US2: Use YooAssetService.ReleaseHandle or UnloadUnusedAssetsAsync instead.")]
    public static void UnLoadPrefab(string _assetName)
    {
        #pragma warning disable CS0618
        ResManager.Instance.UnloadAsset(ResourcesPath.UI_PREFAB_SUFFIX, _assetName);
        #pragma warning restore CS0618
    }
    //通过ICON表加载
    [System.Obsolete("US2: Use LoadSpriteAsync instead.")]
    public static Sprite LoadSprite(string _iconKey)
    {
        var iconConfig = IconConfig.Get(_iconKey);
@@ -36,31 +47,44 @@
        return LoadSprite(iconConfig.folder, iconConfig.sprite);
    }
    [System.Obsolete("US2: Use LoadSpriteAsync instead.")]
    public static Sprite LoadSprite(string _folder, string _file)
    {
        #pragma warning disable CS0618
        return ResManager.Instance.LoadAsset<Sprite>(StringUtility.Concat(ResourcesPath.UI_SPRITE_SUFFIX, "/", _folder), _file);
        #pragma warning restore CS0618
    }
    [System.Obsolete("US2: Use YooAssetService.ReleaseHandle instead.")]
    public static void UnLoadSprite(string _iconKey)
    {
        var iconConfig = IconConfig.Get(_iconKey);
        if (iconConfig != null)
        {
            var bundleName = StringUtility.Concat(ResourcesPath.UI_SPRITE_SUFFIX, "/", iconConfig.folder);
            #pragma warning disable CS0618
            ResManager.Instance.UnloadAsset(bundleName, iconConfig.sprite);
            #pragma warning restore CS0618
        }
    }
    [System.Obsolete("US2: Use LoadFontAsync instead.")]
    public static Font LoadFont(string _fontName)
    {
        #pragma warning disable CS0618
        return ResManager.Instance.LoadAsset<Font>(ResourcesPath.UI_FONT_SUFFIX, _fontName);
        #pragma warning restore CS0618
    }
    [System.Obsolete("US2: Use YooAssetService.ReleaseHandle instead.")]
    public static void UnLoadFont(string _fontName)
    {
        #pragma warning disable CS0618
        ResManager.Instance.UnloadAsset(ResourcesPath.UI_FONT_SUFFIX, _fontName);
        #pragma warning restore CS0618
    }
    [System.Obsolete("US2: Use LoadTexture2DAsync instead.")]
    public static Texture2D LoadTexture2D(string _iconKey)
    {
        var iconConfig = IconConfig.Get(_iconKey);
@@ -68,11 +92,62 @@
        {
            return null;
        }
        #pragma warning disable CS0618
        return ResManager.Instance.LoadAsset<Texture2D>(StringUtility.Concat(ResourcesPath.UI_TEXTURE_SUFFIX, "/" + iconConfig.folder), iconConfig.sprite);
        #pragma warning restore CS0618
    }
    
    [System.Obsolete("US2: Use LoadTexture2DPNGAsync instead.")]
    public static Texture2D LoadTexture2DPNG(string name)
    {
        #pragma warning disable CS0618
        return ResManager.Instance.LoadAsset<Texture2D>(StringUtility.Concat(ResourcesPath.UI_TEXTURE_SUFFIX, "/FullScreenBg"), name + ".png", false);
        #pragma warning restore CS0618
    }
    // ====================================================================
    // US2: Async UniTask variants
    // ====================================================================
    public static UniTask<GameObject> LoadWindowAsync(string name, CancellationToken ct = default)
    {
        return ResManager.Instance.LoadAssetAsync<GameObject>(ResourcesPath.UI_WINDOW_SUFFIX, name, ct: ct);
    }
    public static UniTask<GameObject> LoadPrefabAsync(string _name, CancellationToken ct = default)
    {
        return ResManager.Instance.LoadAssetAsync<GameObject>(ResourcesPath.UI_PREFAB_SUFFIX, _name, ct: ct);
    }
    public static async UniTask<Sprite> LoadSpriteAsync(string _iconKey, CancellationToken ct = default)
    {
        var iconConfig = IconConfig.Get(_iconKey);
        if (iconConfig == null) return null;
        return await LoadSpriteAsync(iconConfig.folder, iconConfig.sprite, ct);
    }
    public static UniTask<Sprite> LoadSpriteAsync(string _folder, string _file, CancellationToken ct = default)
    {
        return ResManager.Instance.LoadAssetAsync<Sprite>(
            StringUtility.Concat(ResourcesPath.UI_SPRITE_SUFFIX, "/", _folder), _file, ct: ct);
    }
    public static UniTask<Font> LoadFontAsync(string _fontName, CancellationToken ct = default)
    {
        return ResManager.Instance.LoadAssetAsync<Font>(ResourcesPath.UI_FONT_SUFFIX, _fontName, ct: ct);
    }
    public static async UniTask<Texture2D> LoadTexture2DAsync(string _iconKey, CancellationToken ct = default)
    {
        var iconConfig = IconConfig.Get(_iconKey);
        if (iconConfig == null) return null;
        return await ResManager.Instance.LoadAssetAsync<Texture2D>(
            StringUtility.Concat(ResourcesPath.UI_TEXTURE_SUFFIX, "/" + iconConfig.folder), iconConfig.sprite, ct: ct);
    }
    public static UniTask<Texture2D> LoadTexture2DPNGAsync(string name, CancellationToken ct = default)
    {
        return ResManager.Instance.LoadAssetAsync<Texture2D>(
            StringUtility.Concat(ResourcesPath.UI_TEXTURE_SUFFIX, "/FullScreenBg"), name + ".png", false, ct);
    }
}
Main/ResModule/YooAssetPackageConfig.cs
New file
@@ -0,0 +1,184 @@
// ============================================================================
// YooAssetPackageConfig.cs — YooAsset 多 Package 架构配置
// 定义 Package 名称常量和资源目录→Package 路由表
// ============================================================================
//
// 在 YooAsset Editor (YooAsset → AssetBundle Collector) 中配置:
// 每个 Package 对应一个独立的构建产物,可独立下载和更新。
//
// ┌───────────────┬─────────────────────────────────────────┬──────────────┐
// │ Package       │ 收集路径 (CollectPath)                    │ 部署方式      │
// ├───────────────┼─────────────────────────────────────────┼──────────────┤
// │ UI            │ Assets/ResourcesOut/UI                  │ Remote       │
// │               │ Assets/ResourcesOut/UIComp              │              │
// │               │ Assets/ResourcesOut/Sprite              │              │
// │               │ Assets/ResourcesOut/Texture             │              │
// │               │ Assets/ResourcesOut/Font                │              │
// ├───────────────┼─────────────────────────────────────────┼──────────────┤
// │ Prefab        │ Assets/ResourcesOut/BuiltIn             │ Remote       │
// │               │ Assets/ResourcesOut/Shader              │              │
// │               │ Assets/ResourcesOut/Materials           │              │
// │               │ Assets/ResourcesOut/ScriptableObject    │              │
// │               │ Assets/ResourcesOut/Scenes              │              │
// │               │ Assets/ResourcesOut/Config              │              │
// ├───────────────┼─────────────────────────────────────────┼──────────────┤
// │ UIEffect      │ Assets/ResourcesOut/UIEffect (已有)      │ Remote       │
// ├───────────────┼─────────────────────────────────────────┼──────────────┤
// │ Dll           │ HybridCLR 热更 DLL                       │ Remote       │
// ├───────────────┼─────────────────────────────────────────┼──────────────┤
// │ Battle        │ Assets/ResourcesOut/Hero                │ Remote       │
// │               │ Assets/ResourcesOut/Battle              │              │
// ├───────────────┼─────────────────────────────────────────┼──────────────┤
// │ Audio         │ Assets/ResourcesOut/Audio               │ Remote       │
// └───────────────┴─────────────────────────────────────────┴──────────────┘
//
// ============================================================================
using System.Collections.Generic;
namespace ProjSG.Resource
{
    /// <summary>
    /// YooAsset Package 名称常量。
    /// 必须与 AssetBundleCollectorSetting.asset 中的 PackageName 一致。
    /// </summary>
    public static class YooAssetPackageConfig
    {
        // ====================================================================
        // Package 名称(与 Collector 完全一致)
        // ====================================================================
        /// <summary>UI 资源包:UI 窗口、UIComp、Sprite、Texture、Font</summary>
        public const string UI = "UI";
        /// <summary>通用预制体包:BuiltIn、Shader、Materials、ScriptableObject、Scenes、Config</summary>
        public const string Prefab = "Prefab";
        /// <summary>UI 特效包:UIEffect 目录</summary>
        public const string UIEffect = "UIEffect";
        /// <summary>HybridCLR 热更 DLL 包</summary>
        public const string Dll = "Dll";
        /// <summary>战斗资源包:Hero Spine、Battle Prefabs</summary>
        public const string Battle = "Battle";
        /// <summary>音频资源包:Audio 目录</summary>
        public const string Audio = "Audio";
        /// <summary>
        /// 主包名 — 在 YooAssetService/YooAssetInitializer 中作为 DefaultPackage。
        /// 选择 Prefab 包,因为它包含 BuiltIn/Shader/Materials 等启动必需资源。
        /// </summary>
        public const string DefaultPackage = Prefab;
        /// <summary>
        /// 所有需要初始化的 Package(不含 Dll,Dll 包由热更流程单独管理)。
        /// </summary>
        public static readonly string[] AllPackages = new[]
        {
            Prefab,     // 先初始化默认包
            UI,
            UIEffect,
            Battle,
            Audio,
        };
        // ====================================================================
        // 资源目录 → Package 路由表
        // ====================================================================
        /// <summary>
        /// 资源目录前缀到 Package 名称的映射。
        /// key 是 ResourcesOut 下的一级目录名(小写),value 是 Package 名称。
        /// </summary>
        private static readonly Dictionary<string, string> _directoryToPackage = new Dictionary<string, string>
        {
            // UI Package
            { "ui", UI },
            { "uicomp", UI },
            { "sprite", UI },
            { "texture", UI },
            { "font", UI },
            // Prefab Package (default — BuiltIn, Shader, Materials, etc.)
            { "builtin", Prefab },
            { "shader", Prefab },
            { "materials", Prefab },
            { "graphic", Prefab },        // 旧路径兼容 "Graphic/Shader", "Graphic/Material"
            { "scriptableobject", Prefab },
            { "scenes", Prefab },
            { "config", Prefab },
            { "prefab", Prefab },
            // UIEffect Package
            { "uieffect", UIEffect },
            { "effect", UIEffect },
            // Battle Package
            { "hero", Battle },
            { "battle", Battle },
            // Audio Package
            { "audio", Audio },
        };
        /// <summary>
        /// 根据完整资源路径确定应使用的 Package 名称。
        /// 例如 "Assets/ResourcesOut/UI/LoginWin.prefab" → "UI"
        /// 例如 "Assets/ResourcesOut/BuiltIn/Font/MainFont.ttf" → "Prefab"
        /// </summary>
        /// <param name="location">资源路径(完整路径或相对路径)</param>
        /// <returns>Package 名称,匹配不到返回 DefaultPackage</returns>
        public static string GetPackageForLocation(string location)
        {
            if (string.IsNullOrEmpty(location))
                return DefaultPackage;
            // 去掉 "Assets/ResourcesOut/" 前缀
            string relativePath = location;
            const string PREFIX = "Assets/ResourcesOut/";
            if (relativePath.StartsWith(PREFIX))
            {
                relativePath = relativePath.Substring(PREFIX.Length);
            }
            // 取一级目录名
            int slashIndex = relativePath.IndexOf('/');
            string topDir = slashIndex >= 0
                ? relativePath.Substring(0, slashIndex)
                : relativePath;
            if (_directoryToPackage.TryGetValue(topDir.ToLower(), out string packageName))
            {
                return packageName;
            }
            return DefaultPackage;
        }
    }
    /// <summary>
    /// YooAsset 资源标签常量。
    /// 用于细粒度资源分组和按需下载。
    /// </summary>
    public static class YooAssetTagConfig
    {
        // 战斗相关
        public const string BattleSpine = "tag_battle_spine";
        public const string BattleEffect = "tag_battle_effect";
        public const string BattleSound = "tag_battle_sound";
        public const string BattleMap = "tag_battle_map";
        // UI 相关
        public const string UIMain = "tag_ui_main";
        public const string UIShop = "tag_ui_shop";
        public const string UIBattle = "tag_ui_battle";
        public const string UIHero = "tag_ui_hero";
        // 通用
        public const string Shader = "tag_shader";
        public const string Font = "tag_font";
        public const string Material = "tag_material";
    }
}
Main/ResModule/YooAssetPackageConfig.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f90c77034c46bdc4784d80b3f2a96715
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/ResModule/YooAssetService.cs
New file
@@ -0,0 +1,677 @@
// ============================================================================
// YooAssetService.cs — YooAsset 封装服务
// 实现 IYooAssetService 和 IYooAssetBridge,替代 AssetBundleUtility
// ============================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
namespace ProjSG.Resource
{
    /// <summary>
    /// YooAsset 资源加载服务单例。
    /// 封装 YooAsset ResourcePackage 的核心加载能力,提供 UniTask 异步 API。
    /// 同时实现 IYooAssetBridge 供 Launch 程序集跨程序集调用。
    /// </summary>
    public class YooAssetService : Singleton<YooAssetService>, IYooAssetService, IYooAssetBridge
    {
        private readonly Dictionary<string, ResourcePackage> _packages = new Dictionary<string, ResourcePackage>();
        private ResourcePackage _defaultPackage;
        private IRemoteServices _remoteServices;
        private bool _isInitialized;
        private EPlayMode _playMode;
        // ====================================================================
        // IYooAssetService Properties
        // ====================================================================
        /// <inheritdoc />
        public bool IsInitialized => _isInitialized;
        /// <inheritdoc />
        public EPlayMode PlayMode => _playMode;
        // ====================================================================
        // IYooAssetBridge Properties
        // ====================================================================
        bool IYooAssetBridge.IsRegistered => _isInitialized;
        // ====================================================================
        // Initialization
        // ====================================================================
        /// <inheritdoc />
        public async UniTask InitializeAsync(EPlayMode playMode, IRemoteServices remoteServices = null)
        {
            if (_isInitialized)
            {
                Debug.LogWarning("[YooAssetService] Already initialized.");
                return;
            }
            _playMode = playMode;
            _remoteServices = remoteServices;
            // YooAsset 全局初始化(幂等操作)
            YooAssets.Initialize();
            // 初始化所有配置中的包裹
            foreach (var pkgName in YooAssetPackageConfig.AllPackages)
            {
                try
                {
                    // 优先复用 Launch 阶段已创建的包裹
                    var package = YooAssets.TryGetPackage(pkgName);
                    if (package != null)
                    {
                        Debug.Log($"[YooAssetService] Reusing existing package '{pkgName}' from YooAssetInitializer");
                    }
                    else
                    {
                        // 自行创建并初始化(首次启动或该包未在 Launch 阶段创建)
                        package = YooAssets.CreatePackage(pkgName);
                        var initParams = CreateInitParameters(playMode, remoteServices, pkgName);
                        var initOp = package.InitializeAsync(initParams);
                        await initOp.ToUniTask();
                        if (initOp.Status != EOperationStatus.Succeed)
                        {
                            Debug.LogWarning($"[YooAssetService] Package '{pkgName}' init failed: {initOp.Error}");
                            continue;
                        }
                        Debug.Log($"[YooAssetService] Package '{pkgName}' newly initialized.");
                    }
                    _packages[pkgName] = package;
                    // 设置默认包
                    if (_defaultPackage == null || pkgName == YooAssetPackageConfig.DefaultPackage)
                    {
                        _defaultPackage = package;
                        YooAssets.SetDefaultPackage(package);
                    }
                }
                catch (Exception ex)
                {
                    // EditorSimulateMode 下包不在 Collector 中会抛异常,跳过
                    Debug.LogWarning($"[YooAssetService] Package '{pkgName}' init exception (skipped): {ex.Message}");
                }
            }
            if (_defaultPackage == null)
            {
                Debug.LogError("[YooAssetService] No packages initialized successfully!");
                throw new InvalidOperationException("YooAsset initialization failed: no packages available.");
            }
            _isInitialized = true;
            Debug.Log($"[YooAssetService] Initialized {_packages.Count}/{YooAssetPackageConfig.AllPackages.Length} packages with PlayMode={playMode}");
        }
        /// <summary>
        /// 初始化指定名称的额外资源包裹。
        /// </summary>
        public async UniTask InitializePackageAsync(string packageName, EPlayMode playMode,
            IRemoteServices remoteServices = null)
        {
            if (_packages.ContainsKey(packageName))
            {
                Debug.LogWarning($"[YooAssetService] Package '{packageName}' already initialized.");
                return;
            }
            // 优先复用已存在的包裹(可能由 Launch 阶段创建)
            var package = YooAssets.TryGetPackage(packageName);
            if (package == null)
            {
                package = YooAssets.CreatePackage(packageName);
                var initParams = CreateInitParameters(playMode, remoteServices ?? _remoteServices, packageName);
                var initOp = package.InitializeAsync(initParams);
                await initOp.ToUniTask();
                if (initOp.Status != EOperationStatus.Succeed)
                {
                    Debug.LogError($"[YooAssetService] Initialize package '{packageName}' failed: {initOp.Error}");
                    throw new InvalidOperationException($"YooAsset package '{packageName}' initialization failed: {initOp.Error}");
                }
            }
            _packages[packageName] = package;
            Debug.Log($"[YooAssetService] Package '{packageName}' initialized.");
        }
        private InitializeParameters CreateInitParameters(EPlayMode playMode, IRemoteServices remoteServices, string packageName)
        {
            switch (playMode)
            {
                case EPlayMode.EditorSimulateMode:
                {
#if UNITY_EDITOR
                    var simulateResult = EditorSimulateModeHelper.SimulateBuild(packageName);
                    return new EditorSimulateModeParameters
                    {
                        EditorFileSystemParameters = FileSystemParameters
                        .CreateDefaultEditorFileSystemParameters(simulateResult.PackageRootDirectory)
                    };
#else
                    throw new InvalidOperationException("EditorSimulateMode is only available in Unity Editor.");
#endif
                }
                case EPlayMode.HostPlayMode:
                {
                    return new HostPlayModeParameters
                    {
                        BuildinFileSystemParameters = FileSystemParameters
                            .CreateDefaultBuildinFileSystemParameters(),
                        CacheFileSystemParameters = FileSystemParameters
                            .CreateDefaultCacheFileSystemParameters(remoteServices)
                    };
                }
                case EPlayMode.OfflinePlayMode:
                {
                    return new OfflinePlayModeParameters
                    {
                        BuildinFileSystemParameters = FileSystemParameters
                            .CreateDefaultBuildinFileSystemParameters()
                    };
                }
                case EPlayMode.WebPlayMode:
                {
                    var webParams = new WebPlayModeParameters();
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
                    string packageRoot = $"{WeChatWASM.WX.env.USER_DATA_PATH}/__GAME_FILE_CACHE";
                    webParams.WebServerFileSystemParameters = WechatFileSystemCreater
                        .CreateFileSystemParameters(packageRoot, remoteServices);
#elif UNITY_WEBGL && DOUYINMINIGAME && !UNITY_EDITOR
                    string packageRoot = TTSDK.TTFileSystem.USER_DATA_PATH + "/__GAME_FILE_CACHE";
                    webParams.WebServerFileSystemParameters = TiktokFileSystemCreater
                        .CreateFileSystemParameters(packageRoot, remoteServices);
#else
                    webParams.WebServerFileSystemParameters = FileSystemParameters
                        .CreateDefaultWebServerFileSystemParameters();
                    if (remoteServices != null)
                    {
                        webParams.WebRemoteFileSystemParameters = FileSystemParameters
                            .CreateDefaultWebRemoteFileSystemParameters(remoteServices);
                    }
#endif
                    return webParams;
                }
                default:
                    throw new ArgumentOutOfRangeException(nameof(playMode), playMode, "Unsupported PlayMode.");
            }
        }
        // ====================================================================
        // Asset Loading
        // ====================================================================
        /// <summary>
        /// 资源加载重试配置
        /// </summary>
        private const int MAX_RETRY_COUNT = 3;
        private const int BASE_RETRY_DELAY_MS = 500; // 500ms, 1000ms, 2000ms (exponential)
        /// <summary>
        /// 带重试的异步操作执行器。
        /// 使用指数退避策略(500ms → 1000ms → 2000ms)。
        /// </summary>
        /// <param name="operation">要执行的异步操作</param>
        /// <param name="operationName">操作名称(用于日志)</param>
        /// <param name="ct">取消令牌</param>
        /// <returns>操作结果</returns>
        private async UniTask<T> ExecuteWithRetryAsync<T>(
            Func<UniTask<T>> operation,
            string operationName,
            CancellationToken ct = default)
        {
            Exception lastException = null;
            for (int attempt = 0; attempt <= MAX_RETRY_COUNT; attempt++)
            {
                try
                {
                    ct.ThrowIfCancellationRequested();
                    return await operation();
                }
                catch (OperationCanceledException)
                {
                    throw; // Don't retry cancellations
                }
                catch (Exception ex)
                {
                    lastException = ex;
                    if (attempt < MAX_RETRY_COUNT)
                    {
                        int delayMs = BASE_RETRY_DELAY_MS * (1 << attempt); // Exponential backoff
                        Debug.LogWarning($"[YooAssetService] {operationName} failed (attempt {attempt + 1}/{MAX_RETRY_COUNT + 1}), retrying in {delayMs}ms: {ex.Message}");
                        await UniTask.Delay(delayMs, cancellationToken: ct);
                    }
                }
            }
            Debug.LogError($"[YooAssetService] {operationName} failed after {MAX_RETRY_COUNT + 1} attempts: {lastException?.Message}");
            return default;
        }
        private void ThrowIfNotInitialized()
        {
            if (!_isInitialized)
                throw new InvalidOperationException("[YooAssetService] Service not initialized. Call InitializeAsync first.");
        }
        /// <summary>
        /// 根据资源路径查找应使用的 ResourcePackage。
        /// 使用 YooAssetPackageConfig 路由表确定目标包,找不到则回退到默认包。
        /// </summary>
        private ResourcePackage FindPackageForAsset(string location)
        {
            var packageName = YooAssetPackageConfig.GetPackageForLocation(location);
            if (_packages.TryGetValue(packageName, out var package))
                return package;
            // 路由到的包尚未初始化,回退到默认包
            return _defaultPackage;
        }
        /// <inheritdoc />
        public async UniTask<T> LoadAssetAsync<T>(string location, uint priority = 0,
            CancellationToken ct = default) where T : UnityEngine.Object
        {
            ThrowIfNotInitialized();
            if (string.IsNullOrEmpty(location))
            {
                Debug.LogError("[YooAssetService] LoadAssetAsync: location is null or empty.");
                return null;
            }
            var package = FindPackageForAsset(location);
            return await ExecuteWithRetryAsync(async () =>
            {
                var handle = package.LoadAssetAsync<T>(location, priority);
                await handle.ToUniTask(cancellationToken: ct);
                if (handle.Status != EOperationStatus.Succeed)
                {
                    throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}");
                }
                return handle.GetAssetObject<T>();
            }, $"LoadAssetAsync<{typeof(T).Name}>('{location}')", ct);
        }
        /// <inheritdoc />
        public async UniTask<UnityEngine.Object> LoadAssetAsync(string location, Type type, uint priority = 0,
            CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            if (string.IsNullOrEmpty(location))
            {
                Debug.LogError("[YooAssetService] LoadAssetAsync: location is null or empty.");
                return null;
            }
            var package = FindPackageForAsset(location);
            return await ExecuteWithRetryAsync(async () =>
            {
                var handle = package.LoadAssetAsync(location, type, priority);
                await handle.ToUniTask(cancellationToken: ct);
                if (handle.Status != EOperationStatus.Succeed)
                {
                    throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}");
                }
                return handle.AssetObject;
            }, $"LoadAssetAsync('{location}', {type.Name})", ct);
        }
        /// <summary>
        /// 同步加载资产(仅在非 WebGL 平台过渡期使用)。
        /// </summary>
        [System.Obsolete("Use LoadAssetAsync instead. Sync loading will be removed in US2.")]
        public T LoadAssetSync<T>(string location) where T : UnityEngine.Object
        {
            ThrowIfNotInitialized();
            if (string.IsNullOrEmpty(location))
            {
                Debug.LogError("[YooAssetService] LoadAssetSync: location is null or empty.");
                return null;
            }
            var package = FindPackageForAsset(location);
            var handle = package.LoadAssetSync<T>(location);
            if (handle.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[YooAssetService] LoadAssetSync failed for '{location}': {handle.LastError}");
                return null;
            }
            return handle.GetAssetObject<T>();
        }
        /// <inheritdoc />
        public async UniTask<SubAssetsHandle> LoadSubAssetsAsync<T>(string location, uint priority = 0,
            CancellationToken ct = default) where T : UnityEngine.Object
        {
            ThrowIfNotInitialized();
            var package = FindPackageForAsset(location);
            var handle = package.LoadSubAssetsAsync<T>(location, priority);
            await handle.ToUniTask();
            ct.ThrowIfCancellationRequested();
            if (handle.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[YooAssetService] LoadSubAssetsAsync failed for '{location}': {handle.LastError}");
            }
            return handle;
        }
        /// <inheritdoc />
        public async UniTask<AllAssetsHandle> LoadAllAssetsAsync<T>(string location, uint priority = 0,
            CancellationToken ct = default) where T : UnityEngine.Object
        {
            ThrowIfNotInitialized();
            var package = FindPackageForAsset(location);
            var handle = package.LoadAllAssetsAsync<T>(location, priority);
            await handle.ToUniTask();
            ct.ThrowIfCancellationRequested();
            if (handle.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[YooAssetService] LoadAllAssetsAsync failed for '{location}': {handle.LastError}");
            }
            return handle;
        }
        // ====================================================================
        // RawFile Loading
        // ====================================================================
        /// <inheritdoc />
        public async UniTask<string> LoadRawFileTextAsync(string location, CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            var rawPackage = FindPackageForAsset(location);
            return await ExecuteWithRetryAsync(async () =>
            {
                var handle = rawPackage.LoadRawFileAsync(location);
                await handle.ToUniTask(cancellationToken: ct);
                if (handle.Status != EOperationStatus.Succeed)
                {
                    throw new InvalidOperationException($"LoadRawFileTextAsync failed for '{location}': {handle.LastError}");
                }
                return handle.GetRawFileText();
            }, $"LoadRawFileTextAsync('{location}')", ct);
        }
        /// <inheritdoc />
        public async UniTask<byte[]> LoadRawFileBytesAsync(string location, CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            var rawPackage = FindPackageForAsset(location);
            return await ExecuteWithRetryAsync(async () =>
            {
                var handle = rawPackage.LoadRawFileAsync(location);
                await handle.ToUniTask(cancellationToken: ct);
                if (handle.Status != EOperationStatus.Succeed)
                {
                    throw new InvalidOperationException($"LoadRawFileBytesAsync failed for '{location}': {handle.LastError}");
                }
                return handle.GetRawFileData();
            }, $"LoadRawFileBytesAsync('{location}')", ct);
        }
        // ====================================================================
        // Scene Loading
        // ====================================================================
        /// <inheritdoc />
        public async UniTask<SceneHandle> LoadSceneAsync(string location, LoadSceneMode sceneMode = LoadSceneMode.Single,
            LocalPhysicsMode physicsMode = LocalPhysicsMode.None, bool suspendLoad = false, uint priority = 0, CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            var package = FindPackageForAsset(location);
            var handle = package.LoadSceneAsync(location, sceneMode, physicsMode, suspendLoad, priority);
            await handle.ToUniTask();
            ct.ThrowIfCancellationRequested();
            if (handle.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[YooAssetService] LoadSceneAsync failed for '{location}': {handle.LastError}");
            }
            return handle;
        }
        // ====================================================================
        // Query
        // ====================================================================
        /// <inheritdoc />
        public bool CheckLocationValid(string location)
        {
            ThrowIfNotInitialized();
            // 先用路由包检查,找不到则遍历所有包
            var package = FindPackageForAsset(location);
            if (package.CheckLocationValid(location))
                return true;
            foreach (var kvp in _packages)
            {
                if (kvp.Value != package && kvp.Value.CheckLocationValid(location))
                    return true;
            }
            return false;
        }
        /// <inheritdoc />
        public YooAsset.AssetInfo[] GetAssetInfosByTag(string tag)
        {
            ThrowIfNotInitialized();
            // 从所有包收集指定标签的资源信息
            var allInfos = new List<YooAsset.AssetInfo>();
            foreach (var kvp in _packages)
            {
                var infos = kvp.Value.GetAssetInfos(tag);
                if (infos != null && infos.Length > 0)
                    allInfos.AddRange(infos);
            }
            return allInfos.ToArray();
        }
        /// <inheritdoc />
        public bool IsNeedDownloadFromRemote(string location)
        {
            ThrowIfNotInitialized();
            var package = FindPackageForAsset(location);
            return package.IsNeedDownloadFromRemote(location);
        }
        // ====================================================================
        // Download
        // ====================================================================
        /// <inheritdoc />
        public async UniTask DownloadByTagsAsync(string[] tags, int downloadingMaxNumber = 10,
            int failedTryAgain = 3, IProgress<float> progress = null, CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            foreach (var tag in tags)
            {
                // 对所有包按标签创建下载器
                foreach (var kvp in _packages)
                {
                    var downloader = kvp.Value.CreateResourceDownloader(tag, downloadingMaxNumber, failedTryAgain);
                    if (downloader.TotalDownloadCount == 0)
                        continue;
                    downloader.BeginDownload();
                    while (!downloader.IsDone)
                    {
                        ct.ThrowIfCancellationRequested();
                        progress?.Report(downloader.Progress);
                        await UniTask.Yield();
                    }
                    if (downloader.Status != EOperationStatus.Succeed)
                    {
                        Debug.LogError($"[YooAssetService] Download tag '{tag}' from package '{kvp.Key}' failed: {downloader.Error}");
                        throw new InvalidOperationException($"Resource download failed for tag '{tag}': {downloader.Error}");
                    }
                }
            }
            progress?.Report(1f);
        }
        // ====================================================================
        // Version Management
        // ====================================================================
        /// <inheritdoc />
        public async UniTask<string> RequestPackageVersionAsync(CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            var op = _defaultPackage.RequestPackageVersionAsync();
            await op.ToUniTask();
            ct.ThrowIfCancellationRequested();
            if (op.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[YooAssetService] RequestPackageVersion failed: {op.Error}");
                throw new InvalidOperationException($"Request package version failed: {op.Error}");
            }
            return op.PackageVersion;
        }
        /// <inheritdoc />
        public async UniTask UpdatePackageManifestAsync(string packageVersion, CancellationToken ct = default)
        {
            ThrowIfNotInitialized();
            var op = _defaultPackage.UpdatePackageManifestAsync(packageVersion);
            await op.ToUniTask();
            ct.ThrowIfCancellationRequested();
            if (op.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"[YooAssetService] UpdatePackageManifest failed: {op.Error}");
                throw new InvalidOperationException($"Update package manifest failed: {op.Error}");
            }
        }
        // ====================================================================
        // Release
        // ====================================================================
        /// <inheritdoc />
        public void ReleaseHandle(HandleBase handle)
        {
            if (handle == null) return;
            handle.Release();
        }
        /// <inheritdoc />
        public async UniTask UnloadUnusedAssetsAsync()
        {
            ThrowIfNotInitialized();
            // 对所有包执行卸载
            foreach (var kvp in _packages)
            {
                var op = kvp.Value.UnloadUnusedAssetsAsync();
                await op.ToUniTask();
            }
        }
        /// <inheritdoc />
        public async UniTask UnloadAllAssetsAsync()
        {
            ThrowIfNotInitialized();
            // 对所有包执行卸载
            foreach (var kvp in _packages)
            {
                var op = kvp.Value.UnloadAllAssetsAsync();
                await op.ToUniTask();
            }
        }
        // ====================================================================
        // IYooAssetBridge Implementation
        // ====================================================================
        async UniTask<T> IYooAssetBridge.LoadAssetAsync<T>(string location)
        {
            return await LoadAssetAsync<T>(location);
        }
        async UniTask<string> IYooAssetBridge.LoadRawFileTextAsync(string location)
        {
            return await LoadRawFileTextAsync(location);
        }
        async UniTask<byte[]> IYooAssetBridge.LoadRawFileBytesAsync(string location)
        {
            return await LoadRawFileBytesAsync(location);
        }
        async UniTask IYooAssetBridge.PreloadAsync(string[] locations)
        {
            // 批量预加载,使用 UniTask.WhenAll 并行
            var tasks = new List<UniTask>(locations.Length);
            foreach (var loc in locations)
            {
                tasks.Add(LoadAssetAsync<UnityEngine.Object>(loc).AsUniTask());
            }
            await UniTask.WhenAll(tasks);
        }
        T IYooAssetBridge.GetCached<T>(string location)
        {
            // 委托给 ResourceCacheManager(US4 已集成)
            if (ProjSG.Resource.ResourceCacheManager.IsValid())
            {
                return ProjSG.Resource.ResourceCacheManager.Instance.GetCached<T>(location);
            }
            return null;
        }
        // ====================================================================
        // Sync Wrappers (Transitional — removed in US2)
        // ====================================================================
        /// <summary>
        /// 同步加载所有同类型资源(过渡期使用)。
        /// </summary>
        [System.Obsolete("Use LoadAllAssetsAsync instead. Sync loading will be removed in US2.")]
        public AllAssetsHandle LoadAllAssetsSync<T>(string location) where T : UnityEngine.Object
        {
            ThrowIfNotInitialized();
            var package = FindPackageForAsset(location);
            return package.LoadAllAssetsSync<T>(location);
        }
    }
}
Main/ResModule/YooAssetService.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ad3881ecd3a99ea439a411fd1866ed41
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Arena/ArenaHeroHead.cs
@@ -1,4 +1,5 @@
using UnityEngine;
using Cysharp.Threading.Tasks;
public class ArenaHeroHead : MonoBehaviour
{
@@ -27,4 +28,27 @@
        txtHeroLv.text = Language.Get("Arena22", heroLv);
    }
    public async UniTask DisplayAsync(int heroID, int skinID, int heroLv)
    {
        if (!HeroConfig.HasKey(heroID) || !HeroSkinConfig.HasKey(skinID))
            return;
        var heroConfig = HeroConfig.Get(heroID);
        var heroSkinConfig = HeroSkinConfig.Get(skinID);
        imgQuality.SetSprite("heroheadBG" + heroConfig.Quality);
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", heroSkinConfig.SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
            imgHeadIcon.SetSprite("herohead_default");
        }
        else
        {
            imgHeadIcon.overrideSprite = sprite;
        }
        txtHeroLv.text = Language.Get("Arena22", heroLv);
    }
}
Main/System/AssetVersion/DownLoadAndDiscompressHotTask.cs
@@ -144,7 +144,9 @@
        {
            if (reinitedBuiltInAsset)
            {
                AssetBundleUtility.Instance.ReInitBuiltInAsset();
                // YooAsset 资源更新后自动生效,不再需要 AssetBundleUtility.ReInitBuiltInAsset()
                // 如需主动刷新可通过 YooAsset 的 manifest 更新机制处理
                Debug.Log("[DownLoadAndDiscompressHotTask] BuiltIn asset refresh skipped — YooAsset handles resource versioning");
            }
        }
        catch (System.Exception ex)
Main/System/Attribute/TotalAttributeWin.cs
@@ -121,6 +121,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        var layouts = allContent.GetComponentsInChildren<LayoutGroup>(true);
        foreach (var layout in layouts)
@@ -128,6 +129,7 @@
            LayoutRebuilder.ForceRebuildLayoutImmediate(layout.GetComponent<RectTransform>());
        }
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        foreach (var layout in layouts)
        {
Main/System/Battle/AsyncResourceGuard.cs
New file
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
/// <summary>
/// US3: 异步资源守卫,延迟特效/Spine播放直到资源就绪。
/// 在资源未加载完成时排队播放请求,加载完成后按顺序执行。
/// </summary>
public class AsyncResourceGuard
{
    private bool _isReady;
    private readonly Queue<Action> _pendingActions = new Queue<Action>();
    private UniTaskCompletionSource _readySource;
    public bool IsReady => _isReady;
    public AsyncResourceGuard()
    {
        _isReady = false;
        _readySource = new UniTaskCompletionSource();
    }
    /// <summary>
    /// 标记资源已就绪,执行所有排队的动作。
    /// </summary>
    public void SetReady()
    {
        if (_isReady) return;
        _isReady = true;
        _readySource.TrySetResult();
        // 执行所有排队的动作
        while (_pendingActions.Count > 0)
        {
            var action = _pendingActions.Dequeue();
            try
            {
                action?.Invoke();
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }
        }
    }
    /// <summary>
    /// 排队一个动作:如果资源已就绪则立即执行,否则排队等待。
    /// </summary>
    public void EnqueueOrExecute(Action action)
    {
        if (action == null) return;
        if (_isReady)
        {
            action.Invoke();
        }
        else
        {
            _pendingActions.Enqueue(action);
        }
    }
    /// <summary>
    /// 等待资源就绪(可用于 await)。
    /// </summary>
    public UniTask WaitUntilReady()
    {
        if (_isReady) return UniTask.CompletedTask;
        return _readySource.Task;
    }
    /// <summary>
    /// 重置状态,用于资源重新加载。
    /// </summary>
    public void Reset()
    {
        _isReady = false;
        _pendingActions.Clear();
        _readySource = new UniTaskCompletionSource();
    }
    /// <summary>
    /// 清空所有排队的动作(如场景切换时)。
    /// </summary>
    public void ClearPending()
    {
        _pendingActions.Clear();
    }
}
/// <summary>
/// US3: MonoBehaviour 版异步资源守卫,可挂载到需要等待资源的 GameObject 上。
/// 典型用法:在 BattleField 上管理特效/Spine 的延迟播放。
/// </summary>
public class AsyncResourceGuardBehaviour : MonoBehaviour
{
    private readonly AsyncResourceGuard _guard = new AsyncResourceGuard();
    public bool IsReady => _guard.IsReady;
    /// <summary>
    /// 标记资源就绪,触发所有排队播放。
    /// </summary>
    public void SetReady()
    {
        _guard.SetReady();
    }
    /// <summary>
    /// 提交一个播放请求:资源就绪则立即执行,否则排队。
    /// </summary>
    public void Play(Action playAction)
    {
        if (this == null) return;
        _guard.EnqueueOrExecute(playAction);
    }
    /// <summary>
    /// 等待资源就绪。
    /// </summary>
    public UniTask WaitUntilReady()
    {
        return _guard.WaitUntilReady();
    }
    /// <summary>
    /// 重置守卫状态。
    /// </summary>
    public void ResetGuard()
    {
        _guard.Reset();
    }
    private void OnDestroy()
    {
        _guard.ClearPending();
    }
}
Main/System/Battle/AsyncResourceGuard.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f604b53f3abcb5544b8ec2b68d0146aa
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleField/BattleField.cs
@@ -5,6 +5,7 @@
using DG.Tweening;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
public class BattleField
@@ -87,7 +88,9 @@
    {
        guid = _guid;
        #pragma warning disable CS0618 // Obsolete — sync legacy constructor, use CreateAsync for new code
        GameObject go = ResManager.Instance.LoadAsset<GameObject>("Battle/Prefabs", "BattleRootNode");
        #pragma warning restore CS0618
        GameObject battleRootNodeGO = GameObject.Instantiate(go);
        battleRootNode = battleRootNodeGO.GetComponent<BattleRootNode>();
        battleRootNodeGO.name = this.GetType().Name;
@@ -98,6 +101,33 @@
        recordPlayer = new RecordPlayer();
        soundManager = new BattleSoundManager(this);
        processingDeathObjIds = new HashSet<uint>();
    }
    /// <summary>
    /// US2: Static async factory method. Creates BattleField with async resource loading.
    /// </summary>
    public static async UniTask<BattleField> CreateAsync(string _guid)
    {
        var field = new BattleField(_guid, skipResourceLoad: true);
        GameObject go = await ResManager.Instance.LoadAssetAsync<GameObject>("Battle/Prefabs", "BattleRootNode");
        GameObject battleRootNodeGO = GameObject.Instantiate(go);
        field.battleRootNode = battleRootNodeGO.GetComponent<BattleRootNode>();
        battleRootNodeGO.name = field.GetType().Name;
        return field;
    }
    /// <summary>
    /// US2: Protected constructor without resource loading (for async factory).
    /// </summary>
    protected BattleField(string _guid, bool skipResourceLoad)
    {
        guid = _guid;
        battleObjMgr = new BattleObjMgr();
        battleEffectMgr = new BattleEffectMgr();
        battleTweenMgr = new BattleTweenMgr();
        recordPlayer = new RecordPlayer();
        soundManager = new BattleSoundManager(this);
        processingDeathObjIds = new HashSet<uint>();
    }
@@ -302,7 +332,22 @@
        BattleMapConfig battleMapConfig = BattleMapConfig.Get(mapID);
        if (battleMapConfig != null)
        {
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback, use LoadMapAsync
            Texture texture = ResManager.Instance.LoadAsset<Texture>("Texture/FullScreenBg", battleMapConfig.MapBg);
        #pragma warning restore CS0618
            battleRootNode.SetBackground(texture);
        }
    }
    /// <summary>
    /// US2: Async map loading.
    /// </summary>
    protected virtual async UniTask LoadMapAsync(int mapID)
    {
        BattleMapConfig battleMapConfig = BattleMapConfig.Get(mapID);
        if (battleMapConfig != null)
        {
            Texture texture = await ResManager.Instance.LoadAssetAsync<Texture>("Texture/FullScreenBg", battleMapConfig.MapBg);
            battleRootNode.SetBackground(texture);
        }
    }
Main/System/Battle/BattleField/StoryBattleField.cs
@@ -2,6 +2,7 @@
using LitJson;
using UnityEngine;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
// 【主线战斗流程】
// 发送 B413  (ReqType 为 2 或 3)
@@ -77,7 +78,21 @@
    {
        if (chapterConfig != null)
        {
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback, use LoadMapAsync
            Texture texture = ResManager.Instance.LoadAsset<Texture>("Texture/FullScreenBg", chapterConfig.MapBG);
        #pragma warning restore CS0618
            battleRootNode.SetBackground(texture);
        }
    }
    /// <summary>
    /// US2: Async map loading.
    /// </summary>
    protected override async UniTask LoadMapAsync(int mapID)
    {
        if (chapterConfig != null)
        {
            Texture texture = await ResManager.Instance.LoadAssetAsync<Texture>("Texture/FullScreenBg", chapterConfig.MapBG);
            battleRootNode.SetBackground(texture);
        }
    }
Main/System/Battle/BattleHUDWin.cs
@@ -78,7 +78,15 @@
    private void InitializePools()
    {
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback, use InitializePoolsAsync
        damagePrefabPool = GameObjectPoolManager.Instance.GetPool(UILoader.LoadPrefab("DamageContent"));
        #pragma warning restore CS0618
    }
    private async UniTask InitializePoolsAsync()
    {
        damagePrefabPool = GameObjectPoolManager.Instance.GetPool(await UILoader.LoadPrefabAsync("DamageContent"));
        if (this == null) return;
    }
    public void SetBattleField(BattleField _battleField)
Main/System/Battle/BattleLoadingWin.cs
New file
@@ -0,0 +1,165 @@
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
/// <summary>
/// 战斗加载界面,显示战斗资源加载进度。
/// 在战斗资源异步加载期间展示,加载完成后自动关闭。
/// </summary>
public class BattleLoadingWin : UIBase
{
    [SerializeField] private Slider m_ProgressSlider;
    [SerializeField] private Text m_ProgressText;
    [SerializeField] private Text m_TipText;
    private float _currentProgress;
    private float _targetProgress;
    private bool _isComplete;
    /// <summary>
    /// 创建 IProgress&lt;float&gt; 适配器,用于集成异步加载的进度回报。
    /// </summary>
    public IProgress<float> CreateProgressReporter()
    {
        return new BattleLoadingProgress(this);
    }
    protected override void InitComponent()
    {
        base.InitComponent();
        _currentProgress = 0f;
        _targetProgress = 0f;
        _isComplete = false;
    }
    protected override void OnPreOpen()
    {
        base.OnPreOpen();
        _currentProgress = 0f;
        _targetProgress = 0f;
        _isComplete = false;
        UpdateUI();
        RefreshTip();
    }
    /// <summary>
    /// 设置加载进度 (0.0 ~ 1.0)。
    /// </summary>
    public void SetProgress(float progress)
    {
        _targetProgress = Mathf.Clamp01(progress);
        if (_targetProgress >= 1f)
        {
            _isComplete = true;
        }
    }
    /// <summary>
    /// 直接设置进度并刷新 UI(无平滑过渡)。
    /// </summary>
    public void SetProgressDirectly(float progress)
    {
        _targetProgress = Mathf.Clamp01(progress);
        _currentProgress = _targetProgress;
        UpdateUI();
        if (_targetProgress >= 1f)
        {
            _isComplete = true;
        }
    }
    private void LateUpdate()
    {
        if (Mathf.Abs(_currentProgress - _targetProgress) > 0.001f)
        {
            _currentProgress = Mathf.Lerp(_currentProgress, _targetProgress, Time.deltaTime * 5f);
            // 接近目标时直接对齐,避免无限趋近
            if (Mathf.Abs(_currentProgress - _targetProgress) < 0.005f)
            {
                _currentProgress = _targetProgress;
            }
            UpdateUI();
        }
        if (_isComplete && _currentProgress >= 0.99f)
        {
            _currentProgress = 1f;
            UpdateUI();
            OnLoadComplete();
        }
    }
    private void UpdateUI()
    {
        if (m_ProgressSlider != null)
        {
            m_ProgressSlider.value = _currentProgress;
        }
        if (m_ProgressText != null)
        {
            m_ProgressText.text = $"{(int)(_currentProgress * 100)}%";
        }
    }
    private void RefreshTip()
    {
        if (m_TipText != null && GeneralDefine.loadingTips != null && GeneralDefine.loadingTips.Length > 0)
        {
            var randomIndex = UnityEngine.Random.Range(0, GeneralDefine.loadingTips.Length);
            m_TipText.text = Language.Get(GeneralDefine.loadingTips[randomIndex]);
        }
    }
    private void OnLoadComplete()
    {
        // 延迟关闭,让玩家看到 100% 状态
        CompleteAndCloseAsync().Forget();
    }
    private async UniTask CompleteAndCloseAsync()
    {
        await UniTask.Delay(300, cancellationToken: this.GetCancellationTokenOnDestroy());
        if (this != null)
        {
            UIManager.Instance.CloseWindow(this);
        }
    }
    /// <summary>
    /// 显示战斗加载界面并返回进度回报器。
    /// 用法: var progress = await BattleLoadingWin.ShowAsync();
    /// </summary>
    public static async UniTask<BattleLoadingWin> ShowAsync()
    {
        var win = await UIManager.Instance.OpenWindowAsync("BattleLoadingWin") as BattleLoadingWin;
        return win;
    }
    /// <summary>
    /// IProgress 适配器实现。
    /// </summary>
    private class BattleLoadingProgress : IProgress<float>
    {
        private readonly BattleLoadingWin _win;
        public BattleLoadingProgress(BattleLoadingWin win)
        {
            _win = win;
        }
        public void Report(float value)
        {
            if (_win != null)
            {
                _win.SetProgress(value);
            }
        }
    }
}
Main/System/Battle/BattleLoadingWin.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a91908ec288e6544943af9b9bdbbf03
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleManager.cs
@@ -3,6 +3,8 @@
using LitJson;
using System;
using System.Linq;
using Cysharp.Threading.Tasks;
using ProjSG.Resource;
public class BattleManager : GameSystemManager<BattleManager>
{
@@ -502,6 +504,12 @@
        int MapID = (int)vNetData.MapID;
        int FuncLineID = (int)vNetData.FuncLineID;
        // US4 T043: Trigger BattleScene preload before creating battle field
        if (!ResourcePreloader.Instance.IsConfigLoaded("BattleScene"))
        {
            PreloadBattleResourcesAsync().Forget();
        }
        bool isCreate = true;
        if (battleFields.TryGetValue(guid, out battleField))
        {
@@ -548,6 +556,23 @@
        return battleField;
    }
    /// <summary>
    /// US4 T043: 异步预加载战斗资源。
    /// </summary>
    public async UniTask PreloadBattleResourcesAsync(IProgress<float> progress = null)
    {
        if (ResourcePreloader.Instance.IsConfigLoaded("BattleScene")) return;
        await ResourcePreloader.Instance.PreloadAsync("BattleScene", progress);
    }
    /// <summary>
    /// US4 T043: 卸载战斗预加载资源(所有战斗结束时调用)。
    /// </summary>
    public void UnloadBattleResources()
    {
        ResourcePreloader.Instance.UnloadConfig("BattleScene");
    }
    public void DestroyBattleField(BattleField battleField)
    {
        if (battleField == null)
Main/System/Battle/BattleObject/BattleObjectFactory.cs
@@ -2,6 +2,7 @@
using System;
using UnityEngine;
using Spine.Unity;
using Cysharp.Threading.Tasks;
public class BattleObjectFactory
{
@@ -32,10 +33,12 @@
        }
        // ===== 直接加载资源(非预加载的资源不走缓存系统)=====
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback, async path exists in CreateBattleObjectAsync
        SkeletonDataAsset skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>(
            "Hero/SpineRes/", 
            skinCfg.SpineRes
        );
        #pragma warning restore CS0618
        
        if (skeletonDataAsset == null)
        {
@@ -44,7 +47,9 @@
        }
        // ==============================================
        #pragma warning disable CS0618
        GameObject battleGO = ResManager.Instance.LoadAsset<GameObject>("Hero/SpineRes", "Hero_001"/*skinCfg.SpineRes*/);
        #pragma warning restore CS0618
        GameObject goParent = posNodeList[teamHero.positionNum];
        BattleObject battleObject = Produce(teamHero.positionNum, _battleField);
@@ -93,6 +98,65 @@
        return battleObject;
    }
    /// <summary>
    /// US2: Async version of CreateBattleObject.
    /// </summary>
    public static async UniTask<BattleObject> CreateBattleObjectAsync(BattleField _battleField, List<GameObject> posNodeList, TeamHero teamHero, BattleCamp _Camp)
    {
        var skinCfg = HeroSkinConfig.Get(teamHero.SkinID);
        if (skinCfg == null)
        {
            Debug.LogError($"BattleObjectFactory: skinCfg is null for SkinID {teamHero.SkinID}");
            return null;
        }
        SkeletonDataAsset skeletonDataAsset = await ResManager.Instance.LoadAssetAsync<SkeletonDataAsset>(
            "Hero/SpineRes/",
            skinCfg.SpineRes
        );
        if (skeletonDataAsset == null)
        {
            Debug.LogError($"BattleObjectFactory: Failed to load SkeletonDataAsset for {skinCfg.SpineRes}");
            return null;
        }
        GameObject battleGO = await ResManager.Instance.LoadAssetAsync<GameObject>("Hero/SpineRes", "Hero_001");
        GameObject goParent = posNodeList[teamHero.positionNum];
        BattleObject battleObject = Produce(teamHero.positionNum, _battleField);
        battleObject.ObjID = teamHero.ObjID;
        GameObject realGO = GameObject.Instantiate(battleGO, goParent.transform);
        SkeletonAnimation skeletonAnimation = realGO.GetComponentInChildren<SkeletonAnimation>(true);
        float finalScaleRate = modelScaleRate * teamHero.modelScale;
        skeletonAnimation.initialSkinName = skinCfg.InitialSkinName;
        skeletonAnimation.skeletonDataAsset = skeletonDataAsset;
        skeletonAnimation.Initialize(true);
        if (!string.IsNullOrEmpty(skinCfg.InitialSkinName))
        {
            var skeleton = skeletonAnimation.Skeleton;
            skeleton.SetSkin(skinCfg.InitialSkinName);
            skeleton.SetSlotsToSetupPose();
            skeletonAnimation.Update(0);
        }
        realGO.name = battleObject.ObjID.ToString();
        realGO.transform.localScale = new Vector3(finalScaleRate, finalScaleRate, finalScaleRate);
        RectTransform rectTrans = realGO.GetComponent<RectTransform>();
        rectTrans.anchoredPosition = Vector2.zero;
        if (battleObject is HeroBattleObject heroBattleObject)
        {
            heroBattleObject.Init(realGO, teamHero, _Camp);
        }
        return battleObject;
    }
    public static BattleObject Produce(int positionNum, BattleField battleField)
    {
        if (positionNum >= 0)
Main/System/Battle/SkillEffect/SkillEffectFactory.cs
@@ -25,8 +25,6 @@
            default:
                UnityEngine.Debug.LogError("Unknown Skill Effect Type " + skillConfig.effectType + " skill id is " + skillConfig.SkillID);
                return new NoEffect(skillBase, skillConfig, caster, tagUseSkillAttack);
                break;
        }
        return null;
    }
}
Main/System/Battle/Sound/BattleSoundManager.cs
@@ -1,5 +1,6 @@
using UnityEngine;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
/// <summary>
/// 战斗音效管理器
@@ -236,14 +237,32 @@
        if (config == null)
            return null;
        
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback, use LoadAudioClipAsync
        AudioClip audioClip = ResManager.Instance.LoadAsset<AudioClip>(
            "Audio/" + config.Folder,
            config.Audio,
            false
        );
        #pragma warning restore CS0618
        
        return audioClip;
    }
    /// <summary>
    /// US2: Async audio clip loading.
    /// </summary>
    private async UniTask<AudioClip> LoadAudioClipAsync(int audioId)
    {
        var config = AudioConfig.Get(audioId);
        if (config == null)
            return null;
        return await ResManager.Instance.LoadAssetAsync<AudioClip>(
            "Audio/" + config.Folder,
            config.Audio,
            false
        );
    }
    
    /// <summary>
    /// 获取可用的音频源
Main/System/Battle/UIComp/BossHeadCell.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class BossHeadCell : MonoBehaviour
{
@@ -28,6 +29,23 @@
        // TODO YYL
        // imgFrame跟imgDecoration等幻境阁完成之后再来做
    }
    public async UniTask SetTeamHeroAsync(TeamHero teamHero)
    {
        if (null == teamHero)
        {
            SetDefault();
            return;
        }
        HeroSkinConfig heroSkinConfig = teamHero.skinConfig;
        imgIcon.sprite = await UILoader.LoadSpriteAsync("HeroHead", heroSkinConfig.SquareIcon);
        if (this == null) return;
        txtLv.text = Language.Get("Arena22", teamHero.level);
        // TODO YYL
        // imgFrame跟imgDecoration等幻境阁完成之后再来做
    }
    
    public void SetDefault()
    {
Main/System/Battle/UIComp/SkillTips.cs
@@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using UnityEngine.UI;
using UnityEngine;
@@ -81,6 +82,68 @@
        });
        battleField.battleTweenMgr.OnPlayTween(tween1);
    }
    public async UniTask PlayMotionAsync(BattleField battleField, bool isRed, TeamHero teamHero, SkillConfig skillConfig)
    {
        if (teamHero == null || skillConfig == null)
        {
            return;
        }
        if (skillConfig.FuncType != 2)
            return;
        KillAllTweens();
        imgIcon.sprite = await UILoader.LoadSpriteAsync("HeroHead", teamHero.skinConfig.SquareIcon);
        if (this == null) return;
        imgSkillName.sprite = await UILoader.LoadSpriteAsync("SkillNameIcon", skillConfig.SkillTipsName);
        if (this == null) return;
        imgSkillName.SetNativeSize();
        // 保证开始时所有图片为可见(alpha=1)
        if (imageBg != null) { var c = imageBg.color; c.a = 1f; imageBg.color = c; }
        if (imgIcon != null) { var c = imgIcon.color; c.a = 1f; imgIcon.color = c; }
        if (imgSkillName != null) { var c = imgSkillName.color; c.a = 1f; imgSkillName.color = c; }
        gameObject.SetActive(true);
        float posY = transform.localPosition.y;
        transform.localPosition = isRed ? new Vector3(-beginingX, posY, 0f) : new Vector3(beginingX, posY, 0f);
        tween1 = transform.DOLocalMoveX(0, tweenDuration / battleField.speedRatio, false).SetEase(Ease.Linear).OnComplete(() =>
        {
            tween1 = null;
            tween3 = DOVirtual.DelayedCall(delayDuration / battleField.speedRatio, () =>
            {
                tween3 = null;
                // tween2 改为做减淡(对 imageBg、imgIcon、imgSkillName 同步淡出)
                float fadeDuration = tweenDuration / battleField.speedRatio;
                Sequence seq = DOTween.Sequence();
                if (imageBg != null)
                    seq.Join(imageBg.DOFade(0f, fadeDuration).SetEase(Ease.InQuad));
                if (imgIcon != null)
                    seq.Join(imgIcon.DOFade(0f, fadeDuration).SetEase(Ease.InQuad));
                if (imgSkillName != null)
                    seq.Join(imgSkillName.DOFade(0f, fadeDuration).SetEase(Ease.InQuad));
                seq.OnComplete(() =>
                {
                    tween2 = null;
                    // 恢复图片 alpha,保证下次显示时可见
                    if (imageBg != null) { var cc = imageBg.color; cc.a = 1f; imageBg.color = cc; }
                    if (imgIcon != null) { var cc = imgIcon.color; cc.a = 1f; imgIcon.color = cc; }
                    if (imgSkillName != null) { var cc = imgSkillName.color; cc.a = 1f; imgSkillName.color = cc; }
                    transform.localPosition = isRed ? new Vector3(-beginingX, posY, 0f) : new Vector3(beginingX, posY, 0f);
                    gameObject.SetActive(false);
                });
                tween2 = seq;
                battleField.battleTweenMgr.OnPlayTween(tween2);
            });
            battleField.battleTweenMgr.OnPlayTween(tween3);
        });
        battleField.battleTweenMgr.OnPlayTween(tween1);
    }
    
    public void KillAllTweens()
    {
Main/System/Battle/UIComp/TotalDamageDisplayer.cs
@@ -97,4 +97,78 @@
        battleField.battleTweenMgr.OnPlayTween(punchTween);
    }
    public async UniTask SetDamageAsync(BattleDmgInfo dmgInfo)
    {
        var battleField = BattleManager.Instance.GetBattleField(dmgInfo.battleFieldGuid);
        if (!gameObject.activeInHierarchy)
            gameObject.SetActive(true);
        if (dmgInfo == null)
            return;
        if (dmgInfo.isFirstHit)
        {
            damage = 0;
            heal = 0;
        }
        if (dmgInfo.IsType(DamageType.Recovery))
        {
            // 保持原有处理逻辑位置
            foreach (var h in dmgInfo.damageList)
            {
                heal += h;
            }
            textDamage.text = BattleUtility.DisplayDamageNum(heal, BattleConst.BattleTotalRecoverType);
            damageBackground.sprite = await UILoader.LoadSpriteAsync("Fight", "Fight1_img_83");
            if (this == null) return;
            imgTotalDesc.sprite = await UILoader.LoadSpriteAsync("Fight", "Fight1_img_80");
            if (this == null) return;
        }
        else if (dmgInfo.IsType(DamageType.Damage) || dmgInfo.IsType(DamageType.Realdamage))
        {
            // 保持原有处理逻辑位置
            foreach (var d in dmgInfo.damageList)
            {
                damage += d;
            }
            textDamage.text = BattleUtility.DisplayDamageNum(damage, BattleConst.BattleTotalDamageType);
            imgTotalDesc.sprite = await UILoader.LoadSpriteAsync("Fight", "Fight1_img_85");
            if (this == null) return;
            damageBackground.sprite = await UILoader.LoadSpriteAsync("Fight", "Fight1_img_88");
            if (this == null) return;
        }
        else
        {
            gameObject.SetActive(false);
            return;
        }
        if (punchTween != null && punchTween.IsActive())
        {
            battleField.battleTweenMgr.OnKillTween(punchTween);
            textDamage.transform.localScale = Vector3.one;
            punchTween = null;
        }
        punchTween = DOTween.Sequence();
        var tween1 = textDamage.transform.DOPunchScale(scalePunch, scaleDuration / battleField.speedRatio, 1);
        punchTween.Append(tween1);
        //  播放结束后 延迟1.5秒再消失
        var tween2 = DOVirtual.DelayedCall(delayCloseDuration / battleField.speedRatio, () => { });
        punchTween.Append(tween2);
        punchTween.OnComplete(() =>
        {
            textDamage.transform.localScale = Vector3.one;
            if (dmgInfo.isLastHit)
            {
                gameObject.SetActive(false);
            }
        });
        battleField.battleTweenMgr.OnPlayTween(punchTween);
    }
}
Main/System/BattleDetail/BattleDetailHeroInfoItem.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class BattleDetailHeroInfoItem : MonoBehaviour
{
@@ -66,6 +67,50 @@
        DisplaySlider(imgCureHP, txtCureHP, (ulong)info.CureHP, data.maxCure);
    }
    public async UniTask DisplayAsync(BattleDetailHeroInfoItemData data)
    {
        if (data == null || data.info == null)
            return;
        BattleDetailHeroInfo info = data.info;
        int heroID = info.HeroID;
        if (!HeroConfig.HasKey(heroID))
            return;
        HeroConfig heroConfig = HeroConfig.Get(heroID);
        int skinID = info.Skin;
        if (!HeroSkinConfig.HasKey(skinID))
            return;
        HeroSkinConfig skinConfig = HeroSkinConfig.Get(skinID);
        bool isDead = info.Dead == 1;
        imgMask.SetActive(isDead);
        imgMVP.SetActive(data.index == data.mvpIndex);
        imgHeadBg.SetSprite("heroheadBG" + heroConfig.Quality);
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", skinConfig.SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            imgHead.SetSprite("herohead_default");
        }
        else
        {
            imgHead.overrideSprite = sprite;
        }
        imgCountry.SetSprite(HeroUIManager.Instance.GetCountryIconName(heroConfig.Country));
        txtHeroName.text = heroConfig.Name;
        txtLV.text = StringUtility.Concat(Language.Get("L1094"), info.LV.ToString());
        DisplayStars(info.Star);
        DisplaySlider(imgAtkHurt, txtAtkHurt, (ulong)info.AtkHurt, data.maxAtk);
        DisplaySlider(imgDefHurt, txtDefHurt, (ulong)info.DefHurt, data.maxDef);
        DisplaySlider(imgCureHP, txtCureHP, (ulong)info.CureHP, data.maxCure);
    }
    private void DisplaySlider(ImageEx imgSlider, TextEx txtSlider, ulong value, ulong maxValue)
Main/System/Debug/DebugUtility.cs
@@ -4,6 +4,7 @@
using LitJson;
using System.IO;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class DebugUtility : Singleton<DebugUtility>
@@ -97,6 +98,17 @@
        }
    }
    public async UniTask CreateDebugRootAsync()
    {
        if (debugRoot == null)
        {
            var prefab = await BuiltInLoader.LoadPrefabAsync("UIRootDebug");
            debugRoot = GameObject.Instantiate(prefab);
            MonoBehaviour.DontDestroyOnLoad(debugRoot);
            debugRoot.name = "UIRootDebug";
        }
    }
    public class DebugBranch
    {
Main/System/Equip/EquipExchangeCell.cs
@@ -175,6 +175,7 @@
    async UniTask RefreshEffect(ItemModel equip)
    {
        await UniTask.DelayFrame(1);
        if (this == null) return; // destroyed during await
        int effectID = EquipModel.Instance.equipUIEffects[Math.Min(equip.config.ItemColor, EquipModel.Instance.equipUIEffects.Length) - 1];
        if (effectID == 0)
        {
@@ -191,6 +192,7 @@
        //二次处理放大效果
        await UniTask.Delay(100);
        if (this == null) return; // destroyed during await
        if (effectID == 0)
        {
            uieffect.Stop();
Main/System/Equip/EquipTipWin.cs
@@ -200,6 +200,7 @@
    async UniTask RefreshEffect(int itemColor)
    {
        await UniTask.DelayFrame(3);
        if (this == null) return; // destroyed during await
        int effectID = EquipModel.Instance.equipUIEffects[Math.Min(itemColor, EquipModel.Instance.equipUIEffects.Length) - 1];
        if (effectID == 0)
        {
Main/System/Equip/FloorItemCell.cs
@@ -25,6 +25,7 @@
    public async UniTask Display(int index, bool isAnimate, Vector3 position)
    {
        await UniTask.Delay(300);
        if (this == null) return; // destroyed during await
        itemIndex = index;
        float duration = 0.5f / AutoFightModel.Instance.fightSpeed; //掉落时间
        var item = PackManager.Instance.GetItemByIndex(PackType.DropItem, index);
Main/System/Gubao/GubaoCallCell.cs
@@ -96,14 +96,18 @@
        int delay = isSkip ? 0 : index * 100;   // delay 毫秒
        await UniTask.Delay(delay);
        if (this == null) return; // destroyed during await
        rotationTween.Play();
        await UniTask.Delay(300);
        if (this == null) return; // destroyed during await
        rotationTween.Stop();
        openEffect.Play();
        await UniTask.Delay(200);
        if (this == null) return; // destroyed during await
        canImage.SetActive(false);
        await UniTask.Delay(400);
        if (this == null) return; // destroyed during await
        showEffect.effectId = GetShowEffectID(result.itemId, result.count);
        showEffect.PlayByArrIndex(Math.Max(itemCfg.ItemColor - 1, 0));
        itemIcon.SetActive(true);
Main/System/Gubao/GubaoCallWin.cs
@@ -100,6 +100,7 @@
        await UniTask.Delay((int)(delay*1000*0.6));
        if (this == null) return; // destroyed during await
        if (quality >= 10)
        {
@@ -114,6 +115,7 @@
            roleModel.Play(GubaoManager.Instance.emojiGBDict[quality]);
        }
        await UniTask.Delay((int)(delay*1000*0.4));
        if (this == null) return; // destroyed during await
        if (delay != 0)
        {
            opObj.SetActive(true);
Main/System/Guild/GuildBaseWin.cs
@@ -212,6 +212,7 @@
    async UniTask Talk(int index)
    {
        await UniTask.Delay(5000);
        if (this == null) return; // destroyed during await
        talkRects[index].SetActive(false);
        var npc = funcNPCs[index].GetModel();
        npc.PlayAnimation("idle", true);
Main/System/Guild/GuildBossWin.cs
@@ -413,10 +413,13 @@
        atkBtn.SetColorful(null, false);
        atkCDText.text = "3";
        await UniTask.Delay(1000);
        if (this == null) return; // destroyed during await
        atkCDText.text = "2";
        await UniTask.Delay(1000);
        if (this == null) return; // destroyed during await
        atkCDText.text = "1";
        await UniTask.Delay(1000);
        if (this == null) return; // destroyed during await
        atkBtn.SetColorful(null, true);
        isCD = false;
@@ -485,6 +488,7 @@
                };
                hurtValues[i].text = BattleUtility.DisplayDamageNum(dmg);
                await UniTask.Delay(atkValueShowCD);
                if (this == null) return; // destroyed during await
            }
            else
            {
Main/System/Guild/GuildManager.cs
@@ -233,7 +233,6 @@
        config = FuncConfigConfig.Get("FamilyBillboardSet");
        rankShowMaxCnt = int.Parse(config.Numerical1);
        pageCnt = int.Parse(config.Numerical2);
        queryPointNum = int.Parse(config.Numerical3);
    }
Main/System/HappyXB/HeroCallResultCell.cs
@@ -30,6 +30,7 @@
        int delaytime = LocalSave.GetBool(HeroUIManager.skipKey + PlayerDatas.Instance.baseData.PlayerID, false) ? 50 * index : 100 * index;
        await UniTask.Delay(delaytime);
        if (this == null) return; // destroyed during await
        this.transform.localScale = Vector3.one;
        //先显示台子,再显示小人
        heroModel.SetActive(false);
Main/System/HappyXB/HeroCallResultWin.cs
@@ -143,6 +143,7 @@
        }
        await UniTask.Delay(resultState == ResultState.singleStart ? 800 : 1500);
        if (this == null) return; // destroyed during await
        resultState = ResultState.Lihui;
        try
Main/System/Hero/UIHeroController.cs
@@ -1,5 +1,6 @@
using System;
using Cysharp.Threading.Tasks;
using Spine.Unity;
using UnityEngine;
using UnityEngine.UI;
@@ -150,6 +151,143 @@
        spineAnimationState.Complete += OnAnimationComplete;
    }
    public async UniTask CreateAsync(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.Contains("SkeletonData"))
                    {
                        skeletonGraphic.enabled = true;
                    }
                }
                else
                {
                    skeletonGraphic.enabled = true;
                }
            }
            return;
        }
        skinID = _skinID;
        var skinConfig = HeroSkinConfig.Get(skinID);
        if (isLh)
        {
            //X轴偏移,Y轴偏移,缩放,是否水平翻转(0否1是)
            if (skinConfig.TachieParam.Length == 4)
            {
                this.transform.localPosition = new Vector3(skinConfig.TachieParam[0], skinConfig.TachieParam[1], 0);
                this.transform.localScale = Vector3.one * skinConfig.TachieParam[2];
                this.transform.localRotation = Quaternion.Euler(0, skinConfig.TachieParam[3] == 0 ? 0 : 180, 0);
            }
            else
            {
                this.transform.localPosition = Vector3.zero;
                this.transform.localScale = Vector3.one;
                this.transform.localRotation = Quaternion.identity;
            }
            //立绘特殊处理,没有spine动画的改用图片
            var lhImg = this.AddMissingComponent<RawImage>();
            if (!skinConfig.Tachie.Contains("SkeletonData"))
            {
                //图片替换
                lhImg.SetTexture2DPNG(skinConfig.Tachie);
                lhImg.SetNativeSize();
                if (skeletonGraphic != null)
                {
                    skeletonGraphic.enabled = false;
                }
                lhImg.enabled = true;
                lhImg.raycastTarget = false;
                return;
            }
            else
            {
                if (skeletonGraphic != null)
                {
                    skeletonGraphic.enabled = true;
                }
                lhImg.enabled = false;
            }
        }
        else
        {
            this.transform.localScale = Vector3.one * scale;
        }
        onComplete = _onComplete;
        pool = GameObjectPoolManager.Instance.GetPool(await UILoader.LoadPrefabAsync("UIHero"));
        if (this == null) return;
        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.initialSkinName = skinConfig.InitialSkinName;
        skeletonGraphic.Initialize(true);
        // 初始化完成后设置皮肤
        if (!string.IsNullOrEmpty(skinConfig.InitialSkinName))
        {
            var skeleton = skeletonGraphic.Skeleton;
            skeleton.SetSkin(skinConfig.InitialSkinName);
            skeleton.SetSlotsToSetupPose();
            skeletonGraphic.Update(0);
        }
        skeletonGraphic.enabled = true;
        SetMaterialNone();
        spineAnimationState = skeletonGraphic.AnimationState;
        spineAnimationState.Data.DefaultMix = 0f;
        if (motionName == "")
            motionName = GetFistSpineAnim();
        PlayAnimation(motionName, true);
        spineAnimationState.Complete -= OnAnimationComplete;
        spineAnimationState.Complete += OnAnimationComplete;
    }
Main/System/HeroUI/HeroBestWin.cs
@@ -171,6 +171,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        var layouts = allAttrScroll.GetComponentsInChildren<LayoutGroup>(true);
        foreach (var layout in layouts)
@@ -178,6 +179,7 @@
            LayoutRebuilder.ForceRebuildLayoutImmediate(layout.GetComponent<RectTransform>());
        }
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        foreach (var layout in layouts)
        {
Main/System/HeroUI/HeroConnectionHeadCell.cs
@@ -1,5 +1,6 @@
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
//羁绊中的武将
public class HeroConnectionHeadCell : MonoBehaviour
@@ -52,5 +53,43 @@
        connMarkImg.SetActive(index != 0);
    }
    public async UniTask DisplayAsync(int heroID, int index, bool showCollect = false, int _skinID = 0)
    {
        int skinID = 0;
        HeroConfig heroConfig = HeroConfig.Get(heroID);
        if (_skinID != 0)
        {
            skinID = _skinID;
        }
        else
        {
            skinID = heroConfig.SkinIDList[0];  //默认第一个图鉴展示
        }
        nameText.text = heroConfig.Name;
        qualityImg.SetSprite("heroheadBG" + heroConfig.Quality);
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", HeroSkinConfig.Get(skinID).SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
            heroIcon.SetSprite("herohead_default");
        }
        else
        {
            heroIcon.overrideSprite = sprite;
        }
        if (showCollect)
        {
            //未获得武将要置灰
            heroIcon.gray = !HeroManager.Instance.HasHero(heroID);
        }
        connMarkImg.SetActive(index != 0);
    }
}
Main/System/HeroUI/HeroGiftWashWin.cs
@@ -150,6 +150,7 @@
    {
        //延迟0.5秒发包
        await UniTask.Delay(500);
        if (this == null) return; // destroyed during await
        var hero = HeroManager.Instance.GetHero(HeroUIManager.Instance.selectWashHeroGUID);
        if (hero == null)
        {
Main/System/HeroUI/HeroHeadBaseCell.cs
@@ -2,6 +2,7 @@
using UnityEngine.UI;
using UnityEngine.Events;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
public class HeroHeadBaseCell : MonoBehaviour
{
@@ -197,6 +198,68 @@
    }
    // 武将小头像 Async版本
    public async UniTask InitAsync(int heroID, int skinID, int star = 0, int awakelv = 0, int lv = 0, UnityAction onclick = null)
    {
        LoadPrefab();   //存在被卸载的可能,重新加载
        if (onclick != null)
        {
            clickBtn.AddListener(onclick);
        }
        var heroConfig = HeroConfig.Get(heroID);
        qualityBG.SetSprite("heroheadBG" + heroConfig.Quality);
        // int skinID = 0;
        // if (heroGuid != "")
        // {
        //     skinID = HeroManager.Instance.GetHero(heroGuid).SkinID;
        // }
        // else
        // {
        //     skinID = heroConfig.SkinIDList[0];
        // }
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", HeroSkinConfig.Get(skinID).SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
            heroIcon.SetSprite("herohead_default");
        }
        else
        {
            heroIcon.overrideSprite = sprite;
        }
        if (star == 0)
        {
            starRect.SetActive(false);
        }
        else
        {
            starRect.SetActive(true);
            for (int i = 0; i < starsImg.Count; i++)
            {
                if ((star - 1) % starsImg.Count >= i)
                {
                    starsImg[i].SetActive(true);
                    starsImg[i].SetSprite("herostar" + (((star - 1) / starsImg.Count) + 1) * starsImg.Count);
                }
                else
                {
                    starsImg[i].SetActive(false);
                }
            }
        }
        countryImg.SetSprite(HeroUIManager.Instance.GetCountryIconName(heroConfig.Country));
        lvText.text = lv == 0 ? "" : Language.Get("L1094") + lv;
        awakeLvRect.SetActive(awakelv > 0);
        awakeLvText.text = awakelv.ToString();
    }
    GameObject cellContainer;
    protected void LoadPrefab()
    {
Main/System/HeroUI/HeroHeadBaseNoTrainCell.cs
@@ -2,6 +2,7 @@
using UnityEngine.UI;
using UnityEngine.Events;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
public class HeroHeadBaseNoTrainCell : MonoBehaviour
{
@@ -111,6 +112,26 @@
    }
    public async UniTask InitAsync(int heroID, bool _gray = false, UnityAction onclick = null)
    {
        LoadPrefab();   //存在被卸载的可能,重新加载
        clickBtn.AddListener(onclick);
        var heroConfig = HeroConfig.Get(heroID);
        qualityBG.SetSprite("heroheadBG" + heroConfig.Quality);
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", HeroSkinConfig.Get(heroConfig.SkinIDList[0]).SquareIcon);
        if (this == null) return;
        heroIcon.overrideSprite = sprite;
        heroIcon.gray = _gray;
        qualityBG.gray = _gray;
        countryImg.SetSprite(HeroUIManager.Instance.GetCountryIconName(heroConfig.Country));
        jobImg.SetSprite(HeroUIManager.Instance.GetJobIconName(heroConfig.Class));
        nameText.text = heroConfig.Name;
    }
    GameObject cellContainer;
    protected void LoadPrefab()
    {
Main/System/HeroUI/HeroPosWin.cs
@@ -394,6 +394,7 @@
            while (showConnectTipQueue.Count > 0)
            {
                await UniTask.Delay(300, cancellationToken: token);
                if (this == null) return; // destroyed during await
                showConnectTipQueue.TryDequeue(out int fetterID);
                if (fetterID == 0)
                {
@@ -403,6 +404,7 @@
                connetionForm.Display(fetterID);
                //显示1.5秒后关闭
                await UniTask.Delay(1500, cancellationToken: token);
                if (this == null) return; // destroyed during await
                connetionForm.SetActive(false);
            }
Main/System/HeroUI/HeroScenePosCell.cs
@@ -101,6 +101,7 @@
    {
        //延迟0.5秒显示
        await UniTask.Delay(TimeSpan.FromSeconds(HeroUIManager.clickFlyPosTime));
        if (this == null) return; // destroyed during await
        objForfly.SetActive(true);
    }
}
Main/System/HeroUI/HeroSkillWin.cs
@@ -57,9 +57,11 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        LayoutRebuilder.ForceRebuildLayoutImmediate(bg);
        // 刷新所有Layout组件
        await UniTask.Delay(100);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        LayoutRebuilder.ForceRebuildLayoutImmediate(bg);
    }
Main/System/HeroUI/HeroTrainWin.cs
@@ -320,6 +320,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        var layouts = allAttrScroll.GetComponentsInChildren<LayoutGroup>(true);
        foreach (var layout in layouts)
@@ -327,6 +328,7 @@
            LayoutRebuilder.ForceRebuildLayoutImmediate(layout.GetComponent<RectTransform>());
        }
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        foreach (var layout in layouts)
        {
Main/System/Horse/HorseController.cs
@@ -1,5 +1,6 @@
using System;
using Cysharp.Threading.Tasks;
using Spine.Unity;
using UnityEngine;
using UnityEngine.UI;
@@ -96,6 +97,84 @@
        spineAnimationState.Complete += OnAnimationComplete;
    }
    // 创建坐骑异步版本
    public async UniTask CreateAsync(int _skinID, int _heroSkinID = 0, float scale = 1f, Action _onComplete = null, string motionName = "idle")
    {
        pool = GameObjectPoolManager.Instance.GetPool(await UILoader.LoadPrefabAsync("UIHorse"));
        if (this == null) return;
        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.transform.Find("Horse").GetComponent<SkeletonGraphic>();
        if (skinID == _skinID)
        {
            if (skinID == 0)
            {
                skeletonGraphic.enabled = false;
            }
            CreateHero(_heroSkinID, scale);
            //避免重复创建
            return;
        }
        skinID = _skinID;
        var skinConfig = HorseSkinConfig.Get(skinID);
        this.transform.localScale = Vector3.one * scale;
        onComplete = _onComplete;
        if (!transform.gameObject.activeSelf)
        {
            transform.SetActive(true);
        }
        if (skinConfig == null || string.IsNullOrEmpty(skinConfig.Spine))
        {
            //卸下坐骑的情况
            skeletonGraphic.enabled = false;
            spineAnimationState = null;
            CreateHero(_heroSkinID, scale);
            return;
        }
        skeletonGraphic.skeletonDataAsset = ResManager.Instance.LoadAsset<SkeletonDataAsset>("UIEffect/Spine/Horse", skinConfig.Spine);
        if (skeletonGraphic.skeletonDataAsset == null)
        {
            transform.SetActive(false);
            if (pool != null)
                pool.Release(instanceGO);
            skeletonGraphic = null;
            Destroy(instanceGO);
            Debug.LogError("未配置spine");
            return;
        }
        skeletonGraphic.enabled = true;
        skeletonGraphic.Initialize(true);
        skeletonGraphic.transform.localPosition = new Vector3(skinConfig.Poses[0], skinConfig.Poses[1], 0);
        isHeroShowBefore = skinConfig.heroFirst == 1;
        spineAnimationState = skeletonGraphic.AnimationState;
        spineAnimationState.Data.DefaultMix = 0f;
        if (motionName == "")
            motionName = GetFistSpineAnim();
        PlayAnimation(motionName, true);
        CreateHero(_heroSkinID, scale);
        spineAnimationState.Complete -= OnAnimationComplete;
        spineAnimationState.Complete += OnAnimationComplete;
    }
    public void CreateHero(int heroSkinID, float _scale)
    {
        if (instanceGO == null)
Main/System/InternalAffairs/AffairBaseWin.cs
@@ -188,6 +188,7 @@
    async UniTask Talk(int index)
    {
        await UniTask.Delay(5000);
        if (this == null) return; // destroyed during await
        talkRects[index].SetActive(false);
        var npc = funcNPCs[index].GetModel();
        npc.PlayAnimation("idle", true);
Main/System/InternalAffairs/GoldRushLeader.cs
@@ -46,6 +46,7 @@
    {
        int delayTime = Math.Max(1, (int)(waitTime * 1000));
        await UniTask.Delay(delayTime);
        if (this == null) return; // destroyed during await
        StartLeaderMove(isBack);
    }
@@ -218,6 +219,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        LayoutRebuilder.ForceRebuildLayoutImmediate(leaderWord.GetComponent<RectTransform>());
    }
}
Main/System/InternalAffairs/GoldRushTentCell.cs
@@ -440,6 +440,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        foreach (var word in wordArr)
        {
            LayoutRebuilder.ForceRebuildLayoutImmediate(word.GetComponent<RectTransform>());
Main/System/ItemTip/SmallTipWin.cs
@@ -39,6 +39,7 @@
    async UniTask UpdatePos()
    {
        await UniTask.DelayFrame(3);
        if (this == null) return; // destroyed during await
        // 限制在屏幕范围内
        Vector3[] corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);
Main/System/KnapSack/Logic/CommonGetItemWin.cs
@@ -105,6 +105,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        var layouts = bg.GetComponentsInChildren<LayoutGroup>(true);
        foreach (var layout in layouts)
@@ -112,6 +113,7 @@
            LayoutRebuilder.ForceRebuildLayoutImmediate(layout.GetComponent<RectTransform>());
        }
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        foreach (var layout in layouts)
        {
Main/System/LineupRecommend/LineupRecommendItem.cs
@@ -1,4 +1,5 @@
using UnityEngine;
using Cysharp.Threading.Tasks;
public class LineupRecommendItem : MonoBehaviour
{
@@ -72,5 +73,61 @@
    }
    public async UniTask DisplayAsync(int recommendID, int index)
    {
        if (!manager.TryGetHeroConfigByIndex(recommendID, index, out HeroConfig heroConfig))
            return;
        if (!manager.TryGetMoneyInfo(recommendID, index, out int moneyType, out int moneyNeedCnt))
            return;
        int heroID = heroConfig.HeroID;
        if (!manager.TryGetHeroSkinConfig(heroID, out HeroSkinConfig heroSkinConfig))
            return;
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", heroSkinConfig.SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            imgHeroHead.SetSprite("herohead_default");
        }
        else
        {
            imgHeroHead.overrideSprite = sprite;
        }
        imgSquareIcon.SetSprite("heroheadBG" + heroConfig.Quality);
        imgCountry.SetSprite(HeroUIManager.Instance.GetCountryIconName(heroConfig.Country));
        txtName.text = heroConfig.Name;
        txtDesc.text = heroConfig.Desc;
        imgJob.SetSprite(HeroUIManager.Instance.GetJobIconName(heroConfig.Class));
        LineupRecommendHeroState heroState = manager.GetHeroState(recommendID, index);
        imgMask.SetActive(heroState != LineupRecommendHeroState.ActivateAndHave);
        txtNoHave.SetActive(heroState == LineupRecommendHeroState.ActivateButNoHave);
        imgMoney.SetActive(heroState == LineupRecommendHeroState.NoActivate || heroState == LineupRecommendHeroState.CanActivate);
        txtMoney.SetActive(heroState == LineupRecommendHeroState.NoActivate || heroState == LineupRecommendHeroState.CanActivate);
        imgRed.SetActive(heroState == LineupRecommendHeroState.CanActivate);
        imgMoney.SetIconWithMoneyType(moneyType);
        imgMoney.gray = heroState == LineupRecommendHeroState.NoActivate;
        txtMoney.text = moneyNeedCnt.ToString();
        txtMoney.color = heroState == LineupRecommendHeroState.NoActivate ? colMoneyNoActivate : colMoneyCanActivate;
        btnClick.SetListener(() =>
        {
            if (heroState == LineupRecommendHeroState.CanActivate)
            {
                manager.SendGetReward(recommendID, index);
            }
            else
            {
                HeroUIManager.Instance.selectForPreviewHeroID = heroConfig.HeroID;
                UIManager.Instance.OpenWindow<HeroBestWin>();
            }
        });
    }
}
Main/System/Login/LoginWin.cs
@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using Cysharp.Threading.Tasks;
public class LoginWin : UIBase, ICanvasRaycastFilter
{
@@ -186,6 +187,57 @@
        checkRead.isOn = LocalSave.GetBool("secretToggleStart5");
    }
    public async UniTask RefreshAsync()
    {
        base.Refresh();
        Debug.Log("刷新登录窗口");
        //打包版本 + 功能版本 + 语言ID
        verInfo.text = LoginManager.Instance.GetVersionStr();
        var sprite = await BuiltInLoader.LoadSpriteAsync("TB_DL_Logo");
        if (this == null) return;
        m_Logo.overrideSprite = sprite;
        m_Logo.SetNativeSize();
        m_Logo.rectTransform.anchoredPosition = VersionConfig.Get().logoPosition;
        m_Notice.SetActive(GameNotice.HasNotice());
        bool hasNotice = GameNotice.HasNotice();
        //  账号切换
        m_SwitchAccount.SetActive(false);
        //  用户帮助
        // TODO YYL
        var appId = VersionConfig.Get().appId;
        var branch = VersionConfig.Get().branch;
        // m_UserHelp.SetActive(ContactConfig.GetConfig(appId, branch) != null);
        //  是否已经获取到服务器列表
        bool isGetServerList = ServerListCenter.Instance.serverListGot;
        m_WaitServerList.SetActive(!isGetServerList);
        m_ContainerEnterGame.SetActive(isGetServerList);
        m_ContainerAccount.SetActive(isGetServerList
            && (VersionConfig.Get().versionAuthority == VersionAuthority.InterTest || VersionConfig.Get().isBanShu));
        m_EnterGame.SetActive(isGetServerList);
        if (isGetServerList)
        {
            ChangeServerInfo(ServerListCenter.Instance.currentServer);
        }
        ChangeUserInfo(LoginManager.Instance.localSaveAccountName);
        m_EnterGame.SetActive(true);
        //m_QQLogin.SetActive(false);
        //m_WXLogin.SetActive(false);
        // 用户协议 todo
        checkRead.isOn = LocalSave.GetBool("secretToggleStart5");
    }
    private void OnLoginOk(SDKUtils.FP_LoginOk arg0)
    {
    }
Main/System/Main/EquipOnMainUI.cs
@@ -118,6 +118,7 @@
        while (UIManager.Instance.IsOpened<EquipExchangeWin>())
        {
            await UniTask.Yield();
            if (this == null) return; // destroyed during await
        }
Main/System/Main/HeroFightingCardCell.cs
@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
//主界面卡牌
public class HeroFightingCardCell : MonoBehaviour
@@ -118,6 +119,89 @@
    }
    public async UniTask DisplayAsync(int index, List<TeamHero> heros)
    {
        TeamHero teamHero = null;
        if (index < heros.Count)
        {
            teamHero = heros[index];
        }
        guid = teamHero != null ? teamHero.guid : "";
        if (guid == "")
        {
            clickHeroBtn.SetActive(false);
            clickEmptyBtn.SetActive(true);
            clickEmptyBtn.AddListener(ClickEmpty);
            emptyLockImg.SetActive(false);
            redPointImg.SetActive(false);
            int lockCnt = HeroUIManager.Instance.lockIndexList.Count;
            //根据锁数量 倒序判断锁住
            if (lockCnt > 0)
            {
                lockIndex = lockCnt - (TeamConst.MaxTeamHeroCount - 1 - index) - 1;
                if (lockIndex >= 0 && lockIndex < lockCnt)
                {
                    emptyLockImg.SetActive(true);
                    redPointImg.SetActive(HeroUIManager.Instance.CanUnLock(HeroUIManager.Instance.lockIndexList[lockIndex]));
                }
            }
            return;
        }
        else
        {
            clickHeroBtn.SetActive(true);
            clickEmptyBtn.SetActive(false);
        }
        var hero = HeroManager.Instance.GetHero(guid);
        var heroID = hero.heroId;
        var star = hero.heroStar;
        clickHeroBtn.AddListener(ClickHero);
        var heroConfig = HeroConfig.Get(heroID);
        qualityBG.SetSprite("herocBG" + heroConfig.Quality);
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", HeroSkinConfig.Get(hero.SkinID).RectangleIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
            heroIcon.SetSprite("herohead_big_default");
        }
        else
        {
            heroIcon.overrideSprite = sprite;
        }
        if (star == 0)
        {
            starRect.SetActive(false);
        }
        else
        {
            starRect.SetActive(true);
            for (int i = 0; i < starsImg.Count; i++)
            {
                if ((star - 1) % starsImg.Count >= i)
                {
                    starsImg[i].SetActive(true);
                    starsImg[i].SetSprite("herostar" + (((star - 1) / starsImg.Count) + 1) * starsImg.Count);
                }
                else
                {
                    starsImg[i].SetActive(false);
                }
            }
        }
        countryImg.SetSprite(HeroUIManager.Instance.GetCountryIconName(heroConfig.Country));
        lvText.text = hero.heroLevel == 0 ? "" : Language.Get("L1094") + hero.heroLevel;
        // RefreshFightIng(false);
    }
    void ClickHero()
    {
Main/System/Main/HomeWin.cs
@@ -451,6 +451,7 @@
    async UniTask DelayPlayMusic()
    {
        await UniTask.Delay(1200);
        if (this == null) return; // destroyed during await
        if (!SoundPlayer.Instance.IsPlayBackGroundMuisic())
            SoundPlayer.Instance.PlayBackGroundMusic(38);
    }
Main/System/Message/ImgAnalysis.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ImgAnalysis : TRichAnalysis<ImgAnalysis>
@@ -229,6 +230,46 @@
        }
    }
    private async UniTask LoadSpriteAsync()
    {
        if (presentImgInfo.IsFace) return;
        if (IconConfig.isInit)
        {
            if (!string.IsNullOrEmpty(presentImgInfo.folderName))
            {
                presentImgInfo.sprite = await UILoader.LoadSpriteAsync(presentImgInfo.folderName, presentImgInfo.spriteName);
            }
            else
            {
                presentImgInfo.sprite = await UILoader.LoadSpriteAsync(presentImgInfo.spriteName);
            }
        }
        if (presentImgInfo.sprite != null)
        {
            RichText text = RichTextMgr.Inst.presentRichText;
            if (text != null)
            {
                if (text.LockImgSize)
                {
                    presentImgInfo.width = presentImgInfo.height = text.fontSize;
                    return;
                }
                else if (text.ModifyImgSiez)
                {
                    presentImgInfo.width = text.ModifyImgWidth;
                    presentImgInfo.height = text.ModifyImgHeight;
                    return;
                }
            }
            if (presentImgInfo.scale != 1f)
            {
                presentImgInfo.width = presentImgInfo.sprite.rect.width * presentImgInfo.scale;
                presentImgInfo.height = presentImgInfo.sprite.rect.height * presentImgInfo.scale;
            }
        }
    }
    private const string FACE_REPLACE = @"#~([0-9a-zA-Z][0-9a-zA-Z][0-9a-zA-Z])";
    public static Regex FaceRegex = new Regex(FACE_REPLACE, RegexOptions.Singleline);
    public static string ReplaceFace(string msg)
Main/System/Message/RichText.cs
@@ -7,6 +7,7 @@
using System;
using System.Linq;
using System.Text;
using Cysharp.Threading.Tasks;
public class RichText : Text, IPointerClickHandler
{
    /// <summary>
@@ -191,6 +192,27 @@
        }
    }
    public async UniTask AwakeAsync()
    {
#if UNITY_EDITOR
        if (UnityEditor.PrefabUtility.GetPrefabType(this) == UnityEditor.PrefabType.Prefab)
        {
            return;
        }
#endif
        unline = transform.GetComponentInChildren<TextUnline>();
        if (unline == null)
        {
            GameObject obj = await BuiltInLoader.LoadPrefabAsync("TextUnline");
            if (this == null) return;
            obj = Instantiate(obj);
            obj.transform.SetParent(transform);
            obj.transform.localScale = Vector3.one;
            unline = obj.GetComponent<TextUnline>();
            unline.raycastTarget = false;
        }
    }
    protected override void OnEnable()
    {
        base.OnEnable();
Main/System/NewBieGuidance/NewBieWin.cs
@@ -213,6 +213,7 @@
    async UniTask DelayDisplay()
    {
        await UniTask.Delay((tryGuideCount + 1) * 100);
        if (this == null) return; // destroyed during await
        tryGuideCount++;
        Display();
    }
@@ -275,6 +276,7 @@
    async UniTask Co_FunctionUnLockDelay()
    {
        await UniTask.Delay(1300);
        if (this == null) return; // destroyed during await
        m_ContainerFunctionBg.SetActive(false);
        m_FunctionName.SetActive(false);
@@ -330,6 +332,7 @@
    async UniTask DelayShowClickEffect()
    {
        await UniTask.Delay(stepConfig.delayTime);
        if (this == null) return; // destroyed during await
        m_ClickEffect.SetActive(true);
        m_ClickEffect.Play();
        m_ClickEffect.transform.position = m_ClickTarget.position;
Main/System/OtherPlayerDetail/OtherEquipTipWin.cs
@@ -117,6 +117,7 @@
    async UniTask RefreshEffect(int itemColor)
    {
        await UniTask.DelayFrame(3);
        if (this == null) return; // destroyed during await
        int effectID = EquipModel.Instance.equipUIEffects[Math.Min(itemColor, EquipModel.Instance.equipUIEffects.Length) - 1];
        if (effectID == 0)
        {
Main/System/OtherPlayerDetail/OtherHeroDetailWin.cs
@@ -597,6 +597,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        var layouts = allAttrScroll.GetComponentsInChildren<LayoutGroup>(true);
        foreach (var layout in layouts)
@@ -604,6 +605,7 @@
            LayoutRebuilder.ForceRebuildLayoutImmediate(layout.GetComponent<RectTransform>());
        }
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        foreach (var layout in layouts)
        {
Main/System/OtherPlayerDetail/OtherHeroFightingCardItem.cs
@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
public class OtherHeroFightingCardItem : MonoBehaviour
{
@@ -40,7 +41,24 @@
    void DisplayHero(OtherPlayerDetailManager.RolePlusData.HeroData heroData)
    {
#pragma warning disable CS0618
        var sprite = UILoader.LoadSprite("HeroHead", HeroSkinConfig.Get(heroData.SkinID).RectangleIcon);
#pragma warning restore CS0618
        if (sprite == null)
        {
            // 内网未配置时
            imgHero.SetSprite("herohead_big_default");
        }
        else
        {
            imgHero.overrideSprite = sprite;
        }
    }
    async UniTask DisplayHeroAsync(OtherPlayerDetailManager.RolePlusData.HeroData heroData)
    {
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", HeroSkinConfig.Get(heroData.SkinID).RectangleIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
Main/System/OtherPlayerDetail/OtherNPCDetailWin.cs
@@ -306,6 +306,7 @@
    async UniTask ForceRefreshLayout()
    {
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        var layouts = allAttrScroll.GetComponentsInChildren<LayoutGroup>(true);
        foreach (var layout in layouts)
@@ -313,6 +314,7 @@
            LayoutRebuilder.ForceRebuildLayoutImmediate(layout.GetComponent<RectTransform>());
        }
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        // 刷新所有Layout组件
        foreach (var layout in layouts)
        {
Main/System/PhantasmPavilion/PhantasmPavilionManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
public partial class PhantasmPavilionManager : GameSystemManager<PhantasmPavilionManager>
{
@@ -228,6 +229,41 @@
    }
    public async UniTask ShowFaceAsync(ImageEx imgFace, UIEffectPlayer spine, UIFrame uiFrame, EllipseMask ellipseMask, int id)
    {
        PhantasmPavilionType type = PhantasmPavilionType.Face;
        int UnlockWay = GetUnlockWay(type, id);
        int unlockValue = GetUnlockValue(type, id);
        int resourceType = GetResourceType(type, id);
        string resourceValue = GetResourceValue(type, id);
        if (UnlockWay == 3 && resourceValue == "")
        {
            int heroID = unlockValue;
            if (!HeroConfig.HasKey(heroID))
                return;
            HeroConfig heroConfig = HeroConfig.Get(heroID);
            int skinID = heroConfig.SkinIDList[0];
            if (!HeroSkinConfig.HasKey(skinID))
                return;
            HeroSkinConfig skinConfig = HeroSkinConfig.Get(skinID);
            var sprite = await UILoader.LoadSpriteAsync("HeroHead", skinConfig.SquareIcon);
            if (sprite == null)
            {
                Show(imgFace, spine, uiFrame, resourceType, "herohead_default", null, ellipseMask);
            }
            else
            {
                Show(imgFace, spine, uiFrame, resourceType, string.Empty, sprite, ellipseMask);
            }
        }
        else
        {
            resourceValue = GetResourceValue(type, id);
            Show(imgFace, spine, uiFrame, resourceType, resourceValue, null, ellipseMask);
        }
    }
    public void Show(ImageEx imgFace, UIEffectPlayer spine, UIFrame uiFrame, int resourceType, string resourceValue, Sprite sprite = null, EllipseMask ellipseMask = null)
    {
        spine.Stop();
Main/System/PhantasmPavilion/PhantasmPavilionModelItem.cs
@@ -1,5 +1,6 @@
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class PhantasmPavilionModelItem : MonoBehaviour
{
@@ -60,4 +61,48 @@
        manager.UpdateItemRedPoint(imgRed, type, id);
    }
    public async UniTask DisplayAsync(int id)
    {
        this.id = id;
        btnChoose.SetListener(() =>
        {
            manager.selectId = id;
        });
        PhantasmPavilionState state = manager.GetUnLockState(type, id);
        bool isLimitedTime = manager.IsLimitTime(type, id);
        bool isUsing = manager.IsUsing(type, id);
        imgChoose.SetActive(manager.selectId == id);
        imgLimit.SetActive(state == PhantasmPavilionState.Activated && isLimitedTime);
        imgLock.SetActive(state != PhantasmPavilionState.Activated);
        imgCanUnlock.SetActive(state == PhantasmPavilionState.CanActivate);
        txtUsing.SetActive(state == PhantasmPavilionState.Activated && isUsing);
        if (!ModelConfig.HasKey(id))
            return;
        ModelConfig model = ModelConfig.Get(id);
        int skinID = model.SkinID;
        if (!HeroSkinConfig.HasKey(skinID))
            return;
        HeroSkinConfig skinConfig = HeroSkinConfig.Get(skinID);
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", skinConfig.SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
            imgFace.SetSprite("herohead_default");
        }
        else
        {
            imgFace.overrideSprite = sprite;
        }
        int resourceType = manager.GetResourceType(type, id);
        string resourceValue = manager.GetResourceValue(type, id);
        imgBg.SetSprite(manager.GetModelBgColorStr(id));
        manager.UpdateItemRedPoint(imgRed, type, id);
    }
}
Main/System/Recharge/PrivilegeActiveCardWin.cs
@@ -44,6 +44,7 @@
    {
        LayoutRebuilder.ForceRebuildLayoutImmediate(bg);
        await UniTask.DelayFrame(2);
        if (this == null) return; // destroyed during await
        LayoutRebuilder.ForceRebuildLayoutImmediate(bg);
    }
}
Main/System/Sound/SoundPlayer.cs
@@ -92,6 +92,22 @@
        Debug.Log("CreateSoundPlayer");
    }
    public static async UniTask CreateSoundPlayerAsync()
    {
        if (m_Instance != null)
        {
            return;
        }
        var prefab = await BuiltInLoader.LoadPrefabAsync("SoundPlayer");
        var gameObject = GameObject.Instantiate(prefab);
        m_Instance = gameObject.GetComponent<SoundPlayer>();
        m_Instance.name = "SoundPlayer";
        m_Instance.SetActive(true);
        DontDestroyOnLoad(gameObject);
        Debug.Log("CreateSoundPlayer");
    }
    public void PlayBackGroundMusic(int _audioId)
    {
        if (_audioId <= 0)
@@ -177,7 +193,9 @@
                if (key != _exclude)
                {
                    var config = AudioConfig.Get(key);
                    #pragma warning disable CS0618 // Obsolete — sync legacy unload
                    ResManager.Instance.UnloadAsset("Audio/" + config.Folder, config.Audio);
                    #pragma warning restore CS0618
                }
            }
@@ -196,6 +214,7 @@
    public async UniTask PlayUIAudioDelay(int _audioId)
    {
        await UniTask.Delay(1);
        if (this == null) return; // destroyed during await
        PlayUIAudio(_audioId);
    }
@@ -371,6 +390,16 @@
       }
    }
    public async UniTask PlayLoginMusicAsync()
    {
        var loginMusic = await BuiltInLoader.LoadMusicAsync("login");
        if (this == null) return;
        if (!m_MusicAudioSource.isPlaying || m_MusicAudioSource.clip != loginMusic)
        {
            StartCoroutine(Co_BackGroundMusicFadeOutIn(loginMusic, false));
        }
    }
    //private void LateUpdate()
    //{
    //    if (CameraController.Instance != null && CameraController.Instance.CameraObject != null)
Main/System/TianziBillborad/TianziBillboradBossHead.cs
@@ -1,5 +1,6 @@
using System;
using UnityEngine;
using Cysharp.Threading.Tasks;
public class TianziBillboradBossHead : MonoBehaviour
{
@@ -57,6 +58,52 @@
        }
    }
    public async UniTask DisplayAsync(int bossId)
    {
        if (!NPCConfig.HasKey(bossId))
            return;
        NPCConfig npcConfig = NPCConfig.Get(bossId);
        int heroID = npcConfig.RelatedHeroID;
        if (!HeroConfig.HasKey(heroID))
            return;
        var heroConfig = HeroConfig.Get(heroID);
        int skinID = heroConfig.SkinIDList[0];
        if (!HeroSkinConfig.HasKey(skinID))
            return;
        if (!model.TryGetBossConfig(model.DataMapID, model.todayLineID, out DungeonConfig dungeonConfig, out NPCLineupConfig npcLineupConfig, out NPCConfig npcConfigToday))
            return;
        isTodayBoss = npcConfigToday.NPCID == bossId;
        // --- 设置尺寸 ---
        imgQuality.rectTransform.sizeDelta = isTodayBoss ? new Vector2(104, 104) : new Vector2(94, 94);
        rectTransform.sizeDelta = isTodayBoss ? new Vector2(104, 104) : new Vector2(94, 94);
        // --- 设置图像和状态 ---
        var heroSkinConfig = HeroSkinConfig.Get(skinID);
        imgQuality.SetSprite("heroheadBG" + heroConfig.Quality);
        imgQuality.gray = !isTodayBoss;
        var sprite = await UILoader.LoadSpriteAsync("HeroHead", heroSkinConfig.SquareIcon);
        if (this == null) return;
        if (sprite == null)
        {
            // 内网未配置时
            imgHeadIcon.SetSprite("herohead_default");
        }
        else
        {
            imgHeadIcon.overrideSprite = sprite;
        }
        imgHeadIcon.gray = !isTodayBoss;
        txtTime.SetActive(isTodayBoss);
        if (isTodayBoss)
        {
            UpdateTimer();
        }
    }
    public void UpdateTimer()
    {
        if (!isTodayBoss)
Main/System/Tip/ScrollTip.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ScrollTip
{
@@ -83,6 +84,21 @@
        return tip;
    }
    public static async UniTask<ScrollTipDetail> RequestAsync()
    {
        ScrollTipDetail tip = null;
        if (pool == null)
        {
            var _prefab = await UILoader.LoadPrefabAsync("Tip");
            pool = GameObjectPoolManager.Instance.GetPool(_prefab);
        }
        if (pool != null)
        {
            tip = pool.Request().AddMissingComponent<ScrollTipDetail>();
        }
        return tip;
    }
    public static void Release(ScrollTipDetail tip, bool next = true)
    {
        if (m_ActiveTips.Contains(tip))
Main/System/Tip/ScrollTipWin.cs
@@ -75,6 +75,7 @@
            {
                OnTipReceiveEvent();
                await UniTask.Delay(100);
                if (this == null) return; // destroyed during await
            }
        }
        finally
Main/System/UIBase/UIBase.cs
@@ -176,6 +176,7 @@
        {
            //延迟x帧后可点击,防止点击过快立即关闭了
            await UniTask.Delay(200);
            if (this == null) return; // destroyed during await
            btnClickEmptyClose.enabled = true;
        }
    }
@@ -208,6 +209,7 @@
    protected async void ExecuteNextFrame(Action _action)
    {
        await UniTask.DelayFrame(1);
        if (this == null) return; // destroyed during await
        _action?.Invoke();
    }
@@ -422,6 +424,7 @@
    public async UniTask DelayCloseWindow(int delayTime = 30)
    {
        await UniTask.Delay(delayTime);
        if (this == null) return; // destroyed during await
        CloseWindow();
    }
Main/Tests.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3557e951d4d91304da2928693eb783e1
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Tests/Main.Tests.asmdef
New file
@@ -0,0 +1,28 @@
{
    "name": "Main.Tests",
    "rootNamespace": "",
    "references": [
        "GUID:eac12500b66aea340bf118070ac14bec",
        "GUID:3ffa07c58a98b0445a7a34376b165fd1",
        "GUID:f51ebe6a0ceec4240a699833d6309b23",
        "GUID:e34a5702dd353724aa315fb8011f08c3",
        "GUID:1278a46ce459c5a46b4eaeda148684ef",
        "GUID:27619889b8ba8c24980f86f011571974",
        "GUID:0acc523941302664db1f4e527237feb3"
    ],
    "includePlatforms": [
        "Editor"
    ],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": true,
    "precompiledReferences": [
        "nunit.framework.dll"
    ],
    "autoReferenced": false,
    "defineConstraints": [
        "UNITY_INCLUDE_TESTS"
    ],
    "versionDefines": [],
    "noEngineReferences": false
}
Main/Tests/Main.Tests.asmdef.meta
New file
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3b1d5c2a5a8a8ac4d90faec3a9bbe7e5
AssemblyDefinitionImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Tests/ResourceCacheManagerTests.cs
New file
@@ -0,0 +1,99 @@
// ============================================================================
// ResourceCacheManagerTests.cs — 缓存管理器单元测试
// Feature: 001-async-resource-loading
// ============================================================================
using System.Collections;
using NUnit.Framework;
using UnityEngine.TestTools;
using ProjSG.Resource;
[TestFixture]
public class ResourceCacheManagerTests
{
    private ResourceCacheManager _cacheManager;
    [SetUp]
    public void SetUp()
    {
        _cacheManager = ResourceCacheManager.Instance;
    }
    [TearDown]
    public void TearDown()
    {
        _cacheManager.ForceReleaseAll();
    }
    // ====================================================================
    // 缓存命中 / 未命中
    // ====================================================================
    [Test]
    public void GetCached_ReturnsNull_WhenNotCached()
    {
        var result = _cacheManager.GetCached<UnityEngine.Texture2D>("nonexistent/path");
        Assert.IsNull(result);
    }
    [Test]
    public void IsCached_ReturnsFalse_WhenNotCached()
    {
        Assert.IsFalse(_cacheManager.IsCached("nonexistent/path"));
    }
    [Test]
    public void GetCached_ReturnsNull_WhenNullLocation()
    {
        var result = _cacheManager.GetCached<UnityEngine.Texture2D>(null);
        Assert.IsNull(result);
    }
    [Test]
    public void GetCached_ReturnsNull_WhenEmptyLocation()
    {
        var result = _cacheManager.GetCached<UnityEngine.Texture2D>("");
        Assert.IsNull(result);
    }
    [Test]
    public void IsCached_ReturnsFalse_WhenNullOrEmpty()
    {
        Assert.IsFalse(_cacheManager.IsCached(null));
        Assert.IsFalse(_cacheManager.IsCached(""));
    }
    // ====================================================================
    // 释放
    // ====================================================================
    [Test]
    public void Release_DoesNotThrow_WhenLocationNotCached()
    {
        Assert.DoesNotThrow(() => _cacheManager.Release("nonexistent/path"));
    }
    [Test]
    public void ReleaseAll_DoesNotThrow_WhenEmpty()
    {
        Assert.DoesNotThrow(() => _cacheManager.ReleaseAll());
    }
    [Test]
    public void ForceReleaseAll_ResetsCachedCount_ToZero()
    {
        _cacheManager.ForceReleaseAll();
        Assert.AreEqual(0, _cacheManager.CachedCount);
    }
    // ====================================================================
    // CachedCount
    // ====================================================================
    [Test]
    public void CachedCount_IsZero_WhenEmpty()
    {
        _cacheManager.ForceReleaseAll();
        Assert.AreEqual(0, _cacheManager.CachedCount);
    }
}
Main/Tests/ResourceCacheManagerTests.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6066e660ee423e4f95f22d8d1276a36
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Tests/ResourcePreloaderTests.cs
New file
@@ -0,0 +1,101 @@
// ============================================================================
// ResourcePreloaderTests.cs — 预加载管理器单元测试
// Feature: 001-async-resource-loading
// ============================================================================
using NUnit.Framework;
using ProjSG.Resource;
[TestFixture]
public class ResourcePreloaderTests
{
    private ResourcePreloader _preloader;
    [SetUp]
    public void SetUp()
    {
        _preloader = ResourcePreloader.Instance;
    }
    // ====================================================================
    // 配置注册
    // ====================================================================
    [Test]
    public void RegisterConfig_AcceptsValidConfig()
    {
        var config = new PreloadConfig
        {
            ConfigName = "TestConfig",
            Locations = new[] { "Assets/Test/a.png" },
            IsPermanent = false,
        };
        Assert.DoesNotThrow(() => _preloader.RegisterConfig(config));
    }
    [Test]
    public void RegisterConfig_RejectsNullConfig()
    {
        // Should log error but not throw
        Assert.DoesNotThrow(() => _preloader.RegisterConfig(null));
    }
    [Test]
    public void RegisterConfig_RejectsEmptyName()
    {
        var config = new PreloadConfig
        {
            ConfigName = "",
            Locations = new[] { "Assets/Test/a.png" },
        };
        Assert.DoesNotThrow(() => _preloader.RegisterConfig(config));
    }
    // ====================================================================
    // IsConfigLoaded
    // ====================================================================
    [Test]
    public void IsConfigLoaded_ReturnsFalse_WhenNotLoaded()
    {
        Assert.IsFalse(_preloader.IsConfigLoaded("NotRegistered"));
    }
    [Test]
    public void IsConfigLoaded_ReturnsFalse_ForNewConfig()
    {
        _preloader.RegisterConfig(new PreloadConfig
        {
            ConfigName = "NewConfig",
            Locations = new[] { "Assets/Test/x.png" },
        });
        Assert.IsFalse(_preloader.IsConfigLoaded("NewConfig"));
    }
    // ====================================================================
    // UnloadConfig
    // ====================================================================
    [Test]
    public void UnloadConfig_DoesNotThrow_WhenNotRegistered()
    {
        Assert.DoesNotThrow(() => _preloader.UnloadConfig("NonExistent"));
    }
    [Test]
    public void UnloadConfig_SkipsPermanentConfig()
    {
        _preloader.RegisterConfig(new PreloadConfig
        {
            ConfigName = "PermanentTest",
            Locations = new[] { "Assets/Test/p.png" },
            IsPermanent = true,
        });
        // Should not throw, should log warning
        Assert.DoesNotThrow(() => _preloader.UnloadConfig("PermanentTest"));
    }
}
Main/Tests/ResourcePreloaderTests.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e835caf82b8e5cc49a15766510c3d0cb
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Tests/YooAssetServiceTests.cs
New file
@@ -0,0 +1,211 @@
// ============================================================================
// YooAssetServiceTests.cs — YooAssetService 核心逻辑单元测试
// 覆盖:初始化状态机、LoadAssetAsync null/invalid 处理、ReleaseHandle 幂等性
// ============================================================================
using System;
using NUnit.Framework;
using ProjSG.Resource;
namespace ProjSG.Resource.Tests
{
    [TestFixture]
    public class YooAssetServiceTests
    {
        private YooAssetService _service;
        [SetUp]
        public void SetUp()
        {
            // 确保每个测试开始时销毁旧实例
            if (YooAssetService.IsValid())
            {
                YooAssetService.Destroy();
            }
            _service = YooAssetService.Instance;
        }
        [TearDown]
        public void TearDown()
        {
            if (YooAssetService.IsValid())
            {
                YooAssetService.Destroy();
            }
        }
        // ====================================================================
        // 初始化状态机测试
        // ====================================================================
        [Test]
        public void IsInitialized_BeforeInit_ReturnsFalse()
        {
            Assert.IsFalse(_service.IsInitialized);
        }
        [Test]
        public void ThrowsIfNotInitialized_LoadAssetAsync()
        {
            // 未初始化时调用加载方法应抛出 InvalidOperationException
            // ThrowIfNotInitialized 在异步方法同步部分抛出,GetAwaiter().GetResult() 会立即重新抛出
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.LoadAssetAsync<UnityEngine.Object>("test_location").GetAwaiter().GetResult();
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_LoadAssetAsyncByType()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.LoadAssetAsync("test_location", typeof(UnityEngine.Object)).GetAwaiter().GetResult();
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_LoadRawFileTextAsync()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.LoadRawFileTextAsync("test_location").GetAwaiter().GetResult();
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_LoadRawFileBytesAsync()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.LoadRawFileBytesAsync("test_location").GetAwaiter().GetResult();
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_CheckLocationValid()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.CheckLocationValid("test_location");
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_GetAssetInfosByTag()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.GetAssetInfosByTag("test_tag");
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_IsNeedDownloadFromRemote()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.IsNeedDownloadFromRemote("test_location");
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_UnloadUnusedAssetsAsync()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.UnloadUnusedAssetsAsync().GetAwaiter().GetResult();
            });
        }
        [Test]
        public void ThrowsIfNotInitialized_UnloadAllAssetsAsync()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                _service.UnloadAllAssetsAsync().GetAwaiter().GetResult();
            });
        }
        // ====================================================================
        // ReleaseHandle 幂等性测试
        // ====================================================================
        [Test]
        public void ReleaseHandle_WithNull_DoesNotThrow()
        {
            // ReleaseHandle(null) 应安全返回,不抛异常
            Assert.DoesNotThrow(() =>
            {
                _service.ReleaseHandle(null);
            });
        }
        [Test]
        public void ReleaseHandle_CalledTwiceWithNull_StillDoesNotThrow()
        {
            // 多次调用 null 仍安全
            Assert.DoesNotThrow(() =>
            {
                _service.ReleaseHandle(null);
                _service.ReleaseHandle(null);
            });
        }
        // ====================================================================
        // Singleton 行为测试
        // ====================================================================
        [Test]
        public void Singleton_Instance_ReturnsSameInstance()
        {
            var instance1 = YooAssetService.Instance;
            var instance2 = YooAssetService.Instance;
            Assert.AreSame(instance1, instance2);
        }
        [Test]
        public void Singleton_IsValid_ReturnsTrue()
        {
            _ = YooAssetService.Instance;
            Assert.IsTrue(YooAssetService.IsValid());
        }
        [Test]
        public void Singleton_AfterDestroy_IsValidReturnsFalse()
        {
            _ = YooAssetService.Instance;
            YooAssetService.Destroy();
            Assert.IsFalse(YooAssetService.IsValid());
        }
        [Test]
        public void Singleton_AfterDestroy_NewInstanceCreated()
        {
            var instance1 = YooAssetService.Instance;
            YooAssetService.Destroy();
            var instance2 = YooAssetService.Instance;
            Assert.AreNotSame(instance1, instance2);
        }
        // ====================================================================
        // IYooAssetBridge 接口行为测试
        // ====================================================================
        [Test]
        public void IYooAssetBridge_GetCached_ReturnsNull_BeforeUS4()
        {
            // GetCached 在 US4 集成前返回 null
            IYooAssetBridge bridge = _service;
            var result = bridge.GetCached<UnityEngine.Object>("test_location");
            Assert.IsNull(result);
        }
        [Test]
        public void IYooAssetBridge_IsRegistered_ReturnsFalse_BeforeInit()
        {
            IYooAssetBridge bridge = _service;
            Assert.IsFalse(bridge.IsRegistered);
        }
    }
}
Main/Tests/YooAssetServiceTests.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f0b053bf25c64a34c972bbf4796e2923
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Utility/ComponentExtersion.cs
@@ -2,6 +2,7 @@
using UnityEngine.UI;
using UnityEngine.Events;
using System;
using Cysharp.Threading.Tasks;
public static class ComponentExtersion
@@ -333,6 +334,24 @@
        _image.overrideSprite = sprite;
    }
    public static async UniTask SetSpriteAsync(this Image _image, string _id)
    {
        if (_image == null)
        {
            return;
        }
        if (string.IsNullOrEmpty(_id))
        {
            Debug.LogError("Image SetSpriteAsync id is null or empty " + _id);
            return;
        }
        var sprite = await UILoader.LoadSpriteAsync(_id);
        if (_image != null)
            _image.overrideSprite = sprite;
    }
    public static void SetSprite(this TextImage _textImage, string _id)
    {
@@ -349,6 +368,24 @@
        var sprite = UILoader.LoadSprite(_id);
        _textImage.sprite = sprite;
    }
    public static async UniTask SetSpriteAsync(this TextImage _textImage, string _id)
    {
        if (_textImage == null)
        {
            return;
        }
        if (string.IsNullOrEmpty(_id))
        {
            Debug.LogError("TextImage SetSpriteAsync id is null or empty " + _id);
            return;
        }
        var sprite = await UILoader.LoadSpriteAsync(_id);
        if (_textImage != null)
            _textImage.sprite = sprite;
    }
    //通过图片名加载, 如物品表 技能表等,节省在Icon表做多余配置
@@ -368,6 +405,24 @@
        var sprite = UILoader.LoadSprite(folderName, iconName);
        if (null == sprite) return;
        _image.overrideSprite = sprite;
    }
    public static async UniTask SetOrgSpriteAsync(this Image _image, string iconName, string folderName = "icon")
    {
        if (_image == null)
        {
            return;
        }
        if (string.IsNullOrEmpty(iconName))
        {
            Debug.LogError("SetOrgSpriteAsync iconName is null or empty " + iconName);
            return;
        }
        var sprite = await UILoader.LoadSpriteAsync(folderName, iconName);
        if (_image != null && sprite != null)
            _image.overrideSprite = sprite;
    }
    public static void SetItemSprite(this Image _image, int itemID)
@@ -417,6 +472,30 @@
        _image.overrideSprite = sprite;
    }
    public static async UniTask SetSkillSpriteAsync(this Image _image, int skillID)
    {
        if (_image == null)
        {
            return;
        }
        var skillConfig = SkillConfig.Get(skillID);
        if (skillConfig == null)
        {
            return;
        }
        if (string.IsNullOrEmpty(skillConfig.IconName))
        {
            Debug.LogError("SetSkillSpriteAsync IconName is null or empty for skillID " + skillID);
            return;
        }
        var sprite = await UILoader.LoadSpriteAsync("SkillIcon", skillConfig.IconName);
        if (_image != null)
            _image.overrideSprite = sprite;
    }
    public static void SetActive(this Component compoent, bool active)
    {
        if (compoent != null)
@@ -444,6 +523,18 @@
        _image.texture = texture;
    }
    public static async UniTask SetTexture2DAsync(this RawImage _image, string _id)
    {
        if (_image == null)
        {
            return;
        }
        var texture = await UILoader.LoadTexture2DAsync(_id);
        if (_image != null)
            _image.texture = texture;
    }
    public static void SetTexture2DPNG(this RawImage _image, string _id)
    {
Main/Utility/FontUtility.cs
@@ -2,17 +2,32 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using ProjSG.Resource;
public class FontUtility
{
    static Font m_Preferred;
    public static Font preferred {
        get { return m_Preferred ?? (m_Preferred = ResManager.Instance.LoadAsset<Font>("Font", "GameFont1")); }
    // T044: Fonts must be pre-loaded via StartupEssential preload config
    // (location: "Assets/ResourcesOut/BuiltIn/Font")
    public static Font preferred
    {
        get { return ResourceCacheManager.Instance.GetCached<Font>("Assets/ResourcesOut/BuiltIn/Font/GameFont1.ttf"); }
    }
    static Font m_Secondary;
    public static Font secondary {
        get { return m_Secondary ?? (m_Secondary = ResManager.Instance.LoadAsset<Font>("Font", "GameFont2")); }
    public static Font secondary
    {
        get { return ResourceCacheManager.Instance.GetCached<Font>("Assets/ResourcesOut/BuiltIn/Font/GameFont2.ttf"); }
    }
    /// <summary>
    /// US2: Async initialization — fallback API if preload is not yet available.
    /// </summary>
    public static async UniTask InitAsync()
    {
        // Fallback: async load and cache via ResManager
        await ResManager.Instance.LoadAssetAsync<Font>("Font", "GameFont1");
        await ResManager.Instance.LoadAssetAsync<Font>("Font", "GameFont2");
    }
}
Main/Utility/MaterialUtility.cs
@@ -1,13 +1,17 @@
using UnityEngine;
using System;
using Cysharp.Threading.Tasks;
using ProjSG.Resource;
public static class MaterialUtility
{
    // T044: Materials must be pre-loaded via StartupEssential preload config
    // (location: "Assets/ResourcesOut/BuiltIn/Materials")
    public static Material GetDefaultSpriteGrayMaterial()
    {
        return ResManager.Instance.LoadAsset<Material>("BuiltIn/Materials", "SpriteGray");
        return ResourceCacheManager.Instance.GetCached<Material>("Assets/ResourcesOut/BuiltIn/Materials/SpriteGray.mat");
    }
    public static Material GetInstantiatedSpriteGrayMaterial()
@@ -18,12 +22,12 @@
    public static Material GetSmoothMaskGrayMaterial()
    {
        return ResManager.Instance.LoadAsset<Material>("BuiltIn/Materials", "SmoothMaskGray");
        return ResourceCacheManager.Instance.GetCached<Material>("Assets/ResourcesOut/BuiltIn/Materials/SmoothMaskGray.mat");
    }
    public static Material GetInstantiatedSpriteTwinkleMaterial()
    {
        var material = ResManager.Instance.LoadAsset<Material>("BuiltIn/Materials", "Flash");
        var material = ResourceCacheManager.Instance.GetCached<Material>("Assets/ResourcesOut/BuiltIn/Materials/Flash.mat");
        return new Material(material);
    }
@@ -34,7 +38,32 @@
    public static Material GetGUIRenderTextureMaterial()
    {
        return ResManager.Instance.LoadAsset<Material>("BuiltIn/Materials", "UI_RenderTexture");
        return ResourceCacheManager.Instance.GetCached<Material>("Assets/ResourcesOut/BuiltIn/Materials/UI_RenderTexture.mat");
    }
    // ====================================================================
    // US2: Async variants — temporary API. Replaced by preload+cache in T044.
    // ====================================================================
    public static UniTask<Material> GetDefaultSpriteGrayMaterialAsync()
    {
        return ResManager.Instance.LoadAssetAsync<Material>("BuiltIn/Materials", "SpriteGray");
    }
    public static UniTask<Material> GetSmoothMaskGrayMaterialAsync()
    {
        return ResManager.Instance.LoadAssetAsync<Material>("BuiltIn/Materials", "SmoothMaskGray");
    }
    public static async UniTask<Material> GetInstantiatedSpriteTwinkleMaterialAsync()
    {
        var material = await ResManager.Instance.LoadAssetAsync<Material>("BuiltIn/Materials", "Flash");
        return new Material(material);
    }
    public static UniTask<Material> GetGUIRenderTextureMaterialAsync()
    {
        return ResManager.Instance.LoadAssetAsync<Material>("BuiltIn/Materials", "UI_RenderTexture");
    }
    public static void SetRenderSortingOrder(this GameObject root, int sortingOrder, bool includeChildren)
Main/Utility/ShaderUtility.cs
@@ -1,6 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using ProjSG.Resource;
public class ShaderUtility
{
@@ -15,13 +17,23 @@
        Shader.SetGlobalColor("_Gbl_Wat", new Color(1, 1, 1, 1));
    }
    [System.Obsolete("US2: Use WarmUpAllAsync. Sync loading removed. Final preload+cache pattern in T044.")]
    public static void WarmUpAll()
    {
        // US2: Sync AB loading removed. Use WarmUpAllAsync instead.
        Shader.WarmupAllShaders();
    }
    /// <summary>
    /// US2: Async shader warm up via YooAsset. Temporary API — will be replaced by preload+cache in T044.
    /// </summary>
    public static async UniTask WarmUpAllAsync()
    {
        if (AssetSource.isUseAssetBundle)
        {
            AssetBundleUtility.Instance.Sync_LoadAllAssets("Graphic/Shader");
            Shader.WarmupAllShaders();
            await YooAssetService.Instance.LoadAllAssetsAsync<Shader>("Assets/ResourcesOut/Shader");
        }
        Shader.WarmupAllShaders();
    }
Main/Utility/UIUtility.cs
@@ -12,7 +12,22 @@
    public static GameObject CreateWidget(string _sourceName, string _name)
    {
        #pragma warning disable CS0618 // Obsolete — sync legacy fallback
        var prefab = UILoader.LoadPrefab(_sourceName);
        #pragma warning restore CS0618
        if (prefab == null)
        {
            return null;
        }
        var instance = GameObject.Instantiate(prefab);
        instance.name = string.IsNullOrEmpty(_name) ? _sourceName : _name;
        return instance;
    }
    public static async UniTask<GameObject> CreateWidgetAsync(string _sourceName, string _name)
    {
        var prefab = await UILoader.LoadPrefabAsync(_sourceName);
        if (prefab == null)
        {
            return null;