团本断线重连、复活与结算 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 时,请求包含:
BattleIdplayerIdSessionIdIsReconnectmsgIdx- 平台信息
服务端用 BattleId + playerId 找回原 Player,再用业务 SessionId 判断是哪种连接恢复。
| 条件 | 服务端分类 |
|---|---|
| Player 从未建立过 Battle session | FirstEnter |
| 已有 session,客户端 SessionId 相同 | ChangeNet |
| 已有 session,客户端 SessionId 不同 | ReConnect |
msgIdx 不是战斗帧号,而是可靠消息重发序号。只要它不等于 -1,服务端先调用:
session.ReSend((ushort)(data.MsgIdx + 1));
然后再发送新的 SC_EnterBattle。因此短断线优先补传缺失消息,同时仍会做状态快照修复。
3. 换网和断线重连的共同补发
ChangeNet 与 ReConnect 最终都进入 BattleStateSyncEvent.OnPlayerEnter 的非首次分支:
- 发送最新
SceneInfo与LeftTime。 - 重选 HostPlayer(原 Host 暂停时)。
- 清除
IsAppProcessPause。 - 调用
EventViewEntity(player.OwnerHero, true),补发所有当前可见实体。 - 发送
AddEntityEnd,标记这一轮实体快照结束。 - 补发随机任务数据。
SyncLiveStatus补发所有倒地玩家的复活状态。- 补发暂停状态。
- 补发掉落传送门怪物和关卡传送门。
- 调用
BattleReconntctInfoSync(player)。 - 补发灵力使用结果。
- 补发当前角色技能 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:
- 计算所有玩家的战斗伤害
BattleHurtDic。 - 给所有玩家继承 Battle 的
IsWin。 - 逐个调用团本
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 实体。