|
@@ -0,0 +1,1213 @@
|
|
|
+# 基于具体场景的VR移动端加载方案优化
|
|
|
+
|
|
|
+根据您提供的具体应用场景,我将设计一个精确匹配您需求的资源加载方案。
|
|
|
+
|
|
|
+## 应用场景分析
|
|
|
+
|
|
|
+您的VR应用具有明确的结构:
|
|
|
+
|
|
|
+1. **启动器(主工程)**:包含默认等待关卡和教学关卡
|
|
|
+2. **故事内容(热更新)**:独立美术工程打包的故事资源
|
|
|
+3. **固定流程**:等待关卡 → 教学关卡 → 故事内容 → 等待关卡(闭环)
|
|
|
+4. **资源分离**:基础资源在主包,故事资源通过热更新
|
|
|
+
|
|
|
+## 优化加载方案
|
|
|
+
|
|
|
+针对这种结构,我推荐以下分层加载方案:
|
|
|
+
|
|
|
+### 1. 整体架构设计
|
|
|
+
|
|
|
+```
|
|
|
+├── 主工程 (启动器)
|
|
|
+│ ├── 等待关卡 (永久常驻)
|
|
|
+│ ├── 教学关卡 (按需加载)
|
|
|
+│ └── 过渡关卡/环境 (常驻,但可激活/隐藏)
|
|
|
+│
|
|
|
+└── 热更新内容
|
|
|
+ ├── 故事A
|
|
|
+ ├── 故事B
|
|
|
+ └── 故事C...
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 资源加载管理器实现
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using System.Collections;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using UnityEngine;
|
|
|
+using UnityEngine.AddressableAssets;
|
|
|
+using UnityEngine.ResourceManagement.AsyncOperations;
|
|
|
+using UnityEngine.ResourceManagement.ResourceProviders;
|
|
|
+using UnityEngine.SceneManagement;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// VR应用流程和资源管理器
|
|
|
+/// </summary>
|
|
|
+public class VRExperienceManager : MonoBehaviour
|
|
|
+{
|
|
|
+ #region 单例实现
|
|
|
+ private static VRExperienceManager _instance;
|
|
|
+ public static VRExperienceManager Instance => _instance;
|
|
|
+
|
|
|
+ private void Awake()
|
|
|
+ {
|
|
|
+ if (_instance != null && _instance != this)
|
|
|
+ {
|
|
|
+ Destroy(gameObject);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _instance = this;
|
|
|
+ DontDestroyOnLoad(gameObject);
|
|
|
+
|
|
|
+ InitializeManager();
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 序列化字段
|
|
|
+ [Header("关卡配置")]
|
|
|
+ [SerializeField] private string _waitingSceneKey = "waiting_scene";
|
|
|
+ [SerializeField] private string _tutorialSceneKey = "tutorial_scene";
|
|
|
+ [SerializeField] private string _transitionEnvironmentKey = "transition_environment";
|
|
|
+
|
|
|
+ [Header("VR设置")]
|
|
|
+ [SerializeField] private Transform _playerRig;
|
|
|
+ [SerializeField] private float _fadeTransitionDuration = 0.8f;
|
|
|
+ [SerializeField] private bool _enableDynamicQuality = true;
|
|
|
+
|
|
|
+ [Header("引用")]
|
|
|
+ [SerializeField] private VRTransitionEnvironment _transitionEnvironment;
|
|
|
+ [SerializeField] private InitializationProgress _initProgress;
|
|
|
+ [SerializeField] private MemoryMonitor _memoryMonitor;
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 私有字段
|
|
|
+ // 场景加载状态
|
|
|
+ private AsyncOperationHandle<SceneInstance> _currentSceneHandle;
|
|
|
+ private AsyncOperationHandle<SceneInstance> _preloadedSceneHandle;
|
|
|
+ private string _currentSceneKey;
|
|
|
+ private string _preloadedSceneKey;
|
|
|
+ private bool _isScenePreloaded = false;
|
|
|
+
|
|
|
+ // 故事管理
|
|
|
+ private StoryMetadata _currentStory;
|
|
|
+ private int _currentStorySceneIndex = -1;
|
|
|
+ private List<StoryMetadata> _availableStories = new List<StoryMetadata>();
|
|
|
+
|
|
|
+ // 应用状态
|
|
|
+ private enum AppState { Initializing, WaitingRoom, Tutorial, Story, Transitioning }
|
|
|
+ private AppState _currentState = AppState.Initializing;
|
|
|
+
|
|
|
+ // 资源管理
|
|
|
+ private Dictionary<string, AsyncOperationHandle> _persistentHandles = new Dictionary<string, AsyncOperationHandle>();
|
|
|
+ private bool _isTransitionEnvironmentLoaded = false;
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 初始化
|
|
|
+ private void InitializeManager()
|
|
|
+ {
|
|
|
+ // 注册内存警告回调
|
|
|
+ if (_memoryMonitor != null)
|
|
|
+ {
|
|
|
+ _memoryMonitor.OnMemoryCritical += HandleLowMemory;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化Addressables
|
|
|
+ Addressables.InitializeAsync().Completed += (op) =>
|
|
|
+ {
|
|
|
+ // 加载过渡环境(常驻资源)
|
|
|
+ LoadTransitionEnvironment();
|
|
|
+
|
|
|
+ // 开始应用初始化流程
|
|
|
+ StartCoroutine(InitializeApplicationFlow());
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator InitializeApplicationFlow()
|
|
|
+ {
|
|
|
+ _currentState = AppState.Initializing;
|
|
|
+
|
|
|
+ // 1. 确保等待关卡已加载(应该是应用启动场景)
|
|
|
+ if (SceneManager.GetActiveScene().name != _waitingSceneKey)
|
|
|
+ {
|
|
|
+ Debug.LogWarning("应用未从等待场景启动,正在加载等待场景...");
|
|
|
+ // 加载等待场景
|
|
|
+ var waitingSceneLoad = Addressables.LoadSceneAsync(_waitingSceneKey, LoadSceneMode.Single);
|
|
|
+ yield return waitingSceneLoad;
|
|
|
+ _currentSceneHandle = waitingSceneLoad;
|
|
|
+ _currentSceneKey = _waitingSceneKey;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 记录当前场景
|
|
|
+ _currentSceneKey = _waitingSceneKey;
|
|
|
+ // 创建等待场景的句柄(不实际加载,只为了后续能统一处理)
|
|
|
+ _currentSceneHandle = Addressables.LoadSceneAsync(_waitingSceneKey, LoadSceneMode.Single, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取可用的故事列表
|
|
|
+ yield return StartCoroutine(FetchAvailableStories());
|
|
|
+
|
|
|
+ // 3. 预加载教学场景
|
|
|
+ if (_enablePreloading && HasSufficientMemory())
|
|
|
+ {
|
|
|
+ StartCoroutine(PreloadScene(_tutorialSceneKey));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 更新状态
|
|
|
+ _currentState = AppState.WaitingRoom;
|
|
|
+
|
|
|
+ // 5. 通知初始化完成
|
|
|
+ _initProgress?.SetComplete();
|
|
|
+
|
|
|
+ Debug.Log("VR体验管理器初始化完成");
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator FetchAvailableStories()
|
|
|
+ {
|
|
|
+ _availableStories.Clear();
|
|
|
+
|
|
|
+ // 从服务器或本地缓存获取可用故事
|
|
|
+ // 此处简化为示例,实际应从配置或服务器获取
|
|
|
+ var storyListHandle = Addressables.LoadAssetAsync<StoryListData>("story_list");
|
|
|
+ yield return storyListHandle;
|
|
|
+
|
|
|
+ if (storyListHandle.Status == AsyncOperationStatus.Succeeded)
|
|
|
+ {
|
|
|
+ var storyList = storyListHandle.Result;
|
|
|
+ _availableStories = new List<StoryMetadata>(storyList.stories);
|
|
|
+ Debug.Log($"已加载 {_availableStories.Count} 个可用故事");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Debug.LogError("无法加载故事列表!");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 释放列表资源
|
|
|
+ Addressables.Release(storyListHandle);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void LoadTransitionEnvironment()
|
|
|
+ {
|
|
|
+ if (_isTransitionEnvironmentLoaded)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var handle = Addressables.LoadAssetAsync<GameObject>(_transitionEnvironmentKey);
|
|
|
+ handle.Completed += (op) =>
|
|
|
+ {
|
|
|
+ if (op.Status == AsyncOperationStatus.Succeeded)
|
|
|
+ {
|
|
|
+ var instance = Instantiate(op.Result);
|
|
|
+ DontDestroyOnLoad(instance);
|
|
|
+ _transitionEnvironment = instance.GetComponent<VRTransitionEnvironment>();
|
|
|
+ _isTransitionEnvironmentLoaded = true;
|
|
|
+
|
|
|
+ // 隐藏过渡环境
|
|
|
+ _transitionEnvironment.gameObject.SetActive(false);
|
|
|
+
|
|
|
+ // 保存句柄以便常驻内存
|
|
|
+ _persistentHandles["transitionEnvironment"] = handle;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Debug.LogError("无法加载过渡环境!");
|
|
|
+ Addressables.Release(handle);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 场景转换公共API
|
|
|
+ /// <summary>
|
|
|
+ /// 进入教学关卡
|
|
|
+ /// </summary>
|
|
|
+ public void EnterTutorial()
|
|
|
+ {
|
|
|
+ if (_currentState == AppState.WaitingRoom)
|
|
|
+ {
|
|
|
+ StartCoroutine(TransitionToScene(_tutorialSceneKey));
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Debug.LogWarning("只能从等待室进入教学!");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 开始故事体验
|
|
|
+ /// </summary>
|
|
|
+ public void StartStoryExperience(string storyId)
|
|
|
+ {
|
|
|
+ // 检查状态是否允许
|
|
|
+ if (_currentState != AppState.Tutorial && _currentState != AppState.WaitingRoom)
|
|
|
+ {
|
|
|
+ Debug.LogWarning("必须从等待室或教学关卡启动故事!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找故事
|
|
|
+ var story = _availableStories.Find(s => s.id == storyId);
|
|
|
+ if (story == null)
|
|
|
+ {
|
|
|
+ Debug.LogError($"找不到ID为 {storyId} 的故事!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置当前故事
|
|
|
+ _currentStory = story;
|
|
|
+ _currentStorySceneIndex = 0;
|
|
|
+
|
|
|
+ // 启动故事的第一个场景
|
|
|
+ string firstSceneKey = story.sceneKeys[0];
|
|
|
+ StartCoroutine(TransitionToScene(firstSceneKey));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 进入故事的下一个场景
|
|
|
+ /// </summary>
|
|
|
+ public void NextStoryScene()
|
|
|
+ {
|
|
|
+ if (_currentState != AppState.Story || _currentStory == null)
|
|
|
+ {
|
|
|
+ Debug.LogWarning("当前不在故事体验中!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否有下一个场景
|
|
|
+ if (_currentStorySceneIndex < _currentStory.sceneKeys.Count - 1)
|
|
|
+ {
|
|
|
+ _currentStorySceneIndex++;
|
|
|
+ string nextSceneKey = _currentStory.sceneKeys[_currentStorySceneIndex];
|
|
|
+ StartCoroutine(TransitionToScene(nextSceneKey));
|
|
|
+
|
|
|
+ // 预加载下一个场景(如果有)
|
|
|
+ if (_enablePreloading && _currentStorySceneIndex < _currentStory.sceneKeys.Count - 1)
|
|
|
+ {
|
|
|
+ string preloadSceneKey = _currentStory.sceneKeys[_currentStorySceneIndex + 1];
|
|
|
+ StartCoroutine(PreloadScene(preloadSceneKey));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 已是最后一个场景,完成故事体验
|
|
|
+ FinishStoryExperience();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 完成故事体验,返回等待室
|
|
|
+ /// </summary>
|
|
|
+ public void FinishStoryExperience()
|
|
|
+ {
|
|
|
+ if (_currentState == AppState.Story)
|
|
|
+ {
|
|
|
+ // 清理故事状态
|
|
|
+ _currentStory = null;
|
|
|
+ _currentStorySceneIndex = -1;
|
|
|
+
|
|
|
+ // 返回等待室
|
|
|
+ StartCoroutine(TransitionToScene(_waitingSceneKey));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 中断当前体验,返回等待室
|
|
|
+ /// </summary>
|
|
|
+ public void ReturnToWaitingRoom()
|
|
|
+ {
|
|
|
+ StartCoroutine(TransitionToScene(_waitingSceneKey));
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 场景加载实现
|
|
|
+ private IEnumerator TransitionToScene(string sceneKey)
|
|
|
+ {
|
|
|
+ // 更新状态
|
|
|
+ _currentState = AppState.Transitioning;
|
|
|
+
|
|
|
+ // 显示过渡环境
|
|
|
+ _transitionEnvironment.gameObject.SetActive(true);
|
|
|
+
|
|
|
+ // 开始淡入过渡
|
|
|
+ yield return _transitionEnvironment.FadeIn(_fadeTransitionDuration);
|
|
|
+
|
|
|
+ // 检查是否有预加载的场景
|
|
|
+ if (_isScenePreloaded && _preloadedSceneKey == sceneKey)
|
|
|
+ {
|
|
|
+ // 使用预加载的场景
|
|
|
+ yield return StartCoroutine(ActivatePreloadedScene());
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 直接加载新场景
|
|
|
+ yield return StartCoroutine(LoadNewScene(sceneKey));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新应用状态
|
|
|
+ UpdateAppState(sceneKey);
|
|
|
+
|
|
|
+ // 调整玩家位置
|
|
|
+ PositionPlayerInScene(sceneKey);
|
|
|
+
|
|
|
+ // 淡出过渡环境
|
|
|
+ yield return _transitionEnvironment.FadeOut(_fadeTransitionDuration);
|
|
|
+
|
|
|
+ // 隐藏过渡环境
|
|
|
+ _transitionEnvironment.gameObject.SetActive(false);
|
|
|
+
|
|
|
+ // 执行资源清理
|
|
|
+ StartCoroutine(PerformDelayedCleanup());
|
|
|
+
|
|
|
+ // 如果是故事场景,尝试预加载下一个场景
|
|
|
+ if (_currentState == AppState.Story && _enablePreloading && HasSufficientMemory())
|
|
|
+ {
|
|
|
+ if (_currentStorySceneIndex < _currentStory.sceneKeys.Count - 1)
|
|
|
+ {
|
|
|
+ string nextSceneKey = _currentStory.sceneKeys[_currentStorySceneIndex + 1];
|
|
|
+ StartCoroutine(PreloadScene(nextSceneKey));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator LoadNewScene(string sceneKey)
|
|
|
+ {
|
|
|
+ // 显示加载进度
|
|
|
+ _transitionEnvironment.ShowLoadingUI(true);
|
|
|
+
|
|
|
+ // 释放之前预加载的场景(如果有)
|
|
|
+ if (_isScenePreloaded && _preloadedSceneHandle.IsValid())
|
|
|
+ {
|
|
|
+ Addressables.Release(_preloadedSceneHandle);
|
|
|
+ _isScenePreloaded = false;
|
|
|
+ _preloadedSceneKey = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 下载依赖
|
|
|
+ var downloadHandle = Addressables.DownloadDependenciesAsync(sceneKey);
|
|
|
+ while (!downloadHandle.IsDone)
|
|
|
+ {
|
|
|
+ float progress = downloadHandle.PercentComplete * 0.3f; // 权重30%
|
|
|
+ _transitionEnvironment.UpdateProgress(progress, $"下载资源... {Mathf.FloorToInt(progress * 100)}%");
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+ Addressables.Release(downloadHandle);
|
|
|
+
|
|
|
+ // 2. 加载新场景
|
|
|
+ var loadHandle = Addressables.LoadSceneAsync(sceneKey, LoadSceneMode.Single);
|
|
|
+ while (!loadHandle.IsDone)
|
|
|
+ {
|
|
|
+ float progress = 0.3f + loadHandle.PercentComplete * 0.7f; // 权重70%
|
|
|
+ _transitionEnvironment.UpdateProgress(progress, $"加载场景... {Mathf.FloorToInt(progress * 100)}%");
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查加载是否成功
|
|
|
+ if (loadHandle.Status != AsyncOperationStatus.Succeeded)
|
|
|
+ {
|
|
|
+ Debug.LogError($"场景 {sceneKey} 加载失败!");
|
|
|
+ // 返回等待室
|
|
|
+ yield return StartCoroutine(LoadNewScene(_waitingSceneKey));
|
|
|
+ yield break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 释放当前场景句柄
|
|
|
+ if (_currentSceneHandle.IsValid())
|
|
|
+ {
|
|
|
+ Addressables.Release(_currentSceneHandle);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新当前场景引用
|
|
|
+ _currentSceneHandle = loadHandle;
|
|
|
+ _currentSceneKey = sceneKey;
|
|
|
+
|
|
|
+ // 隐藏加载UI
|
|
|
+ _transitionEnvironment.ShowLoadingUI(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator ActivatePreloadedScene()
|
|
|
+ {
|
|
|
+ // 显示简化的加载UI
|
|
|
+ _transitionEnvironment.ShowLoadingUI(true, "准备场景...");
|
|
|
+
|
|
|
+ // 激活预加载的场景
|
|
|
+ var activateHandle = _preloadedSceneHandle.Result.ActivateAsync();
|
|
|
+ while (!activateHandle.isDone)
|
|
|
+ {
|
|
|
+ _transitionEnvironment.UpdateProgress(activateHandle.progress);
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 释放当前场景句柄
|
|
|
+ if (_currentSceneHandle.IsValid())
|
|
|
+ {
|
|
|
+ Addressables.Release(_currentSceneHandle);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新引用
|
|
|
+ _currentSceneHandle = _preloadedSceneHandle;
|
|
|
+ _currentSceneKey = _preloadedSceneKey;
|
|
|
+
|
|
|
+ // 重置预加载状态
|
|
|
+ _isScenePreloaded = false;
|
|
|
+ _preloadedSceneKey = null;
|
|
|
+ _preloadedSceneHandle = default;
|
|
|
+
|
|
|
+ // 隐藏加载UI
|
|
|
+ _transitionEnvironment.ShowLoadingUI(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator PreloadScene(string sceneKey)
|
|
|
+ {
|
|
|
+ // 检查内存状况
|
|
|
+ if (!HasSufficientMemory())
|
|
|
+ {
|
|
|
+ Debug.Log($"内存不足,取消预加载场景 {sceneKey}");
|
|
|
+ yield break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 取消之前的预加载
|
|
|
+ if (_isScenePreloaded && _preloadedSceneHandle.IsValid())
|
|
|
+ {
|
|
|
+ Addressables.Release(_preloadedSceneHandle);
|
|
|
+ _isScenePreloaded = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Debug.Log($"开始预加载场景: {sceneKey}");
|
|
|
+
|
|
|
+ // 后台下载依赖
|
|
|
+ var downloadHandle = Addressables.DownloadDependenciesAsync(sceneKey);
|
|
|
+ yield return downloadHandle;
|
|
|
+ Addressables.Release(downloadHandle);
|
|
|
+
|
|
|
+ // 再次检查内存状况
|
|
|
+ if (!HasSufficientMemory())
|
|
|
+ {
|
|
|
+ Debug.Log($"下载依赖后内存不足,取消预加载场景 {sceneKey}");
|
|
|
+ yield break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 后台加载场景但不激活
|
|
|
+ var loadHandle = Addressables.LoadSceneAsync(sceneKey, LoadSceneMode.Single, false);
|
|
|
+ yield return loadHandle;
|
|
|
+
|
|
|
+ // 更新预加载状态
|
|
|
+ _preloadedSceneHandle = loadHandle;
|
|
|
+ _preloadedSceneKey = sceneKey;
|
|
|
+ _isScenePreloaded = true;
|
|
|
+
|
|
|
+ Debug.Log($"场景预加载完成: {sceneKey}");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateAppState(string sceneKey)
|
|
|
+ {
|
|
|
+ // 根据场景更新应用状态
|
|
|
+ if (sceneKey == _waitingSceneKey)
|
|
|
+ {
|
|
|
+ _currentState = AppState.WaitingRoom;
|
|
|
+ }
|
|
|
+ else if (sceneKey == _tutorialSceneKey)
|
|
|
+ {
|
|
|
+ _currentState = AppState.Tutorial;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ _currentState = AppState.Story;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void PositionPlayerInScene(string sceneKey)
|
|
|
+ {
|
|
|
+ // 查找场景中的玩家起始点
|
|
|
+ var spawnPoint = GameObject.FindGameObjectWithTag("PlayerSpawnPoint");
|
|
|
+ if (spawnPoint != null && _playerRig != null)
|
|
|
+ {
|
|
|
+ _playerRig.position = spawnPoint.transform.position;
|
|
|
+ _playerRig.rotation = spawnPoint.transform.rotation;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator PerformDelayedCleanup()
|
|
|
+ {
|
|
|
+ // 等待场景稳定
|
|
|
+ yield return new WaitForSeconds(2.0f);
|
|
|
+
|
|
|
+ // 清理未使用资源
|
|
|
+ var unloadOp = Resources.UnloadUnusedAssets();
|
|
|
+ yield return unloadOp;
|
|
|
+
|
|
|
+ // 执行垃圾回收
|
|
|
+ GC.Collect();
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 内存与性能管理
|
|
|
+ [Header("预加载设置")]
|
|
|
+ [SerializeField] private bool _enablePreloading = true;
|
|
|
+ [SerializeField] private float _memoryThresholdForPreloading = 0.7f; // 70%以上内存使用率时不预加载
|
|
|
+
|
|
|
+ private bool HasSufficientMemory()
|
|
|
+ {
|
|
|
+ if (!_enablePreloading)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (_memoryMonitor != null)
|
|
|
+ {
|
|
|
+ float memoryUsage = _memoryMonitor.GetMemoryUsage();
|
|
|
+ return memoryUsage < _memoryThresholdForPreloading;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认保守处理
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void HandleLowMemory(float usagePercent)
|
|
|
+ {
|
|
|
+ Debug.LogWarning($"内存不足警告! 使用率: {usagePercent:P2}");
|
|
|
+
|
|
|
+ // 如果有预加载的场景,释放它以节省内存
|
|
|
+ if (_isScenePreloaded && _preloadedSceneHandle.IsValid())
|
|
|
+ {
|
|
|
+ Debug.Log("释放预加载场景以节省内存");
|
|
|
+ Addressables.Release(_preloadedSceneHandle);
|
|
|
+ _isScenePreloaded = false;
|
|
|
+ _preloadedSceneKey = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 强制内存清理
|
|
|
+ StartCoroutine(PerformEmergencyCleanup());
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator PerformEmergencyCleanup()
|
|
|
+ {
|
|
|
+ // 释放未使用的资源
|
|
|
+ var unloadOp = Resources.UnloadUnusedAssets();
|
|
|
+ yield return unloadOp;
|
|
|
+
|
|
|
+ // 执行垃圾回收
|
|
|
+ GC.Collect();
|
|
|
+
|
|
|
+ // 如果还是不足,可以降低质量设置
|
|
|
+ if (_enableDynamicQuality && _memoryMonitor.GetMemoryUsage() > 0.85f)
|
|
|
+ {
|
|
|
+ int currentQuality = QualitySettings.GetQualityLevel();
|
|
|
+ if (currentQuality > 0)
|
|
|
+ {
|
|
|
+ QualitySettings.SetQualityLevel(currentQuality - 1, true);
|
|
|
+ Debug.Log($"降低质量等级至 {QualitySettings.names[currentQuality - 1]}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 辅助方法
|
|
|
+ public List<StoryMetadata> GetAvailableStories()
|
|
|
+ {
|
|
|
+ return new List<StoryMetadata>(_availableStories);
|
|
|
+ }
|
|
|
+
|
|
|
+ public StoryMetadata GetCurrentStory()
|
|
|
+ {
|
|
|
+ return _currentStory;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void SetPlayerRig(Transform playerRig)
|
|
|
+ {
|
|
|
+ _playerRig = playerRig;
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3. VR过渡环境实现
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Collections;
|
|
|
+using UnityEngine;
|
|
|
+using UnityEngine.UI;
|
|
|
+using TMPro;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// VR友好的过渡环境控制器
|
|
|
+/// </summary>
|
|
|
+public class VRTransitionEnvironment : MonoBehaviour
|
|
|
+{
|
|
|
+ [Header("视觉设置")]
|
|
|
+ [SerializeField] private Material _fadeScreenMaterial;
|
|
|
+ [SerializeField] private MeshRenderer _fadeScreenRenderer;
|
|
|
+ [SerializeField] private Transform _environmentRoot;
|
|
|
+ [SerializeField] private GameObject _loadingUI;
|
|
|
+ [SerializeField] private Image _progressBar;
|
|
|
+ [SerializeField] private TextMeshProUGUI _progressText;
|
|
|
+ [SerializeField] private TextMeshProUGUI _statusText;
|
|
|
+
|
|
|
+ [Header("音频设置")]
|
|
|
+ [SerializeField] private AudioSource _transitionAudioSource;
|
|
|
+ [SerializeField] private AudioClip _fadeInSound;
|
|
|
+ [SerializeField] private AudioClip _fadeOutSound;
|
|
|
+
|
|
|
+ [Header("舒适度设置")]
|
|
|
+ [SerializeField] private float _vignetteFadeSpeed = 1.5f;
|
|
|
+ [SerializeField] private Color _backgroundColor = Color.black;
|
|
|
+ [SerializeField] private Color _vignetteColor = new Color(0, 0, 0, 0.8f);
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ private void Awake()
|
|
|
+ {
|
|
|
+ // 确保初始状态
|
|
|
+ _fadeScreenMaterial.SetFloat("_FadeAmount", 0);
|
|
|
+ _fadeScreenMaterial.SetColor("_Color", _backgroundColor);
|
|
|
+ _fadeScreenMaterial.SetColor("_VignetteColor", _vignetteColor);
|
|
|
+
|
|
|
+ if (_loadingUI != null)
|
|
|
+ _loadingUI.SetActive(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 执行淡入过渡
|
|
|
+ /// </summary>
|
|
|
+ public IEnumerator FadeIn(float duration)
|
|
|
+ {
|
|
|
+ // 播放过渡音效
|
|
|
+ if (_transitionAudioSource != null && _fadeInSound != null)
|
|
|
+ {
|
|
|
+ _transitionAudioSource.clip = _fadeInSound;
|
|
|
+ _transitionAudioSource.Play();
|
|
|
+ }
|
|
|
+
|
|
|
+ float startTime = Time.time;
|
|
|
+ float elapsed = 0;
|
|
|
+
|
|
|
+ while (elapsed < duration)
|
|
|
+ {
|
|
|
+ elapsed = Time.time - startTime;
|
|
|
+ float t = Mathf.Clamp01(elapsed / duration);
|
|
|
+
|
|
|
+ // 应用渐进式过渡
|
|
|
+ _fadeScreenMaterial.SetFloat("_FadeAmount", Mathf.Lerp(0, 1, t));
|
|
|
+ _fadeScreenMaterial.SetFloat("_VignetteAmount", Mathf.Lerp(0, 1, t * 1.5f)); // 光晕比淡入稍快
|
|
|
+
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保最终状态
|
|
|
+ _fadeScreenMaterial.SetFloat("_FadeAmount", 1);
|
|
|
+ _fadeScreenMaterial.SetFloat("_VignetteAmount", 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 执行淡出过渡
|
|
|
+ /// </summary>
|
|
|
+ public IEnumerator FadeOut(float duration)
|
|
|
+ {
|
|
|
+ // 播放过渡音效
|
|
|
+ if (_transitionAudioSource != null && _fadeOutSound != null)
|
|
|
+ {
|
|
|
+ _transitionAudioSource.clip = _fadeOutSound;
|
|
|
+ _transitionAudioSource.Play();
|
|
|
+ }
|
|
|
+
|
|
|
+ float startTime = Time.time;
|
|
|
+ float elapsed = 0;
|
|
|
+
|
|
|
+ while (elapsed < duration)
|
|
|
+ {
|
|
|
+ elapsed = Time.time - startTime;
|
|
|
+ float t = Mathf.Clamp01(elapsed / duration);
|
|
|
+
|
|
|
+ // 应用渐进式过渡
|
|
|
+ _fadeScreenMaterial.SetFloat("_FadeAmount", Mathf.Lerp(1, 0, t));
|
|
|
+ // 光晕保持较长时间,然后快速淡出
|
|
|
+ _fadeScreenMaterial.SetFloat("_VignetteAmount", elapsed < duration * 0.7f ? 1 : Mathf.Lerp(1, 0, (elapsed - duration * 0.7f) / (duration * 0.3f)));
|
|
|
+
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保最终状态
|
|
|
+ _fadeScreenMaterial.SetFloat("_FadeAmount", 0);
|
|
|
+ _fadeScreenMaterial.SetFloat("_VignetteAmount", 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 显示或隐藏加载UI
|
|
|
+ /// </summary>
|
|
|
+ public void ShowLoadingUI(bool show, string status = null)
|
|
|
+ {
|
|
|
+ if (_loadingUI != null)
|
|
|
+ _loadingUI.SetActive(show);
|
|
|
+
|
|
|
+ if (status != null && _statusText != null)
|
|
|
+ _statusText.text = status;
|
|
|
+
|
|
|
+ // 重置进度条
|
|
|
+ if (show && _progressBar != null)
|
|
|
+ _progressBar.fillAmount = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 更新加载进度
|
|
|
+ /// </summary>
|
|
|
+ public void UpdateProgress(float progress, string status = null)
|
|
|
+ {
|
|
|
+ if (_progressBar != null)
|
|
|
+ _progressBar.fillAmount = progress;
|
|
|
+
|
|
|
+ if (_progressText != null)
|
|
|
+ _progressText.text = $"{Mathf.Round(progress * 100)}%";
|
|
|
+
|
|
|
+ if (status != null && _statusText != null)
|
|
|
+ _statusText.text = status;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4. 内存监控器
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using System.Collections;
|
|
|
+using UnityEngine;
|
|
|
+using UnityEngine.UI;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 内存使用监控器
|
|
|
+/// </summary>
|
|
|
+public class MemoryMonitor : MonoBehaviour
|
|
|
+{
|
|
|
+ [Header("监控设置")]
|
|
|
+ [SerializeField] private float _monitorInterval = 3.0f;
|
|
|
+ [SerializeField] private float _criticalMemoryThreshold = 0.85f; // 85%内存使用率视为危险
|
|
|
+ [SerializeField] private bool _showDebugInfo = false;
|
|
|
+
|
|
|
+ [Header("UI引用")]
|
|
|
+ [SerializeField] private Text _memoryText;
|
|
|
+
|
|
|
+ // 事件
|
|
|
+ public event Action<float> OnMemoryCritical;
|
|
|
+
|
|
|
+ private void Start()
|
|
|
+ {
|
|
|
+ if (_showDebugInfo)
|
|
|
+ {
|
|
|
+ StartCoroutine(MonitorMemoryRoutine());
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 隐藏调试UI
|
|
|
+ if (_memoryText != null)
|
|
|
+ _memoryText.gameObject.SetActive(false);
|
|
|
+
|
|
|
+ // 仍然监控内存,但不显示
|
|
|
+ StartCoroutine(BackgroundMonitorRoutine());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator MonitorMemoryRoutine()
|
|
|
+ {
|
|
|
+ var wait = new WaitForSeconds(_monitorInterval);
|
|
|
+
|
|
|
+ while (true)
|
|
|
+ {
|
|
|
+ float usage = GetMemoryUsage();
|
|
|
+ UpdateMemoryText(usage);
|
|
|
+
|
|
|
+ // 检查是否达到临界值
|
|
|
+ if (usage > _criticalMemoryThreshold)
|
|
|
+ {
|
|
|
+ OnMemoryCritical?.Invoke(usage);
|
|
|
+ }
|
|
|
+
|
|
|
+ yield return wait;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator BackgroundMonitorRoutine()
|
|
|
+ {
|
|
|
+ var wait = new WaitForSeconds(_monitorInterval * 2); // 更低频率的后台监控
|
|
|
+
|
|
|
+ while (true)
|
|
|
+ {
|
|
|
+ float usage = GetMemoryUsage();
|
|
|
+
|
|
|
+ // 只在接近危险值时触发
|
|
|
+ if (usage > _criticalMemoryThreshold)
|
|
|
+ {
|
|
|
+ OnMemoryCritical?.Invoke(usage);
|
|
|
+ }
|
|
|
+
|
|
|
+ yield return wait;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 获取当前内存使用率
|
|
|
+ /// </summary>
|
|
|
+ public float GetMemoryUsage()
|
|
|
+ {
|
|
|
+ #if UNITY_ANDROID && !UNITY_EDITOR
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using (var activityManager = new AndroidJavaObject("android.app.ActivityManager"))
|
|
|
+ using (var memoryInfo = new AndroidJavaObject("android.app.ActivityManager$MemoryInfo"))
|
|
|
+ {
|
|
|
+ activityManager.Call("getMemoryInfo", memoryInfo);
|
|
|
+ long availMem = memoryInfo.Get<long>("availMem");
|
|
|
+ long totalMem = memoryInfo.Get<long>("totalMem");
|
|
|
+
|
|
|
+ return 1f - ((float)availMem / totalMem);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ Debug.LogError($"获取Android内存信息失败: {e.Message}");
|
|
|
+ // 回退到通用方法
|
|
|
+ return (float)GC.GetTotalMemory(false) / (SystemInfo.systemMemorySize * 1024 * 1024);
|
|
|
+ }
|
|
|
+ #else
|
|
|
+ // 简化的非Android平台实现
|
|
|
+ return (float)GC.GetTotalMemory(false) / (SystemInfo.systemMemorySize * 1024 * 1024);
|
|
|
+ #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateMemoryText(float usage)
|
|
|
+ {
|
|
|
+ if (_memoryText != null)
|
|
|
+ {
|
|
|
+ // 显示内存使用率
|
|
|
+ _memoryText.text = $"内存: {(usage * 100):F1}%";
|
|
|
+
|
|
|
+ // 根据使用率变色
|
|
|
+ if (usage > _criticalMemoryThreshold)
|
|
|
+ _memoryText.color = Color.red;
|
|
|
+ else if (usage > _criticalMemoryThreshold * 0.8f)
|
|
|
+ _memoryText.color = Color.yellow;
|
|
|
+ else
|
|
|
+ _memoryText.color = Color.green;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5. 故事和场景数据结构
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using UnityEngine;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 故事元数据
|
|
|
+/// </summary>
|
|
|
+[Serializable]
|
|
|
+public class StoryMetadata
|
|
|
+{
|
|
|
+ public string id;
|
|
|
+ public string title;
|
|
|
+ public string description;
|
|
|
+ public string thumbnailKey;
|
|
|
+ public string bundleId;
|
|
|
+ public List<string> sceneKeys = new List<string>();
|
|
|
+ public string entrySceneKey => sceneKeys.Count > 0 ? sceneKeys[0] : "";
|
|
|
+ public long estimatedSize; // 字节数
|
|
|
+ public bool isDownloaded;
|
|
|
+}
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 故事列表数据资源
|
|
|
+/// </summary>
|
|
|
+[CreateAssetMenu(fileName = "StoryListData", menuName = "VR Experience/Story List Data")]
|
|
|
+public class StoryListData : ScriptableObject
|
|
|
+{
|
|
|
+ public List<StoryMetadata> stories = new List<StoryMetadata>();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6. 初始化进度展示
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Collections;
|
|
|
+using UnityEngine;
|
|
|
+using UnityEngine.UI;
|
|
|
+using TMPro;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 应用初始化进度显示
|
|
|
+/// </summary>
|
|
|
+public class InitializationProgress : MonoBehaviour
|
|
|
+{
|
|
|
+ [SerializeField] private Image _progressFill;
|
|
|
+ [SerializeField] private TextMeshProUGUI _statusText;
|
|
|
+ [SerializeField] private TextMeshProUGUI _versionText;
|
|
|
+ [SerializeField] private GameObject _startButton;
|
|
|
+ [SerializeField] private float _animationSpeed = 0.5f;
|
|
|
+
|
|
|
+ private void Start()
|
|
|
+ {
|
|
|
+ if (_versionText != null)
|
|
|
+ {
|
|
|
+ _versionText.text = $"版本 {Application.version}";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_startButton != null)
|
|
|
+ {
|
|
|
+ _startButton.SetActive(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ SetProgress(0, "正在初始化...");
|
|
|
+ }
|
|
|
+
|
|
|
+ public void SetProgress(float progress, string status = null)
|
|
|
+ {
|
|
|
+ if (_progressFill != null)
|
|
|
+ {
|
|
|
+ StartCoroutine(AnimateProgress(_progressFill.fillAmount, progress));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_statusText != null && !string.IsNullOrEmpty(status))
|
|
|
+ {
|
|
|
+ _statusText.text = status;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private IEnumerator AnimateProgress(float from, float to)
|
|
|
+ {
|
|
|
+ float elapsed = 0;
|
|
|
+ float duration = Mathf.Abs(to - from) / _animationSpeed;
|
|
|
+
|
|
|
+ while (elapsed < duration)
|
|
|
+ {
|
|
|
+ elapsed += Time.deltaTime;
|
|
|
+ float t = Mathf.Clamp01(elapsed / duration);
|
|
|
+ _progressFill.fillAmount = Mathf.Lerp(from, to, t);
|
|
|
+ yield return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ _progressFill.fillAmount = to;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void SetComplete()
|
|
|
+ {
|
|
|
+ SetProgress(1, "初始化完成");
|
|
|
+
|
|
|
+ if (_startButton != null)
|
|
|
+ {
|
|
|
+ _startButton.SetActive(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 启动流程图解
|
|
|
+
|
|
|
+根据您提供的具体需求,以下是完整的启动和运行流程图解:
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────┐
|
|
|
+│ 应用启动 │
|
|
|
+└───────┬─────────┘
|
|
|
+ ▼
|
|
|
+┌─────────────────┐ ┌─────────────────┐
|
|
|
+│ 等待关卡 │──────│ 初始化 │
|
|
|
+│ (默认场景) │ │ - 加载配置 │
|
|
|
+└───────┬─────────┘ │ - 获取故事列表 │
|
|
|
+ │ │ - 加载过渡环境 │
|
|
|
+ │ └─────────────────┘
|
|
|
+ ▼
|
|
|
+┌─────────────────┐
|
|
|
+│ 教学关卡 │◄─────┐
|
|
|
+│ (按需加载) │ │
|
|
|
+└───────┬─────────┘ │
|
|
|
+ │ │
|
|
|
+ ▼ │
|
|
|
+┌─────────────────┐ │
|
|
|
+│ 故事内容 │ │
|
|
|
+│ (热更新资源) │ │
|
|
|
+└───────┬─────────┘ │
|
|
|
+ │ │
|
|
|
+ ▼ │
|
|
|
+┌─────────────────┐ │
|
|
|
+│ 等待关卡 │──────┘
|
|
|
+│ (返回初始状态) │
|
|
|
+└─────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+## 资源组织建议
|
|
|
+
|
|
|
+### 1. 主工程 Addressables 组织
|
|
|
+
|
|
|
+```
|
|
|
+1. 基础常驻资源组 (Bundle Mode: Pack Together)
|
|
|
+ - 等待关卡场景
|
|
|
+ - 过渡环境预制体
|
|
|
+ - UI框架资源
|
|
|
+ - 公共材质和着色器
|
|
|
+
|
|
|
+2. 教学关卡资源组 (Bundle Mode: Pack Together)
|
|
|
+ - 教学关卡场景
|
|
|
+ - 教学专用模型和纹理
|
|
|
+ - 教学UI和提示资源
|
|
|
+
|
|
|
+3. 故事目录资源组 (Bundle Mode: Pack Together)
|
|
|
+ - 故事列表数据
|
|
|
+ - 故事缩略图
|
|
|
+ - 故事描述资源
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 热更新故事资源组织
|
|
|
+
|
|
|
+每个故事应作为独立的美术工程,其Addressables组织如下:
|
|
|
+
|
|
|
+```
|
|
|
+1. 故事核心资源组 (Bundle Mode: Pack Together)
|
|
|
+ - 场景描述数据
|
|
|
+ - 故事配置
|
|
|
+ - 核心机制脚本
|
|
|
+
|
|
|
+2. 故事场景资源组 (每个场景一个组, Bundle Mode: Pack Together)
|
|
|
+ - 场景1资源
|
|
|
+ - 场景2资源
|
|
|
+ - ...
|
|
|
+
|
|
|
+3. 故事共享资源组 (Bundle Mode: Pack Together)
|
|
|
+ - 多个场景共用的角色模型
|
|
|
+ - 共享纹理和材质
|
|
|
+ - 共享音频
|
|
|
+```
|
|
|
+
|
|
|
+## 内存优化策略
|
|
|
+
|
|
|
+针对您的移动端VR应用,特别推荐以下内存优化策略:
|
|
|
+
|
|
|
+### 1. 避免场景重复资源
|
|
|
+
|
|
|
+确保每个故事中的共享资源只加载一次:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 在VRExperienceManager.cs中添加
|
|
|
+private Dictionary<string, AsyncOperationHandle> _storySharedAssets = new Dictionary<string, AsyncOperationHandle>();
|
|
|
+
|
|
|
+private IEnumerator LoadStorySharedAssets(string storyId)
|
|
|
+{
|
|
|
+ // 加载故事共享资源
|
|
|
+ string sharedAssetKey = $"{storyId}_shared_assets";
|
|
|
+
|
|
|
+ var handle = Addressables.LoadAssetAsync<SharedAssetReferences>(sharedAssetKey);
|
|
|
+ yield return handle;
|
|
|
+
|
|
|
+ if (handle.Status == AsyncOperationStatus.Succeeded)
|
|
|
+ {
|
|
|
+ var sharedAssets = handle.Result;
|
|
|
+
|
|
|
+ // 加载并保持引用
|
|
|
+ foreach (var assetRef in sharedAssets.references)
|
|
|
+ {
|
|
|
+ var assetHandle = assetRef.LoadAssetAsync<UnityEngine.Object>();
|
|
|
+ yield return assetHandle;
|
|
|
+
|
|
|
+ // 保存句柄以保持资源加载状态
|
|
|
+ _storySharedAssets[assetRef.AssetGUID] = assetHandle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存引用以便故事结束时释放
|
|
|
+ _storySharedAssets["sharedAssetList"] = handle;
|
|
|
+}
|
|
|
+
|
|
|
+private void UnloadStorySharedAssets()
|
|
|
+{
|
|
|
+ foreach (var handle in _storySharedAssets.Values)
|
|
|
+ {
|
|
|
+ Addressables.Release(handle);
|
|
|
+ }
|
|
|
+ _storySharedAssets.Clear();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 渐进式细节加载
|
|
|
+
|
|
|
+对于大型场景,可以先加载基础结构,然后逐步加载细节:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 在场景脚本中添加
|
|
|
+private IEnumerator LoadSceneDetails()
|
|
|
+{
|
|
|
+ // 1. 首先加载关键游戏玩法元素
|
|
|
+ yield return StartCoroutine(LoadGameplayElements());
|
|
|
+
|
|
|
+ // 2. 短暂延迟让场景稳定
|
|
|
+ yield return new WaitForSeconds(0.5f);
|
|
|
+
|
|
|
+ // 3. 然后加载次要视觉元素
|
|
|
+ StartCoroutine(LoadVisualEnhancements());
|
|
|
+
|
|
|
+ // 4. 最后在玩家不注意时加载装饰元素
|
|
|
+ Invoke("LoadDecorativeElements", 5.0f);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 动态内存回收
|
|
|
+
|
|
|
+```csharp
|
|
|
+// 添加到VRExperienceManager.cs
|
|
|
+private Coroutine _memoryMonitorCoroutine;
|
|
|
+
|
|
|
+private void StartMemoryMonitoring()
|
|
|
+{
|
|
|
+ if (_memoryMonitorCoroutine != null)
|
|
|
+ StopCoroutine(_memoryMonitorCoroutine);
|
|
|
+
|
|
|
+ _memoryMonitorCoroutine = StartCoroutine(MonitorAndRecoverMemory());
|
|
|
+}
|
|
|
+
|
|
|
+private IEnumerator MonitorAndRecoverMemory()
|
|
|
+{
|
|
|
+ var wait = new WaitForSeconds(10f); // 每10秒检查一次
|
|
|
+
|
|
|
+ while (true)
|
|
|
+ {
|
|
|
+ float memoryUsage = _memoryMonitor.GetMemoryUsage();
|
|
|
+
|
|
|
+ // 根据内存使用情况采取不同策略
|
|
|
+ if (memoryUsage > 0.8f) // 80%以上
|
|
|
+ {
|
|
|
+ // 紧急内存回收
|
|
|
+ yield return StartCoroutine(PerformEmergencyCleanup());
|
|
|
+ }
|
|
|
+ else if (memoryUsage > 0.7f) // 70-80%
|
|
|
+ {
|
|
|
+ // 轻度内存回收
|
|
|
+ yield return Resources.UnloadUnusedAssets();
|
|
|
+ }
|
|
|
+
|
|
|
+ yield return wait;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 总结建议
|
|
|
+
|
|
|
+基于您描述的具体VR应用场景,我推荐以下关键策略:
|
|
|
+
|
|
|
+1. **采用常驻过渡环境**:
|
|
|
+
|
|
|
+ - 避免单独的过渡场景,而是使用DontDestroyOnLoad的过渡环境
|
|
|
+ - 为各种转换场景提供统一、舒适的VR体验
|
|
|
+2. **闭环流程优化**:
|
|
|
+
|
|
|
+ - 利用线性闭环特性预加载下一个阶段资源
|
|
|
+ - 利用返回等待室时彻底清理内存
|
|
|
+3. **热更新策略**:
|
|
|
+
|
|
|
+ - 主工程尽量精简,只包含启动器、教学和过渡必要资源
|
|
|
+ - 各故事作为独立更新单元,便于单独下载和更新
|
|
|
+4. **性能安全网**:
|
|
|
+
|
|
|
+ - 内存监控和自动清理机制确保稳定性
|
|
|
+ - 动态质量调整应对不同设备性能
|
|
|
+5. **VR舒适度考虑**:
|
|
|
+
|
|
|
+ - 所有转场效果考虑VR用户舒适度
|
|
|
+ - 加载过程中避免视觉/位置突变
|
|
|
+
|
|
|
+这套方案针对您的移动端VR项目进行了精确定制,既考虑了热更新内容的特点,又兼顾了VR体验的流畅性和舒适度,适合您描述的线性闭环体验流程。
|