【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 13399|回复: 23

[网络技术] 2D网络游戏开发(网络篇)

[复制链接]
发表于 2006-8-14 10:51:56 | 显示全部楼层 |阅读模式
2D网络游戏开发(网络篇)(一)

作者:akinggw

“2D网游开发”,我有时都觉得这个标题包含的内容太多,要实现起来也太难。于是,我决定将它分门别类,我按照我要实现的一个网络游戏将它分成下面几个部分:

l 客户端

l 网络端

l 服务器端

l 网页端

我们来讲解一下,我们分别要在每个端口完成什么内容:

(1) 客户端 劈开接受网络数据不谈,我们主要在客户端实现游戏界面的显示,游戏界面又包括那些呢?比如,游戏地图的显示,精灵的显示,UI(用户界面)的显示,还有就是一些游戏规则的制定等等。

(2) 网络端 网络端我们要做些什么呢?我想主要包括两个方面:在服务器端,从数据库中取出数据,然后将数据发送给客户端,从客户端得到数据,然后更新数据库;而在客户端,我们要干些什么呢?从网络中取出数据,然后更新游戏变量,得到游戏变量,然后将它发送给服务器。这里,我只是谈了一下网络端的大概内容,实际中可能还要修改。

(3) 服务器端 服务器端的主要内容,我想就是和数据库打交道。因为我们是通过网站来注册用户,所以,服务器端的主要内容就是取数据库内容,更新数据库内容。可能会涉及到删数据库内容,但这不常见。

(4) 网页端 网页端实现的主要内容,我想大概就是用户的注册,修改,信息的发布,玩家的交流和互动。

我在这里大概讲解了2D网游开发开发所涉及的内容,可能这些内容会随着实际开发修改,再修改。

我想我制作的这个游戏客户端用SDL,网络端用Raknet,数据库用mySQL,网站制作用JSP。

这些内容可能不能算作一篇文章,我想叫心得还可以。当然,我也希望我的这些心得不会让你误入歧途。

既然这篇心得叫“2D网络游戏开发(网络篇)”,我就不会写上其它的一些内容,我会在这以下的文章中写我在使用Raknet的一些感受,同样,我希望它对你有用。

反正,我觉得它很有用。

下面,我们就开始吧!

大概在这半年时间里,我接触了3款网络引擎,它们分别是:

l openTNL (http://www.opentnl.org )

l SDL_net (http://www.libsdl.org/projects/SDL_net/ )

l Radnet (http://www.rakkarsoft.com )

三款网络引擎都是为游戏设计的,下面我来谈一谈我对三款引擎的看法。

OpenTNL 来自于Torque 3D游戏引擎,关于Torque 的信息,请访问 http://www.garagegames.com/ 。应该说openTNL是Torque 的一部分。可以跨平台运行,也有许多丰富的文档和教程,但它却有一个致命的弱点——使用过于复杂。因为Torque属于那种早期的游戏引擎,所以在OpenTNL中,很多的编码方式都与你所学的不同,你需要花费很多的时间去学习它。我想这也是为什么OpenTNL没有做过许多项目的原因。

SDL_net 来源于SDL,也是一个跨平台的网络引擎。SDL_net使用C语言写成,学习起来也简单明了,但SDL_net太年轻了,只发展了短短几年时间。因此,SDL_net还存在太多的BUG(错误),另外缺少足够的支持文件也是它发展缓慢的原因,毕竟,它太年轻了。

Radnet 可以说是基于上述两款引擎的优点为一体。它既有OpenTNL的文档丰富,又有SDL_net的使用简单。

在接下来的日子里,我们将讲解如何使用Radnet,让你充分享受网络给你的快感。

Radnet是一个基于UDP网络传输协议的C++网络库,允许程序员在他们自己的程序中实现高效的网络传输服务。通常情况下用于游戏,但也可以用于其它项目。

Radnet有以下好处:

l 高性能 在同一台计算机上,Radnet可以实现在两个程序之间每秒传输25,000条信息;

l 容易使用 Radnet有在线用户手册,视频教程。每一个函数和类都有详细的讲解,每一个功能都有自己的例程;

l 跨平台,当前Radnet支持Windows, Linux, Macs,可以建立在Visual Studio, GCC, Code: Blocks, DevCPP 和其它平台上;

l 在线技术支持 RakNet有一个活跃的论坛,邮件列表,你只要给他们发信,他们可以在几小时之内回复你。

l 安全的传输 RakNet在你的代码中自动使用SHA1, AES128, SYN,用RSA避免传输受到攻击

l 音频传输 用Speex编码解码,8位的音频只需要每秒500字节传输。

l 远程终端 用RakNet,你能远程管理你的程序,包括程序的设置,密码的管理和日志的管理。

l 目录服务器 目录服务器允许服务器列举他们自己需要的客户端,并与他们连接。

l Autopatcher Autopatcher系统将限制客户端传输到服务端的文件,这样是为了避免一些不合法的用户将一些不合法的文件传输到服务端。

l 对象重载系统

l 网络数据压缩 BitStream类允许压缩矢量,矩阵,四元数和在-1到1之间的实数。

l 远程功能调用

l 强健的通信层 可以保障信息按照不同的信道传输

RakNet支持两种版权,如果你是做免费游戏,RakNet将是免费的。相反,你必须支付一定的费用。

从这里你可以下载到最新的RakNet:

http://www.rakkarsoft.com/raknet/downloads/RakNet.zip

关于RakNet的设置方式,我们将在下一篇讲解。

关于更多内容请访问金桥科普网站( http://popul.jqcq.com )游戏开发栏目,如你需要游戏开发方面的书籍请参考金桥书城游戏频道(http://book.jqcq.com/category/1_70_740.html )。 如果你在阅读本篇文章时有什么好的建议请来信给我,我的E_mail: akinggw@126.com. 如果你在使用SDL时有什么问题,请到金桥科普网站(http://popul.jqcq.com )游戏开发栏目,我将详细地为你解答。

[此贴子已经被作者于2006-8-14 11:13:49编辑过]
发表于 2006-8-19 09:56:59 | 显示全部楼层

可能是我没有仔细看

但我想问一下如果做的像CS那样在一个公众的大厅里建造房间然后玩家进入是怎么编的呢?

 楼主| 发表于 2006-8-14 10:53:08 | 显示全部楼层

2D网络游戏开发(网络篇)(二)

作者:akinggw

在上一章中,我简单的讲解了什么是Raknet,它有那些好处。在这一章中,我们将讲解如何在IDE中配置Raknet,并将测试一个程序。

由于Raknet的作者使用的是VC++.NET,所以在作者的主页上,他详细地讲解了如何在VC++.NET中配置Raknet,如果你使用的VC++.NET,可以参看上面的文章。

而我使用的是DC++,因此,我将讲解如何在DC++中配置Raknet。如果你使用的是VC6,我建议你立即升级到.NET。因为在VC6中配置很麻烦,Raknet中的许多函数库,它都没有,需要重新安装,其中就包括STL。

将Raknet.zip解压后,并不能直接使用,因为它没有LIB文件,这需要你重新编译。在Raknet文件下,有多个项目文件,有VC的和DC的。我们需要的就是DC的。如果你先安装了DC++,那马上你就能看见DC的项目文件图标。

打开它,然后按“F9”,编译文件。如果成功的话,你将在Raknet文件中发现以RakNet.a为名称的文件,这就是DC++使用的库文件。

在VC中,库文件扩展名是以LIB结尾,而在DC中是以A结尾。

将你的RakNet.a拷贝到你的DC++文件中LIB目录下,地址参考:c:dev-cpplib.

然后将解压后的RakNet文件中的include目录下的所有文件拷贝到DC++文件中include目录下,地址参考:c:dev-cppinclude.

到现在为止,我们的文件拷贝算是完成,然后打开DC,新建一个项目。

然后建立一个空的项目:

在“Project”中选择”Project Option”.

在“Parameters”表中”Linker”选项中添加下面的语句:

lib/RakNet.a

lib/libws2_32.a

然后选择OK。

项目配置完成。

然后新建一个源代码文件,改名为main.cpp.

打开一个Raknet的例子,路径参考为:

E:RakNetSamplesCode SamplesChat Example

打开一个C++文件,如:Chat Example Server.cpp.

将Chat Example Server.cpp中的内容全部拷贝到main.cpp文件中。

这是一个服务器文件,按“F9“,编译后,显示结果如下:

如果你的程序执行结果如上图,说明你已经配置好了;如果没有,可能你在某个地方出错了,请认真检查一下。

下一节中,我们将讲解RakNet中的函数。

 楼主| 发表于 2006-8-14 10:54:06 | 显示全部楼层
2D网络游戏开发(网络篇)(三)

作者:akinggw

在前面的章节中,我们已经讲解了Raknet是什么,如何在DC中配置Raknet,并测试了我们的第一个程序。

在这一篇中,我们将讲解Raknet的函数,并将写出我们的第一程序。

因为Raknet是基于Berkeley Sockets和Winsock开发的,所以它支持WINDOWS系统和LINUX系统。可以在局域网,因特网上运行。

当今的游戏大多支持两种模式的网络连接:对等模式和客服端/服务器模式。其实,在现今的在线休闲游戏中,这两种模式都支持。

Raknet支持上述的两种模式。

在网络上,我们传输信息一般都是依靠TCP/IP协议的,而TCP/IP协议中传输信息的协议又包括TCP和UDP。

TCP是指的什么呢?它是指的面向连接的虚电路协议。也就是说,它在发送数据之前,要和用户建立连接,并一直保持和用户连接,然后发送数据,并不断询问用户是否收到正确的数据,如果不正确,就重发,直到正确为止。

UDP又是指的什么呢?它是指用户数据报协议,它在发送数据之前,先和用户建立连接,连接建立好以后,并不一直保持和用户的连接,然后发送数据,也不管对方是否收到数据,然后关闭连接。

从上面的描述可以看出TCP是相当可靠的一种连接方式,但它并不适合于游戏中。你可以想一下,如果一个玩家和服务器建立TCP连接,那么要等到这个用户断开和服务器的连接以后,其它用户才能使用服务器。这显然是不行的。

而UDP能做到和多个用户同时通信。例如,一个玩家要取得他的个人资料,然后他向服务器发出一个请求,服务器用UDP回答他,并关闭和他的连接;服务器然后就可以处理其他玩家的信息了。

我们在本例中要使用三个头文件:

RakClientInterface.h

RakNetworkFactory.h

RakServerInterface.h

第一个头文件包含了建立客服端所需要的信息,其中包括客服端的建立,连接和数据的发送和接收。

第二个头文件用于管理我们在程序中使用的类,包括类内存分配和类内存的释放。

第三个头文件用于建立服务器所需用的信息,包括服务器的建立,连接和数据的发送和接收。

char str[512];

RakClientInterface *rakClientInterface;

RakServerInterface *rakServerInterface;

str[512]是用来判断我们是要建立服务器还是客户端。接下来就声明一个客户端实例和一个服务器实例。

printf("(C)客服端 (S)服务器?n");

gets(str);

if (str[0]=='c')

{

rakClientInterface=RakNetworkFactory::GetRakClientInterface();

rakServerInterface=0;

printf("客服端已经建立。");

}

else

{

rakClientInterface=0;

rakServerInterface=RakNetworkFactory::GetRakServerInterface();

printf("服务器已经建立。");

}

得到一个输入值,如果输入值为c,就建立客户端,然后将服务器实例设置为空,如果输入值为其它,就建立服务器,然后就将客户端实例设置为空。

RakNetworkFactory::GetRakClientInterface(); 初始化一个客户端实例,为它分配内存;

RakNetworkFactory::GetRakServerInterface(); 初始化一个服务器实例,为它分配内存;

最后,程序执行完成,我们就需要释放掉我们刚才分配的内存。

if (rakClientInterface)

RakNetworkFactory:estroyRakClientInterface(rakClientInterface);

else if (rakServerInterface)

RakNetworkFactory:estroyRakServerInterface(rakServerInterface);

完整的程序代码如下:

#include "stdio.h"

#include "conio.h"

#include "raknet/RakClientInterface.h"

#include "raknet/RakNetworkFactory.h"

#include "raknet/RakServerInterface.h"

int main(void)

{

char str[512];

RakClientInterface *rakClientInterface;

RakServerInterface *rakServerInterface;

printf("(C)客服端 (S)服务器?n");

gets(str);

if (str[0]=='c')

{

rakClientInterface=RakNetworkFactory::GetRakClientInterface();

rakServerInterface=0;

printf("客服端已经建立。");

}

else

{

rakClientInterface=0;

rakServerInterface=RakNetworkFactory::GetRakServerInterface();

printf("服务器已经建立。");

}

// TODO - Add code body here

getch();

if (rakClientInterface)

RakNetworkFactory:estroyRakClientInterface(rakClientInterface);

else if (rakServerInterface)

RakNetworkFactory:estroyRakServerInterface(rakServerInterface);

return 0;

}

程序执行结果如图,你可以建立客户端或服务器:

[此贴子已经被作者于2006-8-14 11:25:09编辑过]
 楼主| 发表于 2006-8-14 10:55:08 | 显示全部楼层
2D网络游戏开发(网络篇)(四)

作者:akinggw

在上一篇中,我们只是讲解了如何建立一个服务器或客户端。这一篇中,我们将讲解客户端如何和服务器进行连接。

#include "stdio.h" // Printf and gets

#include "string.h" // strcpy

#include "RakClientInterface.h"

#include "RakNetworkFactory.h"

#include "RakServerInterface.h"

#include "PacketEnumerations.h"

这是我们程序中包括的头文件,其中最主要的就是多了一个PacketEnumerations.h。这个头文件是干什么的呢?

打开它的文件可以看到就是一些宏变量,用于处理网络引擎在运行过程中得到的信息。我在这里就不一一进行翻译,在以后的应用中,我们将进行详细得讲解。

首先,在变量的声明中,多了一个包的声明:

Packet *packet;

Packet是网络传输中用于存储数据的一个数据结构,它的结构如下:

Struct Packet

{

PlayerID playerId;

Unsigned long length;

Unsigned long bitsize;

Char *data;

}

PlayerID表明了包的出处。每一个连接服务器的客户端都将被分配一个唯一的ID号,用于标识自己。

Length和bitsize告诉你这个结构中的数据长度和比特大小。

*data 就是这个包中的数据。

然后,我们就建立客户端或服务器,代码和前面的一样。

客户端或服务器建立好以后,我们就判断建立的是客户端还是服务器:

if (rakServerInterface)

{

// 服务器运行在端口60000处

rakServerInterface->Start(32, 0, 0, 60000);

}

else

{

// 运行客户端

printf("输入服务器IP地址:n");

gets(str);

// 127.0.0.1 designates the feedback loop so we can test on one computer

if (str[0]==0)

strcpy(str, "127.0.0.1");

rakClientInterface->Connect(str, 60000, 0, 0, 0);

}

在rakServerInterface->Start(32, 0, 0, 60000);这个语句中,第一个参数表明你的服务器允许同时连接多少个客户端,在这里,我们设置的是32。就表示同时可连接32个客户端。这个参数最大可以设置成65535;第二个参数做保留之用,设置成0;第三个参数用于设置多久进行服务器更新,参数要大于等于0,表示用每隔当前设置的毫秒数进行更新,这里设置的是0;最后一个参数用于设置服务器的端口 (值得注意的是,客户端的端口应和服务器的端口一样),另外,设置的端口号应该在32000之上,因为,在32000之下的端口都被保留了,用于其它,诸如WWW,FTP,POP3等服务了。

我们接下来看一下rakClientInterface->Connect(str, 60000, 0, 0, 0),这个方法用于客户端连接服务器。第一个参数表示你要连接的服务器的IP地址,如果是在自己这台计算机调试程序,直接输入”127.0.0.1”或“localhost”;第二个参数表示要连接的服务器的端口;第三个参数表示要连接的客户端端口,主要就是用于客户端之间交换数据;第四个参数不要;第五个参数和服务器start函数中的第三个参数一样.

然后,我们就在循环中处理数据:

while (1)

{

if (rakServerInterface)

packet=rakServerInterface->Receive();

else

packet=rakClientInterface->Receive();

if (packet)

{

switch (packet->data[0])

{

case ID_REMOTE_DISCONNECTION_NOTIFICATION:

printf("另一个连接已经断开.n");

break;

case ID_REMOTE_CONNECTION_LOST:

printf("一个客户端丢失连接.n");

break;

case ID_REMOTE_NEW_INCOMING_CONNECTION:

printf("一个客户端已上线.n");

break;

case ID_CONNECTION_REQUEST_ACCEPTED:

printf("我们的连接要求已经接受.n");

break;

case ID_NEW_INCOMING_CONNECTION:

printf("有新连接.n");

break;

case ID_NO_FREE_INCOMING_CONNECTIONS:

printf("服务器已满.n");

break;

case ID_DISCONNECTION_NOTIFICATION:

if (rakServerInterface)

printf("客户端丢失.n");

else

printf("连接中断.n");

break;

case ID_CONNECTION_LOST:

if (rakServerInterface)

printf("客户端丢失连接.n");

else

printf("连接丢失.n");

break;

default:

printf("ID信息 %i 已经到达.n", packet->data[0]);

break;

}

if (rakServerInterface)

rakServerInterface->DeallocatePacket(packet);

else

rakClientInterface->DeallocatePacket(packet);

}

}

下面,我们详细地讲解它们的作用。

if (rakServerInterface)

packet=rakServerInterface->Receive();

else

packet=rakClientInterface->Receive();

从服务器或客户端接受数据,将它保存在packet中。

接下来进行处理。

因为网络引擎在运行过程中要返回一些信息,这些信息有的是给客户端的,有的是给服务器的,而有的是两个都给的。

Packet中返回的第一个data[0]表明了这些类型,这些类型的解释在PacketEnumerations.h中。

由于篇幅的关系,我在这里就不一一进行解释,大家还是自己去看吧。

if (rakServerInterface)

rakServerInterface->DeallocatePacket(packet);

else

rakClientInterface->DeallocatePacket(packet);

是指接收处理好了的包,让它生效。

最后,和前面一篇文章一样,释放掉我们所占有的内存。

if (rakClientInterface)

RakNetworkFactory:estroyRakClientInterface(rakClientInterface);

else if (rakServerInterface)

RakNetworkFactory:estroyRakServerInterface(rakServerInterface);

return 0;

}

服务器截图:

 楼主| 发表于 2006-8-14 10:55:58 | 显示全部楼层

2D网络游戏开发(网络篇)(五)

作者:akinggw

在第四篇中,我们学习了如何使用raknet进行服务器和客户端的连接,在这一篇中,我们将讲解如何让客户端和服务器进行通信,比如说聊天。

好吧,其实我已经知道你等不及了,那就让我们开始吧。

程序代码和“2D网络游戏开发(网络篇)(四)”中的一样,只是我们需要在其中添加一些内容。

在进入循环之前,我们需要定义一个信息变量,用于存储我们将要发送的信息。

char message[2048];

进入循环,在循环开始处,也就是接收信息之前,添加下面代码:

if(kbhit())

{

gets(message);

if(rakServerInterface)

{

rakServerInterface->Send(message, (const int) strlen(message)+1, HIGH_PRIORITY, RELIABLE_ORDERED, 0, UNASSIGNED_PLAYER_ID, true);

}

else

{

rakClientInterface->Send(message, (int) strlen(message)+1, HIGH_PRIORITY, RELIABLE_ORDERED, 0);

}

}

好吧,让我们来分析一下上面这段代码。

kbhit()用于监测是否有按键,如果没有,先得到信息,保存在message中,然后监测你建立的是服务器还是客户端。

如果是服务器,就用服务器发送数据,相反就用客户端发送数据。

下面我们来具体分析一下服务器的send()函数:

第一个参数,指向你要发送的数据;

第二个参数,你发送的数据的大小,也许你就会问,为什么要加一个1呢?那是因为我们的数据都是按照数据流发送的,如果你不在数据之间留下空格,网络引擎就无法分辨出数据,所以我们要在每个数据之后加上一个空格。也就相当于把一根线切成一截一截的。

第三个参数,用于设置你发送数据的重要性,一共有三个参数:

HIGH_PRIORITY
MEDIUM_PRIORITY
LOW_PRIORITY

分别是重要性高,中,底。网络引擎在发送数据时,首先要将数据排队,按照他的重要性来发送,重要性高的先发送,否则就后发。

第四个参数,可靠性,一个有五个参数:

UNRELIABLE - 5, 1, 6
UNRELIABLE_SEQUENCED - 5
RELIABLE - 5, 1, 4, 6, 2, 3
RELIABLE_ORDERED - 1, 2, 3, 4, 5, 6
RELIABLE_SEQUENCED - 5, 6

可靠性又表示什么呢?也就是在网络引擎发送数据时,如果你选择可靠的发送数据,那么数据就会按照正确的循序到达,而如果选择不可靠,那么数据可能就是无续的到达了。

通常使用的有RELIABLE - 5, 1, 4, 6, 2, 3和RELIABLE_ORDERED - 1, 2, 3, 4, 5, 6。

第五个参数,通常选择为0,不是很重要。

第六个参数,接收者的ID,直接用UNASSIGNED_PLAYER_ID 进行设置。

第七个参数,是否广播,有两个参数0和1,0表示不广播,1表示将这条信息发送到和服务器连接的所有客户端。

rakClientInterface的send函数和服务端的参数是一样的,只是少了最后的两个。

最后,我们还得修改下面的内容:

default:

之下,将程序修改成如下:

printf("%sn", packet->data);

if(rakServerInterface)

{

sprintf(message,"%s",packet->data);

rakServerInterface->Send(message, (const int) strlen(message)+1, HIGH_PRIORITY, RELIABLE_ORDERED, 0, UNASSIGNED_PLAYER_ID, true);

}

第一条语句是打印我们收到的数据;

如果我们建立的是服务器,那么就将我们接收到的数据转换成char格式,(因为packet->data的数据格式是unsigned char),然后将这条数据广播给服务器中所有的客户端。

到这里,我们的程序完成,运行效果如下:

服务器:

图注1

客户端1:

图注2

客户端2

图注3

OK,今天的内容就到这里了,祝你们周末玩得高兴,玩得愉快!

 楼主| 发表于 2006-8-14 10:56:51 | 显示全部楼层
2D网络游戏开发(网络篇)(六)

作者:akinggw

在前面的章节中,我们实现了一个简单的聊天室。今天,我们仍然要围绕这个主题,但采取别的方法,这个方法很有用,应该说是整个网络引擎的关键,它就是――RPC(Remote Procedure Calls),翻译成中文就可以理解成”远程功能调用”。

通常情况下,你发送一个信息,你必须实现下面的四个步骤:

1. 建一个数据包来存储你的数据;

2. 必须建立一个函数来实现数据包的编码和发送;

3. 建立一个数据包识别函数,用来识别你的数据包,以便于你调用哪个函数来处理它;

4. 最后建立一个函数来解码你的数据包并且处理它。

  

以上就是我们在编写网络程序要做的四个步骤。

但raknet已经为你做好了这一切,那就是RPC,有了它,你只用实现两个步骤,这样你就有更多的时间集中到游戏上。

 

1. 将你的数据编码;

2. 然后调用远程系统的一个相应函数来处理它.

RPC在你的游戏中实现的过程如下:

A. 告诉网络系统允许使用RPC调用函数

当然,你不需要RPC调用系统中的任何一个函数,这样势必为你带来很多的麻烦。你只需要几个用RPC参数定义的函数就行了,你可以照着下面的例子来定义你的RPC函数。

C函数
void MyFunction(RPCParameters *rpcParameters) {}
// 客户端指针
RakClient *rakClient;
//注册成为RPC
REGISTER_AS_REMOTE_PROCEDURE_CALL(rakClient, MyFunction);

C++静态函数
static void MyClass::MyFunction(RPCParameters *rpcParameters) {}
// 客户端指针
RakClient *rakClient;
//注册成为RPC
REGISTER_AS_REMOTE_PROCEDURE_CALL(rakClient, MyClass::MyFunction);

C++ 成员函数
class MyClass : public NetworkIDGenerator {
void __cdecl func1(RPCParameters *rpcParms);
};
//客户端指针
RakClient *rakClient;
//注册成为RPC
REGISTER_CLASS_MEMBER_RPC(rakClient, MyClass, func1)

服务器的注册同上一样。

B.给你的数据编码

你的RPC方法能够处理一个有长度的字符串或比特流,这就等同于将数据打包。

C.调用RPC函数进行处理

D.在远程系统上相应的函数将对你的数据进行处理

以上就是RPC的处理过程。

下面,我们来看一下RPC的参数和结构:

char * input; 来自于远程系统的数据;

unsigned int numberOfBitsOfData; 我们接收的数据的大小;

PlayerID sender; 哪一个系统调用这个RPC;

RakPeerInterface * recipient; rakpeer中的哪一个实例将得到这次调用;

Bool hasTimestamp; 如果为真,那么输入的开始4个字节表示时间;

RakNet::BitStream * replyToSender 用相应的数据流回应发送者.

下面,我们来具体地看一下代码:

char message1[300];

void PrintMessage(RPCParameters *rpcParameters)

{

printf("%sn",rpcParameters->input);

sprintf(message1,"%s",rpcParameters->input);

if(rakServerInterface)

{

rakServerInterface->RPC("PrintMessage", message1, (strlen(message1)+1)*8, HIGH_PRIORITY, RELIABLE_ORDERED, 0,rpcParameters->sender, true, false, UNASSIGNED_NETWORK_ID,0);

}

}

下面,我们来具体地讲解这个函数。

Message1用于存储得到的信息。首先,我们打印我们得到的信息,然后判断我们是否是服务器,如果是,那么就调用客户端的RPC.

这里这个RPC函数原型如下:

bool RakServer::RPC ( char * uniqueID,

const char * data,

unsigned int bitLength,

PacketPriority priority,

PacketReliability reliability,

char orderingChannel,

PlayerID playerId,

bool broadcast,

bool shiftTimestamp,

ObjectID objectID,

RakNet::BitStream * replyFromTarget

) [virtual, inherited]

第一个参数,我们注册的RPC函数名;

第二个参数,我们要发送的数据;

第三个参数,发送的数据的大小;

第四个参数,数据包的安全级别,和send函数一样;

第五个参数,数据包的可靠性,和send函数一样;

第六个参数,和send函数一样;

第七个参数,接收者ID;

第八个参数,是否广播;

第九个参数,与时间有关,以后讲解;

第十个参数,如果是静态函数,直接设置成UNASSIGNED_OBJECT_ID

第十一个参数,保留。

if (rakServerInterface)

{

// 服务器运行在端口60000处

rakServerInterface->Start(32, 0, 0, 60000);

REGISTER_STATIC_RPC(rakServerInterface, PrintMessage);

}

else

{

// 运行客户端

printf("输入服务器IP地址:n");

gets(str);

// 127.0.0.1 designates the feedback loop so we can test on one computer

if (str[0]==0)

strcpy(str, "127.0.0.1");

rakClientInterface->Connect(str, 60000, 0, 0, 0);

REGISTER_STATIC_RPC(rakClientInterface, PrintMessage);

}

在服务器或客户端注册RPC。

gets(message);

if(rakServerInterface)

{

rakServerInterface->RPC("PrintMessage", message, (strlen(message)+1)*8, HIGH_PRIORITY, RELIABLE_ORDERED, 0,UNASSIGNED_PLAYER_ID , true, false, UNASSIGNED_NETWORK_ID,0); }

else

{

rakClientInterface->RPC("PrintMessage", message, (strlen(message)+1)*8, HIGH_PRIORITY, RELIABLE_ORDERED, 0, false, UNASSIGNED_NETWORK_ID,0);

}

得到信息,然后调用RPC。

其它代码不用改动,程序运行的效果如下:

图注1
图注2

OK,今天的内容就到这里了。

 楼主| 发表于 2006-8-14 10:57:24 | 显示全部楼层
2D网络游戏开发(网络篇)(七)

作者:akinggw

在前面的章节中,我们讲解了如何在服务器和客户端之间如何传输信息,今天,我们将讲解在它们之间如何传输文件。这个功能很重要,比如,在网络游戏中,为了保持游戏的新颖,游戏每次开始时都需要更新数据。

而我们今天的内容就是讲解如何在客户端和服务器之间传输文件。

这个系统的名字叫自动补丁系统(autopatcher),它不包括在raknet库文件中。是一个分开的类,在后面,我们将讲解它的使用。

Autopatcher系统主要的工作就是在两个和两个以上的系统之间传输一些它们之前没有的,或是改变了的文件。它主要的工作就是传输文件,压缩这些传输文件,保证文件传输安全和文件操作。它并不提供基本的连接或用户接口,(这些你可以采用raknet中的功能完成)。Autopatcher通常用于商业游戏。

每个系统都需要建立autopatcher类实例。Autopatcher类包含四个文件Autopatcher.cpp, Autopatcher.h, DownloadableFileDescriptor.cpp, and DownloadableFileDescriptor.h。再次重申,Autopatcher并不包含在raknet库文件中,所以调试的时候有点不同。

这些文件都包含在Source/Autopatcher目录下,在编译时,要包含这个文件夹下的所有文件。因为在实际的游戏编程中可能需要修改这些文件。

接下来,就是要告诉autopatcher哪些文件可以被下载。这件事可以用SetFileDownloadable(char *filename, bool checkFileSignature)函数来设置。这个函数的作用就是将文件读入到内存,如果文件很大,就需要压缩它,最后将这些可下载文件添加到一个中心列表上。函数的第一个参数是文件的路径。第二个参数是一个bool值,这个参数的目的就是用于数字签名,用于检测文件在传输过程中是否被修改。如果设置成真的话,就将有一个和我们的文件名相同的文件XX.sha来描述我们的文件。你可以调用CreateFileSignature函数来建立这个签名文件。如何文件签名检测失败,SetFileDownloadable将返回SET_FILE_DOWNLOADABLE_FILE_NO_SIGNATURE_FILE或者SET_FILE_DOWNLOADABLE_FILE_SIGNATURE_CHECK_FAILED.

假如这些系统都已经连接,如果你想下载的话,就可以调用RequestDownloadableFileList函数来建立下载系统;如果你只想更新的话,就可以调用SendDownloadableFileList来建立你的更新系统。

当你得到你的Packet::data中的第一个字节包含下面的包ID时:

ID_AUTOPATCHER_FILE_LIST

ID_AUTOPATCHER_REQUEST_FILES

ID_AUTOPATCHER_SET_DOWNLOAD_LIST

ID_AUTOPATCHER_WRITE_FILE

就将将那整个包发送给相应的函数:

void OnAutopatcherFileList(Packet *packet,bool onlyAcceptFilesIfRequested);

void OnAutopatcherRequestFiles(Packet *packet);

void OnAutopatcherSetDownloadList(Packet *packet);

void OnAutopatcherWriteFile(Packet *packet);

下面,我们来具体地看一下,代码是如何实现的。

首先,你需要在你的项目中设置一个参数,因为autopatcher在压缩文件的时候使用了zlib这个压缩包。因此,在编译文件时,你应该加载它。我使用的是dev c++,因此,我需要现下载一个zlib.devpak,然后安装它,在项目设置中,包含它们的库文件:

lib/RakNet.a

lib/libws2_32.a

lib/libz.a

如果你使用VC,也需要同样的操作。

项目设置好了以后,就需要在代码中包含autopatcher的头文件:

#include "Autopatcher.h"

#include "Autopatcher.cpp"

#include "DownloadableFileDescriptor.h"

#include "DownloadableFileDescriptor.cpp"

这个例子在raknet的example文件下。

然后定义一个AutoPatcher 实例:

AutoPatcher autoPatcher;

你需要写一个函数来分析你所下载文件的信息:

unsigned int ShowDownloadStatus(AutoPatcher *autoPatcher)

{

char fileName[256];

unsigned numberOfDownloadingFiles;

unsigned fileLength, compressedFileLength;

bool dataIsCompressed;

numberOfDownloadingFiles = autoPatcher->GetDownloadStatus(fileName, &fileLength, &dataIsCompressed, &compressedFileLength);

if (numberOfDownloadingFiles>0)

{

printf("n%i 有多少个文件在下载列表中n", numberOfDownloadingFiles);

printf("当前文件: %s (%i 字节数)n", fileName, fileLength);

if (dataIsCompressed==false)

printf("文件无压缩传输。n");

else

printf("文件被压缩成了 %i 字节n",compressedFileLength);

}

else

printf("当前没有可下载的文件n");

return numberOfDownloadingFiles;

}

这个函数最后返回我们要下载的文件的个数。

其中,GetDownloadStatus函数原型如下:

unsigned int AutoPatcher::GetDownloadStatus ( char * filename,

unsigned * fileLength,

bool * fileDataIsCompressed,

unsigned * compressedFileLength

)

这个函数返回下载列表中有多少个文件。

第一个参数filename , 当前已下载的文件;

第二个参数fileLength, 当前已下载文件的长度;

第三个参数fileDatalsCompressed, 文件是否压缩;

第四个参数compressedFileLength, 如果文件压缩,返回它的大小。

这个函数返回下载列表中文件的个数。

我们在这个例子中使用的是peer to peer方式,这样做的好处是为了使其简单,关于c/s的方式,还在进一步研究当中。

 楼主| 发表于 2006-8-14 10:58:38 | 显示全部楼层

autoPatcher.SetNetworkingSystem(rakPeer);

SetNetworkingSystem函数的原型如下:

void SetNetworkingSystem (RakPeerInterface *localSystem)

void SetNetworkingSystem (RakClientInterface *localSystem)

void SetNetworkingSystem (RakServerInterface *localSystem)

这个函数的作用就是设置使用哪个系统的实例来传输文件。

这些设置好了以后,下一步我们将在服务器端设置加载文件,在客户端设置下载文件,设置如下:

服务器端:

if (AutoPatcher::CreateFileSignature(input)==false)

printf("Unable to create file signature %s.shan", input);

if (autoPatcher.SetFileDownloadable(input, true)!=SET_FILE_DOWNLOADABLE_SUCCESS)

printf("Unable to make %s downloadablen", input);

}

printf("Enter name of second file to make downloadable or hit enter for nonen");

gets(input);

if (input[0] && autoPatcher.SetFileDownloadable(input, false)!=SET_FILE_DOWNLOADABLE_SUCCESS)

printf("Unable to make %s downloadablen", input);

前面两个if语句用于设置带签名的文件,最后一个if用于设置不带签名的下载。三个函数原型如下:

bool AutoPatcher::CreateFileSignature ( char * filename ) [static]

为filename建立一个以.sha为扩展名的签名文件。

SetFileDownloadableResult AutoPatcher::SetFileDownloadable ( char * filename,

bool checkFileSignature

)

这个函数,我们已经在前面讲解了。

然后需要在客户端设置文件的下载目录:

autoPatcher.SetDownloadedFileDirectoryPrefix(input);

函数原型如下:

void AutoPatcher::SetDownloadedFileDirectoryPrefix ( char * prefix )

就是设置文件下载到什么地方,用prefix设置,例如 c:.

现在进入循环当中,在消息检测当中,加入下面的代码:

case ID_AUTOPATCHER_REQUEST_FILE_LIST:

printf("Got a request for the file listn");

autoPatcher.SendDownloadableFileList(packet->playerId);

break;

case ID_AUTOPATCHER_FILE_LIST:

printf("n-------nGot the list of available server files.nRequesting downloads.n-------n");

autoPatcher.OnAutopatcherFileList(packet, true);

if (ShowDownloadStatus(&autoPatcher)==0)

{

rakPeer->DeallocatePacket(packet);

goto QUIT;

}

break;

case ID_AUTOPATCHER_REQUEST_FILES:

printf("Got a request for filesn");

autoPatcher.OnAutopatcherRequestFiles(packet);

break;

case ID_AUTOPATCHER_SET_DOWNLOAD_LIST:

printf("* Confirmed download listn");

autoPatcher.OnAutopatcherSetDownloadList(packet);

break;

case ID_AUTOPATCHER_WRITE_FILE:

printf("-------nGot a filen-------n");

autoPatcher.OnAutopatcherWriteFile(packet);

if (ShowDownloadStatus(&autoPatcher)==0)

{

rakPeer->DeallocatePacket(packet);

goto QUIT;

}

break;

}

autoPatcher.SendDownloadableFileList函数的原型如下:

void AutoPatcher::SendDownloadableFileList ( const PlayerID remoteSystem )

这个函数的作用就是将下载列表中的全部文件发送到以PlayerID为标致的客户端去。

.OnAutopatcherFileList的函数如下:

void AutoPatcher::OnAutopatcherFileList ( Packet * packet,

bool onlyAcceptFilesIfRequested

)

这个函数的作用就是分析服务器端的下载文件列表,看是否有我们没有的文件,或是不匹配的文件。然后以包的形式将分析结果传给服务器。

第二个参数,如果设置成真,那么就调用了这个函数以后就用GetDownloadStatus函数进行分析。如果设置成假,就马上下载文件。

autoPatcher.OnAutopatcherRequestFiles(packet);

这个函数原型如下:

void AutoPatcher::OnAutopatcherRequestFiles ( Packet * packet )

这个函数的作用就是从内存中读取文件,然后将它们发送到客户端。

autoPatcher.OnAutopatcherSetDownloadList(packet);

这个函数原型如下:

void AutoPatcher::OnAutopatcherSetDownloadList ( Packet * packet )

这个函数的作用就是服务器端同意下载下载列表上的文件。

autoPatcher.OnAutopatcherWriteFile(packet);

函数原型如下:

bool AutoPatcher::OnAutopatcherWriteFile ( Packet * packet )

这个函数的作用就是将接受到的文件全部写入到我们设置的硬盘里。

其它就没什么了,大家可以参考其源代码就在raknet的例程当中。关于程序执行的效果,请大家自己去编译。

Over!

 楼主| 发表于 2006-8-14 10:59:20 | 显示全部楼层
2D网络游戏开发(网络篇)(八)

作者:akinggw

前言

已经写到raknet编程的第八篇了,在前面的内容当中,我们讲解了raknet如何传输普通的信息,可光是这些是不够的。因为一个游戏不可能就只传输这些信息,它们可能会传输数据结构。

而我们今天的内容就和数据结构有关。

建立一个数据包

建立什么样的数据包,或是在数据包中要包含什么样的信息完全由你想发送什么样的数据决定。而我们在游戏当中到底要发送什么样的信息呢?比如,我们要发送一个游戏玩家Mine在游戏世界中某一时间的坐标,那么我们就需要下面的数据:

l 玩家Mine在游戏世界中的坐标,包含3个浮点值:x,y,z。你可以用一个矢量值来表示。

l 用一些方法来确定所有玩家都知道玩家Mine的存在。关于这个问题,可以直接采用Network ID Generator类来完成。我们假设类Mine从NetworkObject继承而来,那么其它所有玩家就不得不得到并存储Mine的ObjectID.

l 谁是玩家Mine,关于这个问题请参考players,PlayerID。如果你是在服务器上玩游戏,我们可以将你的playerID值设置成一些傀儡值,比如255。而如果在客户端,你可以用GetPlayerID得到它。

l 当一个玩家Mine在某个地方,但可能10秒以后,它就到达另一个地方,因此最重要的是我们得到那相同的时间,这样做的好处就是避免玩家Mine在不同的计算机上拥有不同的时间。幸运的是,Raknet可以使用Timestamping来处理这个问题。

使用结构或比特流

最终,你发送数据将发送用户的角色的流。有两种方法可以编码你要发送的数据。一种方法是建立一个结构,另一个方法是内建一个比特流类。

建立一个结构的优点就是能够非常容易地改变结构并且看到你实际发送了什么数据。这样,发送者和接收者就能够共享结构中相同的源文件,这样做就避免了不必要的错误。建立结构的缺点就是你将不得不经常改变和重新编译许多文件。也将失去在比特流中传输时压缩的机会。

使用比特流的优点就是你不用改变任何其它的外部文件。简单地建立一个比特流,把你想发送的数据写入比特流,然后发送。你也可以通过压缩来读写数据,然后通过bool值来判断数据是否被压缩。比特流的缺点是你现在一旦范了一个错误,那就不容易那么改变。

用结构建立一个包

打开NetworkStructures.h

在文件中间将有一个大的部分,像下面这样:

// -----------------------------------------

//你的结构在下面

//------------------------------------------

// ------------------------------------

//你的结构在这里

// ---------------------------------

// -----------------------------------------------

// 你的结构在上面

// --------------------------------------

有两个通用的形式用于结构中,一个是带有timestamping,一个是没有。

不带Timestamping的形式

#pragma pack(1)

struct structName

{

unsigned char typeId; //把你的结构类型放在这里

//你的数据放在这里

};

带 Timestamping的形式

#pragma pack(1)

struct structName

{

unsigned char useTimeStamp; // 分配它到ID_TIMESTAMP

unsigned long timeStamp; // 把通过timeGetTime()返回的系统时间放在这里

unsigned char typeId; // 结构类型放在这里

//你的数据放在这里

};

对于前面我们的角色,我们在这里想使用timestamping,因此,结构填充的结果如下:

#pragma pack(1)

struct structName

{

unsigned char useTimeStamp; //分配这个到ID_TIMESTAMP

unsigned long timestamp; //把由getTime()得到的系统时间放在这里

unsigned char typeId; //这将分配一个类型id到PacketEnumerations.h中,这里,我们可以说是ID_SET_TIMED_MINE

float x,y,z; //角色Mine的坐标

ObjectID objected; //角色Mine的目标ID,用于在不同计算机上参考Mine的通用方法

PlayerID playerId; //角色Mine拥有的玩家的PlayerId

};

正如上面我们写的结构,我们添加typeId的目的就是当我们得到一个数据流到达时,我们将知道我们看见了什么。因此最后一步就是在PacketEnumerations.h中添加ID_SET_TIMED_MINE.

注意你不能在结构中直接或间接地包含指针。

你将注意到我在结构中调用了ObjectID objected和PlayerID playerId。为什么不使用一些更具有描述性的名字,比如mineId和mineOwerId? 我以我的实际经练告诉你,在实际运用中使用描述性的名字是不明智的。因为虽然你知道这些数据包中的参数意味着什么,但它们自己却并不知道。使用通用名字的优点是你可以通过粘贴,复制来处理你的数据包,而不用重新给这些数据包装填数据。当你有许多的数据包的时候,你将在一个很大的游戏中,这种方法保存了许多分歧。

巢结构 

关于巢结构并没有多少问题,值得注意的是第一个字节总是决定了数据包的类型。

#pragma pack(1)

struct A

{

unsigned char typeId; // ID_A

};

#pragma pack(1)

struct B

{

unsigned char typeId; //ID_A

};
 楼主| 发表于 2006-8-14 10:59:57 | 显示全部楼层

#pragma pack(1)

struct C //结构C的类型和结构A的类型一样

{

A a;

B b;

}

#pragma pack(1)

struct D //结构C的类型和结构A的类型一样

{

B b;

A al

}

使用比特流来建立数据包

我们仍然使用上面的例子,但这次我们决定用比特流来重写他。我们有和前面相同的数据。

Unsigned char useTimeStamp; // 将这个分配给ID_TIMESTAMP

Unsigned long timestamp; //把由getTime()返回的系统时间放在这里

Unsigned char typeId; //这里将把一个数据类型添加到PacketEnumerations.h文件中,这里我们设置的是ID_SET_TIMED_MINE

UseTimeStamp=ID_TIMESTAMP;

TimeStamp=getTime();

TypeId=ID_SET_TIMED_MINE;

Bitstream myBitStream;

MyBitStream.Write(useTimeStamp);

MyBitStream.Write(timestamp);

MyBitStream.Write(typeId);

//假设我们有一个定义为Mine*的mine对象

myBitStream.Write(mine->GetPosition().x);

myBitStream.Write(mine->GetPosition().y);

myBitStream.Write(mine->GetPosition().z);

myBitStream.Write(mine->GetID()); //在这个结构中,这是ObjectId 对象Id

myBitStream.Write(mine->GetOwner()); //在这个结构中,这是PlayerID,玩家Id

如果我们现在直接使用RakClient::Send或RakServer::Send来发送我们的比特流将导致结构的混乱。现在让我们试着做一些改进。现在,假设我们玩家的坐标由于某种原因,在0,0,0,处。那么前面的代码将修改成下面的形式:

unsigned char useTimeStamp; //分配这个到ID_TIMESTAMP

unsigned long timestamp; //存储由函数getTime()返回的系统时间

unsigned char typeId; //分配一个类型到PacketEnumerations.h中,我们这里设置的是ID_SET_TIMED_MINE

useTimeStamp=ID_TIMESTAMP;

timestamp=getTime();

typeId=ID_SET_TIMED_MINE;

Bitstream myBitStream;

MyBitStream.Write(useTimeStamp);

MyBitStream.Write(timestamp);

MyBitStream.Write(typeId);

If(mine->GetPosition().x==0.0f && mine->GetPostion().y==0.0f && mine->GetPosition().z==0.0f)

{

myBitStream.Write(true);

}

else

{

myBitStream.Write(false);

myBitStream.Write(mine->GetPosition().x);

myBitStream.Write(mine->GetPosition().y);

myBitStream.Write(mine->GetPositon().z);

}

myBitStream.Write(mine->GetID()); //在这个结构中,这是物体

myBitStream.Write(mine->GetOwner()); //在这个结构中,这是玩家

写字符流

通过重载BitStream,可以用数组来写字符流。一个方法是先写数据长度,然后再写数据,内容如下:

void WriteStringToBitStream(char *myString,BitStream *output)

{

output->Write((unsigned short) strlen(myString));

output->Write(myString,strlen(myString));

}

它们的编码是相同的,不管怎样,那不是很高效。Raknet有一个字符压缩类StringCompressor可以进行对字符串的压缩。如果我们想使用这个类来编码数据,可以写成下面的形式:

void WriteStringToBitStream(char *myString,BitStream *output)

{

stringCompressor->EncodeString(myString,256,output);

}

你可以通过下面的形式将压缩的字符串解压出来。

Void WriteBitStreamToString(char *myString,BitStream *input)

{

stringCompressor->DecodeString(myString,256,input);

}

在这里,256表示了最大的字节数。在编码过程中,如果你的字符串少于256个字节,那么将写那整个字符串。相反,如果你的字符串大于256个字节,那么就需要将它截断,然后在256个字节的数组中进行编码,当然包括终结符null。

 楼主| 发表于 2006-8-14 11:00:27 | 显示全部楼层
2D网络游戏开发(网络篇)(九)

作者:akinggw

前言

转眼之间,就来到了这一系列教程的第九课来了,事先声明一下,这些文章大多来源于raknet的官方网站或者例程。如果你觉得我的文章有地方看不懂,那也请你原谅我,因为我也和你一样,也是重头学习这个函数库。在这种情况下,我还是请你阅读它官方网站上的。

比特流简介

在上一篇文章中,我们已经讲解了什么是数据包,也涉及到比特流,但一直没有对比特流进行详细地描述。而我们今天在讲解接收数据包之前,先简单地讲解一下比特流。

描述

bitstream 是名字空间Raknet下的一个类,它的作用就是对动态数组进行打包和解包bitsteam主要有三个优点:

1. 动态地建立数据包

2. 压缩

3. 写比特

要使用bitsteam,你必须首先预定义你的结构,并且把它们转换成(char*)形式。依赖与具体内容,你可以选择在运行的时候写数据块。Bitstream也可以根据数据包的类型进行压缩。

压缩采用下面的方法,比较的简单:

1. 上面的一半是否全是0(1表示无符号类型)?(译者注:不知道这里是否翻译正确,我也不明白,对不起哦)

TRUE – 写一个1

FALSE - 上面的一半为0

2.重复那下面的一半,直到字节的一半

这意味着如果你的数据在它自己最大范围一半的情况下,你将保存比特流。这样你就可以用WriteCompressed来代替Write和用ReadCompressed 来代替Read。

最后,你可以写比特。但大多数时间,你不会注意到这些。不管怎样,当写bool值时,将自动只写一个比特。这对你的数据的加密也很有用。

写数据

建立一个bitstream,然后为你的每个数据类型建立一个写方法。如果你的数据类型是Raknet中自带的,那么正确的重载函数将被调用。如果你的数据类型是你自己的类型,那么你必须向下面这样做。

BitStream.Write((char*)&myType,sizeof(myType);

这是构造函数中的一个带参数的版本。

读数据

读数据是同样简单。建立一个bitstream,然后在构造函数中将它指派给你的数据。

BitStream.myBitStream(packet->data,packet->length,false);

主要函数

构造函数

BitStream(int initialBytesToAllocate);

这个构造函数的作用是决定预先分配多少内存给这个bitstream。

构造函数

BitStream(const char* _data, unsigned int lengthInBytes,bool _copyData);

这个版本的构造函数是给bitstream一些初始数据。这通常用于将当前数据流解释成bitstream,最后一个参数_copyData如果为false,那么指针将指向当前的数据。如果为true,那么你可能想改变数据,想做一个内部的拷贝。

写数据函数

写数据函数用于在比特流结尾将数据变换成比特流,你将使用相反的方法Read将数读回来。

写压缩数据函数

就是将经过压缩后的数据写入bitstream,你同样可以使用相反的方法ReadCompressed将数据读回来。

读数据函数

read数据函数用于按顺序从比特流中将数据读出来。如果在bitsteam中没有数据,就返回false。

读压缩数据函数

与前面相同,只不过是对压缩数据进行操作。

GetNumberOfBitsUsed

GetNumberOfBytesUsed

得到写入比特流的字节数或比特数。

GetData

给你一个指针,指向内部数据。可以让你直接访问数据。

Over!

结尾语

本来还打算讲解一下如何接收数据包,但我好想玩一下3D哦,大家就可怜可怜我,让我玩一下3D嘛…..不关它,先玩了再说。

 楼主| 发表于 2006-8-14 11:00:54 | 显示全部楼层
2D网络游戏开发(网络篇)(十) 2006-06-21 11:02:52 金桥信息 2D网络游戏开发(网络篇)(十)

作者:akinggw

前言

其实,我也很想将游戏开发简单化,可不管我怎么努力,游戏开发还是那么难,还是涉及那么多知识。同样,在这里,我也希望我的翻译和讲解能够让你明白,为了使你和我更容易地理解游戏开发的内容,因此你的反馈对我很重要,我的邮箱是akinggw@126.com 。欢迎你提出你宝贵的意见。最近天气比较热,因此,我也改编程为翻译,希望没有译错。

发送数据包

决定你的数据

关于这个东西,已经在建立数据包那一篇讲解了,你最好翻看前面的内容。

决定使用那种方式来进行

你通常是想发送一个触发动作的标志,而不是发送动作的结果。通常来说,一个数据包的来源主要集中在下面三个方面:

来自于一个函数来触发动作

来自于一个标志驱使函数去触发动作

来自于一个数据管理器

它们每种方法都有自己的优点和缺点。

来自于一个函数来触发动作

例子:

假如我们有一个函数叫ShootBullet,带有大量的参数,包括射击的方式,在哪里射击和向哪个方向射击。每次ShootBullet函数一旦调用,那么我就将发送一个数据包去告诉网络,已经有事情发生了。

这种方法的优点

很容易维护。ShootBullet函数可能在许多不同的地方被调用(比如,鼠标输出,键盘输出,AI等),因此,我们就不得不保证在每一个接收到的地方的一致性。这种方法在现在的单玩家游戏中很容易实现。

这种方法的缺点

编程实现很难。如果我用ShootBullet去初始那个包,那么当网络想实现这个函数时,它将调用什么?如果ShootBullet初始那数据包,并且网络调用ShootBullet函数,那么另一个数据包将寄出,建立一个反馈循环。因此,我能写另一个函数,比如DoShootBullet,或者传输一个参数到ShootBullet,告诉它是否发送一个数据包。我们也不得不考虑授权,客户端是直接发出子弹,还是在发出子弹之前,要经过服务器授权?如果需要授权,那么ShootBullet将发送一个数据包并且马上返回,只有一种情况不会返回,那就是它被网络调用并且它不能发送数据包。那网络也许还需要一些ShootBullet函数没有的附加信息,比如子弹存在的数量。有时这些可以从上下文中得到,但是有时也不能。

来自于一个标志驱使函数去触发动作

我们还是使用上面的例子,但这次有一点不同,我们这次是在ShootBullet函数内部发送数据包,而何时发送数据包是通过一个标志来确定。例如,当用户点击鼠标,AI决定射击或空格键被按下。

优点

我可以在网络中调用ShootBullet函数,而不用担心返回循环。并且我将得到更多函数以外的信息可以利用。因此,利用网络,可以很容易地发送数据。

缺点

难于维护。如果我稍后添加另一种方法射击子弹,可能我会忘记发送数据包。

来自于一个数据管理器

例子

每次,一个玩家的生命值到达0,发送一个数据包。不管怎样,我不会这样做。我会把它添加到一些运行每一帧的一些函数里,也许在代码中实现玩家的更新。当代码发现玩家的生命值是0时,它马上发送一个数据包。然后注册这个已经发送的数据包以避免又一次重新被发送。

优点

从网络来看,非常的清楚。我不得不担心反馈和在实际动作中改变那函数。不用维护,除非一些人改变了它。还有就是非常的高效。

缺点

但是从设计的角度讲,非常的不整洁并且只能用于数据的一定类型。另外就是不得不反复设置。

决定可靠性的类型和你需要什么样的顺序流

PcaketPriority.h 包含了可靠性的类型。你需要从三中级别中作出选择:HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY。

高可靠性的数据包比中可靠性的数据包先寄出,而中可靠性的数据包比低可靠性的数据包先寄出。在游戏中,你将用RELIABLE_ORDERED来设置数据包。

调用客户端的服务器的发送函数

发送函数并不会改变你的数据,但是会做一次拷贝。

顺序流

对于顺序数据包,有32种顺序流,而对于连续的数据包,同样有32种顺序流。你能把流想象成一种相关的顺序流,它们所有的数据包拥有相同的顺序类型。我们可以举一个例子来描述这个东西。假设你想排序所有的聊天信息,排序所有的玩家移动数据包,排序所有的玩家开火包,连续所有的弹药数据包。你想使聊天信息按顺序到达,又不想聊天信息被挂起,因为你不能很容易的得到玩家移动的数据包。玩家移动数据包并不和聊天信息关联,因此,你不用担心他们是按照什么顺序到达。这样,你就可以为他们使用不同的顺序流,也许,0表示聊天信息,1表示玩家的移动数据包。值得一提的是玩家的开火数据包的顺序应该和玩家移动的数据包相关,因为你不想在错误的位置进行开火。因此,你应该将玩家开火的数据包放在和玩家移动数据包相同的数据流里。这样做的好处就是如果开火数据包比移动数据包后到达,开火数据包不会给你任何东西,它会一直等待移动数据包的到达。

序列将不停地剔除老的数据包,因此,如果你得到包的顺序为2,1,3,那么你将开始收到2,1,然后这些包被剔除,最后你将得到数据包3。这对于弹药来说非常的有用,因为弹药就只是不断地下降。如果你得到一个很老的包,那你就将得到很多的弹药,这明显是个错误。值得注意的是序列数据包不同于顺序数据包,因为顺序数据包是用一组流来表示的

 楼主| 发表于 2006-8-14 11:02:03 | 显示全部楼层
2D网络游戏开发(网络篇)(十一)

作者:akinggw

前言

在前面的内容中,我们讲解了什么是比特流(bitstream),如何建立数据包,如何发送数据包。今天,我们将讲解如何接收数据包,把这三个内容翻译了,我们将来实际编写一下代码。看看这些东西到底有多重要。

接收数据包

当一个数据包从网络上传输过来。接收函数将返回不为0,在这里我们要分三步来处理这个接收过程。Multiplayer类将为你处理第一步和第三步,

1. 决定数据包的类型,这个数据包的类型可以由下面的代码返回:

unsigned char GetPacketIdentifier(Packet *p)

{

if((unsigned char)p->data[0]==ID_TIMESTAMP)

return (unsigned char) p->data[sizeof(unsigned char) + sizeof(unsigned long)];

else

return (unsigned char)p->data[0];

}

接收数据结构

如果你原来发送的是一个结构,你就可以像下面这样接收它:

//如果你不使用Multiplayer,这里就将交给ProcessUnhandledPacket处理

if(packetIdentifier==/*这里是已经分配给用户的数据包标识*/)

DoMyPacketHandler(packet);

//把这个函数放在你想放的任何地方,在状态类中处理游戏是一个好的地方

void DoMyPacketHandler(Packet *packet)

{

//将数据放在结构的适当类型当中

MyStruct *s=(MyStruct *) packet->data;

Assert(p->length==sizeof(MyStruct));

If(p->length!=sizeof(MyStruct))

Return;

//用NetworkIDGenerator和宏定义位置,然后得到一个指针指向标明结构的对象

MyObject *object =(MyObject *) GET_OBJECT_FORM_ID(s.objected);

//对于这个数据包的类型,执行一个功能

object->DoFunction();

}

有用的注释

l 我们将数据包中的数据指向结构的相应类型是为了避免空拷贝。这样做的话,不管怎样,如果我们改变了结构中任何的数据,那么也将同样改变数据包。这可能或也不可能是你想要的结果。小心保管服务器返回的信息,因为这可能引起一些不必要的麻烦。

l 那断言(assert),也并不一定是必须的,但是当我们发送数据包时,分配了错误的标识或者错误的长度的时候,它对于找到错误将是非常有用的。

l 但某些人为了攻击客户端或服务器,而发送一些包含错误长度和类型的数据包,在这种情况下,状态就显得特别的重要了。

2. 通过DeallocatePacket(Packet *packet)=0;函数删除数据包。

接收比特流

如果你寄出一个比特流,我们建立比特流去分析数据应该按照我们写数据相同的顺序。我们建立比特流,使用了数据和数据包的长度。我们然后在使用写(Write)函数之前使用读(Read)函数;在使用写压缩(WriteCompressed)之前使用读压缩(ReadCompressed),无论我们写何种数据都将按照上面给出的相同的逻辑分支。

下面,我们给出一个例子,这个例子使用了在建数据包时使用的结构Mine。这个例子的作用就是如何读出Mine结构中的数据。

Void DoMyPacketHandler(Packet *packet)

{

Bitstream myBitStream(packet->data,packet->length,false); //第三个参数false的作用是不允许我们拷贝传输过来的数据

MyBitStream.Read(useTimeStamp);

MyBitStream.Read(timestamp);

MyBitStream.Read(typeId);

Bool isAtZero;

MyBitStream.Read(isAtZero);

If(isAtZero==false)

{

x=0.0f;

y=0.0f;

z=0.0f;

}

else

{

myBitStream.Read(x);

myBitStream.Read(y);

myBitStream.Read(z);

}

myBitStream.Read(objectId); // 在这个结构中,这是对象标识

myBitStream.Read(playerId); //在这个结构中, 这是玩家标识

}

 楼主| 发表于 2006-8-14 11:03:15 | 显示全部楼层
2D网络游戏开发(网络篇)(十二)

作者:akinggw

前言

一个商业网络游戏不可能只有一台服务器,这样的话就没法达到什么万人在线了,它一般都是由很多的服务器组成的,然后又用一台主服务器来管理这些游戏服务器。我们称这台主服务器为Master Server。

以下,我翻译成主服务器。还有就是不得不提醒你的是,我的翻译中可能存在错误,这些文章全部来源于Raknet的官方手册,如果你有什么看不明白的地方,你可以直接去看英文版,也可以来信告诉我,我的邮箱是akinggw@126.com

主服务器

主游戏服务器是一台管理运行游戏服务器的服务器。它的作用就是帮你的玩家引导到一些活动的游戏服务器上。

在RakNet中,我们在主游戏服务器方面只提供了一些简单的应用。因此,在实际开发中,你将不得不自己添加一些其它的功能。

那些代码是基于类编写的,一个是用于管理游戏服务器类,一个是用于管理游戏客户端类,而你的游戏将直接和管理游戏服务器类通信。为了包含这些功能,你将不得不在适当的时候创建管理游戏服务器的实例和管理游戏客户端的实例。

主游戏服务器通常是一个基于控制台的程序或一个简单的窗口程序。运行它,然后它就将初始化,每一帧更新一次。如果你想的话,你也可以取得并执行OnModifiedPacket这个函数。MasterServerMain文件中包含了一个非常好的测试代码,这段代码用于测试主游戏服务器的各种性能。一个非常好的注意就是使用动态域名来代替一个固定的IP地址,这样做的好处就是你改变服务器的时候不用玩家更新。例如,你的游戏的web地址为 molegame.com 你就可以在管理游戏客户端编码连接这个域名,这样就解决了IP问题。

管理游戏客户端(MasterClient)是一个客户端,但这个客户端却包含并管理游戏服务。它最大的一个好处就是连接动态域名而不是一个固定的IP地址。

这就是这篇文章的内容,关于主游戏服务器的例子请参考Raknet中自带的例程。

Over!

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-2-6 06:49

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表