在一小时内为你的游戏添加多人游戏功能






4.31/5 (9投票s)
2005 年 10 月 8 日
4分钟阅读

95526

1063
想开发多人游戏但不知道从何开始,或者没有时间开发稳健的网络代码?请继续阅读……
背景
我正在业余时间开发自己的 3D 游戏。如果没有内置的多人功能,它可能早就被视为博物馆里的展品了。考虑到我拥有软件开发背景(超过 7 年的经验),我本可以花些时间尝试自己编写网络代码。然而,许多人可能认为网络模块与游戏中的其他组件相比微不足道,但我必须说这是一个常见的误解。如果您曾经考虑过上线,您计划使用的网络库必须健壮可靠,而不仅仅是能够传输数据。对于游戏来说,第一个让人扫兴的可能是导致整个程序崩溃的网络错误。
入门
如果您搜索 Google,可能会找到很多免费的网络库。尽管最基本网络功能(如连接类型(TCP、UDP、IPX)、加密、压缩等)是相同的,但当您想使用它来支持游戏中的多个玩家时,通常会有更多要求。也许对大多数独立开发者(我也是其中一员)来说,最重要的问题是成本。长话短说,我已经进行了相当广泛的搜索,以下是游戏开发者中更普遍的网络库:
说真的,我在这里不会对每个网络工具包进行评测,因为这不是本文的目的,而且我确信它们之间各有优缺点。但在浏览了它们各自的源代码树和示例项目后,我发现 Raknet 是最全面且易于采用的库。
规划您的网络游戏
对网络层与您的游戏进行清晰的抽象理解非常重要。游戏世界或状态必须得到妥善管理,并映射到来自其他连接玩家的更新。一些新手可能听说过客户端-服务器架构这个术语,但对于在设置网络连接、身份验证、消息发送和断开连接等方面,信息如何从客户端流向服务器以及反之亦然,却没有清晰的理解。当网络库要求开发者处理诸如字节序安全(endian-safeness)和流的序列化/反序列化等问题时,事情可能会变得复杂。我将直接开始介绍一个我认为大多数人都可以轻松使用的示例代码。
在客户端-服务器模型中,您需要启动一个服务器(作为主机),然后让客户端连接到它。忘掉后台连接是如何完成的,以及有多少客户端可以连接,因为这些都是由网络库负责的(当然,您可能需要在过程中设置几个参数)。服务器会跟踪连接到它的客户端,并在某个客户端向服务器发送更新时通知其他客户端。因此,正如其名称所示,服务器充当“服务”所有客户端更新的中心点。有些数据可以由服务器独占管理(客户端无法更改)并推送到客户端,反之亦然。但在本文中,我们将探讨客户端如何通过服务器互相更新。
使用代码
您首先需要下载 Raknet 并构建源代码树。您可以解压本文提供的源代码,然后将它们添加到 Raknet .NET 解决方案中。它们应该能够正常集成并成功编译。我在本文中介绍了 BitStreams,通过使用流,您基本上是将数据打包成 char
(比特/字节)的包,然后发送出去。在接收端,它会被解包并重新解释成其原始格式进行处理。
// All the headers...(see actual
// source code) <PACKETENUMERATIONS.H>
const unsigned char PACKET_ID_LINE = 100;
class ClientConnection
{
public:
ClientConnection(char * serverIP, char * portString)
: client(NULL)
{
client = RakNetworkFactory::GetRakClientInterface();
client->Connect(serverIP, atoi(portString), 0, 0, 0);
Player myself;
players.push_back( myself ); // 1st player (self),
// subsequent players are
// created on the fly
}
~ClientConnection()
{
client->Disconnect(300);
RakNetworkFactory::DestroyRakClientInterface(client);
}
// Updates all entities in own world,
// data coming in from external
void updateOwnWorld(Player* incomingPlayer)
{
// Loop through all players including
// ownself and update accordingly
for (int i=0; i<PLAYERS.SIZE();
(incomingPlayer- diff="(int)" int { i++)>ID - players[i].ID);
if (!diff)
{
// Copy all information
players[i].position[0] = incomingPlayer->position[0];
players[i].position[1] = incomingPlayer->position[1];
players[i].position[2] = incomingPlayer->position[2];
players[i].orientation[0] =
incomingPlayer->orientation[0];
players[i].orientation[1] =
incomingPlayer->orientation[1];
players[i].orientation[2] =
incomingPlayer->orientation[2];
players[i].speed = incomingPlayer->speed;
players[i].missiles = incomingPlayer->missiles;
players[i].health = incomingPlayer->health;
}
}
}
// Updates rest of the external world about own changes
void updateWorld(Player p)
{
RakNet::BitStream dataStream;
dataStream.Write(PACKET_ID_LINE);
dataStream.Write((float)(p.position[0]));
dataStream.Write((float)(p.position[1]));
dataStream.Write((float)(p.position[2]));
dataStream.Write((float)(p.orientation[0]));
dataStream.Write((float)(p.orientation[1]));
dataStream.Write((float)(p.orientation[2]));
dataStream.Write((int)(p.missiles));
dataStream.Write((int)(p.speed));
dataStream.Write((int)(p.health));
client->Send(&dataStream, HIGH_PRIORITY,
RELIABLE_ORDERED, 0);
}
void ListenForPackets()
{
Packet * p = client->Receive();
if(p != NULL)
{
HandlePacket(p);
client->DeallocatePacket(p);
}
}
// Handle incoming packet
void HandlePacket(Packet * p)
{
RakNet::BitStream dataStream((const char*)p->data,
p->length, false);
unsigned char packetID;
dataStream.Read(packetID);
switch(packetID)
{
case PACKET_ID_LINE:
Player inPlayer;
// Order is important (reference server code as well)
dataStream.Read((inPlayer.ID));
dataStream.Read((inPlayer.position[0]));
dataStream.Read((inPlayer.position[1]));
dataStream.Read((inPlayer.position[2]));
dataStream.Read((inPlayer.orientation[0]));
dataStream.Read((inPlayer.orientation[1]));
dataStream.Read((inPlayer.orientation[2]));
dataStream.Read((inPlayer.missiles));
dataStream.Read((inPlayer.speed));
dataStream.Read((inPlayer.health));
// If you're not passing "uiversal data"
// (i.e. passing a single type of data
// which is universally shared) and the
// number of players are dynamically
// changing, you'd need a manager to keep
// track of new and dropped out players.
// I'm using a vector as a simple container
// and some manipulation for that.
if (_isNewPlayer(inPlayer.ID))
{
std::cout << "New player " <<
inPlayer.ID << "just joined!" << std::endl;
// Create new player and include in world
Player newplayer;
newplayer.ID = inPlayer.ID;
newplayer.position[0] = inPlayer.position[0];
newplayer.position[1] = inPlayer.position[1];
newplayer.position[2] = inPlayer.position[2];
newplayer.orientation[0] = inPlayer.orientation[0];
newplayer.orientation[1] = inPlayer.orientation[1];
newplayer.orientation[2] = inPlayer.orientation[2];
newplayer.missiles = inPlayer.missiles;
newplayer.speed = inPlayer.speed;
newplayer.health = inPlayer.health;
players.push_back( newplayer );
}
else
{
// Look for the specific player and update accordingly
if (!updateOwnWorld(inPlayer))
{
std::cout << "Error updating Player " <<
inPlayer.ID << "'s information!" << std::endl;
getchar();
}
}
break;
}
}
int getPlayerCount() { return players.size(); }
private:
bool _isNewPlayer(double playerID)
{
int count=0;
for (int i=0; i<players.size(); i++)
{
int diff = playerID-players[i].ID;
if ( !diff ) // If similar, x-y=0;
{
count++;
}
}
if (count==0) // is new
{
return true;
}
return false;
}
bool updateOwnWorld(Player p)
{
int count = 0;
for (int i=0; i<players.size(); i++)
{
int diff = p.ID-players[i].ID;
if ( !diff ) // If similar, x-y=0;
{
// Update the information
players[i].position[0] = p.position[0];
players[i].position[1] = p.position[1];
players[i].position[2] = p.position[2];
players[i].orientation[0] = p.orientation[0];
players[i].orientation[1] = p.orientation[1];
players[i].orientation[2] = p.orientation[2];
players[i].missiles = p.missiles;
players[i].speed = p.speed;
players[i].health = p.health;
return true; // Assuming no identical ID
}
else
{
count++;
}
}
if (count == players.size())
{
return false;
}
}
RakClientInterface * client;
};
int main(int argc, char** argv)
{
// Change this to non-compile dependent
ClientConnection myConnection("127.0.0.1", "10000");
while(1)
{
Sleep(100);
// includes updating of changes from world
myConnection.ListenForPackets();
std::cout << "Total number of players: " <<
myConnection.getPlayerCount() << std::endl;
if(kbhit())
{
char c=getchar();
if (c==' ')
{
// players[0] is always ownself
players[0].position[0]-=0.1f;
players[0].position[1]-=0.2f;
players[0].position[2]-=0.3f;
players[0].orientation[0]+=0.1f;
players[0].orientation[1]+=0.1f;
players[0].orientation[2]+=0.1f;
players[0].health++;
players[0].missiles++;
players[0].speed--;
// includes updating world of own changes
myConnection.updateWorld(players[0]);
}
}
// Get an account of all world items
for (int j=0; j<players.size(); j++)
{
printf("Player[%u] position[0]: %f\n",
players[j].ID, players[j].position[0]);
printf("Player[%u] position[1]: %f\n",
players[j].ID, players[j].position[1]);
printf("Player[%u] position[2]: %f\n",
players[j].ID, players[j].position[2]);
printf("Player[%u] orientation[0]: %f\n",
players[j].ID, players[j].orientation[0]);
printf("Player[%u] orientation[1]: %f\n",
players[j].ID, players[j].orientation[1]);
printf("Player[%u] orientation[2]: %f\n",
players[j].ID, players[j].orientation[2]);
printf("Player[%u] missiles: %d\n",
players[j].ID, players[j].missiles);
printf("Player[%u] speed: %d\n",
players[j].ID, players[j].speed);
printf("Player[%u] health: %d\n",
players[j].ID, players[j].health);
}
}
return 0;
}
我插入了几个 getchar()
调用,以便程序员在调试网络错误时能够同步。虽然速度会慢一些,但这些“钩子”可以清楚地展示客户端-服务器-客户端之间如何交换数据。一旦您熟悉了工作原理并更有信心,就可以删除它们,让您的游戏在网络代码的全面支持下运行。
关注点
我发现使用 Raknet 的分布式对象模型很容易,但它不是字节序安全的(endian-safe),这意味着当您在不同的机器(例如 OSX、Solaris)上运行程序时,可能会遇到问题。我遇到了 Dave Andrews 在使用 Raknet 和 Irrlicht(一个游戏引擎)的一个示例,功劳归于他,他阐明了多人网络方面的内容。
此处提供的代码仅用于展示简单的玩家管理。在 3D 游戏中(例如我正在开发的游戏),您需要将世界对象的管理与网络层的信息联系起来。由此会引出关于信息推断、压缩以及确保网络带宽最大化和世界对象同步的所有其他问题。
历史
- 首次发布。