战斗服同步、权威边界与排错 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 产生时点
AttackEffectDelayCast/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

日志点:

  1. UI TriggerSkill。
  2. Hero.LaunchSkill 写入意图。
  3. State Begin/End 和 NextState。
  4. SkillNotifier RequestSkill。
  5. Battle OnUseSkill/ParseUseSkill。
  6. AttackCollisionDirector 产生 AttackData。
  7. 客户端 FrameSkillHit 发送。
  8. Battle OnFrameSkillHit/ParseSkillHit。
  9. 客户端 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。