第5章 远程资源与热更新 发表于 2026-01-03 更新于 2026-01-03
第5章 远程资源与热更新 Local vs Remote资源配置 本地与远程资源的概念 在Unity Addressables系统中,资源可以存储在本地(设备上)或远程服务器上。这种设计提供了极大的灵活性,允许开发者根据资源的使用频率、大小和更新需求来决定存储位置。
本地资源(Local Resources)
存储在设备本地
加载速度快
不需要网络连接
适合核心资源和频繁使用的资源
远程资源(Remote Resources)
存储在远程服务器
需要网络连接
可以实现热更新
适合大型资源和不常使用的资源
配置本地资源 本地资源组配置 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 using UnityEngine;using UnityEditor.AddressableAssets.Settings;using UnityEditor.AddressableAssets.Settings.GroupSchemas;public class LocalResourceConfig { public static void ConfigureLocalGroup (AddressableAssetGroup group ) { var bundleSchema = group .GetSchema<BundledAssetGroupSchema>(); if (bundleSchema != null ) { bundleSchema.Compression = BundledAssetGroupSchema.BundleCompressionMode.LZ4; bundleSchema.LoadPath = "file://{UnityEngine.Application.streamingAssetsPath}/{Platform}" ; bundleSchema.BuildPath = "Assets/StreamingAssets/{Platform}" ; } var updateSchema = group .GetSchema<ContentUpdateGroupSchema>(); if (updateSchema != null ) { updateSchema.StaticContent = false ; updateSchema.UseAssetBundleCaching = true ; } Debug.Log($"配置本地资源组: {group .Name} " ); } public static bool IsLocalResourceGroup (string groupName ) { string lowerName = groupName.ToLower(); return lowerName.Contains("core" ) || lowerName.Contains("essential" ) || lowerName.Contains("startup" ) || lowerName.Contains("local" ); } }
配置远程资源 远程资源组配置 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 using UnityEngine;using UnityEditor.AddressableAssets.Settings;using UnityEditor.AddressableAssets.Settings.GroupSchemas;public class RemoteResourceConfig { public static void ConfigureRemoteGroup (AddressableAssetGroup group , string cdnBaseUrl = "https://cdn.example.com/bundles" ) { var bundleSchema = group .GetSchema<BundledAssetGroupSchema>(); if (bundleSchema != null ) { bundleSchema.Compression = BundledAssetGroupSchema.BundleCompressionMode.LZMA; bundleSchema.LoadPath = $"{cdnBaseUrl} /{{Platform}}" ; bundleSchema.BuildPath = $"ServerData/{{Platform}}" ; } var updateSchema = group .GetSchema<ContentUpdateGroupSchema>(); if (updateSchema != null ) { updateSchema.StaticContent = false ; updateSchema.UseAssetBundleCaching = true ; updateSchema.UseAssetBundleCacheForLocal = false ; } Debug.Log($"配置远程资源组: {group .Name} , CDN: {cdnBaseUrl} " ); } public static bool IsRemoteResourceGroup (string groupName ) { string lowerName = groupName.ToLower(); return lowerName.Contains("remote" ) || lowerName.Contains("download" ) || lowerName.Contains("optional" ) || lowerName.Contains("expansion" ); } }
本地与远程资源混合配置 混合资源配置管理器 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 using System.Collections.Generic;using UnityEngine;using UnityEditor.AddressableAssets.Settings;public class MixedResourceConfigManager { [System.Serializable ] public class ResourceConfigRule { public string groupNamePattern; public string loadPath; public string buildPath; public bool isRemote; public string compression; public bool allowUpdates; } private List<ResourceConfigRule> rules = new List<ResourceConfigRule>(); public MixedResourceConfigManager () { rules.Add(new ResourceConfigRule { groupNamePattern = "*Core*" , loadPath = "file://{UnityEngine.Application.streamingAssetsPath}/{Platform}" , buildPath = "Assets/StreamingAssets/{Platform}" , isRemote = false , compression = "LZMA" , allowUpdates = false }); rules.Add(new ResourceConfigRule { groupNamePattern = "*Remote*" , loadPath = "https://cdn.yourdomain.com/bundles/{Platform}" , buildPath = "ServerData/{Platform}" , isRemote = true , compression = "LZMA" , allowUpdates = true }); rules.Add(new ResourceConfigRule { groupNamePattern = "*" , loadPath = "file://{UnityEngine.Application.streamingAssetsPath}/{Platform}" , buildPath = "Assets/StreamingAssets/{Platform}" , isRemote = false , compression = "LZ4" , allowUpdates = true }); } public void ApplyConfiguration (AddressableAssetSettings settings ) { foreach (var group in settings.groups) { ApplyConfigurationToGroup(settings, group ); } } public void ApplyConfigurationToGroup (AddressableAssetSettings settings, AddressableAssetGroup group ) { var rule = FindMatchingRule(group .Name); if (rule == null ) return ; var bundleSchema = group .GetSchema<BundledAssetGroupSchema>(); if (bundleSchema != null ) { bundleSchema.LoadPath = rule.loadPath; bundleSchema.BuildPath = rule.buildPath; switch (rule.compression) { case "LZ4" : bundleSchema.Compression = BundledAssetGroupSchema.BundleCompressionMode.LZ4; break ; case "LZMA" : bundleSchema.Compression = BundledAssetGroupSchema.BundleCompressionMode.LZMA; break ; case "Uncompressed" : bundleSchema.Compression = BundledAssetGroupSchema.BundleCompressionMode.Uncompressed; break ; } } var updateSchema = group .GetSchema<ContentUpdateGroupSchema>(); if (updateSchema != null ) { updateSchema.StaticContent = !rule.allowUpdates; updateSchema.UseAssetBundleCaching = true ; } Debug.Log($"为组 {group .Name} 应用配置: 远程={rule.isRemote} , 压缩={rule.compression} " ); } private ResourceConfigRule FindMatchingRule (string groupName ) { foreach (var rule in rules) { if (GroupNameMatchesPattern(groupName, rule.groupNamePattern)) { return rule; } } return null ; } private bool GroupNameMatchesPattern (string groupName, string pattern ) { if (string .IsNullOrEmpty(pattern)) return false ; if (pattern == "*" ) return true ; pattern = pattern.Replace("*" , ".*" ); return System.Text.RegularExpressions.Regex.IsMatch(groupName, pattern); } }
设置远程资源服务器 服务器架构设计 远程资源服务器的基本要求 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 using System.Collections.Generic;using UnityEngine;public class RemoteServerRequirements { public class ServerSpec { public string serverType; public string baseUrl; public string [] supportedPlatforms; public string [] supportedLocales; public int maxConcurrentDownloads; public long maxFileSize; public bool supportsRangeRequests; public bool supportsETags; public string cacheControlHeader; } public static List<ServerSpec> GetRecommendedServerSpecs () { var specs = new List<ServerSpec>(); specs.Add(new ServerSpec { serverType = "CDN" , baseUrl = "https://your-cdn.com/bundles" , supportedPlatforms = new [] { "Android" , "iOS" , "Windows" , "macOS" , "WebGL" }, supportedLocales = new [] { "en-US" , "zh-CN" , "ja-JP" }, maxConcurrentDownloads = 8 , maxFileSize = 100 * 1024 * 1024 , supportsRangeRequests = true , supportsETags = true , cacheControlHeader = "public, max-age=31536000" }); specs.Add(new ServerSpec { serverType = "Custom" , baseUrl = "https://api.yourgame.com/bundles" , supportedPlatforms = new [] { "Android" , "iOS" , "Windows" }, supportedLocales = new [] { "en-US" , "zh-CN" }, maxConcurrentDownloads = 4 , maxFileSize = 50 * 1024 * 1024 , supportsRangeRequests = true , supportsETags = true , cacheControlHeader = "public, max-age=604800" }); return specs; } }
服务器配置工具 服务器配置管理器 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 using System.Collections.Generic;using UnityEngine;using UnityEditor;using UnityEditor.AddressableAssets.Settings;public class ServerConfigManager : EditorWindow { [MenuItem("Tools/Addressables/Server Configuration" ) ] static void Init () { ServerConfigManager window = (ServerConfigManager)EditorWindow.GetWindow(typeof (ServerConfigManager)); window.titleContent = new GUIContent("Server Configuration" ); window.Show(); } string serverBaseUrl = "https://cdn.yourgame.com/bundles" ; string [] supportedPlatforms = { "Android" , "iOS" , "Windows" , "macOS" , "WebGL" }; bool useCompression = true ; string compressionType = "LZMA" ; void OnGUI () { GUILayout.Label("远程资源服务器配置" , EditorStyles.boldLabel); serverBaseUrl = EditorGUILayout.TextField("服务器基础URL:" , serverBaseUrl); GUILayout.Label("支持的平台:" ); for (int i = 0 ; i < supportedPlatforms.Length; i++) { supportedPlatforms[i] = EditorGUILayout.TextField($"平台 {i + 1 } :" , supportedPlatforms[i]); } useCompression = EditorGUILayout.Toggle("使用压缩:" , useCompression); if (useCompression) { compressionType = EditorGUILayout.TextField("压缩类型:" , compressionType); } if (GUILayout.Button("应用配置到远程组" )) { ApplyServerConfig(); } if (GUILayout.Button("测试服务器连接" )) { TestServerConnection(); } } void ApplyServerConfig () { var settings = AddressableAssetSettingsDefaultObject.Settings; if (settings == null ) return ; foreach (var group in settings.groups) { if (IsRemoteGroup(group .Name)) { var bundleSchema = group .GetSchema<BundledAssetGroupSchema>(); if (bundleSchema != null ) { bundleSchema.LoadPath = $"{serverBaseUrl} /{{Platform}}" ; if (useCompression) { bundleSchema.Compression = compressionType == "LZMA" ? BundledAssetGroupSchema.BundleCompressionMode.LZMA : BundledAssetGroupSchema.BundleCompressionMode.LZ4; } } } } Debug.Log($"服务器配置已应用到远程资源组" ); } bool IsRemoteGroup (string groupName ) { return groupName.ToLower().Contains("remote" ) || groupName.ToLower().Contains("download" ) || groupName.ToLower().Contains("optional" ); } async void TestServerConnection () { Debug.Log($"测试服务器连接: {serverBaseUrl} " ); try { using (var request = new UnityEngine.Networking.UnityWebRequest(serverBaseUrl)) { request.method = UnityEngine.Networking.UnityWebRequest.kHttpVerbGET; request.timeout = 10 ; var operation = request.SendWebRequest(); while (!operation.isDone) { await System.Threading.Tasks.Task.Delay(100 ); } if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success) { Debug.Log("服务器连接测试成功" ); } else { Debug.LogError($"服务器连接测试失败: {request.error} " ); } } } catch (System.Exception e) { Debug.LogError($"服务器连接测试异常: {e.Message} " ); } } }
服务器部署脚本 自动化部署工具 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 using System;using System.IO;using System.Collections.Generic;using UnityEngine;public class ServerDeploymentTool { [System.Serializable ] public class DeploymentConfig { public string localBuildPath; public string serverBaseUrl; public string [] platforms; public string [] fileExtensions; public bool includeCatalog; public bool includeHashFiles; public string authHeader; } public static void DeployToServer (DeploymentConfig config ) { Debug.Log("开始部署到服务器..." ); foreach (string platform in config.platforms) { string platformPath = Path.Combine(config.localBuildPath, platform); if (!Directory.Exists(platformPath)) { Debug.LogWarning($"平台目录不存在: {platformPath} " ); continue ; } DeployPlatform(config, platform, platformPath); } Debug.Log("部署完成" ); } private static void DeployPlatform (DeploymentConfig config, string platform, string platformPath ) { Debug.Log($"部署平台: {platform} " ); var filesToDeploy = new List<string >(); foreach (string extension in config.fileExtensions) { string [] files = Directory.GetFiles(platformPath, $"*.{extension} " , SearchOption.AllDirectories); filesToDeploy.AddRange(files); } if (config.includeCatalog) { string [] catalogFiles = Directory.GetFiles(platformPath, "*.json" , SearchOption.TopDirectoryOnly); filesToDeploy.AddRange(catalogFiles); } if (config.includeHashFiles) { string [] hashFiles = Directory.GetFiles(platformPath, "*.hash" , SearchOption.TopDirectoryOnly); filesToDeploy.AddRange(hashFiles); } foreach (string file in filesToDeploy) { UploadFile(config, platform, file ); } } private static void UploadFile (DeploymentConfig config, string platform, string localFilePath ) { string relativePath = localFilePath.Substring(config.localBuildPath.Length + 1 ); string serverUrl = $"{config.serverBaseUrl} /{platform} /{relativePath} " ; Debug.Log($"上传文件: {relativePath} -> {serverUrl} " ); } }
Content Update构建流程 内容更新机制概述 Addressables的内容更新机制允许开发者在不重新发布应用的情况下更新资源。这个机制基于内容目录(Catalog)的版本控制。
更新流程图 1 2 3 4 5 6 1. 标记需要更新的资源 2. 构建新的AssetBundles 3. 生成新的内容目录 4. 上传到服务器 5. 客户端检查更新 6. 下载并应用更新
更新构建工具 内容更新构建器 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 using System;using System.Collections.Generic;using UnityEngine;using UnityEditor;using UnityEditor.AddressableAssets;using UnityEditor.AddressableAssets.Settings;public class ContentUpdateBuilder : EditorWindow { [MenuItem("Tools/Addressables/Content Update Builder" ) ] static void Init () { ContentUpdateBuilder window = (ContentUpdateBuilder)EditorWindow.GetWindow(typeof (ContentUpdateBuilder)); window.titleContent = new GUIContent("Content Update Builder" ); window.Show(); } AddressableAssetSettings settings; List<AddressableAssetGroup> updateGroups = new List<AddressableAssetGroup>(); bool includeCatalogInBuild = true ; string buildPath = "ServerData" ; void OnEnable () { settings = AddressableAssetSettingsDefaultObject.Settings; RefreshUpdateGroups(); } void OnGUI () { GUILayout.Label("内容更新构建工具" , EditorStyles.boldLabel); if (settings == null ) { GUILayout.Label("请先初始化Addressables设置" ); if (GUILayout.Button("初始化Addressables" )) { AddressableAssetSettingsDefaultObject.GetSettings(true ); settings = AddressableAssetSettingsDefaultObject.Settings; RefreshUpdateGroups(); } return ; } buildPath = EditorGUILayout.TextField("构建路径:" , buildPath); includeCatalogInBuild = EditorGUILayout.Toggle("包含Catalog:" , includeCatalogInBuild); GUILayout.Label("选择要更新的组:" , EditorStyles.boldLabel); foreach (var group in settings.groups) { if (group .HasSchema<ContentUpdateGroupSchema>()) { bool isSelected = updateGroups.Contains(group ); bool newSelection = EditorGUILayout.ToggleLeft($" {group .Name} " , isSelected); if (newSelection && !isSelected) { updateGroups.Add(group ); } else if (!newSelection && isSelected) { updateGroups.Remove(group ); } } } if (GUILayout.Button("构建更新" )) { BuildContentUpdate(); } if (GUILayout.Button("刷新组列表" )) { RefreshUpdateGroups(); } } void RefreshUpdateGroups () { updateGroups.Clear(); if (settings != null ) { foreach (var group in settings.groups) { if (group .HasSchema<ContentUpdateGroupSchema>() && !group .GetSchema<ContentUpdateGroupSchema>().StaticContent) { updateGroups.Add(group ); } } } } void BuildContentUpdate () { if (updateGroups.Count == 0 ) { Debug.LogWarning("没有选择要更新的组" ); return ; } try { foreach (var group in updateGroups) { var bundleSchema = group .GetSchema<BundledAssetGroupSchema>(); if (bundleSchema != null ) { bundleSchema.BuildPath = buildPath + "/{Platform}" ; } } var buildParams = new UnityEditor.AddressableAssets.Build.AddressablesDataBuilderInput(settings); buildParams.BundleBuildPath = buildPath; var buildResult = UnityEditor.AddressableAssets.Build.ContentUpdateScript.RunAddressableBuild(settings, buildParams); if (buildResult.Success) { Debug.Log($"内容更新构建成功,输出路径: {buildPath} " ); ShowBuildReport(buildResult); } else { Debug.LogError($"内容更新构建失败: {buildResult.Error} " ); } } catch (Exception e) { Debug.LogError($"构建过程异常: {e.Message} " ); } } void ShowBuildReport (UnityEditor.AddressableAssets.Build.AddressablesBuildResult result ) { Debug.Log("=== 构建报告 ===" ); Debug.Log($"构建时间: {result.elapsedTime:F2} 秒" ); Debug.Log($"构建大小: {FormatFileSize(result.totalSize)} " ); Debug.Log($"构建文件数: {result.fileCount} " ); } string FormatFileSize (long bytes ) { string [] sizes = { "B" , "KB" , "MB" , "GB" }; double len = bytes; int order = 0 ; while (len >= 1024 && order < sizes.Length - 1 ) { order++; len = len / 1024 ; } return $"{len:F2} {sizes[order]} " ; } }
差量更新策略(Check for Content Update Restrictions) 差量更新配置 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 using UnityEngine;using UnityEditor.AddressableAssets.Settings;public class IncrementalUpdateConfig { public static void ConfigureIncrementalUpdates (AddressableAssetSettings settings ) { foreach (var group in settings.groups) { var updateSchema = group .GetSchema<ContentUpdateGroupSchema>(); if (updateSchema != null && !updateSchema.StaticContent) { updateSchema.StaticContent = false ; Debug.Log($"为组 {group .Name} 启用差量更新" ); } } } public class UpdateChecker : MonoBehaviour { public System.Action<bool , string > onUpdateCheckComplete; public async void CheckForContentUpdate () { try { var updateHandle = UnityEngine.AddressableAssets.Addressables.CheckForCatalogUpdates(); var updateList = await updateHandle.Task; if (updateList != null && updateList.Count > 0 ) { string updateInfo = "发现更新:\n" ; foreach (var update in updateList) { updateInfo += $"- {update} \n" ; } onUpdateCheckComplete?.Invoke(true , updateInfo); Debug.Log(updateInfo); } else { onUpdateCheckComplete?.Invoke(false , "没有发现更新" ); Debug.Log("没有发现内容更新" ); } UnityEngine.AddressableAssets.Addressables.Release(updateHandle); } catch (System.Exception e) { Debug.LogError($"检查更新时发生错误: {e.Message} " ); onUpdateCheckComplete?.Invoke(false , $"检查更新失败: {e.Message} " ); } } public async void UpdateContent () { try { var updateHandle = UnityEngine.AddressableAssets.Addressables.UpdateCatalogs(null , true ); var catalog = await updateHandle.Task; if (catalog != null ) { Debug.Log("内容更新成功" ); } else { Debug.LogWarning("内容更新完成,但没有新内容" ); } UnityEngine.AddressableAssets.Addressables.Release(updateHandle); } catch (System.Exception e) { Debug.LogError($"更新内容时发生错误: {e.Message} " ); } } } }
更新版本管理 版本管理工具 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 System.Collections.Generic;using UnityEngine;using UnityEditor.AddressableAssets.Settings;public class VersionManager { [System.Serializable ] public class VersionInfo { public string version; public string buildTime; public List<string > updatedGroups; public long totalSize; public string hash; } private static List<VersionInfo> versionHistory = new List<VersionInfo>(); public static void RecordBuildVersion (AddressableAssetSettings settings, string version ) { var versionInfo = new VersionInfo { version = version, buildTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss" ), updatedGroups = new List<string >(), totalSize = 0 }; foreach (var group in settings.groups) { if (IsGroupUpdated(group )) { versionInfo.updatedGroups.Add(group .Name); } } versionInfo.totalSize = CalculateTotalSize(settings); versionInfo.hash = CalculateVersionHash(versionInfo); versionHistory.Add(versionInfo); Debug.Log($"记录版本: {version} , 更新组: {string .Join(", " , versionInfo.updatedGroups)} " ); } private static bool IsGroupUpdated (AddressableAssetGroup group ) { var updateSchema = group .GetSchema<ContentUpdateGroupSchema>(); return updateSchema != null && !updateSchema.StaticContent; } private static long CalculateTotalSize (AddressableAssetSettings settings ) { long totalSize = 0 ; foreach (var group in settings.groups) { totalSize += group .entries.Count * 1024 * 1024 ; } return totalSize; } private static string CalculateVersionHash (VersionInfo versionInfo ) { string data = versionInfo.version + versionInfo.buildTime + string .Join("" , versionInfo.updatedGroups); return System.Security.Cryptography.SHA256.Create() .ComputeHash(System.Text.Encoding.UTF8.GetBytes(data)) .ToString(); } public static VersionInfo GetLatestVersion () { if (versionHistory.Count == 0 ) return null ; return versionHistory[versionHistory.Count - 1 ]; } public static List <VersionInfo > GetVersionHistory () { return new List<VersionInfo>(versionHistory); } }
版本管理与回滚机制 版本控制策略 版本管理器 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 using System.Collections.Generic;using UnityEngine;public class VersionControlManager { [System.Serializable ] public class VersionRecord { public string versionId; public string timestamp; public string [] updatedAssets; public string [] removedAssets; public long size; public string checksum; public string description; } private List<VersionRecord> versionHistory = new List<VersionRecord>(); private string currentVersion = "1.0.0" ; public void RecordVersionUpdate (string versionId, string [] updatedAssets, string [] removedAssets, string description = "" ) { var record = new VersionRecord { versionId = versionId, timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ" ), updatedAssets = updatedAssets ?? new string [0 ], removedAssets = removedAssets ?? new string [0 ], size = CalculateUpdateSize(updatedAssets, removedAssets), checksum = GenerateChecksum(updatedAssets, removedAssets), description = description }; versionHistory.Add(record ); currentVersion = versionId; Debug.Log($"版本记录: {versionId} , 更新资产: {updatedAssets?.Length ?? 0 } , 移除资产: {removedAssets?.Length ?? 0 } " ); } private long CalculateUpdateSize (string [] updatedAssets, string [] removedAssets ) { long size = 0 ; if (updatedAssets != null ) size += updatedAssets.Length * 1024 * 1024 ; if (removedAssets != null ) size += removedAssets.Length * 512 * 1024 ; return size; } private string GenerateChecksum (string [] updatedAssets, string [] removedAssets ) { string data = (updatedAssets != null ? string .Join(";" , updatedAssets) : "" ) + (removedAssets != null ? string .Join(";" , removedAssets) : "" ) + System.DateTime.UtcNow.Ticks.ToString(); var hash = System.Security.Cryptography.SHA256.Create() .ComputeHash(System.Text.Encoding.UTF8.GetBytes(data)); return System.BitConverter.ToString(hash).Replace("-" , "" ).ToLower(); } public bool CanRollbackTo (string versionId ) { return versionHistory.Exists(r => r.versionId == versionId); } public bool RollbackTo (string versionId ) { if (!CanRollbackTo(versionId)) { Debug.LogError($"无法回滚到版本: {versionId} , 版本不存在" ); return false ; } Debug.Log($"开始回滚到版本: {versionId} " ); currentVersion = versionId; Debug.Log($"成功回滚到版本: {versionId} " ); return true ; } public List <VersionRecord > GetVersionHistory () { return new List<VersionRecord>(versionHistory); } public string GetCurrentVersion () { return currentVersion; } public VersionRecord GetVersionRecord (string versionId ) { return versionHistory.Find(r => r.versionId == versionId); } }
回滚机制实现 安全回滚管理器 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 using System.Collections;using UnityEngine;using UnityEngine.AddressableAssets;public class SafeRollbackManager : MonoBehaviour { private VersionControlManager versionManager = new VersionControlManager(); private string backupVersion; void Start () { backupVersion = versionManager.GetCurrentVersion(); } public IEnumerator PerformSafeUpdate (string newVersion, System.Action<bool > onComplete ) { string currentVersion = versionManager.GetCurrentVersion(); Debug.Log($"开始安全更新到版本: {newVersion} , 当前版本: {currentVersion} " ); if (!PreUpdateCheck()) { Debug.LogError("更新预检查失败" ); onComplete?.Invoke(false ); yield break ; } if (!BackupCurrentState()) { Debug.LogError("备份当前状态失败" ); onComplete?.Invoke(false ); yield break ; } try { bool updateSuccess = yield StartCoroutine (ExecuteUpdate(newVersion )) ; if (!updateSuccess) { Debug.LogError($"更新到版本 {newVersion} 失败,尝试回滚" ); bool rollbackSuccess = RollbackToBackup(); onComplete?.Invoke(rollbackSuccess); yield break ; } if (!PostUpdateValidation()) { Debug.LogError($"更新后验证失败,回滚到备份版本" ); bool rollbackSuccess = RollbackToBackup(); onComplete?.Invoke(rollbackSuccess); yield break ; } ConfirmUpdate(newVersion); onComplete?.Invoke(true ); Debug.Log($"成功更新到版本: {newVersion} " ); } catch (System.Exception e) { Debug.LogError($"更新过程中发生异常: {e.Message} " ); bool rollbackSuccess = RollbackToBackup(); onComplete?.Invoke(rollbackSuccess); } } private bool PreUpdateCheck () { long requiredSpace = 100 * 1024 * 1024 ; long availableSpace = GetAvailableDiskSpace(); if (availableSpace < requiredSpace) { Debug.LogError($"磁盘空间不足,需要: {requiredSpace} , 可用: {availableSpace} " ); return false ; } if (!IsNetworkAvailable()) { Debug.LogError("网络连接不可用" ); return false ; } return true ; } private bool BackupCurrentState () { Debug.Log("备份当前状态完成" ); return true ; } private IEnumerator ExecuteUpdate (string newVersion ) { Debug.Log($"下载版本 {newVersion} 的资源..." ); yield return new WaitForSeconds (1f ) ; Debug.Log($"应用版本 {newVersion} 的更新..." ); yield return new WaitForSeconds (0.5f ) ; yield return true ; } private bool PostUpdateValidation () { string [] testAssets = { "Assets/Prefabs/Player.prefab" , "Assets/Textures/UI/Background.png" }; foreach (string asset in testAssets) { var handle = Addressables.LoadAssetAsync<GameObject>(asset); yield return handle; if (handle.Status != UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded) { Debug.LogError($"验证失败: 无法加载 {asset} " ); Addressables.Release(handle); return false ; } Addressables.Release(handle); } Debug.Log("更新后验证通过" ); return true ; } private bool RollbackToBackup () { if (string .IsNullOrEmpty(backupVersion)) { Debug.LogError("没有备份版本可以回滚" ); return false ; } Debug.Log($"回滚到备份版本: {backupVersion} " ); System.Threading.Thread.Sleep(1000 ); Debug.Log($"成功回滚到版本: {backupVersion} " ); return true ; } private void ConfirmUpdate (string newVersion ) { backupVersion = newVersion; CleanupOldBackups(); } private void CleanupOldBackups () { Debug.Log("清理旧备份完成" ); } private long GetAvailableDiskSpace () { return 1024 * 1024 * 1024 ; } private bool IsNetworkAvailable () { return Application.internetReachability != NetworkReachability.NotReachable; } }
CDN集成方案 CDN选择与配置 CDN集成管理器 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 using System.Collections.Generic;using UnityEngine;public class CDNIntegrationManager { public enum CDNProvider { CloudFlare, AWS_CloudFront, Azure_CDN, Custom } [System.Serializable ] public class CDNConfig { public CDNProvider provider; public string baseUrl; public string apiKey; public string region; public int maxConcurrentDownloads; public string cachePolicy; public string [] supportedRegions; } private Dictionary<CDNProvider, CDNConfig> cdnConfigs = new Dictionary<CDNProvider, CDNConfig>(); public CDNIntegrationManager () { InitializeDefaultConfigs(); } private void InitializeDefaultConfigs () { cdnConfigs[CDNProvider.CloudFlare] = new CDNConfig { provider = CDNProvider.CloudFlare, baseUrl = "https://yourdomain.cloudflareaccess.com/bundles" , maxConcurrentDownloads = 8 , cachePolicy = "public, max-age=31536000" , supportedRegions = new [] { "US" , "EU" , "APAC" } }; cdnConfigs[CDNProvider.AWS_CloudFront] = new CDNConfig { provider = CDNProvider.AWS_CloudFront, baseUrl = "https://d1234567890abcdef.cloudfront.net/bundles" , maxConcurrentDownloads = 10 , cachePolicy = "public, max-age=31536000" , supportedRegions = new [] { "US-East-1" , "US-West-2" , "EU-West-1" , "AP-Northeast-1" } }; cdnConfigs[CDNProvider.Azure_CDN] = new CDNConfig { provider = CDNProvider.Azure_CDN, baseUrl = "https://yourcdn.azureedge.net/bundles" , maxConcurrentDownloads = 8 , cachePolicy = "public, max-age=31536000" , supportedRegions = new [] { "East US" , "West Europe" , "Southeast Asia" } }; } public CDNConfig GetConfig (CDNProvider provider ) { if (cdnConfigs.ContainsKey(provider)) { return cdnConfigs[provider]; } return new CDNConfig { provider = CDNProvider.Custom, baseUrl = "https://your-cdn.com/bundles" , maxConcurrentDownloads = 6 }; } public void ApplyCDNConfig (CDNConfig config ) { Debug.Log($"应用CDN配置: {config.provider} , URL: {config.baseUrl} " ); UpdateAddressablesProfile(config); } private void UpdateAddressablesProfile (CDNConfig config ) { Debug.Log("更新Addressables Profile以使用CDN配置" ); } }
CDN优化策略 CDN优化工具 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 using System.Collections.Generic;using UnityEngine;using UnityEngine.AddressableAssets;public class CDNPerformanceOptimizer { [System.Serializable ] public class PerformanceMetrics { public float averageDownloadSpeed; public float successRate; public float cacheHitRate; public int concurrentDownloads; public float latency; } private Dictionary<string , PerformanceMetrics> cdnPerformance = new Dictionary<string , PerformanceMetrics>(); public async System.Threading.Tasks.Task<PerformanceMetrics> TestCDNPerformance (string cdnUrl, string testAssetPath ) { var metrics = new PerformanceMetrics(); try { System.DateTime startTime = System.DateTime.Now; var handle = Addressables.DownloadDependenciesAsync(testAssetPath, false ); await handle.Task; System.TimeSpan duration = System.DateTime.Now - startTime; metrics.averageDownloadSpeed = CalculateDownloadSpeed(testAssetPath, duration); metrics.successRate = 1.0f ; metrics.cacheHitRate = 0.8f ; metrics.concurrentDownloads = 4 ; metrics.latency = 50 ; Addressables.Release(handle); Debug.Log($"CDN性能测试完成: {cdnUrl} , 速度: {metrics.averageDownloadSpeed:F2} MB/s" ); } catch (System.Exception e) { Debug.LogError($"CDN性能测试失败: {e.Message} " ); metrics.successRate = 0.0f ; } cdnPerformance[cdnUrl] = metrics; return metrics; } private float CalculateDownloadSpeed (string assetPath, System.TimeSpan duration ) { long assetSize = 10 * 1024 * 1024 ; float speed = (assetSize / 1024f / 1024f ) / (float )duration.TotalSeconds; return speed; } public string SelectBestCDN (List<string > availableCDNs ) { string bestCDN = "" ; float bestScore = -1 ; foreach (string cdn in availableCDNs) { if (cdnPerformance.TryGetValue(cdn, out PerformanceMetrics metrics)) { float score = (metrics.averageDownloadSpeed * 0.5f ) + (metrics.successRate * 0.3f ) + (metrics.cacheHitRate * 0.2f ); if (score > bestScore) { bestScore = score; bestCDN = cdn; } } } return bestCDN; } public class AutoCDNSwitcher : MonoBehaviour { public List<string > availableCDNs = new List<string >(); public float performanceCheckInterval = 300f ; private string currentCDN; void Start () { StartCoroutine(MonitorAndSwitchCDN()); } System.Collections.IEnumerator MonitorAndSwitchCDN () { while (true ) { yield return new WaitForSeconds (performanceCheckInterval ) ; var optimizer = new CDNPerformanceOptimizer(); foreach (string cdn in availableCDNs) { yield return optimizer.TestCDNPerformance(cdn, "Assets/TestAsset.asset" ); } string bestCDN = optimizer.SelectBestCDN(availableCDNs); if (!string .IsNullOrEmpty(bestCDN) && bestCDN != currentCDN) { SwitchToCDN(bestCDN); } } } void SwitchToCDN (string newCDN ) { Debug.Log($"切换到CDN: {newCDN} " ); currentCDN = newCDN; UpdateAddressablesCDN(newCDN); } void UpdateAddressablesCDN (string cdnUrl ) { Debug.Log($"更新Addressables使用CDN: {cdnUrl} " ); } } }
实战案例:实现完整的资源热更新系统 热更新系统架构 完整的热更新管理器 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AddressableAssets;using UnityEngine.ResourceManagement.AsyncOperations;public class CompleteHotUpdateSystem : MonoBehaviour { [System.Serializable ] public class UpdateConfig { public string [] updateGroupNames; public bool checkForUpdateOnStart; public float updateCheckInterval; public int maxRetryAttempts; public float retryDelay; public string updateServerUrl; public bool enableAutoUpdate; } [Header("更新配置" ) ] public UpdateConfig updateConfig; [Header("回调事件" ) ] public System.Action<float > onUpdateProgress; public System.Action<bool , string > onUpdateComplete; public System.Action<string > onUpdateError; private bool isUpdating = false ; private float totalUpdateSize = 0 ; private float downloadedSize = 0 ; private VersionControlManager versionManager = new VersionControlManager(); void Start () { if (updateConfig.checkForUpdateOnStart) { StartCoroutine(CheckForUpdates()); } } public IEnumerator CheckForUpdates () { Debug.Log("开始检查更新..." ); try { var updateHandle = Addressables.CheckForCatalogUpdates(); var updateList = await updateHandle.Task; if (updateList != null && updateList.Count > 0 ) { Debug.Log($"发现 {updateList.Count} 个更新" ); if (updateConfig.enableAutoUpdate) { StartCoroutine(PerformUpdate(updateList)); } else { onUpdateComplete?.Invoke(true , "发现可用更新" ); } } else { Debug.Log("没有发现更新" ); onUpdateComplete?.Invoke(false , "当前已是最新版本" ); } Addressables.Release(updateHandle); } catch (Exception e) { string errorMsg = $"检查更新失败: {e.Message} " ; Debug.LogError(errorMsg); onUpdateError?.Invoke(errorMsg); } yield return null ; } public IEnumerator PerformUpdate (IList<string > updateList = null ) { if (isUpdating) { Debug.LogWarning("更新已在进行中" ); yield break ; } isUpdating = true ; Debug.Log("开始执行更新..." ); try { if (updateList == null || updateList.Count == 0 ) { var checkHandle = Addressables.CheckForCatalogUpdates(); updateList = await checkHandle.Task; Addressables.Release(checkHandle); if (updateList == null || updateList.Count == 0 ) { Debug.Log("没有需要更新的内容" ); isUpdating = false ; onUpdateComplete?.Invoke(true , "没有需要更新的内容" ); yield break ; } } totalUpdateSize = await CalculateUpdateSize(updateList); downloadedSize = 0 ; var updateHandle = Addressables.UpdateCatalogs(updateList, true ); updateHandle.PercentCompleteChanged += (handle) => { float progress = handle.PercentComplete; onUpdateProgress?.Invoke(progress); Debug.Log($"更新进度: {(progress * 100 ):F1} %" ); }; var updatedCatalog = await updateHandle.Task; if (updatedCatalog != null ) { Debug.Log("更新完成" ); var updatedGroups = new List<string >(); foreach (string catalog in updateList) { updatedGroups.Add(catalog); } versionManager.RecordVersionUpdate( System.DateTime.Now.ToString("yyyy.MM.dd" ), updatedGroups.ToArray(), new string [0 ], "自动更新" ); onUpdateComplete?.Invoke(true , "更新成功完成" ); } else { string errorMsg = "更新失败:无法获取更新目录" ; Debug.LogError(errorMsg); onUpdateError?.Invoke(errorMsg); } Addressables.Release(updateHandle); } catch (Exception e) { string errorMsg = $"更新过程中发生错误: {e.Message} " ; Debug.LogError(errorMsg); onUpdateError?.Invoke(errorMsg); } finally { isUpdating = false ; } } private async System.Threading.Tasks.Task<float > CalculateUpdateSize (IList<string > updateList ) { float totalSize = 0 ; foreach (string catalog in updateList) { totalSize += 10 * 1024 * 1024 ; } return totalSize; } public IEnumerator ForceUpdate () { Debug.Log("强制执行更新..." ); var updateHandle = Addressables.UpdateCatalogs(null , true ); var updatedCatalog = await updateHandle.Task; if (updatedCatalog != null ) { Debug.Log("强制更新完成" ); onUpdateComplete?.Invoke(true , "强制更新成功" ); } else { Debug.LogError("强制更新失败" ); onUpdateError?.Invoke("强制更新失败" ); } Addressables.Release(updateHandle); yield return null ; } public void ClearCache () { bool success = Addressables.ClearCache(true ); if (success) { Debug.Log("缓存清理成功" ); } else { Debug.LogError("缓存清理失败" ); } } public string GetCurrentVersion () { return versionManager.GetCurrentVersion(); } public List<VersionControlManager.VersionRecord> GetVersionHistory() { var history = versionManager.GetVersionHistory(); return new List<VersionControlManager.VersionRecord>(history); } void OnDestroy () { Addressables.ReleaseAll(); } }
热更新系统的使用示例 热更新系统使用示例 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 using UnityEngine;using UnityEngine.UI;public class HotUpdateSystemExample : MonoBehaviour { public CompleteHotUpdateSystem hotUpdateSystem; public Slider updateProgressSlider; public Text updateStatusText; public Button checkUpdateButton; public Button updateButton; public Button forceUpdateButton; void Start () { if (hotUpdateSystem == null ) { hotUpdateSystem = FindObjectOfType<CompleteHotUpdateSystem>(); } if (hotUpdateSystem != null ) { hotUpdateSystem.onUpdateProgress += OnUpdateProgress; hotUpdateSystem.onUpdateComplete += OnUpdateComplete; hotUpdateSystem.onUpdateError += OnUpdateError; } SetupUI(); } void SetupUI () { if (checkUpdateButton != null ) { checkUpdateButton.onClick.AddListener(CheckForUpdates); } if (updateButton != null ) { updateButton.onClick.AddListener(StartUpdate); } if (forceUpdateButton != null ) { forceUpdateButton.onClick.AddListener(ForceUpdate); } } public void CheckForUpdates () { if (hotUpdateSystem != null ) { StartCoroutine(hotUpdateSystem.CheckForUpdates()); } } public void StartUpdate () { if (hotUpdateSystem != null ) { StartCoroutine(hotUpdateSystem.PerformUpdate()); } } public void ForceUpdate () { if (hotUpdateSystem != null ) { StartCoroutine(hotUpdateSystem.ForceUpdate()); } } void OnUpdateProgress (float progress ) { if (updateProgressSlider != null ) { updateProgressSlider.value = progress; } if (updateStatusText != null ) { updateStatusText.text = $"更新进度: {(progress * 100 ):F1} %" ; } } void OnUpdateComplete (bool success, string message ) { if (updateStatusText != null ) { updateStatusText.text = success ? "更新成功!" : message; } Debug.Log($"更新完成: {message} " ); } void OnUpdateError (string error ) { if (updateStatusText != null ) { updateStatusText.text = $"更新错误: {error} " ; } Debug.LogError(error); } void OnDestroy () { if (hotUpdateSystem != null ) { hotUpdateSystem.onUpdateProgress -= OnUpdateProgress; hotUpdateSystem.onUpdateComplete -= OnUpdateComplete; hotUpdateSystem.onUpdateError -= OnUpdateError; } } }
通过本章的学习,您已经全面掌握了Addressables远程资源管理和热更新的完整流程,包括本地与远程资源配置、服务器设置、内容更新构建、版本管理与回滚机制以及CDN集成方案。下一章我们将探讨场景管理的相关内容。