第5章 远程资源与热更新

第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)
{
// 本地资源通常使用LZ4压缩以平衡大小和加载速度
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)
{
// 远程资源可能使用LZMA以减少传输量
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; // "LZ4", "LZMA", "Uncompressed"
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; // "CDN", "Custom", "Cloud"
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>();

// CDN方案
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, // 100MB
supportsRangeRequests = true,
supportsETags = true,
cacheControlHeader = "public, max-age=31536000" // 1年缓存
});

// 自建服务器方案
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, // 50MB
supportsRangeRequests = true,
supportsETags = true,
cacheControlHeader = "public, max-age=604800" // 7天缓存
});

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; // 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}");

// 这里应该实现实际的文件上传逻辑
// 可以使用HTTP PUT、FTP、AWS S3 SDK等
// 示例使用UnityWebRequest:
/*
var request = new UnityEngine.Networking.UnityWebRequest(serverUrl, "PUT");
byte[] fileData = File.ReadAllBytes(localFilePath);
request.uploadHandler = new UnityEngine.Networking.UploadHandlerRaw(fileData);
request.SetRequestHeader("Authorization", config.authHeader);
request.SetRequestHeader("Content-Type", "application/octet-stream");

var operation = request.SendWebRequest();
// 等待上传完成...
*/
}
}

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; // 假设每个资源1MB
}
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; // 假设每个1MB
if (removedAssets != null) size += removedAssets.Length * 512 * 1024; // 假设每个0.5MB
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;
}

// 这里应该实现实际的回滚逻辑
// 1. 下载指定版本的资源
// 2. 替换当前资源
// 3. 更新本地配置

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();
}

/// <summary>
/// 执行安全更新,支持自动回滚
/// </summary>
public IEnumerator PerformSafeUpdate(string newVersion, System.Action<bool> onComplete)
{
string currentVersion = versionManager.GetCurrentVersion();
Debug.Log($"开始安全更新到版本: {newVersion}, 当前版本: {currentVersion}");

// 1. 预检查
if (!PreUpdateCheck())
{
Debug.LogError("更新预检查失败");
onComplete?.Invoke(false);
yield break;
}

// 2. 备份当前状态
if (!BackupCurrentState())
{
Debug.LogError("备份当前状态失败");
onComplete?.Invoke(false);
yield break;
}

try
{
// 3. 执行更新
bool updateSuccess = yield StartCoroutine(ExecuteUpdate(newVersion));

if (!updateSuccess)
{
Debug.LogError($"更新到版本 {newVersion} 失败,尝试回滚");
bool rollbackSuccess = RollbackToBackup();
onComplete?.Invoke(rollbackSuccess);
yield break;
}

// 4. 验证更新
if (!PostUpdateValidation())
{
Debug.LogError($"更新后验证失败,回滚到备份版本");
bool rollbackSuccess = RollbackToBackup();
onComplete?.Invoke(rollbackSuccess);
yield break;
}

// 5. 确认更新
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; // 假设需要100MB
long availableSpace = GetAvailableDiskSpace();

if (availableSpace < requiredSpace)
{
Debug.LogError($"磁盘空间不足,需要: {requiredSpace}, 可用: {availableSpace}");
return false;
}

// 检查网络连接
if (!IsNetworkAvailable())
{
Debug.LogError("网络连接不可用");
return false;
}

return true;
}

private bool BackupCurrentState()
{
// 实现备份逻辑
// 可能包括:
// - 备份当前的Catalog文件
// - 记录当前资源状态
// - 备份关键配置

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}");

// 实现回滚逻辑
// 1. 恢复备份的Catalog
// 2. 清理新版本的资源
// 3. 重新加载正确的资源

// 模拟回滚过程
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; // 1GB
}

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()
{
// CloudFlare配置
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" }
};

// AWS CloudFront配置
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" }
};

// Azure CDN配置
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)
{
// 这里应该应用CDN配置到Addressables设置
Debug.Log($"应用CDN配置: {config.provider}, URL: {config.baseUrl}");

// 可能需要更新Addressables的Profile设置
UpdateAddressablesProfile(config);
}

private void UpdateAddressablesProfile(CDNConfig config)
{
// 更新Addressables的Profile以使用CDN URL
// 这通常涉及编辑AddressableAssetSettings中的Profile
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; // MB/s
public float successRate; // 0-1
public float cacheHitRate; // 0-1
public int concurrentDownloads;
public float latency; // ms
}

private Dictionary<string, PerformanceMetrics> cdnPerformance = new Dictionary<string, PerformanceMetrics>();

/// <summary>
/// 测试CDN性能
/// </summary>
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; // 假设80%缓存命中率
metrics.concurrentDownloads = 4; // 默认并发数
metrics.latency = 50; // 假设50ms延迟

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)
{
// 简化计算:假设资源大小为10MB
long assetSize = 10 * 1024 * 1024; // 10MB
float speed = (assetSize / 1024f / 1024f) / (float)duration.TotalSeconds; // MB/s
return speed;
}

/// <summary>
/// 选择最佳CDN
/// </summary>
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;
}

/// <summary>
/// 自动CDN切换
/// </summary>
public class AutoCDNSwitcher : MonoBehaviour
{
public List<string> availableCDNs = new List<string>();
public float performanceCheckInterval = 300f; // 5分钟
private string currentCDN;

void Start()
{
StartCoroutine(MonitorAndSwitchCDN());
}

System.Collections.IEnumerator MonitorAndSwitchCDN()
{
while (true)
{
yield return new WaitForSeconds(performanceCheckInterval);

// 测试所有CDN的性能
var optimizer = new CDNPerformanceOptimizer();
foreach (string cdn in availableCDNs)
{
yield return optimizer.TestCDNPerformance(cdn, "Assets/TestAsset.asset");
}

// 选择最佳CDN
string bestCDN = optimizer.SelectBestCDN(availableCDNs);
if (!string.IsNullOrEmpty(bestCDN) && bestCDN != currentCDN)
{
SwitchToCDN(bestCDN);
}
}
}

void SwitchToCDN(string newCDN)
{
Debug.Log($"切换到CDN: {newCDN}");
currentCDN = newCDN;

// 更新Addressables配置
UpdateAddressablesCDN(newCDN);
}

void UpdateAddressablesCDN(string cdnUrl)
{
// 实现CDN URL的动态更新
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; // 更新进度 (0-1)
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());
}
}

/// <summary>
/// 检查是否有可用更新
/// </summary>
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
{
// 通知UI有更新可用
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;
}

/// <summary>
/// 执行更新
/// </summary>
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;
}
}

/// <summary>
/// 计算更新大小
/// </summary>
private async System.Threading.Tasks.Task<float> CalculateUpdateSize(IList<string> updateList)
{
// 简化实现:返回估算大小
// 在实际应用中,这里应该从服务器获取准确的文件大小
float totalSize = 0;
foreach (string catalog in updateList)
{
totalSize += 10 * 1024 * 1024; // 假设每个更新包10MB
}
return totalSize;
}

/// <summary>
/// 强制更新(忽略版本检查)
/// </summary>
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;
}

/// <summary>
/// 清理缓存
/// </summary>
public void ClearCache()
{
bool success = Addressables.ClearCache(true);
if (success)
{
Debug.Log("缓存清理成功");
}
else
{
Debug.LogError("缓存清理失败");
}
}

/// <summary>
/// 获取当前版本信息
/// </summary>
public string GetCurrentVersion()
{
return versionManager.GetCurrentVersion();
}

/// <summary>
/// 获取版本历史
/// </summary>
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集成方案。下一章我们将探讨场景管理的相关内容。