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

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

问题背景

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

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

基本概念

核心思想对比

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

状态同步 (State Synchronization)

核心原理

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

架构设计

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

PHP实现示例

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

核心原理

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

架构设计

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

PHP实现示例

<?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;
}
}

?>

应用示例与对比测试

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

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

<?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游戏(状态同步)

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

RTS游戏(帧同步)

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

混合方案

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 !
赏

留言

想法、疑问或勘误,都可以写在这里。留言会公开显示。

正在准备留言区…

 上一篇
乐观并发控制(OCC):多玩家数据并发修改的优雅解决方案 乐观并发控制(OCC):多玩家数据并发修改的优雅解决方案
如何让多个玩家同时修改游戏数据而不产生冲突?乐观并发控制通过”先操作,后验证”的方式,在保证数据一致性的同时提供高性能的并发访问。 问题背景 在多人在线游戏和分布式系统中,经常面临多个客户端同时修改同一数据的挑战: 资源竞争:多个玩家同时抢
2025-11-20
下一篇 
锁步协议(Lockstep):RTS游戏的完美同步之道 锁步协议(Lockstep):RTS游戏的完美同步之道
如何让数百个作战单位在多个玩家间保持精确同步?锁步协议通过确定性仿真和命令同步,为实时战略游戏提供了完美的网络同步解决方案。 问题背景 在实时战略游戏(RTS)中,网络同步面临独特挑战: 大规模单位:数百个作战单位需要同步状态 精确时序:技
2025-11-20