喜迎
春节

状态同步与帧同步:多人游戏网络同步技术深度解析


如何让分布在世界各地的玩家在虚拟世界中实现流畅的多人互动?状态同步和帧同步提供了两种不同的技术路径,各自在游戏开发中扮演着重要角色。

问题背景

在多人游戏开发中,网络同步是核心技术挑战:

  • 网络延迟:玩家之间的网络延迟从几十毫秒到几百毫秒不等
  • 数据一致性:确保所有客户端看到相同的游戏状态
  • 性能优化:在带宽和计算资源有限的情况下实现流畅体验
  • 作弊防护:防止客户端篡改游戏逻辑
  • 断线重连:玩家掉线后能够重新加入游戏

基本概念

核心思想对比

特性 状态同步 帧同步
同步内容 游戏状态(位置、血量等) 玩家操作指令
计算位置 服务端权威计算 客户端各自计算
数据量 较大(同步状态数据) 较小(同步操作指令)
确定性 依赖服务端状态 依赖确定性逻辑
适用类型 MMO、FPS、开放世界 RTS、MOBA、棋牌

状态同步 (State Synchronization)

核心原理

服务器作为游戏世界的权威,计算所有游戏逻辑,客户端只负责渲染和输入采集。

架构设计

1
2
客户端A → 输入 → 服务器 → 状态更新 → 所有客户端
客户端B → 输入 → 服务器 → 状态更新 → 所有客户端

PHP实现示例

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
<?php

/**
* 状态同步游戏服务器
*/
class StateSyncGameServer {
private $clients;
private $gameState;
private $updateInterval;
private $lastUpdateTime;

public function __construct($updateRate = 10) {
$this->clients = [];
$this->gameState = [
'players' => [],
'objects' => [],
'timestamp' => microtime(true)
];
$this->updateInterval = 1.0 / $updateRate; // 更新频率
$this->lastUpdateTime = microtime(true);
}

/**
* 处理客户端连接
*/
public function handleClientConnect($clientId, $playerData) {
$this->clients[$clientId] = [
'id' => $clientId,
'player_id' => $playerData['player_id'],
'last_heartbeat' => microtime(true),
'pending_commands' => []
];

// 初始化玩家状态
$this->gameState['players'][$playerData['player_id']] = [
'id' => $playerData['player_id'],
'position' => ['x' => 0, 'y' => 0, 'z' => 0],
'rotation' => ['x' => 0, 'y' => 0, 'z' => 0],
'health' => 100,
'last_input_seq' => 0,
'velocity' => ['x' => 0, 'y' => 0, 'z' => 0]
];

return $this->getFullState(); // 返回完整状态给新客户端
}

/**
* 处理客户端输入
*/
public function handleClientInput($clientId, $inputData) {
if (!isset($this->clients[$clientId])) {
return false;
}

$playerId = $this->clients[$clientId]['player_id'];
$inputSeq = $inputData['sequence'];

// 检查输入序列号(防止乱序和重复)
if ($inputSeq <= $this->gameState['players'][$playerId]['last_input_seq']) {
return false; // 旧输入,忽略
}

$this->clients[$clientId]['pending_commands'][] = [
'sequence' => $inputSeq,
'type' => $inputData['type'],
'data' => $inputData['data'],
'timestamp' => $inputData['timestamp']
];

$this->gameState['players'][$playerId]['last_input_seq'] = $inputSeq;

return true;
}

/**
* 服务器更新循环
*/
public function update() {
$currentTime = microtime(true);
$deltaTime = $currentTime - $this->lastUpdateTime;

if ($deltaTime >= $this->updateInterval) {
$this->processInputs();
$this->updateGameLogic($deltaTime);
$this->broadcastStateUpdate();

$this->lastUpdateTime = $currentTime;
return true;
}

return false;
}

/**
* 处理所有待处理输入
*/
private function processInputs() {
foreach ($this->clients as $clientId => &$client) {
$playerId = $client['player_id'];
$player = &$this->gameState['players'][$playerId];

foreach ($client['pending_commands'] as $command) {
$this->applyPlayerInput($player, $command);
}

$client['pending_commands'] = []; // 清空已处理命令
}
}

/**
* 应用玩家输入到游戏状态
*/
private function applyPlayerInput(&$player, $command) {
switch ($command['type']) {
case 'move':
$this->handleMoveInput($player, $command['data']);
break;
case 'jump':
$this->handleJumpInput($player, $command['data']);
break;
case 'attack':
$this->handleAttackInput($player, $command['data']);
break;
// 其他输入类型...
}
}

/**
* 处理移动输入
*/
private function handleMoveInput(&$player, $moveData) {
$speed = 5.0; // 移动速度

$player['velocity']['x'] = $moveData['x'] * $speed;
$player['velocity']['z'] = $moveData['z'] * $speed;

if (abs($moveData['x']) > 0.1 || abs($moveData['z']) > 0.1) {
$player['rotation']['y'] = atan2($moveData['x'], $moveData['z']);
}
}

/**
* 处理跳跃输入
*/
private function handleJumpInput(&$player, $jumpData) {
if ($player['position']['y'] == 0) { // 在地面上
$player['velocity']['y'] = 10.0; // 跳跃速度
}
}

/**
* 处理攻击输入
*/
private function handleAttackInput(&$player, $attackData) {
// 在服务器上验证攻击逻辑
$targetId = $attackData['target_id'];
if (isset($this->gameState['players'][$targetId])) {
$this->gameState['players'][$targetId]['health'] -= 10;

if ($this->gameState['players'][$targetId]['health'] <= 0) {
$this->handlePlayerDeath($targetId);
}
}
}

/**
* 更新游戏逻辑
*/
private function updateGameLogic($deltaTime) {
// 应用物理
foreach ($this->gameState['players'] as &$player) {
// 更新位置
$player['position']['x'] += $player['velocity']['x'] * $deltaTime;
$player['position']['y'] += $player['velocity']['y'] * $deltaTime;
$player['position']['z'] += $player['velocity']['z'] * $deltaTime;

// 简单重力
if ($player['position']['y'] > 0) {
$player['velocity']['y'] -= 20.0 * $deltaTime; // 重力
} else {
$player['position']['y'] = 0;
$player['velocity']['y'] = 0;
}

// 摩擦力
$player['velocity']['x'] *= 0.9;
$player['velocity']['z'] *= 0.9;
}

$this->gameState['timestamp'] = microtime(true);
}

/**
* 广播状态更新给所有客户端
*/
private function broadcastStateUpdate() {
$stateUpdate = $this->getDeltaState(); // 获取增量状态

foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'state_update', $stateUpdate);
}
}

/**
* 获取完整游戏状态(用于新客户端同步)
*/
public function getFullState() {
return $this->gameState;
}

/**
* 获取增量状态(用于常规更新)
*/
public function getDeltaState() {
// 简化实现:返回完整状态
// 实际中应该只返回变化的部分
return [
'players' => $this->gameState['players'],
'timestamp' => $this->gameState['timestamp']
];
}

/**
* 处理玩家死亡
*/
private function handlePlayerDeath($playerId) {
$this->gameState['players'][$playerId]['health'] = 100;
$this->gameState['players'][$playerId]['position'] = ['x' => 0, 'y' => 0, 'z' => 0];

// 广播玩家死亡事件
$this->broadcastEvent('player_died', [
'player_id' => $playerId,
'respawn_time' => 3
]);
}

/**
* 广播事件给所有客户端
*/
private function broadcastEvent($eventType, $eventData) {
foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'game_event', [
'type' => $eventType,
'data' => $eventData
]);
}
}

/**
* 发送数据到客户端(模拟)
*/
private function sendToClient($clientId, $messageType, $data) {
// 实际实现中会通过WebSocket或其他网络协议发送
echo "发送到客户端 {$clientId}: {$messageType}\n";
// 网络发送逻辑...
}
}

/**
* 状态同步客户端
*/
class StateSyncGameClient {
private $playerId;
private $server;
private $localState;
private $pendingInputs;
private $inputSequence;
private $serverTimeOffset;

public function __construct($playerId, $server) {
$this->playerId = $playerId;
$this->server = $server;
$this->localState = [];
$this->pendingInputs = [];
$this->inputSequence = 0;
$this->serverTimeOffset = 0;

// 连接服务器
$this->connectToServer();
}

/**
* 连接到服务器
*/
private function connectToServer() {
$fullState = $this->server->handleClientConnect(uniqid(), [
'player_id' => $this->playerId
]);

$this->localState = $fullState;
echo "客户端 {$this->playerId} 已连接,获取完整状态\n";
}

/**
* 处理玩家输入
*/
public function handleInput($inputType, $inputData) {
$this->inputSequence++;

$inputPacket = [
'sequence' => $this->inputSequence,
'type' => $inputType,
'data' => $inputData,
'timestamp' => $this->getClientTime()
];

// 立即在本地预测执行
$this->applyLocalPrediction($inputType, $inputData);

// 发送到服务器
$this->server->handleClientInput($this->getClientId(), $inputPacket);

// 保存输入用于可能的回滚
$this->pendingInputs[$this->inputSequence] = $inputPacket;
}

/**
* 应用本地预测
*/
private function applyLocalPrediction($inputType, $inputData) {
$player = &$this->localState['players'][$this->playerId];

switch ($inputType) {
case 'move':
$speed = 5.0;
$player['velocity']['x'] = $inputData['x'] * $speed;
$player['velocity']['z'] = $inputData['z'] * $speed;
break;
// 其他输入类型的本地预测...
}
}

/**
* 接收服务器状态更新
*/
public function receiveStateUpdate($serverState) {
// reconciliation:与服务器状态同步
$this->reconcileWithServer($serverState);
}

/**
* 与服务器状态协调
*/
private function reconcileWithServer($serverState) {
$serverPlayer = $serverState['players'][$this->playerId];
$localPlayer = $this->localState['players'][$this->playerId];

// 如果位置差异过大,采用服务器状态
$positionDiff = $this->calculateDistance($serverPlayer['position'], $localPlayer['position']);

if ($positionDiff > 1.0) { // 阈值
echo "客户端 {$this->playerId}: 位置差异过大({$positionDiff}),采用服务器状态\n";
$this->localState = $serverState;
} else {
// 微小差异,平滑插值
$this->interpolateToServerState($serverState);
}

// 移除已确认的输入
$lastServerSeq = $serverPlayer['last_input_seq'];
$this->removeAcknowledgedInputs($lastServerSeq);
}

/**
* 插值到服务器状态
*/
private function interpolateToServerState($serverState) {
$interpolationFactor = 0.2; // 插值系数

foreach ($serverState['players'] as $playerId => $serverPlayer) {
if (!isset($this->localState['players'][$playerId])) {
$this->localState['players'][$playerId] = $serverPlayer;
continue;
}

$localPlayer = &$this->localState['players'][$playerId];

// 对其他玩家使用插值
if ($playerId != $this->playerId) {
$localPlayer['position']['x'] = $this->lerp(
$localPlayer['position']['x'],
$serverPlayer['position']['x'],
$interpolationFactor
);
// 对其他坐标和旋转也进行插值...
}
}
}

/**
* 移除已确认的输入
*/
private function removeAcknowledgedInputs($lastServerSeq) {
foreach ($this->pendingInputs as $seq => $input) {
if ($seq <= $lastServerSeq) {
unset($this->pendingInputs[$seq]);
}
}
}

/**
* 客户端更新循环
*/
public function update($deltaTime) {
// 更新本地预测的状态
$this->updateLocalState($deltaTime);
}

/**
* 更新本地状态
*/
private function updateLocalState($deltaTime) {
foreach ($this->localState['players'] as &$player) {
// 应用物理(与服务器相同的逻辑)
$player['position']['x'] += $player['velocity']['x'] * $deltaTime;
$player['position']['y'] += $player['velocity']['y'] * $deltaTime;
$player['position']['z'] += $player['velocity']['z'] * $deltaTime;

if ($player['position']['y'] > 0) {
$player['velocity']['y'] -= 20.0 * $deltaTime;
} else {
$player['position']['y'] = 0;
$player['velocity']['y'] = 0;
}

$player['velocity']['x'] *= 0.9;
$player['velocity']['z'] *= 0.9;
}
}

// 辅助方法
private function getClientTime() {
return microtime(true) + $this->serverTimeOffset;
}

private function getClientId() {
return "client_{$this->playerId}";
}

private function calculateDistance($pos1, $pos2) {
$dx = $pos1['x'] - $pos2['x'];
$dy = $pos1['y'] - $pos2['y'];
$dz = $pos1['z'] - $pos2['z'];
return sqrt($dx*$dx + $dy*$dy + $dz*$dz);
}

private function lerp($a, $b, $f) {
return $a + $f * ($b - $a);
}

/**
* 获取本地状态(用于渲染)
*/
public function getRenderState() {
return $this->localState;
}
}

?>

帧同步 (Frame Synchronization)

核心原理

所有客户端运行相同的确定性逻辑,服务器只负责转发输入指令,不进行游戏逻辑计算。

架构设计

1
2
3
客户端A → 操作指令 → 服务器 → 转发指令 → 所有客户端
客户端B → 操作指令 → 服务器 → 转发指令 → 所有客户端
所有客户端运行相同的确定性逻辑

PHP实现示例

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
<?php

/**
* 帧同步游戏服务器
*/
class FrameSyncGameServer {
private $clients;
private $currentFrame;
private $frameInputs;
private $lockstepInterval;
private $lastFrameTime;

public function __construct($frameRate = 10) {
$this->clients = [];
$this->currentFrame = 0;
$this->frameInputs = [];
$this->lockstepInterval = 1.0 / $frameRate;
$this->lastFrameTime = microtime(true);
}

/**
* 处理客户端连接
*/
public function handleClientConnect($clientId, $playerData) {
$this->clients[$clientId] = [
'id' => $clientId,
'player_id' => $playerData['player_id'],
'last_heartbeat' => microtime(true),
'ready' => true
];

echo "客户端 {$clientId} 已连接\n";
return $this->currentFrame; // 返回当前帧号
}

/**
* 接收客户端帧输入
*/
public function receiveFrameInput($clientId, $frameInput) {
if (!isset($this->clients[$clientId])) {
return false;
}

$frame = $frameInput['frame'];
$playerId = $this->clients[$clientId]['player_id'];

// 初始化帧输入存储
if (!isset($this->frameInputs[$frame])) {
$this->frameInputs[$frame] = [];
}

// 存储玩家输入
$this->frameInputs[$frame][$playerId] = [
'inputs' => $frameInput['inputs'],
'checksum' => $frameInput['checksum'],
'timestamp' => $frameInput['timestamp']
];

echo "收到客户端 {$clientId}{$frame} 的输入\n";
return true;
}

/**
* 服务器帧同步更新
*/
public function update() {
$currentTime = microtime(true);

// 检查是否应该推进到下一帧
if ($currentTime - $this->lastFrameTime >= $this->lockstepInterval) {
return $this->advanceFrame();
}

return false;
}

/**
* 推进到下一帧
*/
private function advanceFrame() {
$targetFrame = $this->currentFrame + 1;

// 检查是否收集到所有客户端的输入
if ($this->hasAllInputsForFrame($targetFrame)) {
// 广播这一帧的所有输入给所有客户端
$this->broadcastFrameInputs($targetFrame);

$this->currentFrame = $targetFrame;
$this->lastFrameTime = microtime(true);

// 清理旧的帧数据(保留最近几帧用于断线重连)
$this->cleanupOldFrames();

echo "推进到帧 {$targetFrame}\n";
return true;
} else {
echo "等待帧 {$targetFrame} 的完整输入...\n";
return false;
}
}

/**
* 检查是否收集到某帧的所有输入
*/
private function hasAllInputsForFrame($frame) {
if (!isset($this->frameInputs[$frame])) {
return false;
}

// 检查每个已连接的客户端是否都提供了输入
foreach ($this->clients as $client) {
$playerId = $client['player_id'];
if (!isset($this->frameInputs[$frame][$playerId])) {
return false;
}
}

return true;
}

/**
* 广播帧输入给所有客户端
*/
private function broadcastFrameInputs($frame) {
$frameData = [
'frame' => $frame,
'inputs' => $this->frameInputs[$frame]
];

foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'frame_inputs', $frameData);
}
}

/**
* 清理旧的帧数据
*/
private function cleanupOldFrames() {
$framesToKeep = 60; // 保留最近60帧用于断线重连

foreach ($this->frameInputs as $frame => $inputs) {
if ($frame < $this->currentFrame - $framesToKeep) {
unset($this->frameInputs[$frame]);
}
}
}

/**
* 处理客户端断线重连
*/
public function handleClientReconnect($clientId, $lastKnownFrame) {
if (!isset($this->clients[$clientId])) {
return false;
}

$catchUpData = [];

// 提供缺失的帧输入
for ($frame = $lastKnownFrame + 1; $frame <= $this->currentFrame; $frame++) {
if (isset($this->frameInputs[$frame])) {
$catchUpData[$frame] = $this->frameInputs[$frame];
}
}

$this->sendToClient($clientId, 'catch_up', [
'from_frame' => $lastKnownFrame + 1,
'to_frame' => $this->currentFrame,
'inputs' => $catchUpData
]);

echo "客户端 {$clientId} 从帧 {$lastKnownFrame} 追赶至 {$this->currentFrame}\n";
return true;
}

/**
* 发送数据到客户端(模拟)
*/
private function sendToClient($clientId, $messageType, $data) {
// 实际实现中的网络发送逻辑
echo "发送到客户端 {$clientId}: {$messageType}\n";
}

/**
* 获取当前帧号
*/
public function getCurrentFrame() {
return $this->currentFrame;
}
}

/**
* 帧同步游戏客户端
*/
class FrameSyncGameClient {
private $playerId;
private $server;
private $currentFrame;
private $gameLogic;
private $pendingInputs;
private $confirmedFrames;
private $clientId;

public function __construct($playerId, $server) {
$this->playerId = $playerId;
$this->server = $server;
$this->currentFrame = 0;
$this->gameLogic = new DeterministicGameLogic();
$this->pendingInputs = [];
$this->confirmedFrames = [];
$this->clientId = "client_{$playerId}";

// 连接服务器
$this->connectToServer();
}

/**
* 连接到服务器
*/
private function connectToServer() {
$startFrame = $this->server->handleClientConnect($this->clientId, [
'player_id' => $this->playerId
]);

$this->currentFrame = $startFrame;
echo "客户端 {$this->playerId} 已连接,起始帧: {$startFrame}\n";
}

/**
* 处理玩家输入
*/
public function handleInput($inputType, $inputData) {
$frame = $this->currentFrame + 1; // 下一帧的输入

if (!isset($this->pendingInputs[$frame])) {
$this->pendingInputs[$frame] = [];
}

$this->pendingInputs[$frame][] = [
'type' => $inputType,
'data' => $inputData,
'player_id' => $this->playerId
];

echo "客户端 {$this->playerId} 为帧 {$frame} 记录输入: {$inputType}\n";
}

/**
* 客户端更新循环
*/
public function update() {
// 发送待处理的帧输入到服务器
$this->sendPendingInputs();

// 检查是否有可以执行的确认帧
$this->executeConfirmedFrames();
}

/**
* 发送待处理的输入到服务器
*/
private function sendPendingInputs() {
foreach ($this->pendingInputs as $frame => $inputs) {
// 只发送尚未确认的帧
if (!isset($this->confirmedFrames[$frame])) {
$checksum = $this->calculateChecksum($inputs);

$this->server->receiveFrameInput($this->clientId, [
'frame' => $frame,
'inputs' => $inputs,
'checksum' => $checksum,
'timestamp' => microtime(true)
]);
}
}
}

/**
* 执行已确认的帧
*/
private function executeConfirmedFrames() {
ksort($this->confirmedFrames);

foreach ($this->confirmedFrames as $frame => $frameInputs) {
if ($frame > $this->currentFrame) {
echo "客户端 {$this->playerId} 执行帧 {$frame}\n";

// 使用确定性逻辑执行这一帧
$this->gameLogic->executeFrame($frame, $frameInputs);

$this->currentFrame = $frame;

// 移除已执行的输入
unset($this->pendingInputs[$frame]);
unset($this->confirmedFrames[$frame]);
}
}
}

/**
* 接收服务器帧输入广播
*/
public function receiveFrameInputs($frameData) {
$frame = $frameData['frame'];
$allInputs = $frameData['inputs'];

// 验证帧数据完整性
if ($this->validateFrameInputs($frame, $allInputs)) {
$this->confirmedFrames[$frame] = $allInputs;
echo "客户端 {$this->playerId} 确认帧 {$frame} 输入\n";
} else {
echo "客户端 {$this->playerId}{$frame} 输入验证失败\n";
}
}

/**
* 接收追赶数据(断线重连)
*/
public function receiveCatchUpData($catchUpData) {
$fromFrame = $catchUpData['from_frame'];
$toFrame = $catchUpData['to_frame'];
$inputs = $catchUpData['inputs'];

echo "客户端 {$this->playerId} 开始追赶: 帧 {$fromFrame} -> {$toFrame}\n";

// 快速执行缺失的帧
for ($frame = $fromFrame; $frame <= $toFrame; $frame++) {
if (isset($inputs[$frame])) {
$this->gameLogic->executeFrame($frame, $inputs[$frame]);
echo "客户端 {$this->playerId} 追赶执行帧 {$frame}\n";
}
}

$this->currentFrame = $toFrame;
echo "客户端 {$this->playerId} 追赶完成,当前帧: {$toFrame}\n";
}

/**
* 验证帧输入完整性
*/
private function validateFrameInputs($frame, $allInputs) {
// 检查是否有所有玩家的输入
$expectedPlayers = ['player1', 'player2']; // 实际中应该从服务器获取

foreach ($expectedPlayers as $playerId) {
if (!isset($allInputs[$playerId])) {
return false;
}
}

// 验证校验和(可选)
foreach ($allInputs as $playerId => $inputData) {
$calculatedChecksum = $this->calculateChecksum($inputData['inputs']);
if ($calculatedChecksum != $inputData['checksum']) {
return false;
}
}

return true;
}

/**
* 计算输入校验和
*/
private function calculateChecksum($inputs) {
return md5(serialize($inputs));
}

/**
* 获取当前游戏状态(用于渲染)
*/
public function getRenderState() {
return $this->gameLogic->getCurrentState();
}

/**
* 获取当前帧号
*/
public function getCurrentFrame() {
return $this->currentFrame;
}
}

/**
* 确定性游戏逻辑
*/
class DeterministicGameLogic {
private $gameState;

public function __construct() {
$this->gameState = [
'players' => [
'player1' => ['x' => 0, 'y' => 0, 'health' => 100],
'player2' => ['x' => 10, 'y' => 0, 'health' => 100]
],
'projectiles' => []
];
}

/**
* 执行一帧游戏逻辑
*/
public function executeFrame($frame, $frameInputs) {
// 处理所有玩家输入
foreach ($frameInputs as $playerId => $inputData) {
$this->processPlayerInputs($playerId, $inputData['inputs']);
}

// 更新游戏逻辑
$this->updateGameLogic();

// 生成帧校验和(用于调试和验证)
$this->generateFrameChecksum($frame);
}

/**
* 处理玩家输入
*/
private function processPlayerInputs($playerId, $inputs) {
$player = &$this->gameState['players'][$playerId];

foreach ($inputs as $input) {
switch ($input['type']) {
case 'move':
$this->handleMove($player, $input['data']);
break;
case 'attack':
$this->handleAttack($playerId, $input['data']);
break;
// 其他输入类型...
}
}
}

/**
* 处理移动
*/
private function handleMove(&$player, $moveData) {
$speed = 0.5;
$player['x'] += $moveData['x'] * $speed;
$player['y'] += $moveData['y'] * $speed;
}

/**
* 处理攻击
*/
private function handleAttack($attackerId, $attackData) {
$targetId = $attackData['target_id'];

if (isset($this->gameState['players'][$targetId])) {
$this->gameState['players'][$targetId]['health'] -= 10;

if ($this->gameState['players'][$targetId]['health'] <= 0) {
$this->handlePlayerDeath($targetId);
}
}
}

/**
* 处理玩家死亡
*/
private function handlePlayerDeath($playerId) {
$this->gameState['players'][$playerId]['health'] = 100;
$this->gameState['players'][$playerId]['x'] = 0;
$this->gameState['players'][$playerId]['y'] = 0;
}

/**
* 更新游戏逻辑
*/
private function updateGameLogic() {
// 更新抛射物等
// 碰撞检测
// 其他游戏逻辑...
}

/**
* 生成帧校验和
*/
private function generateFrameChecksum($frame) {
$checksum = md5(serialize($this->gameState));
echo "帧 {$frame} 校验和: {$checksum}\n";
return $checksum;
}

/**
* 获取当前游戏状态
*/
public function getCurrentState() {
return $this->gameState;
}
}

?>

应用示例与对比测试

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php

class NetworkSyncExamples {

/**
* 状态同步演示
*/
public static function stateSyncDemo() {
echo "=== 状态同步演示 ===\n";

// 创建服务器
$server = new StateSyncGameServer(10); // 10Hz 更新率

// 创建两个客户端
$client1 = new StateSyncGameClient('player1', $server);
$client2 = new StateSyncGameClient('player2', $server);

// 模拟游戏循环
for ($i = 0; $i < 5; $i++) {
echo "\n--- 回合 {$i} ---\n";

// 客户端1输入
$client1->handleInput('move', ['x' => 1, 'z' => 0]);

// 服务器更新
$server->update();

// 客户端更新
$client1->update(0.1);
$client2->update(0.1);

// 显示状态
$state1 = $client1->getRenderState();
$state2 = $client2->getRenderState();

echo "客户端1位置: " . json_encode($state1['players']['player1']['position']) . "\n";
echo "客户端2位置: " . json_encode($state2['players']['player1']['position']) . "\n";

usleep(100000); // 100ms
}
}

/**
* 帧同步演示
*/
public static function frameSyncDemo() {
echo "\n=== 帧同步演示 ===\n";

// 创建服务器
$server = new FrameSyncGameServer(2); // 2Hz 帧率(便于演示)

// 创建两个客户端
$client1 = new FrameSyncGameClient('player1', $server);
$client2 = new FrameSyncGameClient('player2', $server);

// 模拟游戏循环
for ($i = 0; $i < 10; $i++) {
echo "\n--- 时间点 {$i} ---\n";

// 客户端输入
if ($i % 2 == 0) {
$client1->handleInput('move', ['x' => 1, 'y' => 0]);
$client2->handleInput('move', ['x' => -1, 'y' => 0]);
}

// 客户端更新
$client1->update();
$client2->update();

// 服务器更新
$server->update();

// 显示状态
$frame1 = $client1->getCurrentFrame();
$frame2 = $client2->getCurrentFrame();
$state1 = $client1->getRenderState();

echo "客户端1帧: {$frame1}, 客户端2帧: {$frame2}\n";
echo "玩家1位置: ({$state1['players']['player1']['x']}, {$state1['players']['player1']['y']})\n";

usleep(500000); // 500ms
}
}

/**
* 性能对比测试
*/
public static function performanceComparison() {
echo "\n=== 性能对比测试 ===\n";

// 状态同步性能测试
$stateServer = new StateSyncGameServer(20);
$startTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
$stateServer->update();
}

$stateSyncTime = microtime(true) - $startTime;

// 帧同步性能测试
$frameServer = new FrameSyncGameServer(20);
$startTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
$frameServer->update();
}

$frameSyncTime = microtime(true) - $startTime;

echo "状态同步 100次更新: " . number_format($stateSyncTime * 1000, 2) . "ms\n";
echo "帧同步 100次更新: " . number_format($frameSyncTime * 1000, 2) . "ms\n";
echo "性能差异: " . number_format(($stateSyncTime - $frameSyncTime) / $stateSyncTime * 100, 2) . "%\n";
}

/**
* 网络延迟模拟测试
*/
public static function latencyTest() {
echo "\n=== 网络延迟测试 ===\n";

$server = new StateSyncGameServer(10);
$client = new StateSyncGameClient('test_player', $server);

// 模拟不同网络延迟下的表现
$latencies = [50, 100, 200, 500]; // 毫秒

foreach ($latencies as $latency) {
echo "模拟 {$latency}ms 延迟:\n";

// 这里实际应该模拟网络延迟
$client->handleInput('move', ['x' => 1, 'z' => 0]);
$server->update();
$client->update(0.1);

$state = $client->getRenderState();
echo "玩家位置: " . json_encode($state['players']['test_player']['position']) . "\n";
}
}
}

// 运行演示
NetworkSyncExamples::stateSyncDemo();
NetworkSyncExamples::frameSyncDemo();
NetworkSyncExamples::performanceComparison();
NetworkSyncExamples::latencyTest();

?>

高级优化技术

1. 预测与回滚 (Prediction and Rollback)

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
<?php

/**
* 带预测回滚的状态同步
*/
class PredictiveStateSync extends StateSyncGameServer {
private $historyStates;
private $historySize;

public function __construct($updateRate = 10, $historySize = 60) {
parent::__construct($updateRate);
$this->historyStates = [];
$this->historySize = $historySize;
}

/**
* 保存历史状态
*/
protected function updateGameLogic($deltaTime) {
parent::updateGameLogic($deltaTime);

// 保存历史状态用于回滚
$this->saveHistoryState();
}

private function saveHistoryState() {
$this->historyStates[] = [
'timestamp' => $this->gameState['timestamp'],
'state' => serialize($this->gameState) // 深拷贝
];

// 限制历史记录大小
if (count($this->historyStates) > $this->historySize) {
array_shift($this->historyStates);
}
}

/**
* 处理延迟的输入(回滚并重新模拟)
*/
public function handleDelayedInput($clientId, $inputData) {
$inputTime = $inputData['timestamp'];
$playerId = $this->clients[$clientId]['player_id'];

// 找到输入应该应用的时间点
$targetStateIndex = $this->findStateByTimestamp($inputTime);

if ($targetStateIndex !== null) {
// 回滚到目标状态
$this->rollbackToState($targetStateIndex);

// 重新应用输入并模拟
$this->reapplyInputsFrom($targetStateIndex, $playerId, $inputData);

return true;
}

return false;
}

private function findStateByTimestamp($timestamp) {
foreach ($this->historyStates as $index => $history) {
if ($history['timestamp'] >= $timestamp) {
return $index;
}
}
return null;
}

private function rollbackToState($stateIndex) {
$targetState = $this->historyStates[$stateIndex];
$this->gameState = unserialize($targetState['state']);

// 移除之后的历史记录
$this->historyStates = array_slice($this->historyStates, 0, $stateIndex + 1);
}

private function reapplyInputsFrom($startIndex, $playerId, $newInput) {
// 从指定状态开始重新模拟所有输入
for ($i = $startIndex; $i < count($this->historyStates); $i++) {
// 重新应用输入并更新状态...
}

// 应用新的延迟输入
$this->applyPlayerInput($this->gameState['players'][$playerId], $newInput);
}
}

?>

2. 兴趣管理 (Interest Management)

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
<?php

/**
* 基于兴趣管理的状态同步
*/
class InterestManagedStateSync extends StateSyncGameServer {

/**
* 获取玩家感兴趣的状态(只同步相关对象)
*/
public function getInterestBasedState($playerId) {
$playerPosition = $this->gameState['players'][$playerId]['position'];
$interestRadius = 50.0; // 兴趣范围

$relevantState = [
'players' => [],
'objects' => [],
'timestamp' => $this->gameState['timestamp']
];

// 只包含在兴趣范围内的玩家和对象
foreach ($this->gameState['players'] as $otherPlayerId => $otherPlayer) {
if ($this->isInInterestRange($playerPosition, $otherPlayer['position'], $interestRadius)) {
$relevantState['players'][$otherPlayerId] = $otherPlayer;
}
}

foreach ($this->gameState['objects'] as $objectId => $object) {
if ($this->isInInterestRange($playerPosition, $object['position'], $interestRadius)) {
$relevantState['objects'][$objectId] = $object;
}
}

return $relevantState;
}

private function isInInterestRange($pos1, $pos2, $radius) {
$dx = $pos1['x'] - $pos2['x'];
$dz = $pos1['z'] - $pos2['z'];
$distance = sqrt($dx*$dx + $dz*$dz);

return $distance <= $radius;
}
}

?>

实际应用场景

MMO游戏(状态同步)

1
2
3
4
5
class MMOGameServer extends StateSyncGameServer {
// MMO游戏通常使用状态同步
// 服务器维护整个游戏世界的权威状态
// 客户端只接收视野范围内的状态更新
}

RTS游戏(帧同步)

1
2
3
4
5
class RTSGameServer extends FrameSyncGameServer {
// RTS游戏通常使用帧同步
// 所有客户端运行相同的确定性逻辑
// 服务器只负责指令转发
}

混合方案

1
2
3
4
5
6
7
8
9
10
11
12
class HybridSyncGameServer {
// 对关键逻辑使用状态同步
private $stateSync;

// 对大量单位使用帧同步
private $frameSync;

public function __construct() {
$this->stateSync = new StateSyncGameServer();
$this->frameSync = new FrameSyncGameServer();
}
}

总结

状态同步和帧同步是多人游戏网络同步的两种核心方案:

核心优势对比

特性 状态同步 帧同步
数据一致性 服务器保证 确定性逻辑保证
网络带宽 较高 较低
服务器负载 较高 较低
客户端负载 较低 较高
反作弊能力
断线重连 简单 复杂
开发复杂度 中等 较高

选择建议

  1. 选择状态同步当

    • 游戏逻辑复杂,需要服务器验证
    • 反作弊是重要考虑因素
    • 游戏世界大,玩家分布稀疏
    • 需要支持大量观战者
  2. 选择帧同步当

    • 游戏逻辑相对简单且确定性
    • 需要支持大量单位同时操作
    • 网络带宽受限
    • 游戏回放功能很重要

最佳实践

  1. 网络优化

    • 使用Delta压缩减少数据传输
    • 实现兴趣管理减少不必要同步
    • 采用数据优先级(重要数据优先发送)
  2. 延迟处理

    • 客户端预测 + 服务器校正
    • 插值平滑显示
    • 延迟补偿机制
  3. 可靠性保证

    • 关键数据使用可靠传输
    • 实现断线检测和重连机制
    • 数据完整性验证
  4. 性能监控

    • 实时监控网络延迟和丢包率
    • 动态调整同步频率
    • 客户端性能自适应

状态同步和帧同步各有优劣,在实际项目中经常根据具体需求进行混合使用或定制化改进。理解它们的原理和特性,是构建高质量多人游戏的基础。


文章作者: Crazy Boy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Crazy Boy !
评 论
 上一篇
锁步协议(Lockstep):RTS游戏的完美同步之道
锁步协议(Lockstep):RTS游戏的完美同步之道
如何让数百个作战单位在多个玩家间保持精确同步?锁步协议通过确定性仿真和命令同步,为实时战略游戏提供了完美的网络同步解决方案。 问题背景在实时战略游戏(RTS)中,网络同步面临独特挑战: 大规模单位:数百个作战单位需要同步状态 精确时序
2025-11-20
下一篇 
Fisher-Yates 洗牌算法
Fisher-Yates 洗牌算法
Fisher-Yates 洗牌算法(也称为 Knuth 洗牌)是一种高效且公正的随机洗牌算法,用于将数组或列表中的元素随机重新排列。 🎯 算法原理核心思想:从后往前遍历数组,将当前元素与随机位置的一个元素交换。 📝 算法步骤原始版本:1
2025-11-20

如何让分布在世界各地的玩家在虚拟世界中实现流畅的多人互动?状态同步和帧同步提供了两种不同的技术路径,各自在游戏开发中扮演着重要角色。

问题背景

在多人游戏开发中,网络同步是核心技术挑战:

  • 网络延迟:玩家之间的网络延迟从几十毫秒到几百毫秒不等
  • 数据一致性:确保所有客户端看到相同的游戏状态
  • 性能优化:在带宽和计算资源有限的情况下实现流畅体验
  • 作弊防护:防止客户端篡改游戏逻辑
  • 断线重连:玩家掉线后能够重新加入游戏

基本概念

核心思想对比

特性 状态同步 帧同步
同步内容 游戏状态(位置、血量等) 玩家操作指令
计算位置 服务端权威计算 客户端各自计算
数据量 较大(同步状态数据) 较小(同步操作指令)
确定性 依赖服务端状态 依赖确定性逻辑
适用类型 MMO、FPS、开放世界 RTS、MOBA、棋牌

状态同步 (State Synchronization)

核心原理

服务器作为游戏世界的权威,计算所有游戏逻辑,客户端只负责渲染和输入采集。

架构设计

1
2
客户端A → 输入 → 服务器 → 状态更新 → 所有客户端
客户端B → 输入 → 服务器 → 状态更新 → 所有客户端

PHP实现示例

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
<?php

/**
* 状态同步游戏服务器
*/
class StateSyncGameServer {
private $clients;
private $gameState;
private $updateInterval;
private $lastUpdateTime;

public function __construct($updateRate = 10) {
$this->clients = [];
$this->gameState = [
'players' => [],
'objects' => [],
'timestamp' => microtime(true)
];
$this->updateInterval = 1.0 / $updateRate; // 更新频率
$this->lastUpdateTime = microtime(true);
}

/**
* 处理客户端连接
*/
public function handleClientConnect($clientId, $playerData) {
$this->clients[$clientId] = [
'id' => $clientId,
'player_id' => $playerData['player_id'],
'last_heartbeat' => microtime(true),
'pending_commands' => []
];

// 初始化玩家状态
$this->gameState['players'][$playerData['player_id']] = [
'id' => $playerData['player_id'],
'position' => ['x' => 0, 'y' => 0, 'z' => 0],
'rotation' => ['x' => 0, 'y' => 0, 'z' => 0],
'health' => 100,
'last_input_seq' => 0,
'velocity' => ['x' => 0, 'y' => 0, 'z' => 0]
];

return $this->getFullState(); // 返回完整状态给新客户端
}

/**
* 处理客户端输入
*/
public function handleClientInput($clientId, $inputData) {
if (!isset($this->clients[$clientId])) {
return false;
}

$playerId = $this->clients[$clientId]['player_id'];
$inputSeq = $inputData['sequence'];

// 检查输入序列号(防止乱序和重复)
if ($inputSeq <= $this->gameState['players'][$playerId]['last_input_seq']) {
return false; // 旧输入,忽略
}

$this->clients[$clientId]['pending_commands'][] = [
'sequence' => $inputSeq,
'type' => $inputData['type'],
'data' => $inputData['data'],
'timestamp' => $inputData['timestamp']
];

$this->gameState['players'][$playerId]['last_input_seq'] = $inputSeq;

return true;
}

/**
* 服务器更新循环
*/
public function update() {
$currentTime = microtime(true);
$deltaTime = $currentTime - $this->lastUpdateTime;

if ($deltaTime >= $this->updateInterval) {
$this->processInputs();
$this->updateGameLogic($deltaTime);
$this->broadcastStateUpdate();

$this->lastUpdateTime = $currentTime;
return true;
}

return false;
}

/**
* 处理所有待处理输入
*/
private function processInputs() {
foreach ($this->clients as $clientId => &$client) {
$playerId = $client['player_id'];
$player = &$this->gameState['players'][$playerId];

foreach ($client['pending_commands'] as $command) {
$this->applyPlayerInput($player, $command);
}

$client['pending_commands'] = []; // 清空已处理命令
}
}

/**
* 应用玩家输入到游戏状态
*/
private function applyPlayerInput(&$player, $command) {
switch ($command['type']) {
case 'move':
$this->handleMoveInput($player, $command['data']);
break;
case 'jump':
$this->handleJumpInput($player, $command['data']);
break;
case 'attack':
$this->handleAttackInput($player, $command['data']);
break;
// 其他输入类型...
}
}

/**
* 处理移动输入
*/
private function handleMoveInput(&$player, $moveData) {
$speed = 5.0; // 移动速度

$player['velocity']['x'] = $moveData['x'] * $speed;
$player['velocity']['z'] = $moveData['z'] * $speed;

if (abs($moveData['x']) > 0.1 || abs($moveData['z']) > 0.1) {
$player['rotation']['y'] = atan2($moveData['x'], $moveData['z']);
}
}

/**
* 处理跳跃输入
*/
private function handleJumpInput(&$player, $jumpData) {
if ($player['position']['y'] == 0) { // 在地面上
$player['velocity']['y'] = 10.0; // 跳跃速度
}
}

/**
* 处理攻击输入
*/
private function handleAttackInput(&$player, $attackData) {
// 在服务器上验证攻击逻辑
$targetId = $attackData['target_id'];
if (isset($this->gameState['players'][$targetId])) {
$this->gameState['players'][$targetId]['health'] -= 10;

if ($this->gameState['players'][$targetId]['health'] <= 0) {
$this->handlePlayerDeath($targetId);
}
}
}

/**
* 更新游戏逻辑
*/
private function updateGameLogic($deltaTime) {
// 应用物理
foreach ($this->gameState['players'] as &$player) {
// 更新位置
$player['position']['x'] += $player['velocity']['x'] * $deltaTime;
$player['position']['y'] += $player['velocity']['y'] * $deltaTime;
$player['position']['z'] += $player['velocity']['z'] * $deltaTime;

// 简单重力
if ($player['position']['y'] > 0) {
$player['velocity']['y'] -= 20.0 * $deltaTime; // 重力
} else {
$player['position']['y'] = 0;
$player['velocity']['y'] = 0;
}

// 摩擦力
$player['velocity']['x'] *= 0.9;
$player['velocity']['z'] *= 0.9;
}

$this->gameState['timestamp'] = microtime(true);
}

/**
* 广播状态更新给所有客户端
*/
private function broadcastStateUpdate() {
$stateUpdate = $this->getDeltaState(); // 获取增量状态

foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'state_update', $stateUpdate);
}
}

/**
* 获取完整游戏状态(用于新客户端同步)
*/
public function getFullState() {
return $this->gameState;
}

/**
* 获取增量状态(用于常规更新)
*/
public function getDeltaState() {
// 简化实现:返回完整状态
// 实际中应该只返回变化的部分
return [
'players' => $this->gameState['players'],
'timestamp' => $this->gameState['timestamp']
];
}

/**
* 处理玩家死亡
*/
private function handlePlayerDeath($playerId) {
$this->gameState['players'][$playerId]['health'] = 100;
$this->gameState['players'][$playerId]['position'] = ['x' => 0, 'y' => 0, 'z' => 0];

// 广播玩家死亡事件
$this->broadcastEvent('player_died', [
'player_id' => $playerId,
'respawn_time' => 3
]);
}

/**
* 广播事件给所有客户端
*/
private function broadcastEvent($eventType, $eventData) {
foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'game_event', [
'type' => $eventType,
'data' => $eventData
]);
}
}

/**
* 发送数据到客户端(模拟)
*/
private function sendToClient($clientId, $messageType, $data) {
// 实际实现中会通过WebSocket或其他网络协议发送
echo "发送到客户端 {$clientId}: {$messageType}\n";
// 网络发送逻辑...
}
}

/**
* 状态同步客户端
*/
class StateSyncGameClient {
private $playerId;
private $server;
private $localState;
private $pendingInputs;
private $inputSequence;
private $serverTimeOffset;

public function __construct($playerId, $server) {
$this->playerId = $playerId;
$this->server = $server;
$this->localState = [];
$this->pendingInputs = [];
$this->inputSequence = 0;
$this->serverTimeOffset = 0;

// 连接服务器
$this->connectToServer();
}

/**
* 连接到服务器
*/
private function connectToServer() {
$fullState = $this->server->handleClientConnect(uniqid(), [
'player_id' => $this->playerId
]);

$this->localState = $fullState;
echo "客户端 {$this->playerId} 已连接,获取完整状态\n";
}

/**
* 处理玩家输入
*/
public function handleInput($inputType, $inputData) {
$this->inputSequence++;

$inputPacket = [
'sequence' => $this->inputSequence,
'type' => $inputType,
'data' => $inputData,
'timestamp' => $this->getClientTime()
];

// 立即在本地预测执行
$this->applyLocalPrediction($inputType, $inputData);

// 发送到服务器
$this->server->handleClientInput($this->getClientId(), $inputPacket);

// 保存输入用于可能的回滚
$this->pendingInputs[$this->inputSequence] = $inputPacket;
}

/**
* 应用本地预测
*/
private function applyLocalPrediction($inputType, $inputData) {
$player = &$this->localState['players'][$this->playerId];

switch ($inputType) {
case 'move':
$speed = 5.0;
$player['velocity']['x'] = $inputData['x'] * $speed;
$player['velocity']['z'] = $inputData['z'] * $speed;
break;
// 其他输入类型的本地预测...
}
}

/**
* 接收服务器状态更新
*/
public function receiveStateUpdate($serverState) {
// reconciliation:与服务器状态同步
$this->reconcileWithServer($serverState);
}

/**
* 与服务器状态协调
*/
private function reconcileWithServer($serverState) {
$serverPlayer = $serverState['players'][$this->playerId];
$localPlayer = $this->localState['players'][$this->playerId];

// 如果位置差异过大,采用服务器状态
$positionDiff = $this->calculateDistance($serverPlayer['position'], $localPlayer['position']);

if ($positionDiff > 1.0) { // 阈值
echo "客户端 {$this->playerId}: 位置差异过大({$positionDiff}),采用服务器状态\n";
$this->localState = $serverState;
} else {
// 微小差异,平滑插值
$this->interpolateToServerState($serverState);
}

// 移除已确认的输入
$lastServerSeq = $serverPlayer['last_input_seq'];
$this->removeAcknowledgedInputs($lastServerSeq);
}

/**
* 插值到服务器状态
*/
private function interpolateToServerState($serverState) {
$interpolationFactor = 0.2; // 插值系数

foreach ($serverState['players'] as $playerId => $serverPlayer) {
if (!isset($this->localState['players'][$playerId])) {
$this->localState['players'][$playerId] = $serverPlayer;
continue;
}

$localPlayer = &$this->localState['players'][$playerId];

// 对其他玩家使用插值
if ($playerId != $this->playerId) {
$localPlayer['position']['x'] = $this->lerp(
$localPlayer['position']['x'],
$serverPlayer['position']['x'],
$interpolationFactor
);
// 对其他坐标和旋转也进行插值...
}
}
}

/**
* 移除已确认的输入
*/
private function removeAcknowledgedInputs($lastServerSeq) {
foreach ($this->pendingInputs as $seq => $input) {
if ($seq <= $lastServerSeq) {
unset($this->pendingInputs[$seq]);
}
}
}

/**
* 客户端更新循环
*/
public function update($deltaTime) {
// 更新本地预测的状态
$this->updateLocalState($deltaTime);
}

/**
* 更新本地状态
*/
private function updateLocalState($deltaTime) {
foreach ($this->localState['players'] as &$player) {
// 应用物理(与服务器相同的逻辑)
$player['position']['x'] += $player['velocity']['x'] * $deltaTime;
$player['position']['y'] += $player['velocity']['y'] * $deltaTime;
$player['position']['z'] += $player['velocity']['z'] * $deltaTime;

if ($player['position']['y'] > 0) {
$player['velocity']['y'] -= 20.0 * $deltaTime;
} else {
$player['position']['y'] = 0;
$player['velocity']['y'] = 0;
}

$player['velocity']['x'] *= 0.9;
$player['velocity']['z'] *= 0.9;
}
}

// 辅助方法
private function getClientTime() {
return microtime(true) + $this->serverTimeOffset;
}

private function getClientId() {
return "client_{$this->playerId}";
}

private function calculateDistance($pos1, $pos2) {
$dx = $pos1['x'] - $pos2['x'];
$dy = $pos1['y'] - $pos2['y'];
$dz = $pos1['z'] - $pos2['z'];
return sqrt($dx*$dx + $dy*$dy + $dz*$dz);
}

private function lerp($a, $b, $f) {
return $a + $f * ($b - $a);
}

/**
* 获取本地状态(用于渲染)
*/
public function getRenderState() {
return $this->localState;
}
}

?>

帧同步 (Frame Synchronization)

核心原理

所有客户端运行相同的确定性逻辑,服务器只负责转发输入指令,不进行游戏逻辑计算。

架构设计

1
2
3
客户端A → 操作指令 → 服务器 → 转发指令 → 所有客户端
客户端B → 操作指令 → 服务器 → 转发指令 → 所有客户端
所有客户端运行相同的确定性逻辑

PHP实现示例

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
<?php

/**
* 帧同步游戏服务器
*/
class FrameSyncGameServer {
private $clients;
private $currentFrame;
private $frameInputs;
private $lockstepInterval;
private $lastFrameTime;

public function __construct($frameRate = 10) {
$this->clients = [];
$this->currentFrame = 0;
$this->frameInputs = [];
$this->lockstepInterval = 1.0 / $frameRate;
$this->lastFrameTime = microtime(true);
}

/**
* 处理客户端连接
*/
public function handleClientConnect($clientId, $playerData) {
$this->clients[$clientId] = [
'id' => $clientId,
'player_id' => $playerData['player_id'],
'last_heartbeat' => microtime(true),
'ready' => true
];

echo "客户端 {$clientId} 已连接\n";
return $this->currentFrame; // 返回当前帧号
}

/**
* 接收客户端帧输入
*/
public function receiveFrameInput($clientId, $frameInput) {
if (!isset($this->clients[$clientId])) {
return false;
}

$frame = $frameInput['frame'];
$playerId = $this->clients[$clientId]['player_id'];

// 初始化帧输入存储
if (!isset($this->frameInputs[$frame])) {
$this->frameInputs[$frame] = [];
}

// 存储玩家输入
$this->frameInputs[$frame][$playerId] = [
'inputs' => $frameInput['inputs'],
'checksum' => $frameInput['checksum'],
'timestamp' => $frameInput['timestamp']
];

echo "收到客户端 {$clientId}{$frame} 的输入\n";
return true;
}

/**
* 服务器帧同步更新
*/
public function update() {
$currentTime = microtime(true);

// 检查是否应该推进到下一帧
if ($currentTime - $this->lastFrameTime >= $this->lockstepInterval) {
return $this->advanceFrame();
}

return false;
}

/**
* 推进到下一帧
*/
private function advanceFrame() {
$targetFrame = $this->currentFrame + 1;

// 检查是否收集到所有客户端的输入
if ($this->hasAllInputsForFrame($targetFrame)) {
// 广播这一帧的所有输入给所有客户端
$this->broadcastFrameInputs($targetFrame);

$this->currentFrame = $targetFrame;
$this->lastFrameTime = microtime(true);

// 清理旧的帧数据(保留最近几帧用于断线重连)
$this->cleanupOldFrames();

echo "推进到帧 {$targetFrame}\n";
return true;
} else {
echo "等待帧 {$targetFrame} 的完整输入...\n";
return false;
}
}

/**
* 检查是否收集到某帧的所有输入
*/
private function hasAllInputsForFrame($frame) {
if (!isset($this->frameInputs[$frame])) {
return false;
}

// 检查每个已连接的客户端是否都提供了输入
foreach ($this->clients as $client) {
$playerId = $client['player_id'];
if (!isset($this->frameInputs[$frame][$playerId])) {
return false;
}
}

return true;
}

/**
* 广播帧输入给所有客户端
*/
private function broadcastFrameInputs($frame) {
$frameData = [
'frame' => $frame,
'inputs' => $this->frameInputs[$frame]
];

foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'frame_inputs', $frameData);
}
}

/**
* 清理旧的帧数据
*/
private function cleanupOldFrames() {
$framesToKeep = 60; // 保留最近60帧用于断线重连

foreach ($this->frameInputs as $frame => $inputs) {
if ($frame < $this->currentFrame - $framesToKeep) {
unset($this->frameInputs[$frame]);
}
}
}

/**
* 处理客户端断线重连
*/
public function handleClientReconnect($clientId, $lastKnownFrame) {
if (!isset($this->clients[$clientId])) {
return false;
}

$catchUpData = [];

// 提供缺失的帧输入
for ($frame = $lastKnownFrame + 1; $frame <= $this->currentFrame; $frame++) {
if (isset($this->frameInputs[$frame])) {
$catchUpData[$frame] = $this->frameInputs[$frame];
}
}

$this->sendToClient($clientId, 'catch_up', [
'from_frame' => $lastKnownFrame + 1,
'to_frame' => $this->currentFrame,
'inputs' => $catchUpData
]);

echo "客户端 {$clientId} 从帧 {$lastKnownFrame} 追赶至 {$this->currentFrame}\n";
return true;
}

/**
* 发送数据到客户端(模拟)
*/
private function sendToClient($clientId, $messageType, $data) {
// 实际实现中的网络发送逻辑
echo "发送到客户端 {$clientId}: {$messageType}\n";
}

/**
* 获取当前帧号
*/
public function getCurrentFrame() {
return $this->currentFrame;
}
}

/**
* 帧同步游戏客户端
*/
class FrameSyncGameClient {
private $playerId;
private $server;
private $currentFrame;
private $gameLogic;
private $pendingInputs;
private $confirmedFrames;
private $clientId;

public function __construct($playerId, $server) {
$this->playerId = $playerId;
$this->server = $server;
$this->currentFrame = 0;
$this->gameLogic = new DeterministicGameLogic();
$this->pendingInputs = [];
$this->confirmedFrames = [];
$this->clientId = "client_{$playerId}";

// 连接服务器
$this->connectToServer();
}

/**
* 连接到服务器
*/
private function connectToServer() {
$startFrame = $this->server->handleClientConnect($this->clientId, [
'player_id' => $this->playerId
]);

$this->currentFrame = $startFrame;
echo "客户端 {$this->playerId} 已连接,起始帧: {$startFrame}\n";
}

/**
* 处理玩家输入
*/
public function handleInput($inputType, $inputData) {
$frame = $this->currentFrame + 1; // 下一帧的输入

if (!isset($this->pendingInputs[$frame])) {
$this->pendingInputs[$frame] = [];
}

$this->pendingInputs[$frame][] = [
'type' => $inputType,
'data' => $inputData,
'player_id' => $this->playerId
];

echo "客户端 {$this->playerId} 为帧 {$frame} 记录输入: {$inputType}\n";
}

/**
* 客户端更新循环
*/
public function update() {
// 发送待处理的帧输入到服务器
$this->sendPendingInputs();

// 检查是否有可以执行的确认帧
$this->executeConfirmedFrames();
}

/**
* 发送待处理的输入到服务器
*/
private function sendPendingInputs() {
foreach ($this->pendingInputs as $frame => $inputs) {
// 只发送尚未确认的帧
if (!isset($this->confirmedFrames[$frame])) {
$checksum = $this->calculateChecksum($inputs);

$this->server->receiveFrameInput($this->clientId, [
'frame' => $frame,
'inputs' => $inputs,
'checksum' => $checksum,
'timestamp' => microtime(true)
]);
}
}
}

/**
* 执行已确认的帧
*/
private function executeConfirmedFrames() {
ksort($this->confirmedFrames);

foreach ($this->confirmedFrames as $frame => $frameInputs) {
if ($frame > $this->currentFrame) {
echo "客户端 {$this->playerId} 执行帧 {$frame}\n";

// 使用确定性逻辑执行这一帧
$this->gameLogic->executeFrame($frame, $frameInputs);

$this->currentFrame = $frame;

// 移除已执行的输入
unset($this->pendingInputs[$frame]);
unset($this->confirmedFrames[$frame]);
}
}
}

/**
* 接收服务器帧输入广播
*/
public function receiveFrameInputs($frameData) {
$frame = $frameData['frame'];
$allInputs = $frameData['inputs'];

// 验证帧数据完整性
if ($this->validateFrameInputs($frame, $allInputs)) {
$this->confirmedFrames[$frame] = $allInputs;
echo "客户端 {$this->playerId} 确认帧 {$frame} 输入\n";
} else {
echo "客户端 {$this->playerId}{$frame} 输入验证失败\n";
}
}

/**
* 接收追赶数据(断线重连)
*/
public function receiveCatchUpData($catchUpData) {
$fromFrame = $catchUpData['from_frame'];
$toFrame = $catchUpData['to_frame'];
$inputs = $catchUpData['inputs'];

echo "客户端 {$this->playerId} 开始追赶: 帧 {$fromFrame} -> {$toFrame}\n";

// 快速执行缺失的帧
for ($frame = $fromFrame; $frame <= $toFrame; $frame++) {
if (isset($inputs[$frame])) {
$this->gameLogic->executeFrame($frame, $inputs[$frame]);
echo "客户端 {$this->playerId} 追赶执行帧 {$frame}\n";
}
}

$this->currentFrame = $toFrame;
echo "客户端 {$this->playerId} 追赶完成,当前帧: {$toFrame}\n";
}

/**
* 验证帧输入完整性
*/
private function validateFrameInputs($frame, $allInputs) {
// 检查是否有所有玩家的输入
$expectedPlayers = ['player1', 'player2']; // 实际中应该从服务器获取

foreach ($expectedPlayers as $playerId) {
if (!isset($allInputs[$playerId])) {
return false;
}
}

// 验证校验和(可选)
foreach ($allInputs as $playerId => $inputData) {
$calculatedChecksum = $this->calculateChecksum($inputData['inputs']);
if ($calculatedChecksum != $inputData['checksum']) {
return false;
}
}

return true;
}

/**
* 计算输入校验和
*/
private function calculateChecksum($inputs) {
return md5(serialize($inputs));
}

/**
* 获取当前游戏状态(用于渲染)
*/
public function getRenderState() {
return $this->gameLogic->getCurrentState();
}

/**
* 获取当前帧号
*/
public function getCurrentFrame() {
return $this->currentFrame;
}
}

/**
* 确定性游戏逻辑
*/
class DeterministicGameLogic {
private $gameState;

public function __construct() {
$this->gameState = [
'players' => [
'player1' => ['x' => 0, 'y' => 0, 'health' => 100],
'player2' => ['x' => 10, 'y' => 0, 'health' => 100]
],
'projectiles' => []
];
}

/**
* 执行一帧游戏逻辑
*/
public function executeFrame($frame, $frameInputs) {
// 处理所有玩家输入
foreach ($frameInputs as $playerId => $inputData) {
$this->processPlayerInputs($playerId, $inputData['inputs']);
}

// 更新游戏逻辑
$this->updateGameLogic();

// 生成帧校验和(用于调试和验证)
$this->generateFrameChecksum($frame);
}

/**
* 处理玩家输入
*/
private function processPlayerInputs($playerId, $inputs) {
$player = &$this->gameState['players'][$playerId];

foreach ($inputs as $input) {
switch ($input['type']) {
case 'move':
$this->handleMove($player, $input['data']);
break;
case 'attack':
$this->handleAttack($playerId, $input['data']);
break;
// 其他输入类型...
}
}
}

/**
* 处理移动
*/
private function handleMove(&$player, $moveData) {
$speed = 0.5;
$player['x'] += $moveData['x'] * $speed;
$player['y'] += $moveData['y'] * $speed;
}

/**
* 处理攻击
*/
private function handleAttack($attackerId, $attackData) {
$targetId = $attackData['target_id'];

if (isset($this->gameState['players'][$targetId])) {
$this->gameState['players'][$targetId]['health'] -= 10;

if ($this->gameState['players'][$targetId]['health'] <= 0) {
$this->handlePlayerDeath($targetId);
}
}
}

/**
* 处理玩家死亡
*/
private function handlePlayerDeath($playerId) {
$this->gameState['players'][$playerId]['health'] = 100;
$this->gameState['players'][$playerId]['x'] = 0;
$this->gameState['players'][$playerId]['y'] = 0;
}

/**
* 更新游戏逻辑
*/
private function updateGameLogic() {
// 更新抛射物等
// 碰撞检测
// 其他游戏逻辑...
}

/**
* 生成帧校验和
*/
private function generateFrameChecksum($frame) {
$checksum = md5(serialize($this->gameState));
echo "帧 {$frame} 校验和: {$checksum}\n";
return $checksum;
}

/**
* 获取当前游戏状态
*/
public function getCurrentState() {
return $this->gameState;
}
}

?>

应用示例与对比测试

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php

class NetworkSyncExamples {

/**
* 状态同步演示
*/
public static function stateSyncDemo() {
echo "=== 状态同步演示 ===\n";

// 创建服务器
$server = new StateSyncGameServer(10); // 10Hz 更新率

// 创建两个客户端
$client1 = new StateSyncGameClient('player1', $server);
$client2 = new StateSyncGameClient('player2', $server);

// 模拟游戏循环
for ($i = 0; $i < 5; $i++) {
echo "\n--- 回合 {$i} ---\n";

// 客户端1输入
$client1->handleInput('move', ['x' => 1, 'z' => 0]);

// 服务器更新
$server->update();

// 客户端更新
$client1->update(0.1);
$client2->update(0.1);

// 显示状态
$state1 = $client1->getRenderState();
$state2 = $client2->getRenderState();

echo "客户端1位置: " . json_encode($state1['players']['player1']['position']) . "\n";
echo "客户端2位置: " . json_encode($state2['players']['player1']['position']) . "\n";

usleep(100000); // 100ms
}
}

/**
* 帧同步演示
*/
public static function frameSyncDemo() {
echo "\n=== 帧同步演示 ===\n";

// 创建服务器
$server = new FrameSyncGameServer(2); // 2Hz 帧率(便于演示)

// 创建两个客户端
$client1 = new FrameSyncGameClient('player1', $server);
$client2 = new FrameSyncGameClient('player2', $server);

// 模拟游戏循环
for ($i = 0; $i < 10; $i++) {
echo "\n--- 时间点 {$i} ---\n";

// 客户端输入
if ($i % 2 == 0) {
$client1->handleInput('move', ['x' => 1, 'y' => 0]);
$client2->handleInput('move', ['x' => -1, 'y' => 0]);
}

// 客户端更新
$client1->update();
$client2->update();

// 服务器更新
$server->update();

// 显示状态
$frame1 = $client1->getCurrentFrame();
$frame2 = $client2->getCurrentFrame();
$state1 = $client1->getRenderState();

echo "客户端1帧: {$frame1}, 客户端2帧: {$frame2}\n";
echo "玩家1位置: ({$state1['players']['player1']['x']}, {$state1['players']['player1']['y']})\n";

usleep(500000); // 500ms
}
}

/**
* 性能对比测试
*/
public static function performanceComparison() {
echo "\n=== 性能对比测试 ===\n";

// 状态同步性能测试
$stateServer = new StateSyncGameServer(20);
$startTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
$stateServer->update();
}

$stateSyncTime = microtime(true) - $startTime;

// 帧同步性能测试
$frameServer = new FrameSyncGameServer(20);
$startTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
$frameServer->update();
}

$frameSyncTime = microtime(true) - $startTime;

echo "状态同步 100次更新: " . number_format($stateSyncTime * 1000, 2) . "ms\n";
echo "帧同步 100次更新: " . number_format($frameSyncTime * 1000, 2) . "ms\n";
echo "性能差异: " . number_format(($stateSyncTime - $frameSyncTime) / $stateSyncTime * 100, 2) . "%\n";
}

/**
* 网络延迟模拟测试
*/
public static function latencyTest() {
echo "\n=== 网络延迟测试 ===\n";

$server = new StateSyncGameServer(10);
$client = new StateSyncGameClient('test_player', $server);

// 模拟不同网络延迟下的表现
$latencies = [50, 100, 200, 500]; // 毫秒

foreach ($latencies as $latency) {
echo "模拟 {$latency}ms 延迟:\n";

// 这里实际应该模拟网络延迟
$client->handleInput('move', ['x' => 1, 'z' => 0]);
$server->update();
$client->update(0.1);

$state = $client->getRenderState();
echo "玩家位置: " . json_encode($state['players']['test_player']['position']) . "\n";
}
}
}

// 运行演示
NetworkSyncExamples::stateSyncDemo();
NetworkSyncExamples::frameSyncDemo();
NetworkSyncExamples::performanceComparison();
NetworkSyncExamples::latencyTest();

?>

高级优化技术

1. 预测与回滚 (Prediction and Rollback)

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
<?php

/**
* 带预测回滚的状态同步
*/
class PredictiveStateSync extends StateSyncGameServer {
private $historyStates;
private $historySize;

public function __construct($updateRate = 10, $historySize = 60) {
parent::__construct($updateRate);
$this->historyStates = [];
$this->historySize = $historySize;
}

/**
* 保存历史状态
*/
protected function updateGameLogic($deltaTime) {
parent::updateGameLogic($deltaTime);

// 保存历史状态用于回滚
$this->saveHistoryState();
}

private function saveHistoryState() {
$this->historyStates[] = [
'timestamp' => $this->gameState['timestamp'],
'state' => serialize($this->gameState) // 深拷贝
];

// 限制历史记录大小
if (count($this->historyStates) > $this->historySize) {
array_shift($this->historyStates);
}
}

/**
* 处理延迟的输入(回滚并重新模拟)
*/
public function handleDelayedInput($clientId, $inputData) {
$inputTime = $inputData['timestamp'];
$playerId = $this->clients[$clientId]['player_id'];

// 找到输入应该应用的时间点
$targetStateIndex = $this->findStateByTimestamp($inputTime);

if ($targetStateIndex !== null) {
// 回滚到目标状态
$this->rollbackToState($targetStateIndex);

// 重新应用输入并模拟
$this->reapplyInputsFrom($targetStateIndex, $playerId, $inputData);

return true;
}

return false;
}

private function findStateByTimestamp($timestamp) {
foreach ($this->historyStates as $index => $history) {
if ($history['timestamp'] >= $timestamp) {
return $index;
}
}
return null;
}

private function rollbackToState($stateIndex) {
$targetState = $this->historyStates[$stateIndex];
$this->gameState = unserialize($targetState['state']);

// 移除之后的历史记录
$this->historyStates = array_slice($this->historyStates, 0, $stateIndex + 1);
}

private function reapplyInputsFrom($startIndex, $playerId, $newInput) {
// 从指定状态开始重新模拟所有输入
for ($i = $startIndex; $i < count($this->historyStates); $i++) {
// 重新应用输入并更新状态...
}

// 应用新的延迟输入
$this->applyPlayerInput($this->gameState['players'][$playerId], $newInput);
}
}

?>

2. 兴趣管理 (Interest Management)

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
<?php

/**
* 基于兴趣管理的状态同步
*/
class InterestManagedStateSync extends StateSyncGameServer {

/**
* 获取玩家感兴趣的状态(只同步相关对象)
*/
public function getInterestBasedState($playerId) {
$playerPosition = $this->gameState['players'][$playerId]['position'];
$interestRadius = 50.0; // 兴趣范围

$relevantState = [
'players' => [],
'objects' => [],
'timestamp' => $this->gameState['timestamp']
];

// 只包含在兴趣范围内的玩家和对象
foreach ($this->gameState['players'] as $otherPlayerId => $otherPlayer) {
if ($this->isInInterestRange($playerPosition, $otherPlayer['position'], $interestRadius)) {
$relevantState['players'][$otherPlayerId] = $otherPlayer;
}
}

foreach ($this->gameState['objects'] as $objectId => $object) {
if ($this->isInInterestRange($playerPosition, $object['position'], $interestRadius)) {
$relevantState['objects'][$objectId] = $object;
}
}

return $relevantState;
}

private function isInInterestRange($pos1, $pos2, $radius) {
$dx = $pos1['x'] - $pos2['x'];
$dz = $pos1['z'] - $pos2['z'];
$distance = sqrt($dx*$dx + $dz*$dz);

return $distance <= $radius;
}
}

?>

实际应用场景

MMO游戏(状态同步)

1
2
3
4
5
class MMOGameServer extends StateSyncGameServer {
// MMO游戏通常使用状态同步
// 服务器维护整个游戏世界的权威状态
// 客户端只接收视野范围内的状态更新
}

RTS游戏(帧同步)

1
2
3
4
5
class RTSGameServer extends FrameSyncGameServer {
// RTS游戏通常使用帧同步
// 所有客户端运行相同的确定性逻辑
// 服务器只负责指令转发
}

混合方案

1
2
3
4
5
6
7
8
9
10
11
12
class HybridSyncGameServer {
// 对关键逻辑使用状态同步
private $stateSync;

// 对大量单位使用帧同步
private $frameSync;

public function __construct() {
$this->stateSync = new StateSyncGameServer();
$this->frameSync = new FrameSyncGameServer();
}
}

总结

状态同步和帧同步是多人游戏网络同步的两种核心方案:

核心优势对比

特性 状态同步 帧同步
数据一致性 服务器保证 确定性逻辑保证
网络带宽 较高 较低
服务器负载 较高 较低
客户端负载 较低 较高
反作弊能力
断线重连 简单 复杂
开发复杂度 中等 较高

选择建议

  1. 选择状态同步当

    • 游戏逻辑复杂,需要服务器验证
    • 反作弊是重要考虑因素
    • 游戏世界大,玩家分布稀疏
    • 需要支持大量观战者
  2. 选择帧同步当

    • 游戏逻辑相对简单且确定性
    • 需要支持大量单位同时操作
    • 网络带宽受限
    • 游戏回放功能很重要

最佳实践

  1. 网络优化

    • 使用Delta压缩减少数据传输
    • 实现兴趣管理减少不必要同步
    • 采用数据优先级(重要数据优先发送)
  2. 延迟处理

    • 客户端预测 + 服务器校正
    • 插值平滑显示
    • 延迟补偿机制
  3. 可靠性保证

    • 关键数据使用可靠传输
    • 实现断线检测和重连机制
    • 数据完整性验证
  4. 性能监控

    • 实时监控网络延迟和丢包率
    • 动态调整同步频率
    • 客户端性能自适应

状态同步和帧同步各有优劣,在实际项目中经常根据具体需求进行混合使用或定制化改进。理解它们的原理和特性,是构建高质量多人游戏的基础。


文章作者: Crazy Boy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Crazy Boy !
评 论
 上一篇
锁步协议(Lockstep):RTS游戏的完美同步之道
锁步协议(Lockstep):RTS游戏的完美同步之道
如何让数百个作战单位在多个玩家间保持精确同步?锁步协议通过确定性仿真和命令同步,为实时战略游戏提供了完美的网络同步解决方案。 问题背景在实时战略游戏(RTS)中,网络同步面临独特挑战: 大规模单位:数百个作战单位需要同步状态 精确时序
2025-11-20
下一篇 
Fisher-Yates 洗牌算法
Fisher-Yates 洗牌算法
Fisher-Yates 洗牌算法(也称为 Knuth 洗牌)是一种高效且公正的随机洗牌算法,用于将数组或列表中的元素随机重新排列。 🎯 算法原理核心思想:从后往前遍历数组,将当前元素与随机位置的一个元素交换。 📝 算法步骤原始版本:1
2025-11-20

如何让分布在世界各地的玩家在虚拟世界中实现流畅的多人互动?状态同步和帧同步提供了两种不同的技术路径,各自在游戏开发中扮演着重要角色。

问题背景

在多人游戏开发中,网络同步是核心技术挑战:

  • 网络延迟:玩家之间的网络延迟从几十毫秒到几百毫秒不等
  • 数据一致性:确保所有客户端看到相同的游戏状态
  • 性能优化:在带宽和计算资源有限的情况下实现流畅体验
  • 作弊防护:防止客户端篡改游戏逻辑
  • 断线重连:玩家掉线后能够重新加入游戏

基本概念

核心思想对比

特性 状态同步 帧同步
同步内容 游戏状态(位置、血量等) 玩家操作指令
计算位置 服务端权威计算 客户端各自计算
数据量 较大(同步状态数据) 较小(同步操作指令)
确定性 依赖服务端状态 依赖确定性逻辑
适用类型 MMO、FPS、开放世界 RTS、MOBA、棋牌

状态同步 (State Synchronization)

核心原理

服务器作为游戏世界的权威,计算所有游戏逻辑,客户端只负责渲染和输入采集。

架构设计

1
2
客户端A → 输入 → 服务器 → 状态更新 → 所有客户端
客户端B → 输入 → 服务器 → 状态更新 → 所有客户端

PHP实现示例

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
<?php

/**
* 状态同步游戏服务器
*/
class StateSyncGameServer {
private $clients;
private $gameState;
private $updateInterval;
private $lastUpdateTime;

public function __construct($updateRate = 10) {
$this->clients = [];
$this->gameState = [
'players' => [],
'objects' => [],
'timestamp' => microtime(true)
];
$this->updateInterval = 1.0 / $updateRate; // 更新频率
$this->lastUpdateTime = microtime(true);
}

/**
* 处理客户端连接
*/
public function handleClientConnect($clientId, $playerData) {
$this->clients[$clientId] = [
'id' => $clientId,
'player_id' => $playerData['player_id'],
'last_heartbeat' => microtime(true),
'pending_commands' => []
];

// 初始化玩家状态
$this->gameState['players'][$playerData['player_id']] = [
'id' => $playerData['player_id'],
'position' => ['x' => 0, 'y' => 0, 'z' => 0],
'rotation' => ['x' => 0, 'y' => 0, 'z' => 0],
'health' => 100,
'last_input_seq' => 0,
'velocity' => ['x' => 0, 'y' => 0, 'z' => 0]
];

return $this->getFullState(); // 返回完整状态给新客户端
}

/**
* 处理客户端输入
*/
public function handleClientInput($clientId, $inputData) {
if (!isset($this->clients[$clientId])) {
return false;
}

$playerId = $this->clients[$clientId]['player_id'];
$inputSeq = $inputData['sequence'];

// 检查输入序列号(防止乱序和重复)
if ($inputSeq <= $this->gameState['players'][$playerId]['last_input_seq']) {
return false; // 旧输入,忽略
}

$this->clients[$clientId]['pending_commands'][] = [
'sequence' => $inputSeq,
'type' => $inputData['type'],
'data' => $inputData['data'],
'timestamp' => $inputData['timestamp']
];

$this->gameState['players'][$playerId]['last_input_seq'] = $inputSeq;

return true;
}

/**
* 服务器更新循环
*/
public function update() {
$currentTime = microtime(true);
$deltaTime = $currentTime - $this->lastUpdateTime;

if ($deltaTime >= $this->updateInterval) {
$this->processInputs();
$this->updateGameLogic($deltaTime);
$this->broadcastStateUpdate();

$this->lastUpdateTime = $currentTime;
return true;
}

return false;
}

/**
* 处理所有待处理输入
*/
private function processInputs() {
foreach ($this->clients as $clientId => &$client) {
$playerId = $client['player_id'];
$player = &$this->gameState['players'][$playerId];

foreach ($client['pending_commands'] as $command) {
$this->applyPlayerInput($player, $command);
}

$client['pending_commands'] = []; // 清空已处理命令
}
}

/**
* 应用玩家输入到游戏状态
*/
private function applyPlayerInput(&$player, $command) {
switch ($command['type']) {
case 'move':
$this->handleMoveInput($player, $command['data']);
break;
case 'jump':
$this->handleJumpInput($player, $command['data']);
break;
case 'attack':
$this->handleAttackInput($player, $command['data']);
break;
// 其他输入类型...
}
}

/**
* 处理移动输入
*/
private function handleMoveInput(&$player, $moveData) {
$speed = 5.0; // 移动速度

$player['velocity']['x'] = $moveData['x'] * $speed;
$player['velocity']['z'] = $moveData['z'] * $speed;

if (abs($moveData['x']) > 0.1 || abs($moveData['z']) > 0.1) {
$player['rotation']['y'] = atan2($moveData['x'], $moveData['z']);
}
}

/**
* 处理跳跃输入
*/
private function handleJumpInput(&$player, $jumpData) {
if ($player['position']['y'] == 0) { // 在地面上
$player['velocity']['y'] = 10.0; // 跳跃速度
}
}

/**
* 处理攻击输入
*/
private function handleAttackInput(&$player, $attackData) {
// 在服务器上验证攻击逻辑
$targetId = $attackData['target_id'];
if (isset($this->gameState['players'][$targetId])) {
$this->gameState['players'][$targetId]['health'] -= 10;

if ($this->gameState['players'][$targetId]['health'] <= 0) {
$this->handlePlayerDeath($targetId);
}
}
}

/**
* 更新游戏逻辑
*/
private function updateGameLogic($deltaTime) {
// 应用物理
foreach ($this->gameState['players'] as &$player) {
// 更新位置
$player['position']['x'] += $player['velocity']['x'] * $deltaTime;
$player['position']['y'] += $player['velocity']['y'] * $deltaTime;
$player['position']['z'] += $player['velocity']['z'] * $deltaTime;

// 简单重力
if ($player['position']['y'] > 0) {
$player['velocity']['y'] -= 20.0 * $deltaTime; // 重力
} else {
$player['position']['y'] = 0;
$player['velocity']['y'] = 0;
}

// 摩擦力
$player['velocity']['x'] *= 0.9;
$player['velocity']['z'] *= 0.9;
}

$this->gameState['timestamp'] = microtime(true);
}

/**
* 广播状态更新给所有客户端
*/
private function broadcastStateUpdate() {
$stateUpdate = $this->getDeltaState(); // 获取增量状态

foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'state_update', $stateUpdate);
}
}

/**
* 获取完整游戏状态(用于新客户端同步)
*/
public function getFullState() {
return $this->gameState;
}

/**
* 获取增量状态(用于常规更新)
*/
public function getDeltaState() {
// 简化实现:返回完整状态
// 实际中应该只返回变化的部分
return [
'players' => $this->gameState['players'],
'timestamp' => $this->gameState['timestamp']
];
}

/**
* 处理玩家死亡
*/
private function handlePlayerDeath($playerId) {
$this->gameState['players'][$playerId]['health'] = 100;
$this->gameState['players'][$playerId]['position'] = ['x' => 0, 'y' => 0, 'z' => 0];

// 广播玩家死亡事件
$this->broadcastEvent('player_died', [
'player_id' => $playerId,
'respawn_time' => 3
]);
}

/**
* 广播事件给所有客户端
*/
private function broadcastEvent($eventType, $eventData) {
foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'game_event', [
'type' => $eventType,
'data' => $eventData
]);
}
}

/**
* 发送数据到客户端(模拟)
*/
private function sendToClient($clientId, $messageType, $data) {
// 实际实现中会通过WebSocket或其他网络协议发送
echo "发送到客户端 {$clientId}: {$messageType}\n";
// 网络发送逻辑...
}
}

/**
* 状态同步客户端
*/
class StateSyncGameClient {
private $playerId;
private $server;
private $localState;
private $pendingInputs;
private $inputSequence;
private $serverTimeOffset;

public function __construct($playerId, $server) {
$this->playerId = $playerId;
$this->server = $server;
$this->localState = [];
$this->pendingInputs = [];
$this->inputSequence = 0;
$this->serverTimeOffset = 0;

// 连接服务器
$this->connectToServer();
}

/**
* 连接到服务器
*/
private function connectToServer() {
$fullState = $this->server->handleClientConnect(uniqid(), [
'player_id' => $this->playerId
]);

$this->localState = $fullState;
echo "客户端 {$this->playerId} 已连接,获取完整状态\n";
}

/**
* 处理玩家输入
*/
public function handleInput($inputType, $inputData) {
$this->inputSequence++;

$inputPacket = [
'sequence' => $this->inputSequence,
'type' => $inputType,
'data' => $inputData,
'timestamp' => $this->getClientTime()
];

// 立即在本地预测执行
$this->applyLocalPrediction($inputType, $inputData);

// 发送到服务器
$this->server->handleClientInput($this->getClientId(), $inputPacket);

// 保存输入用于可能的回滚
$this->pendingInputs[$this->inputSequence] = $inputPacket;
}

/**
* 应用本地预测
*/
private function applyLocalPrediction($inputType, $inputData) {
$player = &$this->localState['players'][$this->playerId];

switch ($inputType) {
case 'move':
$speed = 5.0;
$player['velocity']['x'] = $inputData['x'] * $speed;
$player['velocity']['z'] = $inputData['z'] * $speed;
break;
// 其他输入类型的本地预测...
}
}

/**
* 接收服务器状态更新
*/
public function receiveStateUpdate($serverState) {
// reconciliation:与服务器状态同步
$this->reconcileWithServer($serverState);
}

/**
* 与服务器状态协调
*/
private function reconcileWithServer($serverState) {
$serverPlayer = $serverState['players'][$this->playerId];
$localPlayer = $this->localState['players'][$this->playerId];

// 如果位置差异过大,采用服务器状态
$positionDiff = $this->calculateDistance($serverPlayer['position'], $localPlayer['position']);

if ($positionDiff > 1.0) { // 阈值
echo "客户端 {$this->playerId}: 位置差异过大({$positionDiff}),采用服务器状态\n";
$this->localState = $serverState;
} else {
// 微小差异,平滑插值
$this->interpolateToServerState($serverState);
}

// 移除已确认的输入
$lastServerSeq = $serverPlayer['last_input_seq'];
$this->removeAcknowledgedInputs($lastServerSeq);
}

/**
* 插值到服务器状态
*/
private function interpolateToServerState($serverState) {
$interpolationFactor = 0.2; // 插值系数

foreach ($serverState['players'] as $playerId => $serverPlayer) {
if (!isset($this->localState['players'][$playerId])) {
$this->localState['players'][$playerId] = $serverPlayer;
continue;
}

$localPlayer = &$this->localState['players'][$playerId];

// 对其他玩家使用插值
if ($playerId != $this->playerId) {
$localPlayer['position']['x'] = $this->lerp(
$localPlayer['position']['x'],
$serverPlayer['position']['x'],
$interpolationFactor
);
// 对其他坐标和旋转也进行插值...
}
}
}

/**
* 移除已确认的输入
*/
private function removeAcknowledgedInputs($lastServerSeq) {
foreach ($this->pendingInputs as $seq => $input) {
if ($seq <= $lastServerSeq) {
unset($this->pendingInputs[$seq]);
}
}
}

/**
* 客户端更新循环
*/
public function update($deltaTime) {
// 更新本地预测的状态
$this->updateLocalState($deltaTime);
}

/**
* 更新本地状态
*/
private function updateLocalState($deltaTime) {
foreach ($this->localState['players'] as &$player) {
// 应用物理(与服务器相同的逻辑)
$player['position']['x'] += $player['velocity']['x'] * $deltaTime;
$player['position']['y'] += $player['velocity']['y'] * $deltaTime;
$player['position']['z'] += $player['velocity']['z'] * $deltaTime;

if ($player['position']['y'] > 0) {
$player['velocity']['y'] -= 20.0 * $deltaTime;
} else {
$player['position']['y'] = 0;
$player['velocity']['y'] = 0;
}

$player['velocity']['x'] *= 0.9;
$player['velocity']['z'] *= 0.9;
}
}

// 辅助方法
private function getClientTime() {
return microtime(true) + $this->serverTimeOffset;
}

private function getClientId() {
return "client_{$this->playerId}";
}

private function calculateDistance($pos1, $pos2) {
$dx = $pos1['x'] - $pos2['x'];
$dy = $pos1['y'] - $pos2['y'];
$dz = $pos1['z'] - $pos2['z'];
return sqrt($dx*$dx + $dy*$dy + $dz*$dz);
}

private function lerp($a, $b, $f) {
return $a + $f * ($b - $a);
}

/**
* 获取本地状态(用于渲染)
*/
public function getRenderState() {
return $this->localState;
}
}

?>

帧同步 (Frame Synchronization)

核心原理

所有客户端运行相同的确定性逻辑,服务器只负责转发输入指令,不进行游戏逻辑计算。

架构设计

1
2
3
客户端A → 操作指令 → 服务器 → 转发指令 → 所有客户端
客户端B → 操作指令 → 服务器 → 转发指令 → 所有客户端
所有客户端运行相同的确定性逻辑

PHP实现示例

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
<?php

/**
* 帧同步游戏服务器
*/
class FrameSyncGameServer {
private $clients;
private $currentFrame;
private $frameInputs;
private $lockstepInterval;
private $lastFrameTime;

public function __construct($frameRate = 10) {
$this->clients = [];
$this->currentFrame = 0;
$this->frameInputs = [];
$this->lockstepInterval = 1.0 / $frameRate;
$this->lastFrameTime = microtime(true);
}

/**
* 处理客户端连接
*/
public function handleClientConnect($clientId, $playerData) {
$this->clients[$clientId] = [
'id' => $clientId,
'player_id' => $playerData['player_id'],
'last_heartbeat' => microtime(true),
'ready' => true
];

echo "客户端 {$clientId} 已连接\n";
return $this->currentFrame; // 返回当前帧号
}

/**
* 接收客户端帧输入
*/
public function receiveFrameInput($clientId, $frameInput) {
if (!isset($this->clients[$clientId])) {
return false;
}

$frame = $frameInput['frame'];
$playerId = $this->clients[$clientId]['player_id'];

// 初始化帧输入存储
if (!isset($this->frameInputs[$frame])) {
$this->frameInputs[$frame] = [];
}

// 存储玩家输入
$this->frameInputs[$frame][$playerId] = [
'inputs' => $frameInput['inputs'],
'checksum' => $frameInput['checksum'],
'timestamp' => $frameInput['timestamp']
];

echo "收到客户端 {$clientId}{$frame} 的输入\n";
return true;
}

/**
* 服务器帧同步更新
*/
public function update() {
$currentTime = microtime(true);

// 检查是否应该推进到下一帧
if ($currentTime - $this->lastFrameTime >= $this->lockstepInterval) {
return $this->advanceFrame();
}

return false;
}

/**
* 推进到下一帧
*/
private function advanceFrame() {
$targetFrame = $this->currentFrame + 1;

// 检查是否收集到所有客户端的输入
if ($this->hasAllInputsForFrame($targetFrame)) {
// 广播这一帧的所有输入给所有客户端
$this->broadcastFrameInputs($targetFrame);

$this->currentFrame = $targetFrame;
$this->lastFrameTime = microtime(true);

// 清理旧的帧数据(保留最近几帧用于断线重连)
$this->cleanupOldFrames();

echo "推进到帧 {$targetFrame}\n";
return true;
} else {
echo "等待帧 {$targetFrame} 的完整输入...\n";
return false;
}
}

/**
* 检查是否收集到某帧的所有输入
*/
private function hasAllInputsForFrame($frame) {
if (!isset($this->frameInputs[$frame])) {
return false;
}

// 检查每个已连接的客户端是否都提供了输入
foreach ($this->clients as $client) {
$playerId = $client['player_id'];
if (!isset($this->frameInputs[$frame][$playerId])) {
return false;
}
}

return true;
}

/**
* 广播帧输入给所有客户端
*/
private function broadcastFrameInputs($frame) {
$frameData = [
'frame' => $frame,
'inputs' => $this->frameInputs[$frame]
];

foreach ($this->clients as $clientId => $client) {
$this->sendToClient($clientId, 'frame_inputs', $frameData);
}
}

/**
* 清理旧的帧数据
*/
private function cleanupOldFrames() {
$framesToKeep = 60; // 保留最近60帧用于断线重连

foreach ($this->frameInputs as $frame => $inputs) {
if ($frame < $this->currentFrame - $framesToKeep) {
unset($this->frameInputs[$frame]);
}
}
}

/**
* 处理客户端断线重连
*/
public function handleClientReconnect($clientId, $lastKnownFrame) {
if (!isset($this->clients[$clientId])) {
return false;
}

$catchUpData = [];

// 提供缺失的帧输入
for ($frame = $lastKnownFrame + 1; $frame <= $this->currentFrame; $frame++) {
if (isset($this->frameInputs[$frame])) {
$catchUpData[$frame] = $this->frameInputs[$frame];
}
}

$this->sendToClient($clientId, 'catch_up', [
'from_frame' => $lastKnownFrame + 1,
'to_frame' => $this->currentFrame,
'inputs' => $catchUpData
]);

echo "客户端 {$clientId} 从帧 {$lastKnownFrame} 追赶至 {$this->currentFrame}\n";
return true;
}

/**
* 发送数据到客户端(模拟)
*/
private function sendToClient($clientId, $messageType, $data) {
// 实际实现中的网络发送逻辑
echo "发送到客户端 {$clientId}: {$messageType}\n";
}

/**
* 获取当前帧号
*/
public function getCurrentFrame() {
return $this->currentFrame;
}
}

/**
* 帧同步游戏客户端
*/
class FrameSyncGameClient {
private $playerId;
private $server;
private $currentFrame;
private $gameLogic;
private $pendingInputs;
private $confirmedFrames;
private $clientId;

public function __construct($playerId, $server) {
$this->playerId = $playerId;
$this->server = $server;
$this->currentFrame = 0;
$this->gameLogic = new DeterministicGameLogic();
$this->pendingInputs = [];
$this->confirmedFrames = [];
$this->clientId = "client_{$playerId}";

// 连接服务器
$this->connectToServer();
}

/**
* 连接到服务器
*/
private function connectToServer() {
$startFrame = $this->server->handleClientConnect($this->clientId, [
'player_id' => $this->playerId
]);

$this->currentFrame = $startFrame;
echo "客户端 {$this->playerId} 已连接,起始帧: {$startFrame}\n";
}

/**
* 处理玩家输入
*/
public function handleInput($inputType, $inputData) {
$frame = $this->currentFrame + 1; // 下一帧的输入

if (!isset($this->pendingInputs[$frame])) {
$this->pendingInputs[$frame] = [];
}

$this->pendingInputs[$frame][] = [
'type' => $inputType,
'data' => $inputData,
'player_id' => $this->playerId
];

echo "客户端 {$this->playerId} 为帧 {$frame} 记录输入: {$inputType}\n";
}

/**
* 客户端更新循环
*/
public function update() {
// 发送待处理的帧输入到服务器
$this->sendPendingInputs();

// 检查是否有可以执行的确认帧
$this->executeConfirmedFrames();
}

/**
* 发送待处理的输入到服务器
*/
private function sendPendingInputs() {
foreach ($this->pendingInputs as $frame => $inputs) {
// 只发送尚未确认的帧
if (!isset($this->confirmedFrames[$frame])) {
$checksum = $this->calculateChecksum($inputs);

$this->server->receiveFrameInput($this->clientId, [
'frame' => $frame,
'inputs' => $inputs,
'checksum' => $checksum,
'timestamp' => microtime(true)
]);
}
}
}

/**
* 执行已确认的帧
*/
private function executeConfirmedFrames() {
ksort($this->confirmedFrames);

foreach ($this->confirmedFrames as $frame => $frameInputs) {
if ($frame > $this->currentFrame) {
echo "客户端 {$this->playerId} 执行帧 {$frame}\n";

// 使用确定性逻辑执行这一帧
$this->gameLogic->executeFrame($frame, $frameInputs);

$this->currentFrame = $frame;

// 移除已执行的输入
unset($this->pendingInputs[$frame]);
unset($this->confirmedFrames[$frame]);
}
}
}

/**
* 接收服务器帧输入广播
*/
public function receiveFrameInputs($frameData) {
$frame = $frameData['frame'];
$allInputs = $frameData['inputs'];

// 验证帧数据完整性
if ($this->validateFrameInputs($frame, $allInputs)) {
$this->confirmedFrames[$frame] = $allInputs;
echo "客户端 {$this->playerId} 确认帧 {$frame} 输入\n";
} else {
echo "客户端 {$this->playerId}{$frame} 输入验证失败\n";
}
}

/**
* 接收追赶数据(断线重连)
*/
public function receiveCatchUpData($catchUpData) {
$fromFrame = $catchUpData['from_frame'];
$toFrame = $catchUpData['to_frame'];
$inputs = $catchUpData['inputs'];

echo "客户端 {$this->playerId} 开始追赶: 帧 {$fromFrame} -> {$toFrame}\n";

// 快速执行缺失的帧
for ($frame = $fromFrame; $frame <= $toFrame; $frame++) {
if (isset($inputs[$frame])) {
$this->gameLogic->executeFrame($frame, $inputs[$frame]);
echo "客户端 {$this->playerId} 追赶执行帧 {$frame}\n";
}
}

$this->currentFrame = $toFrame;
echo "客户端 {$this->playerId} 追赶完成,当前帧: {$toFrame}\n";
}

/**
* 验证帧输入完整性
*/
private function validateFrameInputs($frame, $allInputs) {
// 检查是否有所有玩家的输入
$expectedPlayers = ['player1', 'player2']; // 实际中应该从服务器获取

foreach ($expectedPlayers as $playerId) {
if (!isset($allInputs[$playerId])) {
return false;
}
}

// 验证校验和(可选)
foreach ($allInputs as $playerId => $inputData) {
$calculatedChecksum = $this->calculateChecksum($inputData['inputs']);
if ($calculatedChecksum != $inputData['checksum']) {
return false;
}
}

return true;
}

/**
* 计算输入校验和
*/
private function calculateChecksum($inputs) {
return md5(serialize($inputs));
}

/**
* 获取当前游戏状态(用于渲染)
*/
public function getRenderState() {
return $this->gameLogic->getCurrentState();
}

/**
* 获取当前帧号
*/
public function getCurrentFrame() {
return $this->currentFrame;
}
}

/**
* 确定性游戏逻辑
*/
class DeterministicGameLogic {
private $gameState;

public function __construct() {
$this->gameState = [
'players' => [
'player1' => ['x' => 0, 'y' => 0, 'health' => 100],
'player2' => ['x' => 10, 'y' => 0, 'health' => 100]
],
'projectiles' => []
];
}

/**
* 执行一帧游戏逻辑
*/
public function executeFrame($frame, $frameInputs) {
// 处理所有玩家输入
foreach ($frameInputs as $playerId => $inputData) {
$this->processPlayerInputs($playerId, $inputData['inputs']);
}

// 更新游戏逻辑
$this->updateGameLogic();

// 生成帧校验和(用于调试和验证)
$this->generateFrameChecksum($frame);
}

/**
* 处理玩家输入
*/
private function processPlayerInputs($playerId, $inputs) {
$player = &$this->gameState['players'][$playerId];

foreach ($inputs as $input) {
switch ($input['type']) {
case 'move':
$this->handleMove($player, $input['data']);
break;
case 'attack':
$this->handleAttack($playerId, $input['data']);
break;
// 其他输入类型...
}
}
}

/**
* 处理移动
*/
private function handleMove(&$player, $moveData) {
$speed = 0.5;
$player['x'] += $moveData['x'] * $speed;
$player['y'] += $moveData['y'] * $speed;
}

/**
* 处理攻击
*/
private function handleAttack($attackerId, $attackData) {
$targetId = $attackData['target_id'];

if (isset($this->gameState['players'][$targetId])) {
$this->gameState['players'][$targetId]['health'] -= 10;

if ($this->gameState['players'][$targetId]['health'] <= 0) {
$this->handlePlayerDeath($targetId);
}
}
}

/**
* 处理玩家死亡
*/
private function handlePlayerDeath($playerId) {
$this->gameState['players'][$playerId]['health'] = 100;
$this->gameState['players'][$playerId]['x'] = 0;
$this->gameState['players'][$playerId]['y'] = 0;
}

/**
* 更新游戏逻辑
*/
private function updateGameLogic() {
// 更新抛射物等
// 碰撞检测
// 其他游戏逻辑...
}

/**
* 生成帧校验和
*/
private function generateFrameChecksum($frame) {
$checksum = md5(serialize($this->gameState));
echo "帧 {$frame} 校验和: {$checksum}\n";
return $checksum;
}

/**
* 获取当前游戏状态
*/
public function getCurrentState() {
return $this->gameState;
}
}

?>

应用示例与对比测试

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php

class NetworkSyncExamples {

/**
* 状态同步演示
*/
public static function stateSyncDemo() {
echo "=== 状态同步演示 ===\n";

// 创建服务器
$server = new StateSyncGameServer(10); // 10Hz 更新率

// 创建两个客户端
$client1 = new StateSyncGameClient('player1', $server);
$client2 = new StateSyncGameClient('player2', $server);

// 模拟游戏循环
for ($i = 0; $i < 5; $i++) {
echo "\n--- 回合 {$i} ---\n";

// 客户端1输入
$client1->handleInput('move', ['x' => 1, 'z' => 0]);

// 服务器更新
$server->update();

// 客户端更新
$client1->update(0.1);
$client2->update(0.1);

// 显示状态
$state1 = $client1->getRenderState();
$state2 = $client2->getRenderState();

echo "客户端1位置: " . json_encode($state1['players']['player1']['position']) . "\n";
echo "客户端2位置: " . json_encode($state2['players']['player1']['position']) . "\n";

usleep(100000); // 100ms
}
}

/**
* 帧同步演示
*/
public static function frameSyncDemo() {
echo "\n=== 帧同步演示 ===\n";

// 创建服务器
$server = new FrameSyncGameServer(2); // 2Hz 帧率(便于演示)

// 创建两个客户端
$client1 = new FrameSyncGameClient('player1', $server);
$client2 = new FrameSyncGameClient('player2', $server);

// 模拟游戏循环
for ($i = 0; $i < 10; $i++) {
echo "\n--- 时间点 {$i} ---\n";

// 客户端输入
if ($i % 2 == 0) {
$client1->handleInput('move', ['x' => 1, 'y' => 0]);
$client2->handleInput('move', ['x' => -1, 'y' => 0]);
}

// 客户端更新
$client1->update();
$client2->update();

// 服务器更新
$server->update();

// 显示状态
$frame1 = $client1->getCurrentFrame();
$frame2 = $client2->getCurrentFrame();
$state1 = $client1->getRenderState();

echo "客户端1帧: {$frame1}, 客户端2帧: {$frame2}\n";
echo "玩家1位置: ({$state1['players']['player1']['x']}, {$state1['players']['player1']['y']})\n";

usleep(500000); // 500ms
}
}

/**
* 性能对比测试
*/
public static function performanceComparison() {
echo "\n=== 性能对比测试 ===\n";

// 状态同步性能测试
$stateServer = new StateSyncGameServer(20);
$startTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
$stateServer->update();
}

$stateSyncTime = microtime(true) - $startTime;

// 帧同步性能测试
$frameServer = new FrameSyncGameServer(20);
$startTime = microtime(true);

for ($i = 0; $i < 100; $i++) {
$frameServer->update();
}

$frameSyncTime = microtime(true) - $startTime;

echo "状态同步 100次更新: " . number_format($stateSyncTime * 1000, 2) . "ms\n";
echo "帧同步 100次更新: " . number_format($frameSyncTime * 1000, 2) . "ms\n";
echo "性能差异: " . number_format(($stateSyncTime - $frameSyncTime) / $stateSyncTime * 100, 2) . "%\n";
}

/**
* 网络延迟模拟测试
*/
public static function latencyTest() {
echo "\n=== 网络延迟测试 ===\n";

$server = new StateSyncGameServer(10);
$client = new StateSyncGameClient('test_player', $server);

// 模拟不同网络延迟下的表现
$latencies = [50, 100, 200, 500]; // 毫秒

foreach ($latencies as $latency) {
echo "模拟 {$latency}ms 延迟:\n";

// 这里实际应该模拟网络延迟
$client->handleInput('move', ['x' => 1, 'z' => 0]);
$server->update();
$client->update(0.1);

$state = $client->getRenderState();
echo "玩家位置: " . json_encode($state['players']['test_player']['position']) . "\n";
}
}
}

// 运行演示
NetworkSyncExamples::stateSyncDemo();
NetworkSyncExamples::frameSyncDemo();
NetworkSyncExamples::performanceComparison();
NetworkSyncExamples::latencyTest();

?>

高级优化技术

1. 预测与回滚 (Prediction and Rollback)

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
<?php

/**
* 带预测回滚的状态同步
*/
class PredictiveStateSync extends StateSyncGameServer {
private $historyStates;
private $historySize;

public function __construct($updateRate = 10, $historySize = 60) {
parent::__construct($updateRate);
$this->historyStates = [];
$this->historySize = $historySize;
}

/**
* 保存历史状态
*/
protected function updateGameLogic($deltaTime) {
parent::updateGameLogic($deltaTime);

// 保存历史状态用于回滚
$this->saveHistoryState();
}

private function saveHistoryState() {
$this->historyStates[] = [
'timestamp' => $this->gameState['timestamp'],
'state' => serialize($this->gameState) // 深拷贝
];

// 限制历史记录大小
if (count($this->historyStates) > $this->historySize) {
array_shift($this->historyStates);
}
}

/**
* 处理延迟的输入(回滚并重新模拟)
*/
public function handleDelayedInput($clientId, $inputData) {
$inputTime = $inputData['timestamp'];
$playerId = $this->clients[$clientId]['player_id'];

// 找到输入应该应用的时间点
$targetStateIndex = $this->findStateByTimestamp($inputTime);

if ($targetStateIndex !== null) {
// 回滚到目标状态
$this->rollbackToState($targetStateIndex);

// 重新应用输入并模拟
$this->reapplyInputsFrom($targetStateIndex, $playerId, $inputData);

return true;
}

return false;
}

private function findStateByTimestamp($timestamp) {
foreach ($this->historyStates as $index => $history) {
if ($history['timestamp'] >= $timestamp) {
return $index;
}
}
return null;
}

private function rollbackToState($stateIndex) {
$targetState = $this->historyStates[$stateIndex];
$this->gameState = unserialize($targetState['state']);

// 移除之后的历史记录
$this->historyStates = array_slice($this->historyStates, 0, $stateIndex + 1);
}

private function reapplyInputsFrom($startIndex, $playerId, $newInput) {
// 从指定状态开始重新模拟所有输入
for ($i = $startIndex; $i < count($this->historyStates); $i++) {
// 重新应用输入并更新状态...
}

// 应用新的延迟输入
$this->applyPlayerInput($this->gameState['players'][$playerId], $newInput);
}
}

?>

2. 兴趣管理 (Interest Management)

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
<?php

/**
* 基于兴趣管理的状态同步
*/
class InterestManagedStateSync extends StateSyncGameServer {

/**
* 获取玩家感兴趣的状态(只同步相关对象)
*/
public function getInterestBasedState($playerId) {
$playerPosition = $this->gameState['players'][$playerId]['position'];
$interestRadius = 50.0; // 兴趣范围

$relevantState = [
'players' => [],
'objects' => [],
'timestamp' => $this->gameState['timestamp']
];

// 只包含在兴趣范围内的玩家和对象
foreach ($this->gameState['players'] as $otherPlayerId => $otherPlayer) {
if ($this->isInInterestRange($playerPosition, $otherPlayer['position'], $interestRadius)) {
$relevantState['players'][$otherPlayerId] = $otherPlayer;
}
}

foreach ($this->gameState['objects'] as $objectId => $object) {
if ($this->isInInterestRange($playerPosition, $object['position'], $interestRadius)) {
$relevantState['objects'][$objectId] = $object;
}
}

return $relevantState;
}

private function isInInterestRange($pos1, $pos2, $radius) {
$dx = $pos1['x'] - $pos2['x'];
$dz = $pos1['z'] - $pos2['z'];
$distance = sqrt($dx*$dx + $dz*$dz);

return $distance <= $radius;
}
}

?>

实际应用场景

MMO游戏(状态同步)

1
2
3
4
5
class MMOGameServer extends StateSyncGameServer {
// MMO游戏通常使用状态同步
// 服务器维护整个游戏世界的权威状态
// 客户端只接收视野范围内的状态更新
}

RTS游戏(帧同步)

1
2
3
4
5
class RTSGameServer extends FrameSyncGameServer {
// RTS游戏通常使用帧同步
// 所有客户端运行相同的确定性逻辑
// 服务器只负责指令转发
}

混合方案

1
2
3
4
5
6
7
8
9
10
11
12
class HybridSyncGameServer {
// 对关键逻辑使用状态同步
private $stateSync;

// 对大量单位使用帧同步
private $frameSync;

public function __construct() {
$this->stateSync = new StateSyncGameServer();
$this->frameSync = new FrameSyncGameServer();
}
}

总结

状态同步和帧同步是多人游戏网络同步的两种核心方案:

核心优势对比

特性 状态同步 帧同步
数据一致性 服务器保证 确定性逻辑保证
网络带宽 较高 较低
服务器负载 较高 较低
客户端负载 较低 较高
反作弊能力
断线重连 简单 复杂
开发复杂度 中等 较高

选择建议

  1. 选择状态同步当

    • 游戏逻辑复杂,需要服务器验证
    • 反作弊是重要考虑因素
    • 游戏世界大,玩家分布稀疏
    • 需要支持大量观战者
  2. 选择帧同步当

    • 游戏逻辑相对简单且确定性
    • 需要支持大量单位同时操作
    • 网络带宽受限
    • 游戏回放功能很重要

最佳实践

  1. 网络优化

    • 使用Delta压缩减少数据传输
    • 实现兴趣管理减少不必要同步
    • 采用数据优先级(重要数据优先发送)
  2. 延迟处理

    • 客户端预测 + 服务器校正
    • 插值平滑显示
    • 延迟补偿机制
  3. 可靠性保证

    • 关键数据使用可靠传输
    • 实现断线检测和重连机制
    • 数据完整性验证
  4. 性能监控

    • 实时监控网络延迟和丢包率
    • 动态调整同步频率
    • 客户端性能自适应

状态同步和帧同步各有优劣,在实际项目中经常根据具体需求进行混合使用或定制化改进。理解它们的原理和特性,是构建高质量多人游戏的基础。


文章作者: Crazy Boy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Crazy Boy !
评 论
 上一篇
锁步协议(Lockstep):RTS游戏的完美同步之道
锁步协议(Lockstep):RTS游戏的完美同步之道
如何让数百个作战单位在多个玩家间保持精确同步?锁步协议通过确定性仿真和命令同步,为实时战略游戏提供了完美的网络同步解决方案。 问题背景在实时战略游戏(RTS)中,网络同步面临独特挑战: 大规模单位:数百个作战单位需要同步状态 精确时序
2025-11-20
下一篇 
Fisher-Yates 洗牌算法
Fisher-Yates 洗牌算法
Fisher-Yates 洗牌算法(也称为 Knuth 洗牌)是一种高效且公正的随机洗牌算法,用于将数组或列表中的元素随机重新排列。 🎯 算法原理核心思想:从后往前遍历数组,将当前元素与随机位置的一个元素交换。 📝 算法步骤原始版本:1
2025-11-20
  目录
  目录
  目录
hexo