重连更新

前言

今天更新了闪断重连,其他比较简单,就客户端麻烦点,要重写很多东西。

用坐标逆向求行进距离

断线重连,可以是恢复是因为我每隔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("重连后等待结算超时")

欧克,这个还算简单。下周继续。

posted @ 2026-11-01 00:51  布里吉斯  阅读(16)  评论(0)    收藏  举报