资产全生命周期:从创作数据到可见结果
自动关联目录:资产全生命周期:从创作数据到可见结果
资产系统最容易被误解成“根据路径把文件加载进内存”。但在支持创作、远端内容和多运行目标的客户端里,真正的问题是:
怎样让一个逻辑资产经过编辑、依赖收集、构建、交付、缓存、解码和实体化后,在正确的生命周期内以正确的质量可见;任一步失败时,又不破坏已有的可用状态。
因此,资产方案的终点不是 DownloadCompleted 或 Loaded,而是 Visible Ready:玩家需要的最小完整状态已经呈现,交互、碰撞和表现之间没有不可解释的空窗。
业务问题不是“把文件下载下来”
一条资产链路同时服务五类需求:
| 需求 | 用户真正关心的结果 | 系统必须处理的约束 |
|---|---|---|
| 创作者编辑 | 引用稳定、预览可信、改动可撤销 | 源数据可读、引用可检查、缺失可定位 |
| 发布构建 | 相同输入得到可验证结果 | 冻结快照、确定性、依赖闭包、原子提交 |
| 玩家进入内容 | 尽快达到可玩和可见 | 优先级、占位、缓存、异步、首帧提交 |
| 多平台运行 | 功能一致,质量和成本可降级 | 平台格式、质量档、可选数据、兼容 Reader |
| 故障恢复 | 重试不能让状态更差 | 分类错误、临时文件、校验、回滚、旧版本保留 |
如果只关注文件下载,下面这些状态会全部被压成一个 bool loaded:
- 找到了逻辑资产,但没有适合当前平台的制品。
- 主文件已在缓存中,但依赖缺失。
- 字节已经下载,但完整性校验失败。
- 解码完成,但主线程尚未提交运行时对象。
- 运行时对象已经创建,但材质或纹理还没达到最低可见要求。
- 请求完成时,原来的世界、角色或 UI 已被销毁。
loaded 无法表达这些差异,也就无法决定重试、降级、告警和回收。
先分清七种“资产”
同一个“资产”在生命周期中至少有七种表示:
flowchart LR
A[可编辑源数据] --> B[逻辑资产身份]
B --> C[依赖图与构建输入]
C --> D[目标平台制品]
D --> E[交付单元]
E --> F[本地缓存条目]
F --> G[内存中的已准备资源]
G --> H[场景中的可见表现]| 表示 | 为谁优化 | 是否适合作为长期真相 |
|---|---|---|
| 可编辑源数据 | 创作者、版本比较、迁移 | 是,但不能直接运行 |
| 逻辑资产身份 | 内容引用、脚本、网络消息 | 是,应与物理路径解耦 |
| 构建输入快照 | 可重复构建 | 只在一次构建中冻结 |
| 目标平台制品 | 特定 Reader 与设备能力 | 不可变,但有版本和目标范围 |
| 交付单元 | 下载、安装、校验和淘汰 | 不应成为编辑身份 |
| 缓存条目 | 本地复用和恢复 | 是派生数据,可删除重建 |
| 运行时对象 | 模拟或呈现 | 生命周期短,不可写回源数据 |
很多架构问题都来自混用:用编辑器路径充当网络身份、用缓存目录判断资产版本、把运行时对象状态写回发布数据,或者让发布制品反向承担编辑功能。
架构演进
阶段 A:路径与包驱动
早期最自然的方案是:资产跟随工程目录,用路径引用;构建时按目录、表格或业务包分组;进入某个场景或功能时下载并挂载对应包。
这个方案在以下条件下是合理的:
- 内容主要由内部生产,路径和命名受控。
- 目标平台少,资产格式相对统一。
- 大部分依赖在构建期可静态发现。
- 内容和客户端版本共同发布。
- 业务包数量可人工维护。
它的优势也很明确:编辑器原生支持好,引用直观,Cook 和打包工具成熟,出问题时可以直接查目录和包。
但这里存在两个不同的依赖系统:
静态依赖:资产 A 的材质、纹理、骨架等引用链
业务依赖:进入某功能时,代码主动要求下载的一组内容静态依赖分析无法发现所有业务触发;业务分包表又不一定知道真实的资产引用闭包。二者长期并存后,容易出现“包配置看起来完整,运行时仍缺资源”或“为了保险把大量不需要的内容一起下载”。
规模拐点:路径不再是身份
当内容开始独立生产、远端获取并跨多个运行目标复用时,原假设逐步失效:
- 同一个逻辑资产在不同平台对应不同编码和可选数据。
- 资产可以改名、迁移或由工具生成,路径不再稳定。
- 玩家内容引用的资产不一定已经存在于客户端工程中。
- DS 需要碰撞或规则数据,但不需要纹理、材质和渲染网格。
- 一个资产的依赖可能继续引用远端资产,构建期无法总是展开完整闭包。
- 旧内容、旧缓存、新制品和新 Reader 必须同时兼容。
- 下载、解码和实例化已经成为并行流水线,生命周期竞态成为常态。
此时常见症状不是某一个 API 慢,而是身份和状态失控:
| 症状 | 更深层原因 |
|---|---|
| 改路径后历史内容失效 | 路径被当成长期身份 |
| 同一资产重复下载或重复解码 | 请求没有按逻辑身份与目标制品去重 |
| 切换下载后端后读不到缓存 | 写入路径、扫描路径和命名规则不是同一合同 |
| 下载成功仍显示占位 | Downloaded 被误当成 VisibleReady |
| 切换世界后旧资源覆盖新对象 | 异步请求没有所有者 Generation |
| 某平台正常、另一平台损坏 | 制品身份未包含目标和格式能力 |
| 构建失败留下半个发布包 | 输出直接写共享目录,没有事务提交 |
| 缺一个次要资源导致整个内容不可用 | 没有区分必需依赖和可降级依赖 |
平台化阶段:身份与制品分离
下一阶段通常引入稳定的逻辑资产 ID,再通过 Resolver 选择物理制品:
AssetRef
-> Resolve(target, quality, readerVersion)
-> ArtifactDescriptor
-> Acquire
-> Verify
-> Decode
-> Materialize这解决了路径漂移和跨平台选择,但也引入了新的状态:
- 逻辑资产存在,不代表当前目标制品存在。
- Resolver 成功,不代表下载成功。
- 下载成功,不代表 hash、大小或签名正确。
- 字节正确,不代表 Reader 支持格式。
- Reader 支持,不代表依赖齐全。
- 对象已准备,不代表请求所有者仍有效。
如果只是把路径替换成数字 ID,却没有补齐错误分类、状态机、缓存键和生命周期,复杂度只会从路径层转移到回调层。
制品化阶段:把发布当成编译
进一步的演进是把发布看成编译系统:
- 冻结本次发布输入,而不是让构建过程读取不断变化的编辑目录。
- 用可版本化 Processor 把输入转换为不同目标的制品。
- 每个 Processor 在私有事务目录中输出,成功后再合并。
- 生成覆盖精确文件集、大小、hash、来源摘要和处理器指纹的 Manifest。
- 只有完整验证通过后,才原子切换“当前可用版本”。
这能把构建、安装和回滚变成明确合同,但代价是需要维护格式版本、处理器兼容、缓存命名空间、确定性测试和迁移策略。
目标模型:四个稳定身份
一个可演进的资产系统至少要区分四种 identity:
struct AssetRef {
string logicalId; // 稳定逻辑身份;跨路径、跨会话
AssetKind kind; // Mesh / Texture / Material / Audio / ...
};
struct SourceRevision {
AssetRef asset;
Digest sourceDigest; // 可编辑输入的内容摘要
SchemaVersion schema;
};
struct ArtifactKey {
SourceRevision source;
TargetProfile target; // 平台、质量档、Reader 能力
Digest processor; // 工具、配置和规则的指纹
};
struct RuntimeHandle {
ArtifactKey artifact;
Generation owner;
RuntimeObjectId object;
};四者不能互相替代:
AssetRef用于内容引用和网络语义。SourceRevision判断源内容是否变化。ArtifactKey判断已构建结果能否复用。RuntimeHandle只对当前世界和对象生命周期有效。
如果逻辑 ID 可能超过某些文本运行时的安全整数范围,JSON 边界应使用十进制字符串或受约束的二进制整数,而不是经过浮点数中转。
目标架构
flowchart TB
subgraph Authoring[创作域]
S[可编辑源数据]
R[类型化资产引用]
V[编辑期校验]
end
subgraph Build[构建域]
F[冻结快照]
P[可版本化处理器]
M[制品清单]
end
subgraph Delivery[交付域]
O[解析与目标选择]
Q[请求去重与调度]
C[下载缓存与原子安装]
end
subgraph Runtime[运行域]
D[有界解码]
L[依赖就绪]
X[选择性实体化]
Y[可见就绪]
end
S --> V --> F
R --> F --> P --> M
M --> O --> Q --> C
C --> D --> L --> X --> Y关键边界如下:
| 边界 | 输入 | 输出 | 必须守住的合同 |
|---|---|---|---|
| 创作 -> 构建 | 可编辑数据、引用、配置 | 冻结快照 | 输入不可漂移、引用可诊断 |
| 构建 -> 交付 | 目标制品 | Manifest + 文件集 | 确定性、路径安全、完整性 |
| 交付 -> 运行 | 已安装制品 | 受验证字节 | 原子切换、旧版本可回退 |
| 运行 -> 呈现 | 已准备资源 | 可见对象 | Generation、主线程预算、最低完整状态 |
构建:确定性比“成功一次”更重要
冻结输入
构建不能一边读取编辑目录,一边允许保存继续改变文件。需要先冻结 inventory:
Snapshot Freeze(ProjectState state)
{
Snapshot out;
for (File file : EnumerateDeclaredInputs(state)) {
ValidateRelativePath(file.path);
Bytes bytes = ReadAndRecheck(file);
out.Add(file.logicalPath, bytes, Hash(bytes), file.role);
}
out.sourceDigest = Hash(CanonicalProjection(out.files));
return out;
}这里的关键不是复制文件,而是:输入集合、路径、字节和顺序都有稳定定义;构建期间源数据变化不能悄悄混入同一制品。
Processor 与 Core 分责
通用 Builder Core 应负责:
- 作业隔离、路径边界和资源上限。
- Processor 依赖图和稳定执行顺序。
- 私有事务输出、冲突检测和原子合并。
- 文件大小、hash、来源摘要和最终制品摘要。
- 精确文件集验证和确定性打包。
具体资产类型的 Processor 负责:
- 理解业务 schema。
- 将源格式转换为目标格式。
- 声明必需、可选或可降级输出。
- 产生类型相关诊断。
如果 Core 持续增加“某类角色字段”“某种材质文件”“某个特效目录”的判断,它就会成为所有业务变化的汇合点,工具版本也会被迫与内容 schema 同步发布。
事务化输出
Artifact Build(Snapshot input, ProcessorPack pack)
{
Plan plan = ResolveProcessorGraph(input, pack);
StagingArea global;
for (Layer layer : plan.topologicalLayers) {
parallel_for (Processor processor : layer.processors) {
Transaction tx = global.BeginPrivate(processor.id);
Result result = processor.Run(input, tx.outputSink);
if (!result.ok)
tx.Rollback();
else
tx.Seal();
}
global.MergeSealedInStableOrder();
}
Manifest manifest = BuildManifest(global.files, input, pack);
VerifyExactFileSet(global, manifest);
return AtomicCommit(global, manifest);
}失败不能留下可被误认成成功的部分制品。确定性也不只意味着 ZIP 字节相同,还包括文件顺序、字段编码、诊断码和 artifact digest 的投影稳定。
Manifest:运行时的信任入口
Manifest 至少应描述:
struct ArtifactDescriptor {
AssetRef asset;
FormatVersion format;
TargetProfile target;
uint64 byteSize;
Digest contentHash;
vector<Dependency> dependencies;
Optional<Digest> signature;
};
struct Dependency {
AssetRef asset;
DependencyPolicy policy; // Required / Degradable / Optional
};需要区分三种安全能力:
| 机制 | 解决的问题 | 不能解决的问题 |
|---|---|---|
| Hash | 字节是否与期望一致 | 期望 hash 本身是否可信 |
| 签名或受信摘要 | Manifest/制品是否来自受信发布者 | 客户端运行环境是否可信 |
| 加密 | 降低静态提取和传输暴露 | 阻止拥有密钥和明文的客户端被逆向 |
客户端持有的解密能力不能成为权威信任根。安全重点应放在:受信 Manifest、完整性、路径隔离、有界解析、格式兼容和服务端权威,而不是假设“文件加密后客户端就可信”。
获取与下载:请求是一段生命周期
状态机
stateDiagram-v2
[*] --> Resolving
Resolving --> Queued: descriptor selected
Queued --> Acquiring: scheduler grants budget
Acquiring --> Verifying: bytes complete
Verifying --> Installed: integrity valid
Installed --> Decoding
Decoding --> Prepared
Prepared --> Materialized
Materialized --> VisibleReady
Resolving --> Failed
Acquiring --> Cancelled
Acquiring --> Failed
Verifying --> Corrupted
Decoding --> Unsupported
Materialized --> Superseded每个终态都应该有结构化原因,不能只返回字符串。
enum class AssetFailure {
NotFound, // 权威确认不存在
ResolveUnavailable, // 暂时无法确认位置
NetworkFailure,
Cancelled,
IntegrityFailure,
UnsupportedFormat,
DependencyFailure,
BudgetRejected,
StaleGeneration
};只有 NotFound 才能驱动“确认缺失”的产品行为。超时、空响应、普通网络错误和格式错误都不能偷偷降级成不存在,否则会掩盖基础设施或兼容故障。
单飞、取消和后端快照
相同 ArtifactKey 的并发请求应共享传输和解码,但每个调用者保留自己的生命周期:
RequestHandle Request(AssetRef ref, RequestContext context)
{
ArtifactKey key = resolver.Resolve(ref, context.target);
SharedJob job = jobs.GetOrCreate(key);
Subscriber sub = job.Subscribe(context.ownerGeneration,
context.priority);
sub.OnPrepared([context](PreparedAsset value) {
if (!context.owner.IsCurrent(context.ownerGeneration))
return;
context.commitQueue.Enqueue(value);
});
return sub.CancellableHandle();
}如果运行时支持切换下载源或实验开关,请求创建时必须把 Resolver/Backend 选择快照进 Job。飞行中的任务不能一半按旧规则写、一半按新规则读。
临时文件与原子安装
推荐的本地流程:
<key>.partial
-> 分块写入 / 可选续传
-> 长度与 hash 校验
-> Manifest 与目标能力校验
-> 原子 rename 为不可变缓存文件
-> 原子更新 current 指针取消或失败只删除 .partial;旧的 current 保持可用。直接覆盖当前文件会让崩溃、磁盘满或校验失败破坏原本可恢复的状态。
上传与下载优化的顺序
优化前先判断瓶颈在哪一层:
| 瓶颈 | 优先手段 | 常见误区 |
|---|---|---|
| 重复字节 | 内容寻址、hash 去重、跨引用单飞 | 只压缩,不解决重复传输 |
| 大文件失败重来 | 分块、断点续传、临时文件 | 把未验证分块暴露为可用缓存 |
| 小文件过多 | 合理聚合、请求批处理、Manifest 预取 | 聚成巨包后失去按需加载和局部更新 |
| 首屏等待 | 优先级、关键依赖预取、最低可见集合 | 把所有内容都标成最高优先级 |
| 主线程卡顿 | 后台校验/解码、主线程分帧提交 | 只优化网络吞吐 |
| 跨平台浪费 | Resolver 只选择目标制品 | 下载通用源数据后在低端设备现场转换 |
上传侧同样应该围绕不可变制品和内容摘要设计。客户端可以负责冻结、分块、续传和校验,但最终发布身份应由受信服务确认;本地“上传完成”不等于发布版本已经可解析和可分发。
依赖:不要在“全打包”和“全运行时解析”之间二选一
资产依赖有不同稳定性:
| 依赖类型 | 更适合何时解析 | 原因 |
|---|---|---|
| 格式内部必需数据 | 构建期放入自包含制品 | 缺失时 Reader 无法正确工作 |
| 稳定直接引用 | 构建期记录 Manifest | 可提前校验、预取和诊断 |
| 远端资产的远端依赖 | 运行时按统一 Resolver 展开 | 发布期未必拥有完整内容 |
| 脚本条件分支 | 运行时请求,必要时由提示清单预热 | 静态分析无法保证完整 |
| DS 碰撞/规则数据 | 构建期独立 headless 制品 | 不应依赖视觉网格加载 |
目标不是构建一个理论上的“全依赖闭包”,而是给每类依赖明确责任:
- 必需依赖缺失:阻止该制品进入可用态。
- 可降级依赖缺失:保留引用和诊断,以占位或简化表现继续。
- 可选依赖缺失:不影响最低完整状态。
- 未知依赖类型:不能猜,应产生 coverage warning 并走运行时回退。
循环依赖要在构建期或解析阶段显式拒绝,不能靠加载超时发现。
缓存与驻留是两个问题
磁盘缓存回答“字节是否需要重新获取”,内存驻留回答“资源是否值得继续占用运行预算”。二者应使用不同状态和淘汰策略。
磁盘缓存键
至少包含:逻辑资产 revision、目标平台、格式版本、质量档和处理器指纹。只用逻辑 ID 会让不同平台或新旧工具产物互相污染。
内存驻留
驻留策略需要区分:
- 正在可见或即将可见。
- 被多个对象共享。
- 角色出生、交互或 UI 首屏等关键用途。
- 后台预热。
- 已无订阅但仍可短期复用。
ResidencyScore Score(const AssetUsage& usage)
{
return usage.visibilityWeight
+ usage.gameplayCriticalWeight
+ usage.sharedReferenceWeight
+ usage.predictedNeedWeight
- usage.memoryCostWeight
- usage.idleAgeWeight;
}公式只表达决策维度,权重必须通过目标设备和代表性内容验证,不能把示意参数当成历史预算。
优先级传播
一个近处或高重要性对象请求网格时,它的直接材质和最低 mip 应继承同一需求优先级;共享依赖取所有订阅者中的最高有效优先级。否则主资产先到、关键依赖长期排在后台,用户仍看不到完整结果。
DS 与客户端不应加载同一份东西
“同一内容”不代表“同一制品”。可以共享逻辑身份和功能不变量,但目标产物不同:
| 目标 | 需要 | 通常不需要 |
|---|---|---|
| 编辑器 | 源元数据、预览资源、诊断、可逆信息 | 只为运行压缩的不可读表示作为唯一来源 |
| 游戏客户端 | 渲染、音频、动画、必要碰撞和运行数据 | 编辑器事务和迁移中间数据 |
| DS | 权威规则、碰撞、导航或查询数据 | 纹理、材质、渲染网格、客户端音频 |
| 轻量目标 | 降级网格/纹理/音频和受限格式 | 不支持的高质量可选数据 |
如果 DS 为了获得碰撞而必须加载完整视觉网格,说明构建产物仍然按源资产组织,而不是按运行目标组织。
缺失资产:阻断还是降级
这是业务与技术必须共同决定的问题。一个通用决策表如下:
| 情况 | 建议行为 | 理由 |
|---|---|---|
| Manifest 损坏或被篡改 | 阻断并保留旧版本 | 无法建立制品可信边界 |
| 必需运行数据缺失 | 阻断相关对象或发布 | 正确性无法成立 |
| 权威确认次要视觉资产不存在 | 占位、记录并允许继续 | 可玩性可能仍成立 |
| 网络暂时失败 | 重试或使用旧缓存 | 不能误判永久缺失 |
| Reader 不支持格式 | 选择兼容制品或明确失败 | 重试同一字节没有意义 |
| 请求所有者已换代 | 静默拒绝晚到结果 | 不是内容故障,无需污染日志 |
“允许缺失”不等于吞掉错误。需要保留真实缺失身份、受影响父资产、依赖边和本次结果;下一次内容修复后必须清除过期警告。
可见就绪:重新定义加载性能
资产链路的端到端时间可以写成:
T_visible = T_resolve
+ T_queue
+ T_acquire
+ T_verify
+ T_decode
+ T_dependency_wait
+ T_materialize
+ T_first_present真实流水线可能并行,所以总时间不一定是简单相加;但每段都必须有独立埋点,才能知道优化网络、解码还是主线程提交。
建议指标:
| 指标 | 回答的问题 |
|---|---|
| Resolver 成功率与失败分类 | 找不到制品还是服务暂不可用 |
| 内存/磁盘缓存命中率 | 重复获取和重复准备是否被消除 |
| 单飞合并率 | 多个引用是否共享同一作业 |
| 下载有效字节率 | 取消、过期和未使用字节占多少 |
| 校验与解码吞吐 | CPU 是否成为瓶颈 |
| 主线程提交峰值 | 后台完成后是否仍造成卡顿 |
| 请求到 Prepared / VisibleReady | 技术完成与用户可见相差多少 |
| 峰值内存 | 下载缓冲、解码中间态和运行资源是否叠加 |
| 依赖扇出与关键路径深度 | 哪些资产拖长最低完整集合 |
| 淘汰后短期重载率 | 驻留策略是否过于激进 |
每个指标都要按目标平台、冷热缓存、资产类型、质量档、优先级和失败类型切分;一个全局平均值几乎不可行动。
高频失败模式
| 失败模式 | 表面修复 | 根因修复 |
|---|---|---|
| 下载写一个目录,启动扫描另一个目录 | 增加一次复制 | 由 ArtifactKey 统一命名和读写合同 |
| 后端开关切换后飞行任务混用规则 | 重启客户端 | 请求创建时冻结 Resolver 与 Backend |
| 回调访问已销毁对象 | 增加空指针判断 | 所有者 Generation + 可取消订阅 |
| 同资源被重复解码 | 扩大线程池 | 按 ArtifactKey 单飞并共享 Prepared 结果 |
| 构建结果偶发不同 | 构建后再排序 ZIP | 从 inventory、处理顺序、编码到 Manifest 全链路确定性 |
| 旧可选文件残留进新制品 | 发布前清空目录 | 每次独立事务目录 + 精确文件集校验 |
| 缺失依赖只报父资产 | 日志打印更多路径 | 错误跨层保留真实子资产 identity 与依赖边 |
| DS 等视觉资源后才有碰撞 | 提前预加载网格 | 构建独立 headless 碰撞制品 |
| 全部最高优先级 | 增加带宽 | 建立关键集合、距离/重要性和公平调度 |
| 加密后认为资产可信 | 隐藏密钥 | 受信 Manifest、完整性、有界 Reader 和权威边界 |
怎样选择交付粒度
| 方案 | 优点 | 代价 | 适用条件 |
|---|---|---|---|
| 大包 | 请求少、顺序 IO 友好 | 更新和按需粒度差、峰值高 | 内容集合稳定且总是共同使用 |
| 每资产一包 | 独立更新和缓存 | 小文件、请求、Manifest 和打开成本高 | 资产较大、复用高、调度系统成熟 |
| 按场景/功能分组 | 贴近业务使用 | 人工配置可能偏离真实依赖 | 业务边界稳定并有自动依赖检查 |
| 内容寻址块 | 去重和增量好 | 索引、组装、清理复杂 | 内容规模大且有完整制品服务 |
| 混合方案 | 可平衡关键集和长尾 | 规则和验证成本最高 | 有数据支撑的成熟平台 |
不存在固定最佳粒度。应使用实际访问共现、更新频率、依赖扇出、失败重试成本和目标设备 IO 特性做分组,而不是让目录结构直接决定交付结构。
目标架构的最低合同
在继续扩展网格、纹理、材质和动画管线之前,资产底座至少应满足:
- 内容只持有类型化逻辑引用,不依赖缓存或包内物理路径。
- 每个制品明确源 revision、目标、格式和处理器指纹。
- 构建读取冻结输入,输出确定、隔离、可验证并原子提交。
- Manifest 是精确文件集和依赖政策的合同,而不是提示信息。
- 获取、校验、解码、实体化和可见就绪是不同状态。
- 并发请求单飞;订阅者拥有独立取消和 Generation。
- 错误区分不存在、暂时失败、损坏、不兼容、取消和过期。
- 磁盘缓存与内存驻留分离,缓存键包含完整目标维度。
- DS、客户端、编辑器和轻量目标共享逻辑身份,但消费不同制品。
- 所有优化都能回到端到端可见时间、峰值成本和正确性不变量验证。
个人反思
容易忽略的信号
早期看到“路径表越来越多”“同一资源有多个下载入口”“缓存目录带版本后缀”“加载完成还需要补一次材质”时,容易把它们当成局部维护问题。更准确的判断是:资产已经从工程文件变成跨编辑、构建、交付和运行的领域对象,而架构仍把它当作文件。
应该更早问的问题
- 这个引用表达的是逻辑身份,还是当前实现路径?
- 谁承诺依赖完整,构建期还是运行时?承诺的边界是什么?
- “成功”指字节到达、对象创建,还是用户可见?
- 请求所有者销毁后,谁取消工作、谁拒绝结果?
- 同一内容进入编辑器、客户端和 DS 时,哪些数据必须相同,哪些必须分离?
- 新平台或新 Reader 加入时,缓存和制品身份是否会碰撞?
- 部分失败应该阻断、降级,还是保留旧版本?依据是什么?
可迁移结论
资产平台化的核心不是发明更多加载接口,而是建立三条可验证的边界:
- 逻辑身份与物理制品分离。
- 不可变构建结果与可变运行生命周期分离。
- 技术完成状态与用户可见结果分离。
只要这三条边界稳定,后续的纹理压缩、网格 LOD、材质缓存、动画预取和音频流式都能接入同一套生命周期;否则每增加一种资产,都会复制一套下载、缓存、错误和回调问题。
延伸阅读
- 客户端工程复盘
- 如何复盘复杂客户端系统
- Asset&Pak&Patch:引擎资产、Cook、容器和更新机制
- 资源加载与异步加载:软引用、Handle、保活和主线程卡顿
- Cook 与打包流程:从源资产到平台数据
- Pak 与 IoStore:交付容器、挂载和覆盖