从细小处见缜密—游戏中对时间频率的控制 

2007-02-06 12:48 发布

3800 0 0
从细小处见缜密—游戏中对时间频率的控制 
作者: 上海盛大网络服务器技术专家 王峰

《数字娱乐开发》公开发表,OGDEV独家转载

    一般对于服务器程序开发的一个最基本的要求就是安全稳定,网络游戏的服务器程序也不例外。在实际的游戏运营中,往往程序中一个小小的疏忽就会导致游戏功能丧失,甚至程序崩溃。这一方面影响了用户的体验,另一方面,也给技术维护人员带来额外的负担。 这就要求程序的设计和开发人员在平时就养成严谨、细心的习惯。

    这里从游戏中时间频率的控制方面谈谈服务器编程所需要具备的严密性。

    在网络游戏中,为了控制客户端发送网络封包的速度,也有时为了在时钟轮循中控制某些事件的发生时机,不可避免要用到一些时间函数来解决此类问题。在Windows平台下首选的函数就是GetTickCount()。根据MSDN上的帮助文档,GetTickCount()表示从系统启动到当前时刻所过去的毫秒数,它的返回值是一个DWORD,两个不同时刻的GetTickCount()的差值基本就反映了这两个时刻所间隔的毫秒数。但由于其返回值是用一个DWORD来表示的,所以当系统启动时间超过49.7天后,GetTickCount()将达到DWORD所能表示的最大值,从而此函数的返回值将从0开始从新开始计数。

    下面就拿控制网络封包发送速度来举例说明GetTickCount()的用法,以及在使用时要注意的事项,示例中不考虑网络延迟情况。

    假如说我们现在要控制客户端攻击的速度,使其在1秒内不能连续攻击两次。下面有两种实现的方法。

    方法1:
1. 增加变量DWORD dwLastAttackTime;
2. 初始化dwLastAttackTime = GetTickCount() - 1000;
3. 每次处理客户端攻击协议时,增加如下的判断
if (GetTickCount() – dwLastAttackTime < 1000)
{
    //无效攻击
    return;
}
dwLastAttackTime = GetTickCount();

    方法2:
1. 增加变量DWORD dwNextAttackTime;
2. 初始化dwNextAttackTime = GetTickCount();
3. 每次处理客户端攻击协议时,增加如下的判断
if (GetTickCount() – dwNextAttackTime < 0)
{
    //无效攻击
    return;
}
dwNextAttackTime = GetTickCount() + 1000;

    这两种方法都很简单,看似都能达到效果。而且,在不同的情况下,可能都会碰到使用这两种方法之一。对每个用户的对象实例来说,都是各自保存各自的dwLastAttackTime和dwNextAttackTime,相互之间互不干扰。现在让我们来逐个分析,看为什么要这么做,以及这样做是否真能达到效果。

    我们先从第二种方法来看,假设当前的GetTickCount()的值为0x70000000,dwNextAttackTime中的值为0x70000010,这在当前攻击的时间没达到上次攻击所设置的dwNextAttackTime值的情况下会发生。在VC中测试一下if语句,发现if语句中的表达式竟然是false。为什么会这样呢?当前时间不是比允许的下次攻击时间小吗?原因是表达式中为两个DWORD的运算,结果是作为一个无符号数来比较的,当然就会是false。因此,不论在什么情况下, if条件永远不能满足,从而导致判断失误。

解决办法是加一个强制类型转换,变为:
if ((int)(GetTickCount() – dwNextAttackTime) < 0)
{
    无效攻击
    return;
}
dwNextAttackTime = GetTickCount() + 1000;

这样就解决了上面出现的问题。
现在为了统一起见,可以在方法一和方法二中都使用强制类型转换,变为:

    方法1:
1. 增加变量DWORD dwLastAttackTime;
2. 初始化dwLastAttackTime = GetTickCount() - 1000;
3. 每次处理客户端攻击协议时,增加如下的判断
if ((int)(GetTickCount() – dwLastAttackTime) < 1000)
{
    无效攻击
    return;
}
dwLastAttackTime = GetTickCount();

    方法2:
1. 增加变量DWORD dwNextAttackTime;
2. 初始化dwNextAttackTime = GetTickCount();
3. 每次处理客户端攻击协议时,增加如下的判断
if ((int)(GetTickCount() – dwNextAttackTime) < 0)
{
    无效攻击
    return;
}
dwNextAttackTime = GetTickCount() + 1000;
下面再看看方法一和方法二中变量为什么都要初始化为GetTickCount()值,而一般编程时经常是给一个变量赋初值为0。
考虑一下dwLastAttackTime初始化为0的情况(举dwNextAttackTime为例也一样):
在用户发起攻击时,服务器端会走到如下的判断
if ((int)GetTickCount() – dwLastAttackTime < 1000)
{
    无效攻击
    return;
}
dwLastAttackTime = GetTickCount();

    在用户刚登录时,其dwLastAttackTime的值初始化为0;假设攻击发生时GetTickCount()的返回值小于1000(当系统运行49.7天后会发生,对服务器来说,系统运行49.7天不重启是很正常的),也只有在短短的一秒之内会判断正常用户(使用正常攻击速度的用户,下文中的正常用户均指代这种情况)的攻击非法,这还算可以容忍。

    再考虑一种情况,当系统运行近25天后(之所以考虑这个时刻是因为此时GetTickCount()的返回值处于有符号整数的正负数交界点。由于我们使用了强制类型转换,所以不得不考虑当两个DWORD运算后强制转变为一个int类型的情况。而若把GetTickCount()的返回值作为一个int型的话,那这个时候正是一个正负数交界的时刻,GetTickCount()的返回值会从0x7FFFFFFF变为0x80000000,从一个正数变为一个负数),我们再来看一下我们的if判断语句

if ((int)(GetTickCount() – dwLastAttackTime) < 1000)
因为此时dwLastAttackTime被初始化为0,所以即相当于
if ((int)GetTickCount() < 1000)
由于此时GetTickCount()值已经过渡到0x80000000之后,因此强制类型转变一下就成了一个负数,因而导致用户的攻击永远失效。这时,技术维护人员只有重启机器,让GetTickCount()重新从0开始计数,否则玩家永远都不能攻击了。
所以说dwLastAttackTime和dwNextAttackTime决不能初始化为0。
再考虑if表达式的另一种写法,举方法一中的if表达式为例,变为:
if (GetTickCount() < 1000 + dwLastAttackTime)
这只是对表达式作了一下变换,其含义也是一目了然,看看这种情况下会发生什么。
假如在用户发起攻击时dwLastAttackTime中的值为0xFFFF0FFF,GetTickCount()返回值为0x00003000(这种情况可能会在系统启动49.7天后发生)。这应该是一个有效的攻击。可是在VC下测试一下会发现if语句总是返回true,程序会认为攻击非法。
所以if中的表达式也不能随意写。

    下面总结一下:
1. 使用GetTickCount()作为两段时间的间隔判断时一定要注意两个临界点(正负数交界点,和重新计数点),可通过强制类型转换来达到统一,但使用时一定要在这两个临界点前后取样点做充分测试。
2. 在一个布尔表达式中,对TickCount的操作(对GetTickCount()直接调用是如此,对存放GetTickCount值的变量来说也是如此)一定要放在比较运算符的一边,常量表达式放在比较运算符的另一边,千万不能随意应用数学中的交换率、结合率什么的。
3. 存放TickCount的变量注意不要初始化为0,而要初始化为一个GetTickCount()值。

    在实际的程序编制过程中,上面所列的一些细小点确实很容易被忽视,而它们引起的错误也很难被发觉,往往在程序运行比较长一段时间后才能发现,并且有时很难发觉到是这里一个小的地方出问题了。因此从一开始就养成一种严谨的做事方式,在编码过程中随时抓住可疑点,多推敲、多测试,是我们每个服务器制作人员最基本的职业素质。

=完=

中国计算机报《数字娱乐开发》刊
ceed_10.gif
width: 227 px
size: -1 bytes
double click to view all
TA的作品 TA的主页
B Color Smilies

你可能喜欢

从细小处见缜密—游戏中对时间频率的控制 
联系
我们
快速回复 返回顶部 返回列表