我知道这是一个老问题,我确信 OP 已经以某种方式得到了他们的答案,但为了好玩,我想我还是会做出回应。自从我最近使用 Kryonet 进行游戏开发以来,我一直在想这个确切的问题。
一些早期的网络游戏,例如Bungie's Marathon (1994)似乎正是这样做的:每个玩家的事件都将使用 UDP 发送给其他玩家。因此,如果玩家移动或射击,则该玩家的运动或射击的方向、速度等将被发送给其他玩家。这种方法存在一些问题。如果玩家的其中一个动作在网络上暂时丢失,则一个或多个玩家似乎与其他人不同步。在这种情况下,游戏状态不存在“真相”或“和解”。
另一种方法是让玩家在客户端计算他们的动作和动作,并将更新的位置发送到专用服务器。服务器接收所有玩家状态更新后,就有机会协调它们。如果某些数据在网络上丢失,它们也不会永久不同步。
与前面的示例相比,这相当于每个玩家将他们的位置发送到服务器,然后让服务器将每个玩家的位置发送给所有其他玩家。如果其中一个更新由于某种原因丢失,后续更新将对其进行更正。但是,如果只发送按键,则单个丢失的按键会使游戏失去同步,因为所有客户端都在分别计算其他客户端的位置。
对于动作游戏,您可以使用混合方法来最大程度地减少明显滞后。我一直以这种方式成功地将 Kryonet 用于动作游戏。每个玩家在每个渲染滴答时都将他们的状态发送到服务器(尽管这可能过多,应该进行优化)。状态包括位置、剩余射门次数、生命值等。玩家还发送他们拍摄的射门(起始速度和位置。)
服务器只是将这些内容回显给客户端。每当客户端接收到球时,它都会在客户端计算,包括射门是否击中接收球员。由于接收玩家只计算他们自己的状态,所以从他们自己的角度来看,一切似乎都保持同步。当他们被击中时,他们会感觉到被击中。当他们击中另一个玩家时,他们认为自己已经击中了另一个玩家。由玩家“接收”一个镜头来更新他们的健康状况并将该信息发送回服务器。
这确实意味着理论上一个投篮可能会滞后或“迷路”,并且一名球员可能认为他们的投篮击中了另一名球员,而在另一名球员的屏幕上没有发生任何击中。但在实践中,我发现这种方法效果很好。
这是一个例子(伪代码,不要指望它编译):
class Client {
final Array<Shot> shots;
final HashMap<String, PlayerState> players; // map of player name to state
final String playerName;
void render() {
// handle player input
// compute shot movement
// for shot in shot, shot.position = shot.position + shot.velociy * delta_t
// if one of these shots hits another player, make it appear as though they've been hit, but wait for an update in their state before we know what really happened
// if an update from another player says they died, then render their death
// if one of these shots overlaps _me_, and only if it overlaps me, deduct health from my state (other players are doing their own hit detection)
// only send _my own_ game state to server
server.sendTCP(players.get(playerName));
}
void listener(Object receivedObject) {
if(o instanceOf PlayerState) {
// update everyone else's state for me
// but ignore my own state update (since I computed it.)
PlayerState p = (PlayerState)o;
if(!p.name.equals(playerName) {
players.add(p.name, p);
}
} else if (o instanceof Shot) {
// update everyone else's shots for me
// but ignore my own shot updates (since I computed them.)
Shot s = (Shot)o;
if(!s.firedBy.equals(playerName) {
shots.add(s);
}
}
}
}
class Server {
final HashMap<String, PlayerState> players; // map of player name to
void listener(Object receivedObject) {
// compute whether anybody won based on most recent player state
// send any updates to all players
for(Connection otherPlayerCon : server.getConnections()) {
otherPlayerCon.sendTCP(o);
}
}
}
我确信这种方法也存在缺陷,并且可以通过各种方式对其进行改进。(例如,它很容易让“被黑”的客户端占据主导地位,因为他们总是可以发送不考虑任何损坏等的更新。但我认为这个问题超出了问题的范围。)