# VR剧场Addressables资源管理规范

## 1. 资源管理基本结构

### 目录组织

```
VRTheater Addressables Groups:
├── BuiltIn (本地组)
│   ├── Scenes/              (内置基础场景)
│   │   ├── waiting_room     (等待室)
│   │   ├── tutorial         (教程场景)
│   │   └── transition_room  (转场场景)
│   │
│   ├── UI/                  (所有UI组件)
│   │   ├── Common/
│   │   ├── Dialogs/
│   │   └── HUD/
│   │
│   └── Common/              (通用系统和资源)
│       ├── Systems/         (框架预制体)
│       ├── Materials/       (通用材质)
│       └── Effects/         (通用特效)
│
└── Stories (远程组)
    ├── Story_001/
    │   ├── story_definition  (故事定义文件)
    │   ├── thumbnail         (故事缩略图)
    │   │
    │   ├── scene_intro_main  (大型场景-主体)
    │   ├── scene_intro_env   (大型场景-环境包)
    │   ├── scene_intro_chars (大型场景-角色包)
    │   ├── scene_intro_audio (大型场景-音频包)
    │   └── scene_intro_fx    (大型场景-特效包)
    │
    ├── Story_002/
    │   └── [相似结构]
    │
    └── Story_003/
        └── [相似结构]
```

### 命名约定

1. **故事定义文件**:
    
    ```
    Stories/Story_{storyId}/story_definition
    ```
    
    例如: `Stories/Story_001/story_definition`
    
2. **故事缩略图**:
    
    ```
    Stories/Story_{storyId}/thumbnail
    ```
    
    例如: `Stories/Story_001/thumbnail`
    
3. **场景**:
    
    - 不分包场景:
        
        ```
        Stories/Story_{storyId}/scene_{sceneId}
        ```
        
        例如: `Stories/Story_001/scene_intro`
        
    - 分包场景:
        
        ```
        Stories/Story_{storyId}/scene_{sceneId}_{packageType}
        ```
        
        例如: `Stories/Story_001/scene_intro_main`, `Stories/Story_001/scene_intro_env`
        

## 2. 数据模型

### StoryDefinition (故事定义)

```csharp
[Serializable]
public class StoryDefinition : IConfig
{
    public string storyId;           // 故事唯一标识符
    public string displayName;       // 显示名称
    public string description;       // 故事描述
    public string thumbnailAddress;  // 缩略图地址
    public string version;           // 版本号
    
    // 该故事包含的场景列表
    public List<StorySceneDefinition> scenes = new List<StorySceneDefinition>();
    
    // 元数据字典和序列化支持
    [NonSerialized]
    public Dictionary<string, string> metadata = new Dictionary<string, string>();
    public List<KeyValuePair> serializedMetadata = new List<KeyValuePair>();
    
    [Serializable]
    public class KeyValuePair
    {
        public string key;
        public string value;
        
        public KeyValuePair() { }
        public KeyValuePair(string key, string value)
        {
            this.key = key;
            this.value = value;
        }
    }
    
    // 序列化支持方法
    public void OnAfterDeserialize()
    {
        metadata = new Dictionary<string, string>();
        foreach (var pair in serializedMetadata)
        {
            if (!string.IsNullOrEmpty(pair.key))
                metadata[pair.key] = pair.value;
        }
    }
    
    public void OnBeforeSerialize()
    {
        serializedMetadata = new List<KeyValuePair>();
        foreach (var pair in metadata)
        {
            serializedMetadata.Add(new KeyValuePair(pair.Key, pair.Value));
        }
    }

    public bool IsValid()
    {
        if (string.IsNullOrEmpty(storyId) || string.IsNullOrEmpty(displayName))
            return false;
                
        if (scenes == null || scenes.Count == 0)
            return false;
                
        foreach (var scene in scenes)
        {
            if (!scene.IsValid())
                return false;
        }
        
        return true;
    }
}
```

### StorySceneDefinition (场景定义)

```csharp
[Serializable]
public class StorySceneDefinition : IConfig
{
    public string sceneId;           // 场景ID
    public string displayName;       // 场景名称
    public string addressableKey;    // 场景主体的Addressable键
    public bool isInitialScene;      // 是否是初始场景
    
    // 场景分包定义
    public List<ScenePackageDefinition> packages = new List<ScenePackageDefinition>();
    
    // 是否是分包场景
    public bool IsPackaged => packages != null && packages.Count > 0;
    
    // 获取所有需要加载的键（主场景+各个包）
    public List<string> GetAllAddressableKeys()
    {
        List<string> keys = new List<string> { addressableKey };
        
        if (IsPackaged)
        {
            foreach (var package in packages)
            {
                if (package.loadAtStart || package.priority == PackagePriority.Essential)
                {
                    keys.Add(package.addressableKey);
                }
            }
        }
        
        return keys;
    }
    
    public bool IsValid()
    {
        if (string.IsNullOrEmpty(sceneId) || string.IsNullOrEmpty(addressableKey))
            return false;
            
        if (IsPackaged)
        {
            foreach (var package in packages)
            {
                if (!package.IsValid())
                    return false;
            }
        }
        
        return true;
    }
}
```

### ScenePackageDefinition (场景包定义)

```csharp
[Serializable]
public class ScenePackageDefinition
{
    public string packageId;        // 包ID，如"env"、"chars"
    public string addressableKey;   // 包的Addressable键
    public bool loadAtStart;        // 是否在场景加载时立即加载
    public PackagePriority priority; // 包的优先级
    
    public bool IsValid()
    {
        return !string.IsNullOrEmpty(packageId) && !string.IsNullOrEmpty(addressableKey);
    }
}

public enum PackagePriority
{
    Essential,  // 必要的，无论何时都加载
    High,       // 高优先级，低端设备也尽量加载
    Medium,     // 中等优先级，中端设备加载
    Low         // 低优先级，仅高端设备加载
}
```

## 3. 资源管理实现

### ResourceManager (核心资源管理器)

```csharp
public class ResourceManager : Singleton<ResourceManager>
{
    // 资源加载
    public async Task<T> LoadAssetAsync<T>(string key, bool persistent = false)
    {
        try
        {
            var handle = Addressables.LoadAssetAsync<T>(key);
            await handle.Task;
            
            if (persistent)
            {
                // 记录为常驻资源，不自动释放
                _persistentHandles.Add(key, handle);
            }
            else
            {
                // 记录为临时资源，可以自动释放
                _loadedHandles.Add(key, handle);
            }
            
            return handle.Result;
        }
        catch (Exception ex)
        {
            Debug.LogError($"加载资源失败 {key}: {ex.Message}");
            return default;
        }
    }
    
    // 场景加载
    public async Task<bool> LoadSceneAsync(string sceneKey, LoadSceneMode mode = LoadSceneMode.Single, bool activateOnLoad = true)
    {
        try
        {
            var handle = Addressables.LoadSceneAsync(sceneKey, mode, activateOnLoad);
            await handle.Task;
            
            _loadedSceneHandle = handle;
            _currentSceneKey = sceneKey;
            
            return handle.Status == AsyncOperationStatus.Succeeded;
        }
        catch (Exception ex)
        {
            Debug.LogError($"加载场景失败 {sceneKey}: {ex.Message}");
            return false;
        }
    }
    
    // 加载分包场景
    public async Task<bool> LoadPackagedSceneAsync(StorySceneDefinition sceneDefinition, DevicePerformanceLevel performanceLevel)
    {
        try
        {
            // 显示加载界面
            UIManager.Instance.ShowLoadingScreen();
            
            // 1. 加载场景主体
            var sceneHandle = Addressables.LoadSceneAsync(sceneDefinition.addressableKey, LoadSceneMode.Single, true);
            
            // 2. 同时加载必须的包
            List<Task> essentialPackageTasks = new List<Task>();
            List<Task> backgroundPackageTasks = new List<Task>();
            
            if (sceneDefinition.IsPackaged)
            {
                foreach (var package in sceneDefinition.packages)
                {
                    // 根据性能等级和优先级决定是否加载
                    bool shouldLoad = package.loadAtStart || 
                                    (package.priority == PackagePriority.Essential) ||
                                    (package.priority == PackagePriority.High && performanceLevel >= DevicePerformanceLevel.Medium) ||
                                    (package.priority == PackagePriority.Medium && performanceLevel >= DevicePerformanceLevel.High) ||
                                    (package.priority == PackagePriority.Low && performanceLevel >= DevicePerformanceLevel.Ultra);
                    
                    if (shouldLoad)
                    {
                        var packageHandle = Addressables.LoadAssetAsync<GameObject>(package.addressableKey);
                        
                        if (package.loadAtStart || package.priority == PackagePriority.Essential)
                        {
                            // 必须包：加入必须完成的任务列表
                            essentialPackageTasks.Add(packageHandle.Task);
                        }
                        else
                        {
                            // 非必须包：加入后台任务列表
                            backgroundPackageTasks.Add(packageHandle.Task);
                        }
                        
                        _loadedHandles.Add(package.addressableKey, packageHandle);
                    }
                }
            }
            
            // 3. 等待必须的包加载完成
            if (essentialPackageTasks.Count > 0)
            {
                await Task.WhenAll(essentialPackageTasks);
            }
            
            // 4. 等待场景加载完成
            await sceneHandle.Task;
            
            _loadedSceneHandle = sceneHandle;
            _currentSceneKey = sceneDefinition.addressableKey;
            
            // 5. 隐藏加载界面
            UIManager.Instance.HideLoadingScreen();
            
            // 6. 后台继续加载非必须包
            if (backgroundPackageTasks.Count > 0)
            {
                _ = Task.WhenAll(backgroundPackageTasks);
            }
            
            return sceneHandle.Status == AsyncOperationStatus.Succeeded;
        }
        catch (Exception ex)
        {
            Debug.LogError($"加载分包场景失败 {sceneDefinition.sceneId}: {ex.Message}");
            UIManager.Instance.HideLoadingScreen();
            return false;
        }
    }
    
    // 资源释放
    public void ReleaseAsset(string key)
    {
        if (_loadedHandles.TryGetValue(key, out var handle))
        {
            Addressables.Release(handle);
            _loadedHandles.Remove(key);
        }
    }
    
    // 释放常驻资源
    public void ReleasePersistentAsset(string key)
    {
        if (_persistentHandles.TryGetValue(key, out var handle))
        {
            Addressables.Release(handle);
            _persistentHandles.Remove(key);
        }
    }
    
    // 内存管理
    public void ForceResourceCleanup()
    {
        foreach (var handle in _loadedHandles.Values)
        {
            Addressables.Release(handle);
        }
        _loadedHandles.Clear();
        
        Resources.UnloadUnusedAssets();
        GC.Collect();
    }
    
    // 私有成员
    private Dictionary<string, AsyncOperationHandle> _loadedHandles = new Dictionary<string, AsyncOperationHandle>();
    private Dictionary<string, AsyncOperationHandle> _persistentHandles = new Dictionary<string, AsyncOperationHandle>();
    private AsyncOperationHandle<SceneInstance> _loadedSceneHandle;
    private string _currentSceneKey;
}
```

### StoryManager (故事管理器)

```csharp
public class StoryManager : Singleton<StoryManager>
{
    // 获取可用故事列表
    public async Task<List<StoryInfo>> GetAvailableStoriesAsync()
    {
        List<StoryInfo> availableStories = new List<StoryInfo>();
        
        // 使用Addressables标签查找所有故事定义
        var locationsHandle = Addressables.LoadResourceLocationsAsync("StoryDefinition", typeof(TextAsset));
        await locationsHandle.Task;
        
        foreach (var location in locationsHandle.Result)
        {
            // 从路径中提取故事ID
            string key = location.PrimaryKey;
            string storyId = ExtractStoryIdFromKey(key); // 例如从"Stories/Story_001/story_definition"提取"001"
            
            try {
                // 加载故事定义
                var storyDef = await ResourceManager.Instance.LoadAssetAsync<StoryDefinition>(key);
                if (storyDef != null)
                {
                    // 处理定义文件元数据
                    storyDef.OnAfterDeserialize();
                    
                    // 添加到可用故事列表
                    availableStories.Add(new StoryInfo {
                        StoryId = storyDef.storyId,
                        DisplayName = storyDef.displayName,
                        Description = storyDef.description,
                        ThumbnailKey = storyDef.thumbnailAddress,
                        Version = storyDef.version
                    });
                    
                    // 释放定义资源引用
                    ResourceManager.Instance.ReleaseAsset(key);
                }
            }
            catch (Exception ex) {
                Debug.LogError($"加载故事定义失败 {key}: {ex.Message}");
            }
        }
        
        Addressables.Release(locationsHandle);
        return availableStories;
    }
    
    // 检查故事更新
    public async Task<(bool hasUpdate, long updateSize)> CheckStoryUpdate(string storyId)
    {
        try
        {
            string groupKey = $"Stories/Story_{storyId}";
            
            // 获取下载大小
            var sizeHandle = Addressables.GetDownloadSizeAsync(groupKey);
            await sizeHandle.Task;
            
            long downloadSize = sizeHandle.Result;
            Addressables.Release(sizeHandle);
            
            return (downloadSize > 0, downloadSize);
        }
        catch (Exception ex)
        {
            Debug.LogError($"检查故事更新失败 {storyId}: {ex.Message}");
            return (false, 0);
        }
    }
    
    // 下载故事资源
    public async Task<bool> DownloadStoryAsync(string storyId, IProgress<float> progress = null)
    {
        try
        {
            string groupKey = $"Stories/Story_{storyId}";
            
            // 开始下载
            var downloadHandle = Addressables.DownloadDependenciesAsync(groupKey, Addressables.MergeMode.Union);
            
            // 进度报告
            if (progress != null)
            {
                while (!downloadHandle.IsDone)
                {
                    progress.Report(downloadHandle.PercentComplete);
                    await Task.Yield();
                }
            }
            else
            {
                await downloadHandle.Task;
            }
            
            bool success = downloadHandle.Status == AsyncOperationStatus.Succeeded;
            Addressables.Release(downloadHandle);
            return success;
        }
        catch (Exception ex)
        {
            Debug.LogError($"下载故事资源失败 {storyId}: {ex.Message}");
            return false;
        }
    }
    
    // 开始故事体验
    public async Task<bool> StartStoryExperienceAsync(string storyId)
    {
        try
        {
            // 1. 加载故事定义
            string definitionKey = $"Stories/Story_{storyId}/story_definition";
            var storyDef = await ResourceManager.Instance.LoadAssetAsync<StoryDefinition>(definitionKey, true);
            
            if (storyDef == null)
            {
                Debug.LogError($"无法找到故事定义: {definitionKey}");
                return false;
            }
            
            storyDef.OnAfterDeserialize();
            
            // 2. 找到初始场景
            StorySceneDefinition initialScene = storyDef.scenes.Find(s => s.isInitialScene);
            if (initialScene == null && storyDef.scenes.Count > 0)
            {
                initialScene = storyDef.scenes[0];
            }
            
            if (initialScene == null)
            {
                Debug.LogError($"故事 {storyId} 没有场景");
                return false;
            }
            
            // 3. 保存当前故事上下文
            _currentStoryDefinition = storyDef;
            _currentSceneIndex = storyDef.scenes.IndexOf(initialScene);
            
            // 4. 显示转场效果
            await TransitionManager.Instance.FadeToBlack();
            
            // 5. 加载场景
            bool success;
            if (initialScene.IsPackaged)
            {
                // 加载分包场景
                success = await ResourceManager.Instance.LoadPackagedSceneAsync(
                    initialScene, 
                    DeviceCapability.Instance.GetPerformanceLevel()
                );
            }
            else
            {
                // 加载常规场景
                success = await ResourceManager.Instance.LoadSceneAsync(initialScene.addressableKey);
            }
            
            // 6. 淡出转场效果
            await TransitionManager.Instance.FadeFromBlack();
            
            return success;
        }
        catch (Exception ex)
        {
            Debug.LogError($"开始故事体验失败 {storyId}: {ex.Message}");
            await TransitionManager.Instance.FadeFromBlack();
            return false;
        }
    }
    
    // 进入下一个场景
    public async Task<bool> GoToNextSceneAsync()
    {
        if (_currentStoryDefinition == null || _currentSceneIndex < 0)
        {
            Debug.LogError("没有活动的故事体验");
            return false;
        }
        
        int nextIndex = _currentSceneIndex + 1;
        if (nextIndex >= _currentStoryDefinition.scenes.Count)
        {
            Debug.Log("已经是最后一个场景");
            return false;
        }
        
        return await GoToSceneAsync(nextIndex);
    }
    
    // 进入指定索引的场景
    public async Task<bool> GoToSceneAsync(int sceneIndex)
    {
        if (_currentStoryDefinition == null || 
            sceneIndex < 0 || 
            sceneIndex >= _currentStoryDefinition.scenes.Count)
        {
            Debug.LogError("场景索引无效");
            return false;
        }
        
        try
        {
            var sceneDefinition = _currentStoryDefinition.scenes[sceneIndex];
            
            // 1. 显示转场效果
            await TransitionManager.Instance.FadeToBlack();
            
            // 2. 加载场景
            bool success;
            if (sceneDefinition.IsPackaged)
            {
                // 加载分包场景
                success = await ResourceManager.Instance.LoadPackagedSceneAsync(
                    sceneDefinition, 
                    DeviceCapability.Instance.GetPerformanceLevel()
                );
            }
            else
            {
                // 加载常规场景
                success = await ResourceManager.Instance.LoadSceneAsync(sceneDefinition.addressableKey);
            }
            
            if (success)
            {
                _currentSceneIndex = sceneIndex;
            }
            
            // 3. 淡出转场效果
            await TransitionManager.Instance.FadeFromBlack();
            
            return success;
        }
        catch (Exception ex)
        {
            Debug.LogError($"切换场景失败 {sceneIndex}: {ex.Message}");
            await TransitionManager.Instance.FadeFromBlack();
            return false;
        }
    }
    
    // 结束故事体验
    public async Task EndStoryExperienceAsync()
    {
        try
        {
            // 1. 显示转场效果
            await TransitionManager.Instance.FadeToBlack();
            
            // 2. 加载等待室场景
            await ResourceManager.Instance.LoadSceneAsync("BuiltIn/Scenes/waiting_room");
            
            // 3. 清理故事资源
            if (_currentStoryDefinition != null)
            {
                string definitionKey = $"Stories/Story_{_currentStoryDefinition.storyId}/story_definition";
                ResourceManager.Instance.ReleasePersistentAsset(definitionKey);
            }
            
            ResourceManager.Instance.ForceResourceCleanup();
            
            _currentStoryDefinition = null;
            _currentSceneIndex = -1;
            
            // 4. 淡出转场效果
            await TransitionManager.Instance.FadeFromBlack();
            
            return;
        }
        catch (Exception ex)
        {
            Debug.LogError($"结束故事体验失败: {ex.Message}");
            await TransitionManager.Instance.FadeFromBlack();
        }
    }
    
    // 私有成员
    private StoryDefinition _currentStoryDefinition;
    private int _currentSceneIndex = -1;
    
    // 从键中提取故事ID的辅助方法
    private string ExtractStoryIdFromKey(string key)
    {
        // 例如从"Stories/Story_001/story_definition"提取"001"
        var parts = key.Split('/');
        if (parts.Length >= 2)
        {
            var storyPart = parts[1]; // "Story_001"
            return storyPart.Substring(storyPart.IndexOf('_') + 1); // "001"
        }
        return string.Empty;
    }
}
```

## 4. 场景拆包实践

### 拆包原则

大型场景应按以下原则拆分：

1. **功能模块拆分**：
    
    - **主体包**：场景的核心结构和逻辑，即Unity场景文件及主要内容
    - **环境包**：场景的大型静态环境元素，如地形、建筑等
    - **角色包**：场景中的角色模型、动画和AI行为
    - **音频包**：场景的音效和音乐
    - **特效包**：场景中的粒子效果、后处理等
2. **优先级拆分**：
    
    - **必要资源**（Essential）：场景运行必须的内容，总是加载
    - **高优先级**（High）：绝大多数设备都应该加载的内容
    - **中优先级**（Medium）：中高端设备加载的内容
    - **低优先级**（Low）：仅高端设备加载的特效和细节优化

### 拆包场景配置示例

```json
{
  "sceneId": "intro",
  "displayName": "太空站",
  "addressableKey": "Stories/Story_001/scene_intro_main",
  "isInitialScene": true,
  "packages": [
    {
      "packageId": "env",
      "addressableKey": "Stories/Story_001/scene_intro_env",
      "loadAtStart": true,
      "priority": "Essential"
    },
    {
      "packageId": "chars",
      "addressableKey": "Stories/Story_001/scene_intro_chars",
      "loadAtStart": true,
      "priority": "High"
    },
    {
      "packageId": "audio",
      "addressableKey": "Stories/Story_001/scene_intro_audio",
      "loadAtStart": false,
      "priority": "Medium"
    },
    {
      "packageId": "fx",
      "addressableKey": "Stories/Story_001/scene_intro_fx",
      "loadAtStart": false,
      "priority": "Low"
    }
  ]
}
```

## 5. 美术工作流程

### 单一场景工作流

适用于较小或中等大小的场景：

1. **创建场景**：
    
    - 在Unity中创建正常场景
    - 按惯例组织场景层级
2. **设置Addressable**：
    
    - 选中场景资产
    - 标记为Addressable
    - 设置地址为 `Stories/Story_{storyId}/scene_{sceneId}`
    - 添加 `StoryScene` 和 `Story_{storyId}` 标签
3. **创建场景定义**：
    
    - 在故事定义文件中添加场景条目
    - 设置 `addressableKey` 为场景地址

### 拆包场景工作流

适用于大型复杂场景：

1. **场景制作（与常规相同）**：
    
    - 创建完整场景
    - 按逻辑功能组织层级结构
2. **场景分析**：
    
    - 使用场景分析工具检查大型资源
    - 获取拆包建议，确定需要分离的内容
3. **拆包操作**：
    
    - 使用拆包工具自动创建包结构
    - 工具将大资源按类型分离成独立预制体
4. **验证与调整**：
    
    - 检查拆分后场景的完整性
    - 调整包的加载优先级
    - 测试不同设备性能下的效果
5. **更新场景定义**：
    
    - 工具自动更新场景定义文件
    - 添加包定义和加载参数

### 美术参考工具

```csharp
// 场景分析和拆包工具示例
public class ScenePackager : MonoBehaviour
{
    [SerializeField] private GameObject _scene;
    [SerializeField] private string _storyId;
    [SerializeField] private string _sceneId;
    
    // 分析场景资源
    public void AnalyzeAndSuggestPackages()
    {
        Dictionary<string, long> resourceStats = new Dictionary<string, long>();
        
        // 分析场景中所有资源
        // ...分析代码...
        
        // 输出建议
        Debug.Log("=== 场景资源分析 ===");
        Debug.Log($"总内存需求: {FormatSize(totalMemory)}");
        Debug.Log("建议拆包:");
        Debug.Log($"1. 环境包: {FormatSize(envSize)}");
        Debug.Log($"2. 角色包: {FormatSize(charsSize)}");
        Debug.Log($"3. 音频包: {FormatSize(audioSize)}");
        Debug.Log($"4. 特效包: {FormatSize(fxSize)}");
    }
    
    // 创建拆包结构
    public void CreatePackageStructure()
    {
        // 创建主体场景
        string mainScenePath = $"Assets/Addressables/Stories/Story_{_storyId}/Scenes/{_sceneId}_main.unity";
        // ...创建场景...
        
        // 创建环境包
        GameObject envPrefab = new GameObject($"{_sceneId}_env");
        // ...移动环境对象到此预制体...
        string envPrefabPath = $"Assets/Addressables/Stories/Story_{_storyId}/Packages/{_sceneId}_env.prefab";
        PrefabUtility.SaveAsPrefabAsset(envPrefab, envPrefabPath);
        
        // 创建角色包
        // ...类似逻辑...
        
        // 创建音频包
        // ...类似逻辑...
        
        // 创建特效包
        // ...类似逻辑...
        
        // 更新主场景中的引用
        // ...引用更新逻辑...
    }
    
    // 更新场景定义
    public void UpdateSceneDefinition()
    {
        // 查找故事定义文件
        string definitionPath = $"Assets/Addressables/Stories/Story_{_storyId}/story_definition.asset";
        var storyDef = AssetDatabase.LoadAssetAtPath<StoryDefinition>(definitionPath);
        
        if (storyDef != null)
        {
            // 查找或创建场景定义
            var sceneDef = storyDef.scenes.Find(s => s.sceneId == _sceneId);
            if (sceneDef == null)
            {
                sceneDef = new StorySceneDefinition();
                sceneDef.sceneId = _sceneId;
                storyDef.scenes.Add(sceneDef);
            }
            
            // 更新地址
            sceneDef.addressableKey = $"Stories/Story_{_storyId}/scene_{_sceneId}_main";
            
            // 添加包定义
            sceneDef.packages = new List<ScenePackageDefinition>();
            
            // 环境包
            sceneDef.packages.Add(new ScenePackageDefinition {
                packageId = "env",
                addressableKey = $"Stories/Story_{_storyId}/scene_{_sceneId}_env",
                loadAtStart = true,
                priority = PackagePriority.Essential
            });
            
            // 角色包
            sceneDef.packages.Add(new ScenePackageDefinition {
                packageId = "chars",
                addressableKey = $"Stories/Story_{_storyId}/scene_{_sceneId}_chars",
                loadAtStart = true,
                priority = PackagePriority.High
            });
            
            // 音频包
            sceneDef.packages.Add(new ScenePackageDefinition {
                packageId = "audio",
                addressableKey = $"Stories/Story_{_storyId}/scene_{_sceneId}_audio",
                loadAtStart = false,
                priority = PackagePriority.Medium
            });
            
            // 特效包
            sceneDef.packages.Add(new ScenePackageDefinition {
                packageId = "fx",
                addressableKey = $"Stories/Story_{_storyId}/scene_{_sceneId}_fx",
                loadAtStart = false,
                priority = PackagePriority.Low
            });
            
            // 保存修改
            EditorUtility.SetDirty(storyDef);
            AssetDatabase.SaveAssets();
        }
    }
}
```

## 6. 资源打包与发布流程

### Addressables 组配置

1. **BuiltIn 组**:
    
    - 加载路径: Local
    - 构建路径: StreamingAssets
    - 打包模式: Pack Together
    - 构建选项: 不压缩 (已在应用内)
2. **Stories 组**:
    
    - 加载路径: Remote
    - 构建路径: Server/StandaloneWindows64 (或其他平台路径)
    - 打包模式: 每个故事Pack Together
    - 构建选项: LZ4压缩

### 分组策略

1. **按故事分组**:
    
    - 每个故事应该是一个独立的组
    - 组名格式: `Story_001`, `Story_002` 等
2. **使用标签系统**:
    
    - 故事定义文件: `StoryDefinition`
    - 场景文件: `StoryScene`
    - 故事标识: `Story_001`, `Story_002` 等

### 构建流程

1. **准备阶段**:
    
    - 验证所有故事定义文件
    - 检查资源引用完整性
    - 清理未使用的资源
2. **构建Addressables**:
    
    - 打开Addressables Groups窗口
    - 构建 > New Build > Default Build Script
    - 等待构建完成
3. **部署资源**:
    
    - 上传远程内容到CDN或服务器
    - 确保目录文件正确更新

### 更新流程

1. **增量更新单个故事**:
    
    - 修改故事内容
    - 更新故事版本号
    - 仅构建变更的故事组
    - 上传新的内容和目录
2. **全量更新**:
    
    - 更新多个故事时
    - 构建所有内容
    - 保留旧版本用于回滚

## 7. 运行时加载和优化

### 加载策略

1. **预加载策略**:
    
    - 进入故事前预加载故事定义和初始场景
    - 在场景体验期间后台加载下一个场景
2. **渐进式加载**:
    
    - 先加载场景主体和必要包
    - 显示转场效果期间完成加载
    - 后台逐步加载非必要包
3. **质量适配**:
    
    - 根据设备性能自动调整加载的包
    - 低端设备只加载必要包
    - 高端设备加载所有包

### 内存管理

1. **资源释放时机**:
    
    - 场景切换时释放上一个场景的资源
    - 当接近内存限制时释放非常驻资源
    - 故事结束时彻底清理内存
2. **常驻资源**:
    
    - 故事定义文件保持常驻
    - UI资源常驻内存
    - 共享的系统资源常驻

### 优化建议

1. **资源分级**:
    
    - 为每个资源设定合理大小限制
    - 纹理压缩策略根据重要性调整
    - 远处物体使用LOD
2. **异步加载**:
    
    - 所有资源加载使用异步方式
    - 加载大资源时显示进度
    - 关键路径上使用预加载

## 8. 问题排查与诊断

### 常见问题解决

1. **资源加载失败**:
    
    - 检查addressableKey是否正确
    - 验证资源是否包含在构建中
    - 检查远程URL配置
2. **内存溢出**:
    
    - 检查资源未释放或引用泄漏
    - 监控峰值内存使用
    - 考虑拆分过大的场景
3. **资源重复**:
    
    - 使用Addressables分析工具
    - 检查重复引用的大型资源
    - 将共享资源提取到合适位置

### 诊断工具

1. **内存监控**:
    
    ```csharp
    public class MemoryMonitor : MonoBehaviour
    {
        [SerializeField] private Text _memoryText;
        [SerializeField] private float _updateInterval = 1f;
        
        private void Update()
        {
            if (Time.frameCount % (_updateInterval * 60) < 1)
            {
                UpdateMemoryInfo();
            }
        }
        
        private void UpdateMemoryInfo()
        {
            if (_memoryText != null)
            {
                long totalMemory = System.GC.GetTotalMemory(false);
                _memoryText.text = $"内存: {FormatSize(totalMemory)}";
            }
        }
        
        private string FormatSize(long bytes)
        {
            string[] sizes = { "B", "KB", "MB", "GB" };
            int order = 0;
            double size = bytes;
            
            while (size >= 1024 && order < sizes.Length - 1)
            {
                order++;
                size = size / 1024;
            }
            
            return $"{size:0.##} {sizes[order]}";
        }
    }
    ```
    
2. **加载诊断**:
    
    ```csharp
    public class LoadingDiagnostics : MonoBehaviour
    {
        [SerializeField] private Text _statusText;
        
        public void TrackLoading(string key)
        {
            StartCoroutine(TrackLoadingCoroutine(key));
        }
        
        private IEnumerator TrackLoadingCoroutine(string key)
        {
            float startTime = Time.realtimeSinceStartup;
            
            // 使用ResourceManager加载资源
            var task = ResourceManager.Instance.LoadAssetAsync<GameObject>(key);
            
            while (!task.IsCompleted)
            {
                float elapsed = Time.realtimeSinceStartup - startTime;
                _statusText.text = $"加载中: {key}\n已耗时: {elapsed:F2}秒";
                yield return null;
            }
            
            float totalTime = Time.realtimeSinceStartup - startTime;
            _statusText.text = $"加载完成: {key}\n总耗时: {totalTime:F2}秒";
            
            yield return new WaitForSeconds(2f);
            _statusText.text = "";
        }
    }
    ```
    

## 9. 总结与最佳实践

### 资源结构要点

1. **简洁组织**:
    
    - 保持扁平化目录结构
    - 每个故事独立管理
    - 清晰的命名约定
2. **灵活拆包**:
    
    - 只对大型场景进行拆包
    - 按功能模块和优先级拆分
    - 自动化拆包流程
3. **标准化接口**:
    
    - 统一的资源加载接口
    - 封装Addressables细节
    - 异常处理与自动重试

### 美术工作流最佳实践

1. **资源制作**:
    
    - 遵循内存和大小预算
    - 优化纹理和模型
    - 正确设置LOD级别
2. **场景组织**:
    
    - 逻辑分层组织场景
    - 大型物体单独组织，便于拆包
    - 避免过度嵌套
3. **质量控制**:
    
    - 使用自动化工具检查资源
    - 测试不同设备性能下的效果
    - 注重关键路径优化

### 开发最佳实践

1. **异步编程**:
    
    - 全面使用async/await
    - 避免阻塞主线程
    - 正确处理异常
2. **版本兼容**:
    
    - 兼容旧版本数据结构
    - 版本号递增管理
    - 平滑升级机制
3. **稳定发布**:
    
    - 增量更新为主
    - 保留回滚能力
    - 灰度发布新内容

---

这份完整指南详细介绍了VR剧场项目的Addressables资源管理规范，包括基础结构设计、数据模型定义、资源管理实现、拆包策略、美术工作流程、打包与发布流程、运行时优化，以及问题排查方法。它为开发人员和美术人员提供了清晰的参考，确保资源管理系统既灵活又高效，同时满足VR场景的高性能要求。