团本断线重连、复活与结算 private

团本断线重连、复活与结算

这一篇把最容易误解的三个阶段连起来:客户端断线后 Actor 是否还在、倒地玩家怎样被队友救起、胜利与失败怎样同时通知客户端和 Lobby。

返回栏目首页

1. 断线不等于立即离开团本

Battle 连接断开时,BattleStateSyncEvent.OnDisConnected:

  • 已进入 BattleEnd 或玩家已经结算:立即踢出。
  • 已标记 Kickout:立即踢出。
  • 正常战斗中:把 player.IsAppProcessPause = true。
  • 不会立即删除 Player,也不会立即删除 Hero。

PlayerMgr.OnUpdateSecond 每秒检查离线玩家。超过 Const.Instance.Battle.BattlePlayerTimeout 才执行 RmvPlayer。如果移除后玩家数为 0,Battle 状态转为 Close。

这段保留时间就是重连窗口。窗口内:

BattleGuildPveBoss Actor 仍在
Boss 仍继续运行
队友仍继续战斗
离线玩家的 Player/Hero 仍可被找到

它不是暂停整个公会团本。BattleStateSync.OnUpdate 只看 Battle 自己的 BattlePause,不会因为某个 IsAppProcessPause 就停服。

2. 重连请求携带什么

客户端重新连接 Battle 时,请求包含:

  • BattleId
  • playerId
  • SessionId
  • IsReconnect
  • msgIdx
  • 平台信息

服务端用 BattleId + playerId 找回原 Player,再用业务 SessionId 判断是哪种连接恢复。

条件服务端分类
Player 从未建立过 Battle sessionFirstEnter
已有 session,客户端 SessionId 相同ChangeNet
已有 session,客户端 SessionId 不同ReConnect

msgIdx 不是战斗帧号,而是可靠消息重发序号。只要它不等于 -1,服务端先调用:

session.ReSend((ushort)(data.MsgIdx + 1));

然后再发送新的 SC_EnterBattle。因此短断线优先补传缺失消息,同时仍会做状态快照修复。

3. 换网和断线重连的共同补发

ChangeNet 与 ReConnect 最终都进入 BattleStateSyncEvent.OnPlayerEnter 的非首次分支:

  1. 发送最新 SceneInfo 与 LeftTime。
  2. 重选 HostPlayer(原 Host 暂停时)。
  3. 清除 IsAppProcessPause。
  4. 调用 EventViewEntity(player.OwnerHero, true),补发所有当前可见实体。
  5. 发送 AddEntityEnd,标记这一轮实体快照结束。
  6. 补发随机任务数据。
  7. SyncLiveStatus 补发所有倒地玩家的复活状态。
  8. 补发暂停状态。
  9. 补发掉落传送门怪物和关卡传送门。
  10. 调用 BattleReconntctInfoSync(player)。
  11. 补发灵力使用结果。
  12. 补发当前角色技能 CD。

BattleGuildPveBoss 没有覆盖 BattleReconntctInfoSync,因此这里没有额外的团本专用重连数据。团本恢复主要依赖通用实体序列化和上述补发项。

4. 当前重连并非完整快照

下面两项在服务端重连分支中被注释:

// player.OwnerHero?.SynPrivateData();
// player.OwnerHero?.SyncPrivateEntity();

这意味着背包、私有掉落、只对单个玩家可见的实体,不能仅凭这段代码断言一定在重连时完整恢复。需要依赖消息重发覆盖断线期间的变化;如果 msgIdx = -1 或重发缓存已淘汰,就存在缺口。

另外,团本的 CurRound、公会 BattleId 和 Redis Boss 摘要也没有在 Battle 重连握手中单独发送。场内 Boss 状态来自 AddEntity 的当前 Actor 快照,不是从 Redis 读取。

5. 客户端重连后的移动处理

StateSyncSystem 监听 LogicMessageType.Reconnecting 并标记 m_isReconnected。实体快照重新建立期间,移动与状态包需要经过 PacketCache 和实体存在性检查。

重连问题要分两种:

  • Actor 还活着:重发 + EventViewEntity 可以恢复大部分实时状态。
  • Actor 已不存在:Lobby/Battle 检查返回 NotFindBattle,清理公会 Redis 入口;无法恢复原场战斗。

6. 玩家掉到 0 HP 后的状态机

Hero.property.OnHpChange 检测到 CurHP <= 0 后调用团本的 OnHeroHpZero。

单人团本

hero.ActionStatus = ActorActionStatus.DEAD;
hero.m_ShowSettlementCountDown =
    TimeSpan.FromSeconds(3);
hero.m_isToShowSettlement = true;

没有等待队友,3 秒后 Battle.LeaveGame(hero) 并显示失败结算。

多人团本且仍有人存活

死亡玩家进入 REVIVE:

  • 生成 Packet_Battling_SC_ShowRevive。
  • TargetEntityID 指向倒地 Hero。
  • 团本不收复活物品,Cost 为 0。
  • ReviveIndex 自增。
  • IsAutoRevive = true。
  • ReviveCountTime = Scene.LevelCfg.TimeRevive。
  • 保存 RevivedCount。
  • 把其他玩家加入 m_canRevivePlayerId。
  • 向倒地玩家发送 ShowRevive。

客户端 ReviveSystem 根据 GuildBoss.GetReviveMode:

  • 本人对应 PVP_SELF。
  • 队友对应 PVP_TEAMMATE。

命名沿用了 PVP 枚举,但实际用于团本自救/救队友 UI。

所有人都倒地

服务端遍历所有 Player,只要找不到 ActionStatus < REVIVE 的 Hero,就认定团灭:

  • 所有 REVIVE 转成 DEAD。
  • 每人启动 3 秒结算计时。
  • 立即调用 ClearBattlePveBoss() 清公会入口。

因此“正在等待复活”也被计入团灭人数,而不是继续等自动复活。

7. 队友救援协议

客户端由 Entitas ReviveEvent 发送:

message Packet_Battling_CS_RequestRevive {
    bool isRevive = 1;
    int32 targetEntityID = 2;
}

服务端先按 TargetEntityID 在当前 Scene 找 Hero,再调用 hero.OnRequestRevive(player, packet.IsRevive)。

持续救援状态使用:

message Packet_Battling_CS_OnReviving {
    int32 state = 1; // 0 开始,1 打断
    int32 targetEntityID = 2;
}
message Packet_Battling_SC_OnReviving {
    int32 state = 1;
    int32 targetEntityID = 2;
}

救援者保存 revivingHero,按目标已复活次数从 DataExTool.listReviveTime 选择读条时长。读条归零调用 OnHeroRevive()。移动、受伤或其他打断会广播 state = 1,所有客户端停止救援表现。

断线玩家回来时,SyncLiveStatus 会遍历整场 Hero,把仍为 REVIVE 的 ShowRevive 重新发给该客户端。

8. Boss 死亡为什么不是服务端直接判胜

客户端 SettlementSystem.OnDefeatBoss 收到 DefeatBoss 事件后,再遍历本地 Enemy,确认没有仍存活的 Boss,然后先打开团本预结算界面。

玩家点击继续后,StartNextLevelSystem 发送:

new Packet_Battling_CS_StartNextLevel
{
    SceneGuid =
        NetworkManager.Instance.NetworkDataInGame.SceneGuid
}

战斗服验证 SceneGuid 与当前 Scene 一致,才调用:

public override void StartNextLevel()
{
    IsWin = true;
    ChangeStatus(emBattleStatus.BattleEnd);
}

团本的 HasNextLevel() 固定为 false。“StartNextLevel”在这里其实承担“确认本场胜利并结算”的历史协议职责。

这个设计意味着胜利收口依赖客户端发来的 StartNextLevel,虽然服务端已经知道 Boss HP 为 0。若客户端击杀后崩溃或没有点击,必须依赖战场超时/其他客户端请求来结束;这是值得改造的权威性问题。

9. BattleEnd 怎样给每个玩家结算

BattleStateSync.OnBattleEnd:

  1. 计算所有玩家的战斗伤害 BattleHurtDic。
  2. 给所有玩家继承 Battle 的 IsWin。
  3. 逐个调用团本 LeaveGame(hero)。

LeaveGame:

  • 结束正在进行的救援。
  • 判断是否超时。
  • 调用 SendSettlementData。
  • 解绑 Hero。
  • 把 Hero 标记为 REMOVE。
  • 离线玩家设置延迟 Kickout。

SendSettlementData 同时发送两条结果:

SC_ShowSettlement -> 当前客户端展示
ServerPacket_Battle_CombatSettlement -> Lobby 做持久化

10. PveBossStatus 的真实语义

战斗服设置:

packet.PveBossStatus =
    OwnerHero.ActionStatus != ActorActionStatus.DEAD
    && BattleGPB.IsCountDown()
    && !isTimeout
        ? 1 : 0;

所以它不是“是否胜利”,而是“是否满足参与记录条件”:

  • 结算时玩家不能是 DEAD。
  • 团本必须已经开始倒计时。
  • 不能是战场超时。

同时,只要倒计时已经开始就记录 SJoinPveBossX 任务。

Lobby 收到后:

  • PveBossStatus == 1:PveBossRecord() 记录本轮参与。
  • 否则,如果开启奖励被扣过则返还一次奖励次数。
  • 清除 GameGuildOpeningReward。
  • 无论个人状态如何都调用 PveBossSettle(msg.IsWin)。

11. 公会积分为什么不会按人数重复加

胜利时,每个玩家都可能向 Lobby 发一份 IsWin = true 的结算。如果直接加分,十人团会加十次。

PveBossSettle 使用:

string key = $"{GameGuildData.Id}_{RecRoundInfo.Id}";

if (await database.LockTakeAsync(
    key, 0, TimeSpan.FromDays(1)))
{
    HashIncrement(guildKey, "score", difficulty);
}

幂等键由“公会 ID + 轮次 ID”组成。当天第一个胜利结算取得锁并加分,其他成员同轮结算拿不到锁,所以只加一次。

12. 战斗清理

ClearBattlePveBoss 会在团灭、Battle 关闭等路径执行:

  • isClearPveBoss 防止同一个 Actor 重复清理。
  • 胜利时把 roundInfo 中本轮 Status 改成 0。
  • 读取公会 Redis 当前 battleId。
  • 只有与自己的 BattleIdStr 相同才清空。
  • 同时清空 curRoundInfo。

BattleId 比对避免旧 Actor 晚到的清理把新 Actor 的入口删掉。但当前创建锁检查存在键值错误,详见下一篇。

13. 重连与结算排错字段

至少同时记录:

guildId, roundId, battleId, battleNode
playerId, playerSessionId, battleSessionId
enterType, msgIdx, lastSentIdx, lastAckIdx
battleStatus, player.IsAppProcessPause, player.settled
heroEntityId, LiveStatus, ActionStatus, CurHP
sceneGuid, leftTime
PveBossStatus, IsWin, isTimeout

只记录“客户端重连成功”不够;必须确认它重连的是原 BattleId、原 playerId 和原 Hero 实体。

上一篇:首帧快照、AOI 与实时增量 · 下一篇:并发锁、故障边界与排错