游戏断线重连机制实战经验:从检测、托管到状态恢复

做实时对战游戏的时候,断线是不可避免的问题。网络波动、切WiFi、接个电话都可能让玩家掉线。这篇记录一下我们项目中断线重连的完整实现方案。

断线重连流程概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────────────┐
│ 断线重连完整流程 │
├─────────────────────────────────────────────────────────────────────┤

│ 正常游戏 断线检测 托管处理 重连恢复
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ ▼
│ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ 玩家 │───────►│ 心跳超时 │──────►│ AI 托管 │─────►│ 状态恢复 │
│ │ 操作 │ │ 连接断开 │ │ 保存现场 │ │ 继续游戏 │
│ └──────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │ │
│ │ │ │ │
│ │ ┌─────┘ │ ┌─────┘
│ │ │ │ │
│ │ ▼ │ ▼
│ │ ┌──────────┐ │ ┌──────────┐
│ │ │ 立即检测 │ │ │ 重连失败 │
│ │ │ 延迟检测 │ │ │ 判定逃跑 │
│ │ └──────────┘ │ └──────────┘
│ │ │
│ │ ▼
│ │ ┌──────────┐
│ │ │ 超时结算 │
│ │ │ 游戏结束 │
│ │ └──────────┘
│ │
└─────────────────────────────────────────────────────────────────────┘

核心流程详解

阶段 1:断线检测

心跳机制设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// HeartbeatManager.js
class HeartbeatManager {
constructor(socket) {
this.socket = socket;
this.heartbeatInterval = 5000; // 心跳发送间隔:5秒
this.heartbeatTimeout = 10000; // 心跳超时时间:10秒
this.lastPongTime = Date.now();
this.heartbeatTimer = null;
this.checkTimer = null;
}

start() {
// 发送心跳
this.heartbeatTimer = setInterval(() => {
this.sendPing();
}, this.heartbeatInterval);

// 检测超时
this.checkTimer = setInterval(() => {
this.checkTimeout();
}, 1000);

// 监听服务器响应
this.socket.on('pong', () => {
this.lastPongTime = Date.now();
console.log('Heartbeat received');
});
}

sendPing() {
if (this.socket.connected) {
this.socket.emit('ping', { time: Date.now() });
}
}

checkTimeout() {
const now = Date.now();
const elapsed = now - this.lastPongTime;

if (elapsed > this.heartbeatTimeout) {
console.error('Heartbeat timeout detected');
this.onDisconnect(DisconnectReason.HEARTBEAT_TIMEOUT);
}
}

onDisconnect(reason) {
this.stop();
// 触发断线处理
GameReconnectManager.getInstance().onConnectionLost(reason);
}

stop() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = null;
}
}
}

// 断线原因枚举
const DisconnectReason = {
HEARTBEAT_TIMEOUT: 'heartbeat_timeout',
SOCKET_ERROR: 'socket_error',
NETWORK_CHANGE: 'network_change',
APP_BACKGROUND: 'app_background',
SERVER_CLOSE: 'server_close',
KICKED: 'kicked'
};

多层级断线检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ConnectionMonitor.js
class ConnectionMonitor {
constructor() {
this.layers = {
transport: new TransportLayerMonitor(), // WebSocket 层
application: new ApplicationLayerMonitor(), // 应用层
business: new BusinessLayerMonitor() // 业务层
};
}

init() {
// 传输层检测:WebSocket 连接状态
this.layers.transport.on('stateChange', (state) => {
if (state === 'closed') {
this.onTransportDisconnect();
}
});

// 应用层检测:心跳机制
this.layers.application.startHeartbeat();

// 业务层检测:关键操作确认
this.layers.business.on('operationTimeout', (op) => {
console.warn('Business operation timeout:', op);
});
}

// 传输层断线(最快速检测)
onTransportDisconnect() {
// 立即进入"疑似断线"状态
GameStateManager.getInstance().setState(GameState.SUSPECTED_DISCONNECT);

// 启动快速重连尝试
GameReconnectManager.getInstance().attemptQuickReconnect();
}
}

阶段 2:现场保存与玩家托管

断线时是否需要保存现场

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SaveStateDecider.js
class SaveStateDecider {
static shouldSaveState(gameState, disconnectReason) {
// 不同游戏状态的保存策略
const savePolicy = {
[GameState.LOBBY]: false, // 大厅:无需保存
[GameState.MATCHING]: false, // 匹配中:取消匹配即可
[GameState.PLAYING]: true, // 游戏中:必须保存
[GameState.PAUSED]: true, // 已暂停:保存
[GameState.ROUND_END]: false // 回合结束:无需保存
};

// 特殊原因不保存
if (disconnectReason === DisconnectReason.KICKED) {
return false; // 被踢出,不保存
}

return savePolicy[gameState] || false;
}

static getSavePriority(gameState) {
// 返回保存优先级,用于资源紧张时的取舍
const priority = {
[GameState.PLAYING]: 100, // 最高优先级
[GameState.PAUSED]: 80,
[GameState.LOBBY]: 10
};
return priority[gameState] || 0;
}
}

现场数据保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// GameSnapshot.js
class GameSnapshot {
constructor(gameRoom, playerId) {
this.playerId = playerId;
this.timestamp = Date.now();
this.gameState = this.captureGameState(gameRoom);
this.playerState = this.capturePlayerState(gameRoom, playerId);
this.roomConfig = gameRoom.config;
}

captureGameState(room) {
return {
roomId: room.id,
round: room.currentRound,
turn: room.currentTurn,
gameData: room.gameData.serialize(),
// 公共游戏状态
board: room.board.getSnapshot(),
players: room.players.map(p => ({
id: p.id,
score: p.score,
status: p.status
})),
// 时间戳用于同步
serverTime: room.getServerTime()
};
}

capturePlayerState(room, playerId) {
const player = room.getPlayer(playerId);
return {
playerId: playerId,
handCards: player.handCards, // 手牌
resources: player.resources, // 资源
position: player.position, // 位置
buffs: player.activeBuffs, // 状态效果
privateData: player.privateData // 私有数据
};
}

// 序列化存储
serialize() {
return JSON.stringify({
playerId: this.playerId,
timestamp: this.timestamp,
gameState: this.gameState,
playerState: this.playerState,
roomConfig: this.roomConfig
});
}

// 反序列化
static deserialize(data) {
const obj = JSON.parse(data);
const snapshot = new GameSnapshot();
snapshot.playerId = obj.playerId;
snapshot.timestamp = obj.timestamp;
snapshot.gameState = obj.gameState;
snapshot.playerState = obj.playerState;
snapshot.roomConfig = obj.roomConfig;
return snapshot;
}
}

服务器端保存实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// server/GameStateManager.js
const Redis = require('ioredis');
const redis = new Redis();

class ServerGameStateManager {
constructor() {
// 保存的有效期(根据游戏类型设置)
this.TTL_PLAYING = 300; // 游戏中:5分钟
this.TTL_PAUSED = 600; // 暂停:10分钟
}

async savePlayerSnapshot(roomId, playerId, snapshot) {
const key = `game:snapshot:${roomId}:${playerId}`;

// 保存到 Redis
await redis.setex(
key,
this.getTTL(snapshot.gameState.roomState),
snapshot.serialize()
);

// 同时保存到数据库(持久化)
await this.persistToDatabase(roomId, playerId, snapshot);

console.log(`Snapshot saved for player ${playerId} in room ${roomId}`);
}

async loadPlayerSnapshot(roomId, playerId) {
const key = `game:snapshot:${roomId}:${playerId}`;

// 优先从 Redis 读取
let data = await redis.get(key);

if (!data) {
// Redis 没有,从数据库读取
data = await this.loadFromDatabase(roomId, playerId);
}

if (data) {
return GameSnapshot.deserialize(data);
}

return null;
}

async clearPlayerSnapshot(roomId, playerId) {
const key = `game:snapshot:${roomId}:${playerId}`;
await redis.del(key);
}

getTTL(roomState) {
switch (roomState) {
case 'playing': return this.TTL_PLAYING;
case 'paused': return this.TTL_PAUSED;
default: return 300;
}
}
}

玩家托管(AI 接管)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// AIProxy.js
class AIProxy {
constructor(gameRoom, playerId) {
this.room = gameRoom;
this.playerId = playerId;
this.aiLevel = 'intermediate'; // 托管 AI 级别
this.isActive = false;
this.decisionTimer = null;
}

activate() {
this.isActive = true;
console.log(`AI Proxy activated for player ${this.playerId}`);

// 监听轮到该玩家的回合
this.room.on('turnStart', (playerId) => {
if (playerId === this.playerId && this.isActive) {
this.makeDecision();
}
});
}

deactivate() {
this.isActive = false;
if (this.decisionTimer) {
clearTimeout(this.decisionTimer);
}
}

makeDecision() {
// AI 决策延迟(模拟思考时间)
const thinkTime = this.calculateThinkTime();

this.decisionTimer = setTimeout(() => {
const action = this.selectBestAction();
this.executeAction(action);
}, thinkTime);
}

selectBestAction() {
const gameState = this.room.getGameState();
const player = this.room.getPlayer(this.playerId);

// 根据 AI 级别选择策略
switch (this.aiLevel) {
case 'simple':
return this.simpleStrategy(gameState, player);
case 'intermediate':
return this.intermediateStrategy(gameState, player);
case 'advanced':
return this.advancedStrategy(gameState, player);
default:
return this.randomAction(gameState, player);
}
}

simpleStrategy(state, player) {
// 简单策略:随机选择合法动作
const legalActions = this.getLegalActions(state, player);
return legalActions[Math.floor(Math.random() * legalActions.length)];
}

intermediateStrategy(state, player) {
// 中级策略:启发式评估
const actions = this.getLegalActions(state, player);
let bestAction = actions[0];
let bestScore = -Infinity;

for (const action of actions) {
const score = this.evaluateAction(action, state, player);
if (score > bestScore) {
bestScore = score;
bestAction = action;
}
}

return bestAction;
}

executeAction(action) {
// 执行 AI 动作
this.room.processAction(this.playerId, action);
}
}

阶段 3:重连与状态恢复

重连流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// ReconnectManager.js
class GameReconnectManager {
constructor() {
this.maxRetryCount = 5;
this.currentRetry = 0;
this.retryDelays = [1000, 2000, 5000, 10000, 20000]; // 递增延迟
this.reconnectState = ReconnectState.IDLE;
}

async attemptReconnect() {
if (this.currentRetry >= this.maxRetryCount) {
this.onReconnectFailed();
return;
}

this.reconnectState = ReconnectState.RECONNECTING;
const delay = this.retryDelays[this.currentRetry] || 30000;

console.log(`Reconnect attempt ${this.currentRetry + 1}/${this.maxRetryCount} in ${delay}ms`);

setTimeout(async () => {
try {
await this.doReconnect();
} catch (error) {
console.error('Reconnect failed:', error);
this.currentRetry++;
this.attemptReconnect();
}
}, delay);
}

async doReconnect() {
// 1. 建立新的 WebSocket 连接
const socket = await this.connectWebSocket();

// 2. 身份验证
const authResult = await this.authenticate(socket);
if (!authResult.success) {
throw new Error('Authentication failed');
}

// 3. 检查是否有可恢复的游戏
const hasSavedGame = await this.checkSavedGame(authResult.userId);

if (hasSavedGame) {
// 4. 恢复游戏
await this.restoreGame(socket, authResult.userId);
} else {
// 无游戏可恢复,进入大厅
this.onReconnectToLobby();
}

this.onReconnectSuccess();
}

async restoreGame(socket, userId) {
// 向服务器请求恢复游戏
socket.emit('restoreGame', { userId }, async (response) => {
if (response.success) {
// 获取游戏快照
const snapshot = response.snapshot;

// 验证游戏是否仍在进行中
if (await this.validateGameState(snapshot)) {
// 恢复游戏状态
await this.applyGameSnapshot(snapshot);

// 同步最新状态
await this.syncLatestState(socket, response.serverTime);

this.onGameRestored(snapshot);
} else {
// 游戏已结束,显示结算
this.onGameEnded(snapshot.result);
}
} else {
// 无法恢复,进入大厅
this.onReconnectToLobby();
}
});
}

async validateGameState(snapshot) {
const now = Date.now();
const elapsed = now - snapshot.timestamp;

// 检查是否超过最大断线时间
if (elapsed > GAME_CONFIG.MAX_DISCONNECT_TIME) {
return false;
}

// 检查游戏是否已结束
if (snapshot.gameState.status === 'ended') {
return false;
}

return true;
}

async syncLatestState(socket, serverTime) {
// 请求断线期间错过的所有事件
socket.emit('syncMissedEvents', {
lastKnownTime: serverTime
}, (response) => {
// 重放错过的游戏事件
for (const event of response.missedEvents) {
this.replayEvent(event);
}
});
}
}

// 重连状态枚举
const ReconnectState = {
IDLE: 'idle',
DISCONNECTED: 'disconnected',
RECONNECTING: 'reconnecting',
RESTORING: 'restoring',
SYNCING: 'syncing',
RESTORED: 'restored',
FAILED: 'failed'
};

状态同步与追赶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// StateSyncManager.js
class StateSyncManager {
constructor() {
this.eventLog = []; // 事件日志
this.pendingEvents = []; // 待处理事件
this.syncCheckpoint = null; // 同步检查点
}

// 记录游戏事件
recordEvent(event) {
const logEntry = {
sequence: this.eventLog.length + 1,
timestamp: Date.now(),
event: event,
stateHash: this.calculateStateHash()
};
this.eventLog.push(logEntry);

// 限制日志大小
if (this.eventLog.length > 1000) {
this.eventLog = this.eventLog.slice(-500);
}
}

// 获取断线期间的事件
getMissedEvents(lastKnownSequence) {
return this.eventLog.filter(e => e.sequence > lastKnownSequence);
}

// 客户端追赶同步
async catchUp(missedEvents) {
for (const entry of missedEvents) {
// 验证状态一致性
const currentHash = this.calculateStateHash();
if (entry.stateHash && entry.stateHash !== currentHash) {
console.warn('State mismatch detected, requesting full sync');
await this.requestFullSync();
return;
}

// 应用事件
this.applyEvent(entry.event);

// 每 10 个事件渲染一次,避免卡顿
if (entry.sequence % 10 === 0) {
await this.renderFrame();
}
}
}

// 快速同步(直接应用最终状态)
async fastSync(targetState) {
// 停止当前游戏逻辑
GameEngine.getInstance().pause();

// 直接应用目标状态
GameState.getInstance().apply(targetState);

// 恢复游戏
GameEngine.getInstance().resume();
}
}

超时与逃跑判定

重连超时机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ReconnectTimeoutManager.js
class ReconnectTimeoutManager {
constructor() {
// 不同游戏模式的超时配置
this.timeouts = {
casual: {
decision: 30000, // 操作超时:30秒
reconnect: 120000, // 重连超时:2分钟
totalOffline: 300000 // 累计离线:5分钟
},
ranked: {
decision: 20000,
reconnect: 60000,
totalOffline: 180000
},
tournament: {
decision: 15000,
reconnect: 30000,
totalOffline: 60000
}
};
}

startTimeout(room, playerId, mode = 'casual') {
const config = this.timeouts[mode];

// 设置重连超时定时器
const timer = setTimeout(() => {
this.onReconnectTimeout(room, playerId);
}, config.reconnect);

// 保存定时器引用
room.setPlayerTimeout(playerId, 'reconnect', timer);
}

onReconnectTimeout(room, playerId) {
const player = room.getPlayer(playerId);

// 判定为逃跑
player.status = PlayerStatus.ABANDONED;

// 触发逃跑处理
room.handlePlayerAbandon(playerId);

// 清除保存的状态
ServerGameStateManager.getInstance().clearPlayerSnapshot(room.id, playerId);

console.log(`Player ${playerId} marked as abandoned in room ${room.id}`);
}
}

逃跑惩罚机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// AbandonPenaltyManager.js
class AbandonPenaltyManager {
constructor() {
this.penaltyRules = {
casual: {
scorePenalty: 0, // 休闲模式无扣分
banDuration: 0, // 无禁赛
warning: true // 警告提示
},
ranked: {
scorePenalty: 50, // 排位扣分
banDuration: 300000, // 禁赛5分钟(首次)
progressiveBan: true // 累进惩罚
},
tournament: {
scorePenalty: 100,
banDuration: 86400000, // 禁赛24小时
disqualification: true // 取消参赛资格
}
};
}

async applyPenalty(playerId, gameMode, abandonHistory) {
const rules = this.penaltyRules[gameMode];

// 计算惩罚等级
const penaltyLevel = this.calculatePenaltyLevel(abandonHistory);

const penalty = {
playerId: playerId,
timestamp: Date.now(),
mode: gameMode,
scoreDeduction: rules.scorePenalty * penaltyLevel.multiplier,
banDuration: rules.progressiveBan
? rules.banDuration * penaltyLevel.multiplier
: rules.banDuration,
reason: 'connection_abandoned'
};

// 应用惩罚
await PlayerService.deductScore(playerId, penalty.scoreDeduction);
await PlayerService.banPlayer(playerId, penalty.banDuration);

// 记录逃跑历史
await PlayerService.recordAbandon(playerId);

return penalty;
}

calculatePenaltyLevel(history) {
const recentAbandons = history.filter(
a => Date.now() - a.timestamp < 86400000 * 7 // 最近7天
);

if (recentAbandons.length === 0) return { level: 0, multiplier: 1 };
if (recentAbandons.length < 3) return { level: 1, multiplier: 1 };
if (recentAbandons.length < 5) return { level: 2, multiplier: 2 };

return { level: 3, multiplier: 5 }; // 最高级别
}
}

客户端 UI 处理

断线提示与恢复界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// ReconnectUI.js
class ReconnectUI {
constructor() {
this.reconnectLayer = null;
this.countdownLabel = null;
}

showReconnecting(remainingTime) {
if (!this.reconnectLayer) {
this.createReconnectLayer();
}

this.reconnectLayer.visible = true;
this.updateCountdown(remainingTime);

// 显示旋转加载动画
this.showLoadingAnimation();

// 隐藏游戏操作按钮
this.hideGameControls();
}

createReconnectLayer() {
// 半透明遮罩
const mask = new cc.LayerColor(cc.color(0, 0, 0, 180));
this.reconnectLayer.addChild(mask);

// 重连提示面板
const panel = new cc.Sprite('reconnect_panel.png');
panel.setPosition(cc.winSize.width / 2, cc.winSize.height / 2);
this.reconnectLayer.addChild(panel);

// 提示文字
const title = new cc.LabelTTF('Connection Lost', 'Arial', 32);
title.setPosition(panel.width / 2, panel.height * 0.7);
title.setColor(cc.color(255, 255, 255));
panel.addChild(title);

// 倒计时
this.countdownLabel = new cc.LabelTTF('', 'Arial', 24);
this.countdownLabel.setPosition(panel.width / 2, panel.height * 0.5);
panel.addChild(this.countdownLabel);

// 手动重连按钮
const reconnectBtn = new cc.MenuItemSprite(
new cc.Sprite('btn_reconnect.png'),
new cc.Sprite('btn_reconnect_sel.png'),
this.onManualReconnect,
this
);

const menu = new cc.Menu(reconnectBtn);
menu.setPosition(panel.width / 2, panel.height * 0.3);
panel.addChild(menu);
}

updateCountdown(remainingSeconds) {
if (this.countdownLabel) {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
this.countdownLabel.setString(
`Reconnecting... ${minutes}:${seconds.toString().padStart(2, '0')}`
);
}
}

onReconnectSuccess() {
// 隐藏重连界面
this.reconnectLayer.visible = false;

// 显示恢复提示
this.showToast('Reconnected! Resuming game...');

// 恢复游戏控制
this.showGameControls();
}

onReconnectFailed(reason) {
this.reconnectLayer.visible = false;

// 显示失败提示
const msg = reason === 'timeout'
? 'Reconnection timeout. You have been marked as abandoned.'
: 'Failed to reconnect. Please check your network.';

this.showAlert(msg, () => {
// 返回大厅
cc.director.runScene(new LobbyScene());
});
}
}

写在最后

一个完善的断线重连机制需要涵盖以下环节:

  1. 断线检测

    • 多层级的检测机制(传输层、应用层、业务层)
    • 心跳机制实现快速发现
  2. 现场保存

    • 根据游戏状态决定是否保存
    • 分层存储策略(Redis + 数据库)
    • 敏感数据加密保护
  3. 玩家托管

    • AI 接管保持游戏进行
    • 托管期间的操作记录
  4. 重连恢复

    • 递增延迟的重连策略
    • 状态验证与快速同步
    • 事件追赶机制
  5. 超时处理

    • 合理的超时配置
    • 逃跑判定与惩罚机制
  6. 用户体验

    • 清晰的断线提示
    • 透明的恢复进度
    • 友好的失败处理

通过这些机制的配合,可以最大程度减少断线对玩家体验的影响,确保游戏的公平性和流畅性。