65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.31/5 (9投票s)

2005 年 10 月 8 日

4分钟阅读

viewsIcon

95526

downloadIcon

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 游戏中(例如我正在开发的游戏),您需要将世界对象的管理与网络层的信息联系起来。由此会引出关于信息推断、压缩以及确保网络带宽最大化和世界对象同步的所有其他问题。

历史

  • 首次发布。
© . All rights reserved.