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

 找回密码
 立即注册

QQ登录

只需一步,快速开始

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

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

12
返回列表 发新帖
楼主: 鼯鼠

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

[复制链接]
 楼主| 发表于 2006-8-14 11:05:07 | 显示全部楼层

2D网络游戏开发(网络篇)(十三)

作者:akinggw

时间邮寄(Timestamping

也不知道翻译正确没有,如果没有正确,还请你批评指正,我一定虚心接受。

在相同的时间帧下,如何在不同计算机上参照那些事件

时间邮票并不会和你的系统时间有什么区别。不幸的是,每台计算机都有它们各自不同的时间。如果你直接在网络上传输系统时间,那么你将得到其它机器上的时间值,而这些时间值却告诉你那事件已经发生了。这是为什么呢?因为你只知道你自己系统上的时间,而不知道其它人的时间。Raknet中的时间邮寄(Timestamping)功能允许你去读取与自己系统相关的时间,允许你将这些集中在游戏里而不用担心其它系统的本机时间。而要完成这个功能,时间邮寄(Timestamping它会自动完成,并且达到的时间精确度相当的高。

图注1

假设一个事件发生在客户端,而事件在客户端发生的本机时间为2000,而这时在服务器端的逻辑系统时间为12000,在另一个客户端的逻辑系统时间为8000。如果我们的数据包不用时间邮寄(Timestamping进行调整,那么服务器将得到时间2000,或者10000ms以下,但这时实际上时间是在ping/2 ms以下,它们中间可能就相差100。

幸运的是,RakNet已经为你处理好了这一切,在两个系统的时间和ping之间进行补偿。使用相关的时间,服务器将粗暴的认为事件发生在ping/2 ms以前,而其它的客户端也将同样地认为事件发生在ping/2 ms以前。 这样的话,你就可以直接正确地的将时间邮寄(Timestamping写入你的数据包而不用担心其它事情。

关于如何将时间邮寄(Timestamping写入到你的数据包中,请参考建立数据包那一节。

注意:我们推荐使用类GetTime,因为这是一个高频率的时间发生器。当然你也可以使用windows 函数 timeGetTime(),但是,这个函数不是很好。时间邮寄(Timestamping依赖于自动的频率,因此你将需要调用StartOccasionalPing去使用它。

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

作者:akinggw

可靠的类型

数据包优先权

enum PacketPriority

{

HIGH_PRIORITY,

MEDIUM_PRIORITY,

LOW_PRIORITY,

NUMBER_OF_PRIORITIES

};

首先,数据包优先权是简单的。高优先权数据包在中优先权数据包之上,而中优先权数据包又在低优先权数据包之上。在游戏中不断提高数据包的优先等级是不正确的,因为这样会扰乱游戏。

数据包的可靠性

enum PacketReliability

{

UNRELIABLE,

UNRELIABLE_SEQUENCED,

RELIABLE,

RELIABLE_ORDERED,

RELIABLE_SEQUENCED

};

Unreliable

Unreliable的数据包是直接被UDP寄出,它们可能按照顺序到达,也可能不会。用这种方法寄出的数据最好是不重要的,或者发送很多次,这样做的好处就是,遗失的数据包可以得到补偿。

优点 — 这种类型的数据包不需要被网络告知是否到达目的地。

缺点 — 数据包没有顺序,也可能不会到达目的地,如果发送内存是满的,首先抛弃的就是这种数据。

Unreliable sequenced

不可靠的连续数据包和不可靠的数据包大体相同。就只有一点不同,那就是老的数据包被忽视,只有最新的数据包可以被接受。

优点 — 和不可靠的数据包差不多,因此你不用担心老的数据包会改变你的数据。

缺点 — 大量的数据包在他们可能从未到达目的地的情况下丢失,也可能在到达目的地以后丢失。

当发送缓冲区满了的时候,这些数据包首先被丢失。或是最近的数据包寄出后却从未到达。

Reliable

可靠的数据包是UDP通过一个可靠的层打包后发送到目的地的。

优点 — 数据包将完整无缺的到达目的地。

缺点 — 没有包顺序。

Reliable ordered

可靠性顺序包是UDP通过一个可靠层打包后,按照顺序发送到目的地的。

优点 — 这种数据包将按照一定顺序被寄出。这样做的好处就是很容易编程,因为你不用担心如何组织它们或者遗失的数据包。

缺点 — 可能会占用额外的网络宽带。如果网络很繁忙的时候,数据包可能到达的非常慢。如果一个数据包晚到,那么将延迟许多数据包的到达。不管怎样,这个缺点可以使用顺序流来缓解。

Reliable sequenced

可靠性连续数据包是UDP使用可靠层打包的,它的目的就是确保这些数据包可以按照连续的顺序到达目的地。

优点 — 你可以得到UDP数据包的可靠性,顺序数据包的顺序,不用担心老的数据包。

缺点 — 还是占用了大量的网络宽带。

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

作者:akinggw

前言

最近这天气是越来越热,心情也就越来越不好,可工作还的继续,但好久没编程了(可能有两三天了)。俺也不准备编程了,还是翻译吧,反正这些俺也没有搞清楚。

网络ID产生器

网络ID产生器允许我们在不同的计算机上按照相同的方式参照对象

网络ID产生器类是一个可选类,主要的功能就是自动分配一个ID值到对象中。这在多人对战游戏中非常的有用,因为如果不这样的话,你就没有办法在远程系统中参照动态的对象。

重点

网络ID产生器(NetworkIDGenerator)有一个派生类叫NetworkObject。一旦你使用了Multiplayer类,它就假设有一个外部的立即服务器和客户端。

如果你想使用你自己的实例,在获取的类中执行纯虚的函数,告诉NetworkIDGenerator你的客户端和服务器的身份。

在那最简单的一种情况下,它们是如何工作的:

用GetNetworkID()函数得到一个对象的ID。如果ID没有分配,那么将返回UNASSIGNED_OBJECT_ID

用SetNetworkID()函数得到一个对象的ID。

如果你一旦操作失误,你将得到麻烦,这就是为什么NetworkIDGenerator类使用printf和puts的原因。如果一旦你遇到错误,你将不会使用上面所说的那些,而是运行你自己的消息处理指针,这样你就能得到一些警告或错误。

1. 不要在一个对象中调用SetNetworkID,因为它已经被分配了,除非你想重新分配他们的ID。

2. 如果你在一个系统中删除了一个对象,那么同样它的ID也将作废,因此你需要在所有系统中删除那个对象。

3. 由于传输的原因,可能存在一个对象存在于一个系统之中,但不在另一个系统之中。当你使用GET_OBJECT_FROM_ID转换一个对象ID到一个指针的时候,你必须检查是否返回值为0。

服务器端

ID号是自动分配的,因此GetNetworkID()函数将总是工作,而你也不用调用SetNetworkID()。当服务器建立了一个新的网络对象,那些客户端必须知道它。要实现这一切,你可以像下面这样做:

MyObject* myObject = new MyObject; //MyObject从Network对象继承而来

ObjectID objected= myObject->GetID();

然后建立一个带ObjectID的数据包,然后将这个数据包发送到客户端。

当那客户端得到那数据包:

MyObject* myObject = new MyObject; //myObject从Network对象继承而来

MyObject->SetID(objectId); //objectId是包含在数据包中

客户端

前面已经说过,ID号是自动分配的,并不需要你去分配。你需要做的就是从服务器端得到它们。如果你建立或是想建立一个对象,你必须编写这样一个过程,这个过程做这样一件事,就是服务器将返回一个数据包,而这个数据包将告诉你,你建立的对象的ID是多少。

如果服务器建立了一个对象(或是另一个客户端建立了,然后服务器又告诉你另一个客户端做了什么事,那么你能想平常那样分配ID号。

在客户端,处理对象的建立的一种最容易的方法是询问服务器是否存在该对象,因为只有当服务器存在了该对象,服务器才会返回。

你可以发送像下面这些东西ID_REQUEST_CREATE_OBJECT。

你能先在服务器端建立对象,然后将这个建立的对象的ID号编写到数据包中,然后发送给这个数据包所指的发信人。具体过程如下:

服务器端

//编写代码处理ID_REQUEST_CREATE_OBJECT

MyObject* myObject = new MyObject; //MyObject从Network继承而来

ObjectID objected = myObject->GetID();

建立一个带ObjectID的数据包,然后把它发送给客户端。当有人调用了对象的类型的时候,数据包将返回ID_CREATE_OBJECT。

当客户端得到数据包:

MyObject* myObject =new Myobject; //MyObject 从Network继承而来

MyObject->SetID(objected); //objectID是包含在数据包中

另一方面,一个好的主意是把一些欺骗检测放在这里。例如,如果客户端要建立50辆坦克,但在游戏中只有5秒钟时间,这肯定要发生错误。

编程提示

在你的游戏中,并不是每件事都必须通过NetworkIDGenerator继承而来。只有一件事是必须的,那就是你在这许多的系统中将使用相同的方式。如果有一些特殊的方法参考那些对象,比如每个系统只有对象的一个类型,你就可以不按照上面的方法。

从NetworkIDGenerator继承来的例子:

你的游戏中有许多的敌人,但这些敌人不是按照一定顺序编排的。

你的游戏中有很多的敌人,当这些敌人死后,将被删除。

你的游戏中有许多的机关,当一个玩家站在一个油桶边,将会爆炸。你能通过继承NetworkIDGenerator类来建立这些机关。

不从NetworkIDGenerator继承来的例子:

在你的游戏中,所有的敌人是在一个数组中按照一定的顺序建立的。在这种情况下,你只需要发送数组的索引。

在你的游戏中只有一个城堡,一个数据包参考到这个城堡,你可以直接知道它是那一个。

从不被网络参考的对象,例如,从一杆枪中发出的子弹。这个交互,只是开枪射击的玩家和被子弹击中的玩家的交互,你不用去担心枪。

其它函数

NetworkIDGenerator* GET_OBJECT_FROM_ID(ObjectID x);

这个函数的作用就是在一个内部描述中(通常是一棵AVL平衡二叉树)寻找满足的选项,返回一个指针到NetworkIDDGenerator。然后你就能在这个指针中设置你想要的类型,这个依赖于你想寻找的具体的内容是什么。

例子:

MyObject* myObject = (MyObject*) GET_OBJECT_FROM_ID(packet->objected);

If(myObject==0

Return; //不能找到对象

Static unsigned short GetStaticItemID(void);

Static void SetStaticItemID(unsigned short I);

这些高级的函数可能你不会用到,除非你想全面的了解NetworkIDGenerator。你能使用它们去设置一些特别的值去强迫ID的值,服务器将使用这些ID值去得到当前最高的值。这只实用于那些已经使用了ID,而导入服务器到当前游戏的情况。例如,如果我保存游戏到服务器中,使用的ID从0到1000,如果我想继续游戏,我可以通过调用SetStaticItemID(1001);导入游戏。这种方法分配的ID值不会和现在的ID值发生冲突。

 楼主| 发表于 2006-8-14 11:06:27 | 显示全部楼层

2D网络游戏开发(网络篇)(十六)

作者:akinggw

前言

Welcome to my game program world。也不知道这样写可不可以,反正大概意思对了就行了。我们还是对Raknet中的一些重要概念进行阐述,这样可以加深我们对Raknet结构的认识。以便于我们在以后的学习中不至于问这问那。正所谓磨刀不无砍材工吗?(不知道打对了没有(:)

玩家ID

什么是玩家ID

玩家ID是一个结构,这个结构包含了一个二进制的IP地址和在网络上使用的系统接口。什么时候需要玩家ID呢:

l 服务器从特定的客户端得到信息,并且想把信息转发给其它的客户端。你将在发送函数中指定那发送者的玩家ID(这个通常在Packet:layerID中),并且要将广播方式设置成true。

l 在游戏世界中的一些选项,比如前面介绍的Mine,属于一个特定的玩家,你想点击这个玩家的角色,然后把他杀掉。

l 每个客户端都应该知道每个玩家在地图中是唯一的。例如,每个在游戏中的玩家一个特定的得分。

l 你想在点对点网络中给任意一个发送信息。

重点考虑

1. 数据包的接受者自动知道发送数据包给它的任何系统的玩家ID。因为它从发送者的IP/接口得到这些。如果你需要服务器知道玩家ID是多少,发送者不需要在他们自己的数据结构中编写它们自己的玩家ID。通过Receive函数返回,原始发送者的玩家ID是自动通过数据包结构传输到程序员那里。

2. 当在使用客户端-服务器模型时,客户端并不知道最初发送数据包的玩家ID。客户端是考虑来自服务器端所有的数据包。因此,如果你想一个客户端知道另一个客户端的玩家ID,你就不得不在数据结构中添加玩家ID选项。你可以使用一个发送客户端来填充它,也可以在一个服务端得到一个开始发送者发送的数据包以后填充它。

3. 玩家ID并不总是按照一定的顺序分配,也不一定在一定的范围中分配,大家要记住这一点。

 楼主| 发表于 2006-11-17 16:44:11 | 显示全部楼层

2D网络游戏开发(网络篇)(十七)

作者:akinggw

前言

首先,感谢大家关心这个栏目,虽然很难,但我相信只要我们有信心有决心,我们就一定能达成所愿。

网络消息

来自网络引擎的消息

我们在接收数据包的时候,数据包的第一个字节代表了数据包的类型。这个类型你可以自己定义,当然也有一些是网络引擎自身的,需要网络引擎自己来处理。这些信息,网络引擎使用了宏变量保存在Mutiplayer.h/cpp文件里,我们下面来看看这些宏变量代表了什么意思。

ID_PONG

15:测试一个没有连接的系统。第一个字节是ID_PONG,接下来四个字节是一个ping,最后的字节是系统自己所拥有的数据。

ID_RSA_PUBLIC_KEY_MISMATCH

[客户端|PEER](注:这个代表了由谁来处理,这里指的是由客户端和点对点通信中的点来处理)16:我们预先设置一个RSA密匙让那些不匹配这个RSA密匙的系统使用。

ID_REMOTE_DISCONNECTION_NOTIFICATION

[客户端] 17:在一个客户端/服务器环境下,除了我们自己以外的客户端断开连接。Packet::playerID将被修改来反映客户端的玩家ID。

ID_REMOTE_CONNECTION_LOST

[客户端] 18: 在客户端/服务器环境下,除了我们自己以外的客户端掉线,Packet::playerID将被修改来反映客户端的玩家ID。

ID_REMOTE_NEW_INCOMING_CONNECTION

[客户端] 19:在客户端/服务器环境下,除了我们自己以外的客户端上线,Packet::playerID将被修改来反映客户端的玩家ID。

ID_REMOTE_EXISTING_CONNECTION

[客户端] 20:在我们和服务器进行初始化连接时,我们将告诉在游戏中的其它客户端。Packet::playerID是被修改以反映这个连接服务器的客户端的玩家ID。

ID_REMOTE_STATIC_DATA

[客户端] 21:得到另一个客户端的数据。

ID_CONNECTION_BANNED

[PEER|客户端] 22:我们被禁止和我们想连接的系统进行连接。

ID_CONNECTION_REQUEST_ACCEPTED

[PEER|客户端] 23:在客户端/服务器环境下,我们的连接请求被服务器接受了。

ID_NEW_INCOMING_CONNECTION

[PEER|服务器] 24:一个远程系统已经被成功的连接。

ID_NO_FREE_INCOMING_CONNECTIONS

[PEER|客户端] 25:我们试图连接的系统不接受新的连接。

ID_DISCONNECTION_NOTIFICATION

[PEER|服务器|客户端] 26: 系统在Packet::playerID中标明不和我们进行连接。对于客户端,这意味着服务器已经关闭。

ID_CONNECTION_LOST

[PEER|服务器|客户端] 27:可靠的数据包不能投递到Packet::playerID所标注的系统中。可能它和系统的连接已经关闭。

ID_TIMESTAMP

[PEER|服务器|客户端] 28:在这个字节后的四个字节表示了一个unsigned int类型,它会因发送者和接受者之间的系统时间的不同而被修改。需要你调用StartOccasionalPing函数。

ID_RECEIVED_STATIC_DATA

[PEER|服务器|客户端] 29:我们得到一个包含静态数据的比特流。你现在就可以读取这个数据。这个数据包一旦建立连接就将自动被发送,当然你也可以手动发送。

ID_INVALID_PASSWORD

[PEER|客户端] 30: 远程系统使用了密码,当我们没有提供正确的密码的时候将被拒绝连接。

ID_MODIFIED_PACKET

[PEER|服务器|客户端] 31:数据包在传输过程中被修改,发送者包含在Packet::playerID。

ID_REMOTE_PORT_REFUSED

[PEER|服务器|客户端] 32:远程主机在这个端口不接受数据。

ID_VOICE_PACKET

[PEER] 33 : 这个数据包包含了噪音数据。你应该把他们传输到RakVoice系统中进行处理。

ID_UPDATE_DISTRIBUTED_NETWORK_OBJECT

[客户端|服务器] 34: 表明了分布式网络对象的建立和更新。这些数据将被传输到DistributedNetworkObjectManager::Instance()->HandleDistributedNetworkObjectPacket

ID_DISTRIBUTED_NETWORK_OBJECT_CREATION_ACCEPTED

[服务器] 35: 分布式网络对象客户端的建立已经被接受。传输数据到DistributedNetworkObjectManager::Instance()->HandleDistributedNetworkObjectPacketCreationAccepted

ID_DISTRIBUTED_NETWORK_OBJECT_CREATION_REJECTED

[客户端] 36:分布式网络对象客户端的建立已经被拒绝。传输数据到DistributedNetworkObjectManager::Instance()->HandleDistributedNetworkObjectPacketCreationAccepted

ID_AUTOPATCHER_REQUEST_FILE_LIST

[PEER|服务器|客户端] 37:请求可下载文件的列表。然后传输到Autopatcher::SendDownloadableFileList

ID_AUTOPATCHER_FILE_LIST

[PEER|服务器|客户端] 38:得到可下载文件的列表。然后传输到Autopatcher::OnAutopatcherFileList.

ID_AUTOPATCHER_REQUEST_FILES

[PEER|服务器|客户端] 39:请求可下载文件的特定设置。然后传输到Autopatcher::OnAutopatcherRequestFiles.

ID_AUTOPATCHER_SET_DOWNLOAD_LIST

[PEER|服务器|客户端] 40 : 同意一列文件被下载,传输数据到Autopatcher::OnAutopatcherSetDownloadList.

ID_AUTOPATCHER_WRITE_FILE

[PEER|服务器|客户端] 41:得到我们请求下载的文件。然后传输到Autopatcher::OnAutopatcherWriteFile

ID_QUERY_MASTER_SERVER

[主游戏服务器] 42:请求主游戏服务器中的游戏服务器的至少一个关键码。

ID_MASTER_SERVER_DELIST_SERVER

[主游戏服务器] 43: 从主游戏服务器中删除游戏服务器。

ID_MASTER_SERVER_UPDATE_SERVER

[主游戏服务器|游戏服务器客户端] 44:添加或更新服务器中的信息。

ID_MASTER_SERVER_SET_SERVER

[主游戏服务器|游戏服务器客户端]45:添加或设置服务器中的信息。

ID_RELAYED_CONNECTION_NOTIFICATION

[主游戏服务器|游戏服务器客户端] 46: 这条消息是被主游戏服务器返回的,用于标明游戏服务器客户端和主游戏服务器建立了连接。

ID_ADVERTISE_SYSTEM

[PEER|服务器|客户端] 47:通知远程系统我们的IP地址和端口。

ID_FULLY_CONNECTED_MESH_JOIN_RESPONSE

[通过MessageHandlerInterface设置的PEER] 48 :被FullyConnectedMesh数据包指针用于自动连接其它peers,并且构建一个全面的连接网格。

ID_FULLY_CONNECTED_MESH_JOIN_REQUEST

[PEER] 49: 被FullyConnectedMesh数据包指针用于自动连接其它peers,并且构建一个全面的连接网格。

ID_CONNECTION_ATTEMPT_FAILED

[FEER|服务器|客户端]50:试图再次和服务器进行连接时被拒绝。

ID_SYNCHRONIZE_MEMORY

[PEER|服务器|通过MemorySychronizer建立的客户端] 51:发送内存更新。

ID_SYNCHRONIZE_MEMORY_STR_MAP

[PEER|服务器|通过MemorySychronizer建立的客户端]52: 被MemorySychronizer用于将string转换成unsigned short保存在带宽(bandwidth)中。

 楼主| 发表于 2006-11-17 16:44:45 | 显示全部楼层

2D网络游戏开发(网络篇)(十八)

作者:akinggw

前言

这一系列的教程已经写了很多了,喜欢的人也很多,可就是反馈太少了。同志们,如果你喜欢这篇教程或也想研究这个引擎,请来信给我,我愿意和大家一起探讨,我的邮箱是akinggw@126.com

数据压缩

Raknet提供了将数据进行压缩传输的方法,下面我们就来详细了解其基本原理。

描述

Raknet能够自动压缩将要发送出去的数据并且在接收到数据后将其自动解压。要实现这些,你需要为你的游戏构建一个简单的频率表,这样做的好处就是能够预先计算编码这些数据需要多少存储空间。下面介绍数据压缩大概的过程:

1. 运行一个简单的‘平均’的游戏。得到服务器和一个客户端的频率表(如果你想的话,也可以是所有客户端的平均。

2. 根据客户端的频率表,为服务器产生一个解压层

3. 根据服务器的频率表,为服务器产生一个压缩层

4. 根据服务器的频率表,为客户端产生一个解压层

5. 根据客户端的频率表,为客户端产生一个压缩层

然后,它们就会被自动进行处理。

下面将对这些函数进行讲解,在Raknet中有详细的例程。

数据压缩函数

GenerateCompressionLayer(unsigned long inputFrequencyTable[256],bool inputLayer)

得到一个由函数GetSendFrequencyTabl返回的频率表,这将产生一个内部的压缩层。你需要调用这个函数两次,在一次中设置inputLayer为true,另一次设置inputLayer为false。InputLayer为真的时候意味着输入。当我们是客户端的时候,服务器到客户端的频率表中,当我们是服务器的时候,客户端到服务器的频率表中。(偶也不太明白这句话)我们需要两个层,因为服务器发送的数据通常不同于客户端发送的数据,因此我们不能在客户端和服务器上使用相同的压缩层。在RakServerInterface.h , RakClientInterface.h中有非常全面的头文件描述。

DeleteCompressionLayer(bool inputLayer)

这会释放一个现在存在的压缩层的内存。在开始释放之前,我们应该标明是输入层还是输出层。

GetSendFrequencyTable(unsigned long outputFrequencyTable[256])

返回一个已经发送的频率表。

Float GetCompressionRatio

这里将返回一个数字N,这个N 应该大于0.0f,这个值越小越好。N等于1.0f意味着你的数据比原来的数据大。这显示了你的压缩频率可能受到了影响。

Float GetDecompressionRatio

这个返回值N大于0.0f,并且也越大越好。N等于1.0f意味着接收到的数据没有经过压缩,还是和原来的一样大,这显示了你的压缩频率可能受到了影响。

 楼主| 发表于 2006-11-17 16:46:11 | 显示全部楼层
2D网络游戏开发(网络篇)(十九)

作者:akinggw

前言

hi,伙计们,是不是感觉前面的内容一点让你百般无聊,不要着急,今天我们就将给出一个别人编的例子。这个例子很简单,主要就是使用了我们上一张所讲解的内容。本来都不愿意拿出来分享的。但我这个人心比较好,分享就分享吧。呵呵!

如果你觉得这个例子比较好,请记得千万要给我来信,我的邮箱是 akinggw@126.com

这个例子最开始是出现在Irrlicht这个3D游戏引擎的官方主页上。文章标题叫“ A Primer For RakNet Using Irrlicht” ,中文意思就是如何在Irrlicht引擎中使用Raknet。文章比较简单,内容主要就是Raknet中的比特流和数据结构。你如果将我前面写的文章全部看懂了,理解这篇文章就相当没问题。

但在我这篇文章中,我只会讲解关于RankNet的部分,如果你对Irrlicht游戏引擎有兴趣,请参考原文:http://www.daveandrews.org/articles/irrlicht_raknet/ 。

OK,废话少说,让我们开始吧!

先看一张游戏执行的界图,你可以在http://www.daveandrews.org/articles/irrlicht_raknet/Chalkboard.zip 下载文件源代码和可执行文件。

客户端

如果你执行了游戏,你就会发现。这根本就不是游戏,它只是显示如何在每个客户端同步画图。

因此,我们先讲解我们要用到的一些参数,如果用一个数据结构来表示可以表示如下:

数据包

{

数据包类型;

鼠标开始位置;

鼠标结束位置;

};

这就是我们要传输的数据。如果想象一下,假如我要传输我自己的游戏中的角色信息。那结构就会是下面这个样子:

数据包

{

数据包类型; //角色信息

角色类型; //比如人,精灵,妖怪等等

操作方式; //比如是重新建立,更改,删除

角色所处地图; //就是它在那张地图上

角色所处地图坐标; //就是它在地图什么位置

角色生命值;

.

.

.

};

好象又扯得太远了, 我们还是来说这里吧。

#include “irrlicht.h“
#include ”PacketEnumerations.h”
#include “RakNetworkFactory.h”
#include “NetworkTypes.h”
#include “RakClientInterface.h”
#include “BitStream.h”

#include “windows.h” // for Sleep()
#include “stdio.h”
#include “conio.h”
#include “string.h”
#include “stdlib.h”

头文件是不能不要的。作者说bitstream好象还存在一些问题,所以作者建议你在项目中包括bitstream.h和bitstream.cpp这两个文件。

另外,作者也讲解了为什么要包含windows.h文件。包含windows.h文件的作用就是使用它的一个函

数Sleep()。

通过使用Sleep()函数的目的就是为了让主线程在执行的过程中,能够给Raknet更多的时间去执行。在整个游戏客户端中,我们都将在主循环中使用Sleep()函数。Sleep(1)并不能是程序暂停,但可以让处理器放弃执行另一程序。

“客户端连接“类

作者使用了一个客户端连接类来管理和 服务器的连接,数据包的处理和发送。我们首先来看一下这个类的成员函数和功能:

1. 构造函数 客户端连接类的构造函数有两个参数:用于连接的服务器IP地址(用字符串表示)和用于连接的服务器的端口号(也是用字符串表示)。这个函数的功能就是初始化Raknet网络连接并连接到服务器。

2. 拆构函数 拆构函数的功能就是关闭构造函数建立的连接,并且释放Raknet建立的网络结构。

3. 添加线坐标 这个函数的作用就是将我们为了画线时产生的坐标x,y添加到画线列表中。

4. 发送线到服务器 这个函数的作用就是发送我们画线用的坐标(或者点)到服务器。

5. 画线 这个函数与网络传输无关,主要就是将画线列表中的点连成一条线。

6. 侦听数据包 这个函数的作用就是检测从服务器传输过来的数据包,然后自动用下一个函数来处理它。

7. 处理数据包 它的作用就是从数据包中读出数据,然后显示到屏幕上,

具体类描述如下:

class ClientConnection

{

private:

list<line2d<s32>> lineList;

RakClientInterface *client;

public:

ClientConnection(char * serverIP, char * portString);

~ClientConnection();

void AddLineLocal(s32 x1, s32 y1, s32 x2, s32 y2);

void SendLineToServer(s32 x1, s32 y1, s32 x2, s32 y2);

void DrawLines(IVideoDriver * irrVideo);

void ListenForPackets();

void HandlePacket(Packet * p);

};

还有一个类,这个类来自于Irrlicht。这个类主要就是处理游戏中的一些事件,我在这里不讲解,请大家自己看。

另外,在文件开始处,要加入下面的代码:

const unsigned char PACKET_ID_LINE = 100;

这个用于设置我们的数据包类型,关于这个方面,我已经在前面进行了详细的讲解。

接下来,我们具体地看一下函数的实现。

首先,是构造函数:

ClientConnection(char * serverIP, char * portString)

: client(NULL)

{

client = RakNetworkFactory::GetRakClientInterface();

client->Connect(serverIP, atoi(portString), 0, 0, 0);

}

比较简单,我在这里就不进行讲解。

~ClientConnection()

{

client->Disconnect(300);

RakNetworkFactory:estroyRakClientInterface(client);

}

拆构函数,也比较简单。

void SendLineToServer(s32 x1, s32 y1, s32 x2, s32 y2)

{

RakNet::BitStream dataStream;

dataStream.Write(PACKET_ID_LINE);

dataStream.Write(x1);

dataStream.Write(y1);

dataStream.Write(x2);

dataStream.Write(y2);

client->Send(&dataStream, HIGH_PRIORITY, RELIABLE_ORDERED, 0);

}

发送数据到服务器端,这个完全按照我们我们前面所讲的内容进行的。先往数据流中写数据包类型,然后才是数据。最后发送,关于Send()函数,我们前面已经讲解了。

我们再来看一下侦听函数:

void ListenForPackets()

{

Packet * p = client->Receive();

if(p != NULL) {

HandlePacket(p);

client->DeallocatePacket(p);

}

}

我们接收的时候必须先将数据包中的数据重新写入数据流,然后读取这个数据流的类型。

如果这个数据包满足我们的条件,我们就将数据一一读出,然后画线,最后退出。

这就是客户端的处理,下面我们来看一下服务器的处理过程。

服务器可以用一个控制台程序来写,下面我们来看一下这个程序应该怎样来写。

#include <acketEnumerations.h>

#include <RakNetworkFactory.h>

#include <NetworkTypes.h>

#include <RakServerInterface.h>

#include <windows.h> // for Sleep()

开始和客户端一样,首先包含Raknet的头文件。

const unsigned char PACKET_ID_LINE = 100;

定义数据包的类型。

服务器端不是使用的类,直接使用了三个函数:

1. SendLineToClients() 这个函数的目的就是将接收的信息广播给所有在线的客户端。

2. HandlePacket() 这个函数的目的就是处理数据包中的数据。

3. Main() 这个函数的目的就是建立服务器,然后在游戏循环中接收数据,处理数据。

下面,我们来具体看一下这些函数的处理过程。

void SendLineToClients(RakServerInterface * server, PlayerID clientToExclude, int x1, int y1, int x2, int y2)

{

RakNet::BitStream dataStream;

dataStream.Write(PACKET_ID_LINE);

dataStream.Write(x1);

dataStream.Write(y1);

dataStream.Write(x2);

dataStream.Write(y2);

server->Send(&dataStream, HIGH_PRIORITY, RELIABLE_ORDERED, 0, clientToExclude, true);

}

这个函数的目的就是将接受到的数据写入数据流,然后广播给在线的所有客户端。

void HandlePacket(RakServerInterface * server, Packet * p)

{

unsigned char packetID;

RakNet::BitStream dataStream((const char*)p->data, p->length, false);

dataStream.Read(packetID);

switch(packetID) {

case PACKET_ID_LINE:

int x1, y1, x2, y2;

dataStream.Read(x1);

dataStream.Read(y1);

dataStream.Read(x2);

dataStream.Read(y2);

SendLineToClients(server, p->playerId, x1, y1, x2, y2);

break;

default:

printf("Unhandled packet (not a problem): %in", int(packetID));

}

}

这个函数的作用就是从网络上得到数据,然后判断这个数据是不是我们所需要的,如果是,就将它从数据流中读出,然后用SenLineToClients函数进行处理。

最后我们来看一下main函数。

int main()

{

RakServerInterface * server = RakNetworkFactory::GetRakServerInterface();

Packet * packet = NULL;

int port = 10000;

if(server->Start(32, 0, 0, port)) {

printf("Server started successfully.n");

printf("Server is now listening on port %i.nn", port);

printf("Press a key to close server.n");

}

else {

printf("There was an error starting the server.");

system("pause");

return 0;

}

while(kbhit() == false) {

Sleep(1);

packet = server->Receive();

if(packet != NULL) {

HandlePacket(server, packet);

server->DeallocatePacket(packet);

}

}

server->Disconnect(300);

RakNetworkFactory:estroyRakServerInterface(server);

printf("Server closed successfully.n");

system("pause");

}

这个函数的作用很简单,我简单地说一下,先建立服务器, 然后进入游戏循环,接收数据,如果数据不为空,就进行处理。如果你按动任何按键就退出,关闭服务器。

这就是一个游戏的例程,很简单。

 楼主| 发表于 2006-11-17 16:47:37 | 显示全部楼层

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

作者:akinggw

前言

有几天没有发东西了,在我的记忆里,好象有两天。难怪手痒痒的,这两天在做一个网站,刚刚把关键的技术问题解决,所以没有多少时间写文章。在加上最近这几天又比较热,40几度,吓人!还是回到我们的正题上吧,就是关于网络游戏的制作,今天我不想编程,只是想谈谈游戏服务器的结构。

结构

其实,我们的游戏传输的就是结构,精灵结构,NPC结构,物品结构,聊天信息结构等等很多结构。(这只是我的想法,有可能有错,不要太当真)

我们下面来做一个例子,比如我们所操作的角色。它应该包含哪些东西呢,她叫什么名字,她所在的地图号,在地图的某个地方,有多少等级,她的经练值是多少,其它还有她穿什么装备,使用什么功夫等等信息。

当然我们前面所说道的装备,功夫,又是另一个结构。

下面我们就用语言的方式来描述她。

角色她

{

姓名

等级

所在地图号

所在地图坐标

精练值是多少

所穿的装备

{

}

所使用的功夫

{

}

.

.

.

}

这些数据一旦接收,游戏引擎就可以根据这个结构中的信息做相应的动作。比如根据角色所在地图号加载地图,然后根据角色的坐标信息显示精灵。最后显示其它相关的信息。

这些东西是如何在客户端生成的呢?

我们可以跟踪这个过程。

1. 玩家打开游戏,输入名称和密码。(我们假定他们是在网站上注册的);

2. 服务器接受玩家资料,如果是登陆信息,就检测玩家的名称和密码。如果正确,再检测玩家是否第一次登陆,如果是第一次,就为玩家初始化上述结构中的信息,然后发送给客户端,同时写入数据库;如果玩家不是第一次登陆,就从数据库中取出相关信息,发送给客户端。同时服务器还要建立一个玩家链表,用于管理这些玩家信息。关于玩家在服务器上的管理,光一根链表是不行的,如果游戏是一个小游戏,只有一张地图可以但有多张地图时,或人数很多时,一根链表就不能解决问题了。即使能,那速度就会受到影响,因为它是在一个很长的路上来回跑动啊。这显然不行。最好的办法就是有多少张地图就建立多少根链表。这时又有一个问题,就是当玩家从一张地图到了另一张地图,怎么办呢?这个也是有解决办法的,因为我们的玩家在游戏中都拥有唯一的ID,又因为每次客户端都会回传数据。这时我们先根据玩家回传的玩家ID在所有地图中查找它属于哪个链表,找到地图的ID号,然后和玩家回传的地图ID进行比较,如果相等就不改,如果不相等,先删除服务器上的地图ID所指的链表中的玩家,然后将这个回传的数据重新插入它所在的地图ID所指的链表。(希望我的讲解不会让你迷惑) 这样就解决了玩家在服务器上的管理问题。也许你要问,这样做的好处是什么?那我就可以告诉你,这样做的好处有很多:A. 节省了查找玩家的时间,比如你向你所在地图的某个玩家发动进攻,总不能每次都把服务器中的全部玩家都遍历一次。B. 方便地图的管理 比如你在一个游戏地图中奔跑,如何显示你周围的NPC和其它玩家呢? 不可能每次都去服务器得到你周围的情况吧,即使允许,可能数据传输过来时,你已经到了另一个地方。最好的办法就是在游戏开始之前将地图中所有的情况传输到你的客户端,这样才能达到同步显示。而对于地图中情况的改变,那就成了一个一个的事情,可以在这些情况改变时,只对你的情况列表做一些小的动作,这样就不会影响大局了。但这种方法只适合与小的地图,对于想《传奇》那样的超大地图就行不通了。这时,我们就可以用一个四叉树来解决,看下图:

我图画的不好,只为说明一个问题,按照我们前面所说的,把地图分成一根根链表来存储。但当我们使用的是超大地图,那同样将遇到很多速度上的问题。如何来提高这个查找速度呢?

我认为把上面我们所用到的链表换成四叉数,什么是四叉树?看下面就明白了:

图注1

四叉树的作用就是把一个平面一分为四,这样我们就能把《传奇》中的地图分成一些很小的方块。至于这些方块要分到多小,你可以通过设置四叉树的深度来决定,当然不能太小了。

我们还是使用链表来存储玩家的信息,而这些链表应该预先定义,需要多少,就定义多少,然后将这些链表的ID好存储到四叉树的叶节点中。这样,我们每次我们查找玩家信息就可以先在四叉树中找到链表ID,然后在链表中进行操作,速度有提升了不少。当然这种方法对于那种超大的地图才有效果,如果地图不大,还是采用前面的方法吧。

前面这个方法不仅要管理玩家,同时地图中的物品,NPC,也钦庋?芾怼?/SPAN>

1. 这一步就是将玩家所在的地图中的物品,NPC,等等信息发送给玩家;

2. 玩家接收到自己的信息和地图中的所有信息,然后显示。

3. 这时,玩家就会响应某些动作,比如移动人物,攻击别人等等动作。这里就有两个方面,玩家自身的改变和玩家对他人的改变。我们先说说玩家自身的改变,对于这个方面,先修改客户端,然后将数据传输到服务器,修改数据库,修改地图链表。经过仔细观察,我发现对于游戏中所有的对象不外乎三种操作:删除,更新,添加。你可以自己去想一下,任何东西都只有这三种操作。这样的话,我们就不得不在将我们的数据包改成下面的样子:

数据包

{

数据包类型;

数据包子类型;

角色她

{

姓名

等级

所在地图号

所在地图坐标

精练值是多少

所穿的装备

{

}

所使用的功夫

{

}

.

.

.

}

}

这个数据包中的第一个类型代表数据包的类型,比如它代表了什么对象,是玩家,物品还是NPC。第二个类型代表了对这个数据包中的数据实行何种操作,是删除,修改,还是增加。

以上说的只是玩家自身的改变。而玩家对其它对象的改变呢?

我在这里也不是太清楚,不过有一种方法就是就是发消息到服务器,然后让服务器自己来处理这些事情。

4. 玩家关闭客户端,然后退出。

我想这就是客户端和服务器实现的大概过程。

Over!

不知道你们看没看懂,后面我将借助一个不完整的代码实现客户端玩家的管理。

发表于 2008-1-8 17:01:20 | 显示全部楼层
完了嗎?非常感謝!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-5-22 06:36

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

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