总览
本篇讨论 Mutable 换装系统在多人项目中的生产级落地。多人换装的核心不是“把 Mesh 同步过去”,而是同步一份可验证、可复现、可降级的外观描述,让每个客户端在本地生成自己的最终表现。
服务端负责合法性和权威状态,客户端负责表现生成。只要这条边界被打破,包体、带宽、性能、反作弊和热更新都会一起变复杂。
本篇目标
- 定义多人外观同步的数据结构、权限边界和生命周期。
- 明确 Server、Owning Client、Remote Client、Dedicated Server 和 Listen Server 各自负责什么。
- 给出 Replication、RPC、OnRep、版本号、乱序保护的设计。
- 建立异常处理框架:非法装备、资源缺失、热更不一致、生成失败、网络乱序。
- 输出生产级上线检查清单。
非目标
- 本篇不讲基础 UE Replication 教程。
- 本篇不把 Mutable 生成结果作为网络同步对象。
- 本篇不讨论完整背包、交易、商城支付系统。
- 本篇不覆盖反作弊全体系,只覆盖换装相关校验边界。
多人架构边界
多人换装同步的对象应该是:
AppearanceSpec = CharacterBase + BodyParams + EquippedItemIds + Colors + CosmeticFlags + Version
而不是:
Generated SkeletalMesh
Generated Textures
Material Instances
Mutable Runtime Object
原因:
- Mesh 和贴图体积不可接受。
- 不同平台、画质、LOD、DLC 安装状态可能不同。
- Mutable 生成应由客户端按本地资源和性能预算完成。
- 服务端不应该承担外观生成成本。
网络数据结构
建议同步结构尽量小,并使用稳定 ID。
USTRUCT(BlueprintType)
struct FAppearanceSpec
{
GENERATED_BODY()
UPROPERTY()
FName CharacterArchetypeId;
UPROPERTY()
FName BodyTypeId;
UPROPERTY()
TArray<FName> EquippedItemIds;
UPROPERTY()
TMap<FName, FLinearColor> ColorParams;
UPROPERTY()
TMap<FName, float> ScalarParams;
UPROPERTY()
uint32 AppearanceRevision = 0;
UPROPERTY()
uint32 ContentVersion = 0;
UPROPERTY()
uint8 QualityPolicy = 0;
};
设计要求:
EquippedItemIds要排序或按固定 Slot 存储,保证 Hash 稳定。- 颜色和数值参数必须有范围限制。
AppearanceRevision用于网络乱序保护。ContentVersion用于资源热更和缓存失效。- 不在 Spec 里放 Asset Path,网络上只传稳定业务 ID。
复制给客户端的不是裸 Spec,而是带状态的 RepState:
UENUM(BlueprintType)
enum class EAppearanceRepStatus : uint8
{
PendingValidation,
Confirmed,
Rejected,
Fallback,
ContentUnavailable
};
USTRUCT(BlueprintType)
struct FAppearanceRepState
{
GENERATED_BODY()
UPROPERTY()
FAppearanceSpec Spec;
UPROPERTY()
EAppearanceRepStatus Status = EAppearanceRepStatus::Confirmed;
UPROPERTY()
FGameplayTagContainer CosmeticTags;
UPROPERTY()
uint32 ServerRevision = 0;
UPROPERTY()
bool bUseFallback = false;
};
Status 示例:
| 状态 | 说明 |
|---|---|
PendingValidation |
服务端收到请求但未完成校验 |
Confirmed |
服务端确认外观可用 |
Rejected |
请求被拒绝 |
Fallback |
服务端要求使用默认外观 |
ContentUnavailable |
客户端可能缺少内容,需要降级 |
蓝图接口设计
AppearanceComponent 函数清单
| 函数 | 权限 | 说明 |
|---|---|---|
RequestServerChangeAppearance(NewSpec) |
Owning Client | 客户端请求换装 |
ServerSubmitAppearance(NewSpec, ClientRevision) |
Server RPC | 提交给服务端校验 |
ClientAppearanceRejected(Reason, ServerSpec) |
Client RPC | 服务端拒绝后通知 Owner |
ClientAppearanceConfirmed(ServerSpec) |
Client RPC | 服务端确认后通知 Owner |
OnRep_AppearanceRepState() |
Client | 远端客户端收到外观复制 |
ApplyReplicatedAppearance(RepState) |
Client | 根据复制状态触发本地异步生成 |
PredictLocalAppearance(NewSpec) |
Owning Client | 本地预测,仅用于 UI 与短暂表现 |
RollbackPredictedAppearance(ServerSpec) |
Owning Client | 预测失败后回滚 |
ForceFallbackAppearance(Reason) |
Server/Client | 强制应用默认或安全外观 |
GetNetworkAppearanceDebugString() |
BlueprintPure | 输出调试信息 |
服务端校验函数
| 函数 | 说明 |
|---|---|
ValidateAppearanceOwnership(PlayerId, Spec) |
校验玩家是否拥有物品 |
ValidateAppearanceCompatibility(CharacterId, Spec) |
校验职业、体型、性别、部位兼容 |
ValidateAppearanceContentVersion(Spec) |
校验内容版本是否有效 |
ValidateAppearanceGameplayRules(Spec) |
校验竞技、阵营、模式限制 |
SanitizeAppearanceSpec(Spec) |
移除非法参数,补默认值 |
BuildFallbackRepState(PlayerId, Reason) |
构造安全回退状态 |
变量清单
| 变量 | 复制 | 说明 |
|---|---|---|
AppearanceRepState |
ReplicatedUsing | 服务端权威外观状态 |
LocalPredictedSpec |
不复制 | Owner 本地预测外观 |
LastAppliedServerRevision |
不复制 | 客户端已应用的最新版本 |
LastSubmittedClientRevision |
不复制 | Owner 提交版本 |
bHasPendingServerAppearance |
不复制 | 是否等待服务端确认 |
AppearanceNetMode |
不复制 | Owner、Remote、Dedicated、Listen |
ReplicationRebuildPolicy |
不复制 | 收到复制后立即构建或延迟构建 |
RemoteAppearancePriority |
不复制 | 远端角色生成优先级 |
事件与委托清单
| 委托 | 参数 | 说明 |
|---|---|---|
OnServerAppearanceSubmitted |
Spec, ClientRevision |
Owner 提交请求 |
OnServerAppearanceValidated |
Spec, ServerRevision |
服务端校验通过 |
OnServerAppearanceRejected |
Reason, ServerSpec |
服务端拒绝 |
OnAppearanceRepStateReceived |
RepState |
客户端收到复制 |
OnPredictedAppearanceApplied |
Spec |
本地预测已应用 |
OnPredictedAppearanceRolledBack |
ServerSpec, Reason |
预测回滚 |
OnRemoteAppearanceBuildQueued |
Actor, Spec |
远端外观进入队列 |
OnRemoteAppearanceApplied |
Actor, Spec |
远端外观应用完成 |
OnAppearanceDesyncDetected |
LocalRevision, ServerRevision |
发现版本不同步 |
OnAppearanceSecurityViolation |
PlayerId, Reason |
发现非法提交 |
Owner Client 换装流程
UI 选择装备
-> Owner Client 本地 Validate + Normalize
-> PredictLocalAppearance(NewSpec)
-> ServerSubmitAppearance(NewSpec, ClientRevision)
-> Server 校验拥有权、规则、版本
-> 校验通过:
更新 AppearanceRepState
ClientAppearanceConfirmed(ServerSpec)
Replicate AppearanceRepState to other clients
Remote clients async build appearance
-> 校验失败:
ClientAppearanceRejected(Reason, ServerSpec)
Owner RollbackPredictedAppearance(ServerSpec)
Owner 端可以预测,但预测不能写入权威状态。确认前 UI 应显示“应用中”或轻量状态,避免玩家以为已经成功保存。
Remote Client 收到外观流程
OnRep_AppearanceRepState触发。- 检查
ServerRevision是否大于LastAppliedServerRevision。 - 如果旧版本,丢弃。
- 如果角色不可见或距离过远,进入延迟队列。
- 如果资源已缓存,直接进入 Mutable 生成。
- 如果资源缺失,尝试加载对应 Bundle。
- 生成成功后应用 Mesh,并更新
LastAppliedServerRevision。 - 生成失败则应用远端默认外观,不影响 Actor 生命周期。
玩家中途加入时,不能依赖历史 RPC。必须依赖复制状态:
AppearanceRepState是完整状态,不是增量 Patch。- 新客户端只要收到当前 RepState 就能生成角色外观。
- 如果玩家在资源热更后加入,
ContentVersion必须能判断本地是否支持。 - 初始 Pawn 生成时先显示默认外观,再异步应用服务器外观。
- 队友、本地玩家优先级高于普通远端玩家。
异常处理
服务端拒绝
| 原因 | 处理 |
|---|---|
| 未拥有物品 | 拒绝,回滚到服务器当前外观 |
| 职业不匹配 | 拒绝,记录客户端错误 |
| 参数越界 | Sanitize 后可选接受,严重时拒绝 |
| 物品已下架 | 回退默认或替换为补偿物品 |
| 版本过旧 | 要求客户端刷新配置 |
| 提交频率过高 | 节流,必要时进入风控 |
服务端不要只返回 false,应返回明确 EAppearanceRejectReason。
客户端生成失败
客户端失败不应该影响网络权威状态。它只是本机表现失败。
处理顺序:
- 判断是否资源缺失。
- 尝试加载低质量或默认 Bundle。
- 如果仍失败,应用默认远端外观。
- 上报
SpecHash + ContentVersion + Platform + ErrorCode。 - 不请求服务端重新同步,除非检测到版本不同步。
网络乱序
必须有两个版本号:
| 版本 | 作用 |
|---|---|
ClientRevision |
Owner 提交请求时递增,用于匹配确认/拒绝 |
ServerRevision |
服务端接受后递增,用于所有客户端 OnRep 去重 |
规则:
- 客户端收到旧
ServerRevision直接丢弃。 - Owner 收到旧确认,不覆盖更新的预测状态。
- 服务端收到低于当前状态的提交,可以拒绝或忽略。
- 异步生成完成时再次检查 Revision,避免旧构建覆盖新外观。
内容版本不一致
多人项目中最危险的问题是客户端内容不一致:
- A 客户端安装了新服装 DLC。
- B 客户端没有安装。
- 服务端允许 A 穿戴。
- B 收到 ItemId 后无法加载资源。
解决方式:
- 服务端维护物品可见性和内容包规则。
- Spec 中带
ContentVersion或ContentPackageId。 - 客户端缺内容时显示平台默认替代外观。
- 竞技模式中,禁止使用对部分客户端不可见的外观。
- 商业展示场景中,可以允许缺内容客户端显示占位,但要上报。
生产级检查清单
网络同步检查:
- [ ] 外观同步只传 ID 和参数,不传 Mesh、Texture、MaterialInstance。
- [ ]
AppearanceRepState是完整状态,新加入玩家不依赖历史 RPC。 - [ ] 所有外观请求都经过服务端校验。
- [ ] Owner Client 本地预测可回滚。
- [ ]
ClientRevision和ServerRevision都存在。 - [ ] OnRep 里不会同步阻塞加载资源。
- [ ] 远端玩家换装请求进入低优先级异步队列。
- [ ] 旧异步请求不会覆盖新外观。
- [ ] Listen Server 路径单独测试过。
- [ ] Dedicated Server 不加载 Mutable 生成资源。
安全与规则检查:
- [ ] 服务端校验玩家是否拥有物品。
- [ ] 服务端校验职业、性别、体型、阵营和模式限制。
- [ ] 服务端校验颜色、缩放、体型参数范围。
- [ ] 服务端对提交频率做节流。
- [ ] 非法请求有结构化错误码。
- [ ] 可疑请求能进入风控或日志系统。
- [ ] 客户端不能通过改蓝图变量绕过权威外观。
- [ ] 下架物品和过期物品有回退策略。
资源与内容检查:
- [ ] 所有 ItemId 可反查 DataAsset。
- [ ] DataAsset 中运行时资源使用 Soft Reference 或 PrimaryAssetId。
- [ ] 默认外观在基础包中。
- [ ] DLC 外观缺失时有替代资源。
- [ ] 热更后缓存 Key 包含内容版本。
- [ ] Asset Manager 规则覆盖所有服装部位。
- [ ] 客户端缺内容不会导致角色消失。
- [ ] 资源加载失败会上报平台、版本、SpecHash。
性能检查:
- [ ] 本地玩家换装优先级高于远端玩家。
- [ ] 同屏大量玩家外观刷新不会造成长帧。
- [ ] OnRep 只入队,不直接生成。
- [ ] 远端玩家有距离、可见性和重要度降级。
- [ ] 缓存有内存预算和淘汰策略。
- [ ] 低端平台有单独质量策略。
- [ ] 主城、战斗、商城使用不同预热规则。
- [ ] Shipping 包有外观压力测试。
观测与排障检查:
- [ ] 能打印某角色当前
AppearanceSpec、SpecHash、ServerRevision。 - [ ] 能区分资源加载失败、Mutable 生成失败、应用 Mesh 失败。
- [ ] 有外观请求链路日志:提交、确认、复制、加载、生成、应用。
- [ ] 有外观失败率、平均耗时、缓存命中率统计。
- [ ] 有命令强制刷新角色外观。
- [ ] 有命令清理外观缓存。
- [ ] 有命令切换远端外观降级策略。
- [ ] QA 能用固定 Spec 复现问题。
验收标准
| 场景 | 预期 |
|---|---|
| Owner 换装成功 | 本地预测后收到服务端确认,远端客户端最终一致 |
| Owner 换装被拒 | 本地回滚到服务端外观 |
| 远端玩家快速连续换装 | 客户端最终只应用最新 ServerRevision |
| 新玩家中途加入 | 能根据 RepState 生成当前外观 |
| 资源缺失客户端 | 远端角色显示默认替代外观,不崩溃 |
| Dedicated Server | 不加载 Mutable Mesh,不执行客户端生成逻辑 |
| Listen Server | 本机表现和服务端权威逻辑不互相污染 |
| 网络延迟/乱序 | 旧请求不会覆盖新外观 |
| 热更版本不一致 | 缺内容客户端降级并上报 |
异常验收:
- 非法 ItemId 被服务端拒绝。
- 未拥有物品不能通过客户端请求穿戴。
- 参数越界不会进入 Mutable 生成。
- Mutable 生成失败后角色仍可移动、可见、可被交互。
- 所有失败都有结构化错误码。
- QA 能从日志中定位是“同步问题”还是“本地资源生成问题”。
系列收束
从第一篇到第八篇,完整系统路线是:
架构 -> 资源 -> 数据 -> 蓝图执行 -> UI 预览 -> 存档 -> 性能 -> 多人
Mutable 真正进入生产项目时,重点不在“某个参数怎么 Set”,而在整条链路是否可维护:数据能迁移,资源能 Cook,UI 能预览,状态能回滚,服务端能裁决,客户端能降级,日志能定位。做到这些,Mutable 才从一个能换衣服的插件,变成项目里可以长期迭代的换装系统。