第2章 资源标记与管理 发表于 2026-01-03 更新于 2026-01-03
第2章 资源标记与管理 资源标记为Addressable 手动标记资源 将资源标记为Addressable是最基本的操作,有以下几种方式:
方式一:Inspector面板标记
在Project窗口中选择目标资源
在Inspector面板底部找到”Addressable Asset”组件
勾选”Addressable”复选框
系统会自动生成一个默认地址(通常是资源路径)
可以修改Address字段来自定义地址名称
方式二:Addressables Groups窗口标记
打开Window > Asset Management > Addressables > Groups
在Groups窗口中可以看到所有资源
选中需要标记的资源行
在Address列中输入地址名称
或者勾选Address列的复选框使用默认地址
批量标记资源 对于大量资源的标记,可以使用以下方法:
使用脚本批量标记 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 using UnityEngine;using UnityEditor;using UnityEditor.AddressableAssets;using UnityEditor.AddressableAssets.Settings;public class BatchAddressableMarker : EditorWindow { [MenuItem("Tools/Addressables/Batch Mark Addressable" ) ] static void Init () { BatchAddressableMarker window = (BatchAddressableMarker)EditorWindow.GetWindow(typeof (BatchAddressableMarker)); window.titleContent = new GUIContent("Batch Mark Addressable" ); window.Show(); } string searchPattern = "*.prefab" ; string addressPrefix = "Prefabs/" ; void OnGUI () { GUILayout.Label("批量标记Addressable资源" , EditorStyles.boldLabel); searchPattern = EditorGUILayout.TextField("搜索模式:" , searchPattern); addressPrefix = EditorGUILayout.TextField("地址前缀:" , addressPrefix); if (GUILayout.Button("标记选中资源" )) { MarkSelectedAssets(); } if (GUILayout.Button("标记指定路径资源" )) { MarkAssetsAtPath(); } } void MarkSelectedAssets () { var settings = AddressableAssetSettingsDefaultObject.Settings; var selectedObjects = Selection.objects; foreach (var obj in selectedObjects) { string assetPath = AssetDatabase.GetAssetPath(obj); if (!string .IsNullOrEmpty(assetPath)) { var entry = settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(assetPath), settings.DefaultGroup, false , true ); entry.address = addressPrefix + obj.name; } } settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, settings.groups, true , true ); } void MarkAssetsAtPath () { string [] guids = AssetDatabase.FindAssets(searchPattern); var settings = AddressableAssetSettingsDefaultObject.Settings; foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); var entry = settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false , true ); string fileName = System.IO.Path.GetFileNameWithoutExtension(path); entry.address = addressPrefix + fileName; } settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, settings.groups, true , true ); } }
Address命名规范与最佳实践 命名原则 1. 清晰性原则
地址应该能够清晰地表达资源的用途和类型
避免使用模糊或过于简单的地址名称
使用有意义的描述性名称
1 2 3 4 5 6 7 8 9 "Characters/Player/HeroPrefab" "UI/Sprites/ButtonTexture" "Audio/Music/BattleMusic" "Asset123" "Character" "Music"
2. 结构化原则
使用分层结构来组织地址
类似于文件夹路径的格式
按功能、类型或场景进行分组
3. 一致性原则
在整个项目中保持命名风格一致
团队成员使用相同的命名约定
建立命名规范文档
命名模式 按资源类型分组 1 2 3 4 5 6 Prefabs/Characters/Player.prefab Prefabs/Characters/Enemy.prefab Textures/Characters/PlayerTexture.png Textures/Environment/BackgroundTexture.png Materials/Characters/PlayerMaterial.mat Materials/Environment/BackgroundMaterial.mat
按功能模块分组 1 2 3 4 Gameplay/Player/Character.prefab Gameplay/Player/Weapon.prefab UI/MainMenu/Button.prefab UI/Inventory/Slot.prefab
按场景分组 1 2 3 4 Scene1/Character.prefab Scene1/Environment.prefab Scene2/Character.prefab Scene2/Props.prefab
命名最佳实践 1. 使用小写字母
统一使用小写字母避免大小写敏感问题
提高跨平台兼容性
2. 避免特殊字符
只使用字母、数字、下划线和斜杠
避免空格和其他特殊字符
3. 版本管理
在地址中包含版本信息(如果需要)
使用时间戳或版本号后缀
Labels(标签)系统的使用 标签的概念 标签(Labels)是Addressables系统中用于分类和筛选资源的机制。与地址不同,一个资源可以有多个标签,这使得资源管理更加灵活。
创建和管理标签 在Addressables Groups窗口中管理标签
打开Addressables Groups窗口
点击窗口顶部的”Labels”按钮
在弹出的标签管理窗口中可以:
为资源分配标签
在Groups窗口中选中资源
在标签列中点击”Edit”按钮
选择需要的标签
确认应用
使用标签加载资源 加载带有特定标签的所有资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 using UnityEngine;using UnityEngine.AddressableAssets;using UnityEngine.ResourceManagement.AsyncOperations;using System.Collections.Generic;public class LabelBasedLoader : MonoBehaviour { public string labelName = "Character" ; void LoadAssetsByLabel () { AsyncOperationHandle<IList<GameObject>> handle = Addressables.LoadAssetsAsync<GameObject>(labelName, null ); handle.Completed += (operation) => { if (operation.Status == AsyncOperationStatus.Succeeded) { var loadedAssets = operation.Result; Debug.Log($"加载了 {loadedAssets.Count} 个带有标签 '{labelName} ' 的资源" ); foreach (GameObject asset in loadedAssets) { Debug.Log($"加载的资源: {asset.name} " ); } } else { Debug.LogError($"加载失败: {operation.OperationException} " ); } Addressables.Release(handle); }; } void LoadAssetsByMultipleLabels () { var keys = new List<object > { "Character" , "Player" }; AsyncOperationHandle<IList<GameObject>> handle = Addressables.LoadAssetsAsync<GameObject>(keys, null , Addressables.MergeMode.Intersection); handle.Completed += (operation) => { if (operation.Status == AsyncOperationStatus.Succeeded) { var loadedAssets = operation.Result; Debug.Log($"加载了同时具有 'Character' 和 'Player' 标签的资源数量: {loadedAssets.Count} " ); } else { Debug.LogError($"加载失败: {operation.OperationException} " ); } Addressables.Release(handle); }; } }
标签的应用场景 1. 按类型分类
“Character”, “Weapon”, “Item” - 按资源类型分类
“HighQuality”, “LowQuality” - 按质量等级分类
2. 按功能分类
“Essential”, “Optional” - 按重要性分类
“Updateable”, “Fixed” - 按更新频率分类
3. 按平台分类
“Mobile”, “Desktop”, “Console” - 按平台分类
“VR”, “AR”, “Standard” - 按功能特性分类
Addressables Groups窗口详解 Groups窗口界面介绍 Addressables Groups窗口是管理Addressable资源的主要界面,包含以下主要区域:
1. 工具栏
Create Group :创建新的资源组
Build :构建Addressable资源
Profile :切换不同平台的配置文件
Play Mode :选择运行模式
Settings :打开Addressable设置
2. 组列表区域
显示所有资源组
可以创建、删除、重命名组
查看每个组的基本信息
3. 资源详情区域
显示选中组中的所有资源
编辑资源的地址和标签
查看资源的构建信息
组的类型和配置 1. BundledAssetGroup 这是最常见的组类型,资源会被打包成AssetBundle:
BundledAssetGroupSchema :控制打包和加载路径
ContentUpdateGroupSchema :控制内容更新行为
2. PlayerDataGroup 资源直接包含在Player中,不打包:
3. ContentUpdateGroup 专门用于内容更新的组:
Schema系统详解 BundledAssetGroupSchema配置
Load Path :运行时加载路径
Build Path :构建输出路径
Bundle Mode :打包模式
Compression :压缩方式
ContentUpdateGroupSchema配置
Static Content :是否为静态内容
Content Hash :内容哈希设置
Use Asset Bundle Caching :启用AssetBundle缓存
资源的批量操作与管理 批量选择和操作 使用选择工具
按住Ctrl键选择多个资源
按住Shift键选择连续资源
使用右键菜单进行批量操作
批量编辑功能
选中多个资源后可以批量修改地址
批量分配标签
批量移动到其他组
资源导入后自动标记 创建Editor脚本来实现资源导入后的自动标记:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 using UnityEngine;using UnityEditor;using UnityEditor.AddressableAssets;using UnityEditor.AddressableAssets.Settings;public class AutoAddressableProcessor : AssetPostprocessor { void OnPostprocessAllAssets (string [] importedAssets, string [] deletedAssets, string [] movedAssets, string [] movedFromAssetPaths ) { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null ) return ; foreach (string assetPath in importedAssets) { ProcessAsset(assetPath, settings); } for (int i = 0 ; i < movedAssets.Length; i++) { ProcessMovedAsset(movedFromAssetPaths[i], movedAssets[i], settings); } } void ProcessAsset (string assetPath, AddressableAssetSettings settings ) { if (assetPath.Contains("Assets/Prefabs/" )) { if (assetPath.EndsWith(".prefab" )) { string guid = AssetDatabase.AssetPathToGUID(assetPath); var entry = settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false , true ); string address = assetPath.Substring("Assets/" .Length); address = address.Substring(0 , address.Length - ".prefab" .Length); entry.address = address; entry.SetLabel("Prefab" , true , true ); } } else if (assetPath.Contains("Assets/Textures/" )) { if (assetPath.EndsWith(".png" ) || assetPath.EndsWith(".jpg" )) { string guid = AssetDatabase.AssetPathToGUID(assetPath); var entry = settings.CreateOrMoveEntry(guid, settings.DefaultGroup, false , true ); string address = assetPath.Substring("Assets/" .Length); address = address.Substring(0 , address.LastIndexOf('.' )); entry.address = address; entry.SetLabel("Texture" , true , true ); } } } void ProcessMovedAsset (string oldPath, string newPath, AddressableAssetSettings settings ) { string oldGuid = AssetDatabase.AssetPathToGUID(oldPath); if (!string .IsNullOrEmpty(oldGuid)) { var entry = settings.FindAssetEntry(oldGuid); if (entry != null ) { string newAddress = newPath.Substring("Assets/" .Length); if (newAddress.EndsWith(".prefab" )) { newAddress = newAddress.Substring(0 , newAddress.Length - ".prefab" .Length); } else { newAddress = newAddress.Substring(0 , newAddress.LastIndexOf('.' )); } entry.address = newAddress; } } } }
实战案例:将现有项目资源迁移到Addressables 迁移前的准备工作 1. 资源审计 首先对项目中的所有资源进行审计,了解当前的资源使用情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 using UnityEngine;using UnityEditor;using System.Collections.Generic;using System.Linq;public class ResourceAuditor : EditorWindow { [MenuItem("Tools/Addressables/Resource Audit" ) ] static void Init () { ResourceAuditor window = (ResourceAuditor)EditorWindow.GetWindow(typeof (ResourceAuditor)); window.titleContent = new GUIContent("Resource Auditor" ); window.Show(); } List<ResourceInfo> resourceList = new List<ResourceInfo>(); Vector2 scrollPosition; class ResourceInfo { public string path; public long size; public int referenceCount; public string type; } void OnGUI () { if (GUILayout.Button("扫描项目资源" )) { ScanProjectResources(); } scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); foreach (var info in resourceList.Take(100 )) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(info.path, GUILayout.Width(300 )); EditorGUILayout.LabelField(info.type, GUILayout.Width(100 )); EditorGUILayout.LabelField($"{info.size / 1024 } KB" , GUILayout.Width(100 )); EditorGUILayout.LabelField($"引用: {info.referenceCount} " , GUILayout.Width(100 )); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); } void ScanProjectResources () { resourceList.Clear(); string [] allAssets = AssetDatabase.GetAllAssetPaths(); foreach (string assetPath in allAssets) { if (assetPath.StartsWith("Assets/" ) && (assetPath.EndsWith(".prefab" ) || assetPath.EndsWith(".png" ) || assetPath.EndsWith(".jpg" ) || assetPath.EndsWith(".fbx" ) || assetPath.EndsWith(".mat" ) || assetPath.EndsWith(".asset" ))) { var info = new ResourceInfo { path = assetPath, type = System.IO.Path.GetExtension(assetPath), size = GetFileSize(assetPath) }; resourceList.Add(info); } } resourceList.Sort((a, b) => b.size.CompareTo(a.size)); } long GetFileSize (string assetPath ) { string fullPath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), assetPath); if (System.IO.File.Exists(fullPath)) { var fileInfo = new System.IO.FileInfo(fullPath); return fileInfo.Length; } return 0 ; } }
2. 制定迁移策略 根据资源审计结果,制定迁移策略:
核心资源 :游戏启动必需的资源,考虑放在PlayerDataGroup
场景资源 :按场景分组,便于按需加载
通用资源 :UI、音效等,可按功能分组
更新资源 :经常更新的内容,使用ContentUpdateGroup
迁移实施步骤 步骤1:创建资源组结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 using UnityEngine;using UnityEditor;using UnityEditor.AddressableAssets;using UnityEditor.AddressableAssets.Settings;public class MigrationSetup : EditorWindow { [MenuItem("Tools/Addressables/Migration Setup" ) ] static void Init () { MigrationSetup window = (MigrationSetup)EditorWindow.GetWindow(typeof (MigrationSetup)); window.titleContent = new GUIContent("Migration Setup" ); window.Show(); } void OnGUI () { GUILayout.Label("创建标准资源组结构" , EditorStyles.boldLabel); if (GUILayout.Button("创建标准组结构" )) { CreateStandardGroups(); } if (GUILayout.Button("清理旧的Resources文件夹" )) { CleanupResourcesFolder(); } } void CreateStandardGroups () { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null ) return ; var coreGroup = settings.CreateGroup("Core" , false , false , true , null ); coreGroup.AddSchema<BundledAssetGroupSchema>(); coreGroup.AddSchema<ContentUpdateGroupSchema>(); var coreSchema = coreGroup.GetSchema<BundledAssetGroupSchema>(); coreSchema.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether; var sceneGroup = settings.CreateGroup("Scenes" , false , false , true , null ); sceneGroup.AddSchema<BundledAssetGroupSchema>(); sceneGroup.AddSchema<ContentUpdateGroupSchema>(); var uiGroup = settings.CreateGroup("UI" , false , false , true , null ); uiGroup.AddSchema<BundledAssetGroupSchema>(); uiGroup.AddSchema<ContentUpdateGroupSchema>(); var audioGroup = settings.CreateGroup("Audio" , false , false , true , null ); audioGroup.AddSchema<BundledAssetGroupSchema>(); audioGroup.AddSchema<ContentUpdateGroupSchema>(); Debug.Log("标准资源组结构创建完成" ); } void CleanupResourcesFolder () { if (System.IO.Directory.Exists("Assets/Resources" )) { bool confirm = EditorUtility.DisplayDialog( "确认删除" , "确定要删除Assets/Resources文件夹吗?请确保已备份重要资源。" , "删除" , "取消" ); if (confirm) { AssetDatabase.DeleteAsset("Assets/Resources" ); AssetDatabase.Refresh(); Debug.Log("Resources文件夹已删除" ); } } else { Debug.Log("未找到Resources文件夹" ); } } }
步骤2:资源迁移脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 using UnityEngine;using UnityEditor;using UnityEditor.AddressableAssets;using UnityEditor.AddressableAssets.Settings;using System.Collections.Generic;public class ResourceMigrator : EditorWindow { [MenuItem("Tools/Addressables/Migrate Resources" ) ] static void Init () { ResourceMigrator window = (ResourceMigrator)EditorWindow.GetWindow(typeof (ResourceMigrator)); window.titleContent = new GUIContent("Resource Migrator" ); window.Show(); } string sourcePath = "Assets/" ; string targetGroup = "Default" ; bool includeSubfolders = true ; void OnGUI () { GUILayout.Label("资源迁移工具" , EditorStyles.boldLabel); sourcePath = EditorGUILayout.TextField("源路径:" , sourcePath); targetGroup = EditorGUILayout.TextField("目标组:" , targetGroup); includeSubfolders = EditorGUILayout.Toggle("包含子文件夹:" , includeSubfolders); if (GUILayout.Button("开始迁移" )) { MigrateResources(); } } void MigrateResources () { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null ) { Debug.LogError("Addressable Asset Settings not found" ); return ; } var targetGroupObj = settings.FindGroup(targetGroup); if (targetGroupObj == null ) { Debug.LogError($"Group {targetGroup} not found" ); return ; } string searchPattern = includeSubfolders ? sourcePath + "**/*" : sourcePath + "*" ; string [] guids = AssetDatabase.FindAssets("t:Object" , new [] { sourcePath }); int migratedCount = 0 ; foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); if (assetPath.EndsWith(".cs" ) || assetPath.EndsWith(".meta" ) || assetPath.Contains("/Resources/" ) || assetPath.Contains("/Editor/" )) { continue ; } var entry = settings.CreateOrMoveEntry(guid, targetGroupObj, false , true ); string relativePath = assetPath.Substring("Assets/" .Length); string address = relativePath.Substring(0 , relativePath.LastIndexOf('.' )); entry.address = address; AddLabelsByType(entry, assetPath); migratedCount++; } settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, settings.groups, true , true ); Debug.Log($"成功迁移 {migratedCount} 个资源到组 {targetGroup} " ); } void AddLabelsByType (AddressableAssetEntry entry, string assetPath ) { string extension = System.IO.Path.GetExtension(assetPath).ToLower(); switch (extension) { case ".prefab" : entry.SetLabel("Prefab" , true , true ); break ; case ".png" : case ".jpg" : case ".jpeg" : case ".tga" : case ".psd" : entry.SetLabel("Texture" , true , true ); break ; case ".fbx" : case ".obj" : entry.SetLabel("Model" , true , true ); break ; case ".wav" : case ".mp3" : case ".ogg" : entry.SetLabel("Audio" , true , true ); break ; case ".mat" : entry.SetLabel("Material" , true , true ); break ; case ".shader" : entry.SetLabel("Shader" , true , true ); break ; } } }
迁移后的验证 验证脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 using UnityEngine;using UnityEngine.AddressableAssets;using UnityEngine.ResourceManagement.AsyncOperations;public class MigrationValidator : MonoBehaviour { public string [] testAddresses = new string [] { "Assets/Prefabs/Player.prefab" , "Assets/Textures/UI/Background.png" , "Assets/Materials/PlayerMaterial.mat" }; void Start () { ValidateMigration(); } async void ValidateMigration () { Debug.Log("开始验证迁移结果..." ); foreach (string address in testAddresses) { try { var handle = Addressables.LoadAssetAsync<GameObject>(address); await handle.Task; if (handle.Status == AsyncOperationStatus.Succeeded) { Debug.Log($"✓ 成功加载: {address} " ); Addressables.Release(handle); } else { Debug.LogError($"✗ 加载失败: {address} - {handle.OperationException} " ); } } catch (System.Exception e) { Debug.LogError($"✗ 加载异常: {address} - {e.Message} " ); } } Debug.Log("迁移验证完成" ); } }
通过本章的学习,您已经掌握了Addressables资源标记和管理的完整流程,包括地址命名规范、标签系统使用、Groups窗口操作以及实际的资源迁移方法。下一章我们将深入学习资源的加载和释放机制。