# VR剧场场景和资源管理标准

## 1. 资源管理系统概述

### 核心原则

- **统一加载接口**：所有资源通过Addressables统一加载，无论是内置资源还是热更新资源
- **场景为中心**：以场景为核心单位进行加载和管理
- **故事结构化**：多个场景组成一个完整故事，故事为独立更新和管理单元
- **预加载机制**：智能预加载下一个场景以提高切换流畅度
- **内存安全**：自动监控内存使用并释放不必要资源

### 系统架构图

```
┌─────────────────────────┐
│        应用启动器       │
└───────────┬─────────────┘
            │
┌───────────▼─────────────┐
│      ResourceManager     │◄────┐
│ (Addressables资源管理器) │     │
└───────────┬─────────────┘     │
            │                   │
┌───────────▼─────────────┐     │  ┌─────────────────────────┐
│       SceneManager      │     │  │      MemoryMonitor      │
│      (场景管理器)       │─────┘  │     (内存监控系统)      │
└───────────┬─────────────┘        └─────────────────────────┘
            │                   
┌───────────▼─────────────┐
│    StoryExperience      │
│      (故事体验)         │
└─────────────────────────┘
```

## 2. 开发人员指南

### 核心API参考

#### ResourceManager API

##### 资源加载

```csharp
// 加载任意资源(预制体、纹理、音频等)
Task<T> LoadAssetAsync<T>(string key, bool persistent = false);

// 释放非常驻资源
void ReleaseAsset(string key);

// 释放常驻资源
void ReleasePersistentAsset(string key);
```

##### 场景管理

```csharp
// 加载场景
Task<SceneInstance> LoadSceneAsync(string sceneKey, LoadSceneMode mode = LoadSceneMode.Single, bool activateOnLoad = true);

// 卸载场景
Task<bool> UnloadSceneAsync(string sceneKey);

// 预加载场景(不激活)
Task<bool> PreloadSceneAsync(string sceneKey, LoadSceneMode mode = LoadSceneMode.Single);
```

##### 故事资源管理

```csharp
// 加载故事定义
Task<StoryDefinition> LoadStoryDefinitionAsync(string storyId);

// 下载故事资源包
Task<float> DownloadStoryAssetsAsync(string storyId, IProgress<float> progress = null);

// 清理故事资源
Task CleanupStoryAssetsAsync(string storyId);
```

#### SceneManager API

```csharp
// 进入教学场景
Task EnterTutorial();

// 返回等待室
Task ReturnToWaitingRoom();

// 开始故事体验
Task StartStoryExperience(StoryDefinition story);

// 前往故事的下一个场景
Task GoToNextStoryScene();

// 完成故事体验
Task FinishStoryExperience();
```

### 常见使用场景示例

#### 1. 加载并显示故事列表

```csharp
public async Task LoadStoryLibrary()
{
    // 加载故事目录
    var storyCatalog = await ResourceManager.Instance.LoadAssetAsync<StoryCatalog>("Catalogs/story_catalog");
    
    // 遍历故事目录并加载每个故事的元数据
    foreach (var storyId in storyCatalog.availableStories)
    {
        var storyDefinition = await ResourceManager.Instance.LoadStoryDefinitionAsync(storyId);
        if (storyDefinition != null)
        {
            // 添加到UI列表
            AddStoryToUI(storyDefinition);
        }
    }
}
```

#### 2. 下载并开始体验故事

```csharp
public async Task DownloadAndStartStory(string storyId)
{
    try
    {
        // 显示下载UI
        _downloadPanel.SetActive(true);
        
        // 获取下载大小
        long downloadSize = await ResourceManager.Instance.GetDownloadSizeAsync($"Stories/{storyId}");
        
        // 如果有需要下载的内容
        if (downloadSize > 0)
        {
            // 显示下载大小
            _downloadSizeText.text = $"需要下载: {FormatFileSize(downloadSize)}";
            
            // 开始下载，跟踪进度
            var progressReporter = new Progress<float>(progress => {
                _downloadProgressBar.fillAmount = progress;
                _downloadProgressText.text = $"{(int)(progress * 100)}%";
            });
            
            // 执行下载
            await ResourceManager.Instance.DownloadStoryAssetsAsync(storyId, progressReporter);
        }
        
        // 隐藏下载UI
        _downloadPanel.SetActive(false);
        
        // 加载故事定义
        var storyDefinition = await ResourceManager.Instance.LoadStoryDefinitionAsync(storyId);
        
        // 如果需要先进入教学
        if (storyDefinition.RequiresTutorial && !PlayerPrefs.HasKey("TutorialCompleted"))
        {
            await SceneManager.Instance.EnterTutorial();
        }
        else
        {
            // 直接开始故事体验
            await SceneManager.Instance.StartStoryExperience(storyDefinition);
        }
    }
    catch (Exception ex)
    {
        Debug.LogError($"下载或启动故事时出错: {ex.Message}");
        _downloadPanel.SetActive(false);
        _errorPanel.SetActive(true);
        _errorText.text = "无法加载故事，请检查网络连接后重试。";
    }
}
```

#### 3. 场景转换和预加载示例

```csharp
public class StorySceneController : MonoBehaviour
{
    // 当前场景完成后调用
    public async void OnSceneComplete()
    {
        try
        {
            // 进入下一个场景
            await SceneManager.Instance.GoToNextStoryScene();
        }
        catch (Exception ex)
        {
            Debug.LogError($"场景切换错误: {ex.Message}");
            // 错误处理
            await SceneManager.Instance.ReturnToWaitingRoom();
        }
    }
    
    // 退出故事
    public async void ExitStory()
    {
        try
        {
            // 结束当前故事
            await SceneManager.Instance.FinishStoryExperience();
        }
        catch (Exception ex)
        {
            Debug.LogError($"退出故事错误: {ex.Message}");
            // 确保强制返回等待室
            await SceneManager.Instance.ReturnToWaitingRoom();
        }
    }
}
```

### 资源命名和引用规范

1. **Addressables键命名规范**:
    
    ```
    Scenes/[location]            // 内置场景，如: Scenes/waiting_room, Scenes/tutorial
    Stories/[storyId]/Definition // 故事定义文件
    Stories/[storyId]/Scenes/[sceneId] // 故事场景
    UI/[module]/[component]      // UI资源, 如: UI/Common/LoadingPanel
    ```
    
2. **资源引用方式**:
    
    - 不使用直接路径引用(如Resources.Load或AssetDatabase)
    - 所有资源引用使用Addressables键
    - 场景间不得直接硬引用资源，必须通过ResourceManager加载
3. **AssetReference使用**:
    
    - 场景中使用AssetReference字段关联Addressables资源
    - 加载示例:
        
        ```csharp
        public AssetReference uiPrefabReference;
        
        private async Task LoadUIAsync()
        {
            var prefab = await ResourceManager.Instance.LoadAssetAsync<GameObject>(uiPrefabReference.AssetGUID);
            Instantiate(prefab);
        }
        ```
        

## 3. 美术人员资源制作指南

### 资源分类与组织

#### 1. 场景类型

1. **框架场景(内置)**:
    
    - 等待室场景: 应用默认场景，故事体验开始和结束的地方
    - 教学场景: 教授用户基本交互机制的场景
    - 过渡场景/环境: 在场景切换时显示的中间环境
2. **故事场景(热更新)**:
    
    - 每个故事由多个场景组成
    - 场景按照叙事顺序进行排列和加载

#### 2. 资源包括哪些内容

每个故事应包括以下资源:

- **场景文件**: 包含场景布局、光照、导航网格等
- **场景专用资源**: 仅用于特定场景的模型、材质、音效等
- **共享资源**: 多个场景共用的角色模型、通用道具等
- **故事定义文件**: 描述故事结构、场景顺序等的ScriptableObject

### 资源制作规范

#### 1. 场景优化原则

1. **分离共享资源**:
    
    - 被多个场景使用的资源(如角色模型)应单独放置，避免重复
    - 场景仅包含唯一的场景元素
2. **场景划分**:
    
    - 按照叙事节点划分场景，控制单个场景资源量
    - 理想场景加载时间不超过5秒
    - 场景内对象使用合理分组，便于LOD和优化
3. **预加载设计**:
    
    - 场景设计时考虑预加载需求，重度场景前添加轻量过渡场景
    - 为预加载分配足够的时间窗口

#### 2. 资源打包流程图

```
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  场景制作    │────▶│ 资源检查工具 │────▶│ 适配性调整   │
└──────────────┘     └──────────────┘     └──────────────┘
                                                  │
                                                  ▼
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  版本发布    │◀────│ 打包为AB     │◀────│ 资源组织分类 │
└──────────────┘     └──────────────┘     └──────────────┘
```

#### 3. 性能预算

每个故事的资源预算:

- **总内存占用**: 不超过500MB
- **单场景内存**: 不超过200MB
- **纹理尺寸**: 最大2048x2048，非关键区域512x512
- **单场景网格**: 顶点总数控制在60万以内
- **材质**: 每个场景不超过50种不同材质

## 4. 资源管理系统工作流程

### 初始化流程

```
1. 应用启动
2. 初始化Addressables系统
3. 加载等待室场景
4. 加载故事目录和故事清单
5. 用户选择故事
6. 下载所需资源(如果需要)
7. 开始故事体验
```

### 场景加载流程

```
1. 用户触发场景切换
2. SceneManager请求从当前场景淡出
3. 启动转场效果
4. 检查是否有预加载场景
   a. 有 - 激活预加载场景
   b. 无 - 执行场景加载
5. 加载并初始化场景
6. 场景准备就绪
7. 淡入新场景
8. 开始预加载下一个场景
```

### 内存管理流程

```
1. 系统定期检查内存使用
2. 当内存使用率超过阈值
   a. 释放预加载资源
   b. 清理不使用的资源
   c. 降低预加载优先级
3. 返回等待室时
   a. 卸载故事场景
   b. 清理故事专用资源
   c. 执行彻底内存清理
```

## 5. Addressables组结构设计

### 组织结构

```
VRTheater Addressables Groups:
├── Framework (Local)
│   ├── Scenes
│   │   ├── waiting_room
│   │   └── tutorial
│   ├── TransitionEnvironment
│   └── CommonUI
├── Catalogs (Remote)
│   ├── story_catalog
│   └── version_info
└── Stories (Remote)
    ├── Story_001
    │   ├── Definition
    │   ├── SharedAssets
    │   ├── Scene_001
    │   ├── Scene_002
    │   └── Scene_003
    ├── Story_002
    │   └── ...
    └── ...
```

### 分组原则

1. **Framework组**:
    
    - 包含核心场景和系统资源
    - 打包到应用中，不热更新
    - 设置为Local模式
2. **Catalogs组**:
    
    - 包含故事目录和更新信息
    - 设置为Remote以支持更新
    - 体积小，优先下载
3. **Stories组**:
    
    - 每个故事一个子组
    - 设置为Remote，支持按需下载
    - 内部按场景和共享资源分组

### 打包设置

1. **Framework组**:
    
    - 设置为"Pack Together"
    - 设置内置(内置与主包)
    - 不启用内容更新
2. **Catalogs组**:
    
    - 设置为"Pack Together"
    - 远程(热更新)
    - 启用内容更新
3. **Stories组**:
    
    - 每个故事设置为"Pack Together"
    - 远程(热更新)
    - 启用内容更新
    - 注意依赖管理，避免重复包含共享资源

## 6. 资源更新流程

### 更新检测

```csharp
public async Task CheckForUpdates()
{
    try
    {
        // 检查Catalogs更新
        var catalogUpdateHandle = Addressables.CheckForCatalogUpdates();
        await catalogUpdateHandle.Task;
        
        List<string> catalogsToUpdate = catalogUpdateHandle.Result;
        
        if (catalogsToUpdate != null && catalogsToUpdate.Count > 0)
        {
            // 更新Catalogs
            var updateHandle = Addressables.UpdateCatalogs(catalogsToUpdate);
            await updateHandle.Task;
            
            // 获取故事目录
            var storyCatalog = await ResourceManager.Instance.LoadAssetAsync<StoryCatalog>("Catalogs/story_catalog", true);
            
            // 更新UI显示可更新的故事
            UpdateAvailableStoriesUI(storyCatalog);
        }
        
        Addressables.Release(catalogUpdateHandle);
    }
    catch (Exception ex)
    {
        Debug.LogError($"检查更新失败: {ex.Message}");
    }
}
```

### 内容推送周期

1. **目录更新** - 随时可发布，体积小速度快
2. **故事资源更新** - 根据发布计划按需推送
3. **框架更新** - 需要应用商店审核，频率较低

## 7. 开发测试流程

### 开发模式操作

为方便开发和测试，可使用Play Mode Scripts设置:

1. **Use Asset Database** - 编辑器中直接从资源数据库加载，无需构建
2. **Simulate Groups** - 模拟分组但不实际构建
3. **Use Existing Build** - 使用已有构建，测试热更新流程

### 调试工具

```csharp
public class AddressablesDebugger : MonoBehaviour
{
    public void LogLoadedAssets()
    {
        Debug.Log("==== 已加载资源 ====");
        foreach (var key in ResourceManager.Instance.GetLoadedAssetKeys())
        {
            Debug.Log($"已加载: {key}");
        }
    }
    
    public void SimulateLowMemory()
    {
        // 模拟低内存情况测试内存管理
        ResourceManager.Instance.ForceResourceCleanup();
    }
    
    public void EnableDetailedLogging(bool enable)
    {
        ResourceManager.Instance.SetDetailedLogging(enable);
    }
}
```

## 8. 故障排除指南

### 常见问题及解决方案

1. **资源加载失败**
    
    - 检查Addressables键是否正确
    - 验证资源是否包含在构建中
    - 检查网络连接(远程资源)
2. **内存不足崩溃**
    
    - 检查单个场景资源大小
    - 确认资源释放调用正确
    - 监控内存使用峰值并调整预算
3. **场景切换卡顿**
    
    - 优化预加载时机
    - 检查场景资源数量和大小
    - 调整转场效果持续时间
4. **热更新失败**
    
    - 检查服务器路径配置
    - 验证内容哈希和目录版本
    - 尝试清理本地缓存

### 诊断工具

```csharp
public class DiagnosticTool : MonoBehaviour
{
    [SerializeField] private Text _diagnosticText;
    
    private void Update()
    {
        if (_diagnosticText != null)
        {
            // 显示内存和资源状态
            var stats = ResourceManager.Instance.GetDiagnosticInfo();
            _diagnosticText.text = $"内存: {stats.memoryUsage:P0}\n" +
                                  $"加载资源: {stats.loadedAssetCount}\n" +
                                  $"场景: {stats.currentScene}\n" +
                                  $"预加载: {stats.preloadedScene}";
        }
    }
}
```

---

这份完整指南涵盖了VR剧场项目的资源管理架构、开发标准、美术规范和工作流程。它为开发人员提供了清晰的API参考和使用示例，同时也为美术人员提供了资源制作指南。整个系统基于Addressables统一管理本地和远程资源，优化了场景加载流程并实现了内存安全保障。

您可以将此文档作为项目的开发标准和指南，并根据项目的具体需求进行适当调整。