战斗服同步、权威边界与排错 private
战斗服同步、权威边界与排错
1. 先给结论
当前战斗技能采用“客户端先播放和做碰撞候选,Battle 接收技能意图与命中数据,再广播伤害结果”的混合状态同步。它不是服务端逐帧驱动 Animator,也不是客户端可以直接决定最终 HP。
sequenceDiagram
participant C as 操作者客户端
participant B as Battle
participant R as 其他客户端
C->>C: 输入 -> 本地技能状态/动画/移动
C->>B: CS_UseSkill(skillInstId,pos,rotation)
B->>B: 角色状态、技能实例、Buff/符文触发
C->>C: 碰撞/子弹形成 AttackData
C->>B: CS_FrameSkillHit
B->>B: 归属、目标、技能和伤害计算
B-->>C: SC_DamageResult
B-->>R: SC_DamageResult
C->>C: 预测去重/受击和属性表现
R->>R: 应用受击和属性表现
2. UseSkill 上行
SkillNotifier.StartUseSkill 不直接碰网络层,只投递领域消息:
MessageCenter.Instance.PostMessage(
LogicMessageType.RequestSkill,
new RequestSkillData {
ActorEntity = m_actorEntity,
MoveData = move,
SkillInstId = skillInstId,
UseSkillGuid = useSkillGuid
});
BattleDataSyncSystem.RequestSkill 验证 Actor 和 EntityId 后组包:
NetworkManager.Instance.SendInGameMessage(
emPacket_Battling_CS.UseSkill,
new Packet_Battling_CS_UseSkill {
EntityId = (int)actor.Entity.entityID.Value,
SkillInstId = skillInstId,
NowPos = OptimizeUtils.GetTVector3Int_FF(
actor.GetLogicPosition()),
NowDirection = actor.transform.rotation.Tot_Quaternion()
});
当前实际组包没有写入 RequestSkillData 中的 MoveData 和 UseSkillGuid。协议生成类虽然存在相关历史字段/结构,读代码时应以当前赋值为准。
3. Battle 的 UseSkill 校验边界
入口 HandlePacketStateSync.OnUseSkill 先按请求玩家寻找自己的 Character,并检查 character.playerId == player.Playerid,再进入 UseSkillHandler.OnUseSkill。
ParseUseSkill 当前主要检查:
- Owner、Scene 和 Packet 存在。
LiveStatus == ACTIVE。- ActionStatus 未到禁止技能区间。
- HP 大于 0。
- SkillInstId 能否在服务端 skillCache 找到。
- 装备主动技能的触发条件。
- 符文、特殊技能、随从、Buff 和掉落触发。
然后设置 ActionStatus = USESKILL。
必须注意:服务端代码中“成功广播 SC_UseSkill”和“失败返回 SC_UseSkillFail”的整段实现当前被注释。也就是说,普通角色动作主要通过状态/移动等其他链路让远端表现,不能假设每次 CS_UseSkill 都有对应成功或失败回包。
4. CD 与能量是另一条消息
StartSkillCd 在 Battle 中:
- 检查角色存活和动作状态。
- 从 skillCache 找 SkillInstId。
- 若消耗类型是 HERO_MP,检查并扣除能量。
- 多次充能技能走
StartCacheSkillCD。 - 普通技能按配置 CD、减 CD 属性和符文修正计算。
- 通过
UpdateRoleSkillCD回到客户端更新次数/冷却。
所以“技能动作已经播了但 UI CD 没走”要查 RequestStartSkillCD,不要只查 UseSkill。
5. 命中批处理
AttackCollisionDirector 产生 AttackData 后,SyncDamageSystem.PrePostAttackDataToServer 只处理本地 Killer。客户端可先模拟受击,并给 AttackData 设置随机种子。随后压缩数据暂存在列表:
var data = t_AttackData.Spawn();
optimizer.GetOptimizeNetworkData(attackData, data);
data.FinalHurtValue =
(int)(predictedResult?.FinalDamageValue ?? 0);
data.IsUsed =
TryPostAttributeChangeMsg(attackData, predictedResult);
attackDatas.Add(data);
达到帧内上限或本帧结束时打成 Packet_Battling_CS_FrameSkillHit。包用第一条 KillerId 和 UniqueId 作为基值,其他条目存偏移,减少带宽。
6. Battle 处理 FrameSkillHit
OnFrameSkillHit 逐条还原 KillerId 和 UniqueId,检查:
- Killer 实体存在。
- Victim 实体存在。
- Killer 是否确实归请求玩家控制。
- 子弹 Killer 的 Master 归属是否合法。
- 实体类型和攻击数据是否合法。
随后 UseSkillHandler.OnSkillHit / ParseSkillHit 执行伤害、受击、死亡和特殊技能逻辑,SendSkillResult 广播 SC_DamageResult。
客户端 SyncDamageSystem.ApplyDamageResult 根据 KillerId 和 VictimIdOffset 找双方实体,解码 Flag,然后触发:
killer.DamageResultTriggerSkillLogic(...);
victim.DamageResultBeHitSkillLogic(...);
victim.ApplyOnBeHit(damageResult, true);
UniqueId 配合 StateSimulator 去掉本地已预测的同一次 Hurt。
7. 当前权威模型的真实风险
| 项目 | 当前情况 | 风险 |
|---|---|---|
| 动作开始 | 客户端立即开始 | RTT 低,但失败回滚弱 |
| UseSkill 成功广播 | 服务端代码被注释 | 不能靠该包驱动远端动画 |
| UseSkillFail | 服务端代码被注释 | 本地错误释放缺少统一回退 |
| 命中候选 | 本地碰撞产生 | Battle 必须严格校验归属和数值 |
| 最终伤害 | Battle 计算并广播 | 是最终 HP/死亡依据 |
| 本地受击 | 可预测 | 需 UniqueId 去重 |
| 技能位移 | MoveData 单独同步 | 状态包与 MoveBegin 顺序必须一致 |
这是现状说明,不是推荐安全上限。若项目进入强对抗 PvP,需要加强技能冷却、距离、朝向、命中体积、时间窗口和移动速度的服务端验证。
8. 配置字段对照
| 字段 | 影响 |
|---|---|
CooldownInterval | 客户端 UI/技能更新器基础冷却 |
PreInterruptPercentage | 前摇内技能衔接阈值 |
PostInterruptPercentage | 后段技能/受击/移动通用窗口 |
MoveInterruptPercentage | 独立移动取消窗口 |
AttackLogicDelay | 逻辑攻击、子弹或 DamageZone 产生时点 |
AttackEffectDelay | Cast/Finish VFX 时点 |
HurtDelay | 命中后的延迟伤害 |
AttackAngle | 近战扇形范围 |
MoveDuration/MoveDistance | 技能位移 |
MoveCurve/MoveCurveEnum | 位移速度曲线 |
AttackFreezeAnimTimeFactor | 命中停顿倍率 |
TurnSpeed | 出招转向速度 |
调配置必须同时记录对应英雄、皮肤/变体、SkillType 和实际 Clip;项目存在 Girl03_HeroStateMachine_Five/Seven/Two 等变体,不能只改基础类就认为全部生效。
9. 四端日志建议
一次技能用同一组关联字段追踪:
playerId
entityId
skillInstId
skillType
useSkillGuid(若该链路实际携带)
attackUniqueId
state
normalizedTime
moveStrategy
packet timestamp
victimId
damage flags
日志点:
- UI TriggerSkill。
- Hero.LaunchSkill 写入意图。
- State Begin/End 和 NextState。
- SkillNotifier RequestSkill。
- Battle OnUseSkill/ParseUseSkill。
- AttackCollisionDirector 产生 AttackData。
- 客户端 FrameSkillHit 发送。
- Battle OnFrameSkillHit/ParseSkillHit。
- 客户端 DamageResult 接收和 StateSimulator 去重。
10. 故障树
| 现象 | 检查顺序 |
|---|---|
| 本地不播技能 | 输入 -> NeedLaunchSkill -> CheckLaunchSkill -> Animator 参数 |
| 本地播但远端不播 | State/Move 同步;不要等待被注释的 SC_UseSkill |
| 动作播了不出伤害 | AttackLogicDelay -> 碰撞/子弹 -> FrameSkillHit |
| 有命中特效无扣血 | Battle ParseSkillHit -> DamageResult -> AttributeChangeMsg |
| 扣血两次/受击两次 | UniqueId 是否稳定,StateSimulator 是否命中同一预测 |
| 技能失败仍播完整套 | UseSkillFail 当前缺失,需要实现本地回滚协议 |
| 冲刺后位置跳 | MoveBegin 与 State 顺序、服务端记录位置、纠偏 |
| CD 与动画不同步 | StartUseSkill 与 StartSkillCD 是否在同一释放生命周期 |
| 切技能后仍发子弹 | Interval handler 未在非内部跳转时 Dispose |
| 重连后动作错乱 | 状态快照是否含技能阶段、移动基线和剩余 CD |
11. 回归测试矩阵
- 0/50/100/200 ms RTT。
- 0%、1%、5% 丢包与乱序。
- Idle 出招、移动出招、前摇按下一技能、后摇按移动。
- 技能中受击、击退、死亡、Cutscene。
- C 起手到 Loop、Loop 到 Post、Loop 中切 B。
- 主机、本地 Hero、远端 Hero 分别观察。
- 同帧多目标命中,验证 FrameSkillHit 基值压缩。
- 预测开启/关闭,确认 DamageResult 不重放。
- 换皮/技能变体后验证 Clip 长度和取消窗口。
- 断线重连时验证 ActionStatus、技能阶段、MoveData 和 CD。