重连更新
前言
今天更新了闪断重连,其他比较简单,就客户端麻烦点,要重写很多东西。
用坐标逆向求行进距离
断线重连,可以是恢复是因为我每隔10帧保存一次快照,读取快照中的信息来恢复闪断前的状态。而在快照中没有直接保存Dist(行进距离)的原因是,“绕远路就是近路”(当时没想到)。
总之我们还是先取得敌人的路径点并计算两点间的直线距离
a, b := enemyPath[i-1], enemyPath[i]
dx, dy := b.X-a.X, b.Y-a.Y
seg := math.Hypot(dx, dy)
如果seg大于零(a,b间有距离)则算出敌人坐标p.x和p.y在a,b间的什么地方。利用t作为坐标位于a,b间的几分之几处。最后算出具体行进距离。
t = ((p.X-a.X)*dx + (p.Y-a.Y)*dy) / (seg * seg)
t = math.Max(0, math.Min(1, t))
qx, qy := a.X+t*dx, a.Y+t*dy
if d := math.Hypot(p.X-qx, p.Y-qy); d < bestD {
bestD = d
best = acc + t*seg
}
重连恢复
要重连恢复,最重要的是保存快照。我设定每10帧数(2秒)保存一次。
const snapshotIntervalFrames = 10
...
if r.frame%snapshotIntervalFrames == 0 {
if data, err := proto.Marshal(state); err == nil {
r.snapshots = append(r.snapshots, &BattleSnapshot{Frame: r.frame, Data: data})
} else {
logger.Println("序列化战斗快照失败:", err)
}
}
然后是恢复重连,我们要先把旧循环退出。
close(r.done)
从最近一次快照恢复 mu 保护的状态;无快照则保持当前状态继续
if err := proto.Unmarshal(r.snapshots[n-1].Data, &state);
然后恢复干员数据和敌人数据、还有链接数据。并跑run函数。
客户端设置
这一段应该挺多的。首先我们传入basic的验证场景。
mode = flag.String("mode", "basic", "验证场景:basic | reconnect")
func main() {
flag.Parse()
var err error
switch *mode {
case "basic":
err = runBasic()
case "reconnect":
err = runReconnect()
default:
fmt.Println("未知 -mode:", *mode)
os.Exit(1)
}
if err != nil {
fmt.Println("失败:", err)
os.Exit(1)
}
}
runBasic 进房 → 部署 → 收状态推送 → 收结算
func runBasic() error {
c, err := dial()
...
}
链接网关
gwAddr = flag.String("gw", "127.0.0.1:8888", "网关地址")
func dial() (*client, error) {
conn, err := net.DialTimeout("tcp", *gwAddr, 3*time.Second)
if err != nil {
return nil, fmt.Errorf("连接网关失败: %w", err)
}
return &client{conn: conn}, nil
}
再发送进房请求(roomID=0 为新开,非 0 为断线重连),等待 2001 回包
resp, err := enter(c, 0)//runBasic()
func enter(c *client, roomID uint32) (*api.EnterRoomResp, error) {
body, err := proto.Marshal(&api.EnterRoomReq{LevelId: 1, RoomId: roomID})
if err != nil {
return nil, err
}
if err := c.send(msgEnterRoom, body); err != nil {
return nil, err
}
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
p, err := c.next(time.Second)
if err != nil {
return nil, err
}
if p == nil || p.MessageID != msgEnterRoomR {
continue // 跳过与本次请求无关的早期推送
}
var resp api.EnterRoomResp
if err := proto.Unmarshal(p.Payload, &resp); err != nil {
return nil, err
}
return &resp, nil
}
return nil, fmt.Errorf("等待进房回包超时")
}
读到 2004(状态)或 2005(结算)为止,二者互斥返回
func nextBattle(c *client, deadline time.Time) (*api.BattleStatePush, *api.BattleSettlePush, error) {
for time.Now().Before(deadline) {
p, err := c.next(time.Second)
if err != nil {
return nil, nil, err
}
if p == nil {
continue
}
switch p.MessageID {
case msgState:
var s api.BattleStatePush
if err := proto.Unmarshal(p.Payload, &s); err != nil {
return nil, nil, err
}
return &s, nil, nil
case msgSettle:
var s api.BattleSettlePush
if err := proto.Unmarshal(p.Payload, &s); err != nil {
return nil, nil, err
}
return nil, &s, nil
}
}
return nil, nil, fmt.Errorf("等待战斗推送超时")
}
接着我们再验证断链的场景
runReconnect 进房 → 部署 → 中途断线 → 用 roomId 重连 → 验证恢复后续跑 → 收结算
c1.close() // 模拟客户端断线,服务端房间与 tick 继续
fmt.Println("--- 关闭连接,模拟断线 ---")
用 roomId 重连,检查是否恢复而不是重开
if resp2.Code != 0 {
return fmt.Errorf("重连失败 code=%d msg=%s(断线过久房间可能已结束)", resp2.Code, resp2.Msg)
}
fmt.Printf("[3] 重连 code=%d roomId=%d seed=%d\n", resp2.Code, resp2.RoomId, resp2.Seed)
if resp2.Seed != resp1.Seed {
return fmt.Errorf("重连返回 seed=%d 与首次进房 seed=%d 不一致,疑似换了房间", resp2.Seed, resp1.Seed)
}
state2, settle2, err := nextBattle(c2, time.Now().Add(20*time.Second))
if err != nil {
return err
}
if settle2 != nil {
return fmt.Errorf("重连后直接结算 result=%d,未看到恢复状态", settle2.Result)
}
printState(state2)
if state2.Frame <= 1 {
return fmt.Errorf("恢复后帧号仍为 %d,疑似重开而非续跑", state2.Frame)
}
if len(state2.Operators) != len(last.Operators) {
return fmt.Errorf("恢复后干员数 %d 与断线前 %d 不一致,状态未恢复", len(state2.Operators), len(last.Operators))
}
最后从恢复点继续推进到结算。
deadline2 := time.Now().Add(20 * time.Second)
for time.Now().Before(deadline2) {
state, settle, err := nextBattle(c2, deadline2)
if err != nil {
return err
}
if settle != nil {
printSettle(settle)
if settle.Result == 0 {
fmt.Println("验证通过:断线重连后战斗从原进度继续,并正常结算(胜利)")
return nil
}
return fmt.Errorf("断线重连后结算结果非胜利 result=%d", settle.Result)
}
printState(state)
}
return fmt.Errorf("重连后等待结算超时")
欧克,这个还算简单。下周继续。
我是Strandingwhale,也可以叫我bridges,计算机专业,专注游戏后端与Unreal引擎开发。在git pages和博客园上发布文章,其他网站上的都不是本人。
目前还在上大学,希望能做出很牛逼的项目,然后去到我喜欢的岗位。
本人是冯·诺伊曼派,不是图灵学派。
古法编程,AI提供一点思路,少用AI。
喜欢玩明日方舟
喜欢的游戏还有 死亡搁浅 灵魂摆渡人 异星工厂 等
浙公网安备 33010602011771号