游戏在真实的环境中,有些特殊情况需要处理,本文介绍技能模块是如何处理人为作弊和现实中的网络导致的一些问题。
主要介绍四个部分:
- 防外挂
- 网络延迟问题解决
- 网络卡顿和抖动
- 流量优化
注意,本文默认介绍的是玩家的技能处理,也就是技能的控制端在玩家的客户端。对于控制端在服务器的小怪,基本没有前三个问题。
每个游戏的技能系统的实现不同,处理方式也有可能不太一样,本文所使用的技能系统参考之前写的文章:http://www.jianshu.com/p/551f02f95727
1.防外挂
由于技能是客户端先行,因此技能模块很多逻辑是放在客户端的,由客户端控制技能流程并且通知服务器执行相应的功能。由此可见,技能是由客户端发起的,服务端必须对收到的技能执行命令进行验证以保证技能确实是可用的,防止玩家通过外挂重复发送技能释放消息无限次释放技能。
每个游戏技能系统实现不同,可能对应的逻辑也不太一样。基本原则是:
1.服务端保存技能释放可用性的相关信息,比如技能CD、技能蓝量等。
2.技能结算在服务端执行,客户端管理技能执行流程。
3.服务端每次真正的释放技能之前,对技能进行判断是否可用。
4.服务端收到的技能执行消息后,根据实现系统的规则进行相应验证。
下面,以我们的技能系统为例,介绍我们是如何实现防外挂的。
在我们系统中,技能同步包括三类同步消息:
- 技能根节点enter (root_enter): 表示技能树根结点进入执行,表示一个技能树开始。
- 技能叶子节点enter(action_enter): 表示技能的执行节点进入执行,表示一个技能执行模块开始执行,有一个执行模块有后摇时间,根据后摇时间,他会自动结束。
- 根节点exit(root_exit) :表示技能树结束。
服务端会接受到客户端发来的这三种技能消息,其中,服务端收到root_enter和action_enter消息后,需要对消息的真实性进行验证,root_exit表示技能结束,和防外挂没有关系,无需验证。
由于技能的信息都是以root_enter来控制的,比如技能CD、技能耗蓝和沉默/晕眩等导致的技能是否可用,因此,当服务端收到root_enter的时候,首先要判断这个技能是否真的可以释放,判断后进入相关逻辑。
action_enter是技能真正的执行消息,技能模块并没有方法判断一个执行节点(技能树叶子节点)是否可用,因此,当收到action_enter的消息时,只能根据root节点的信息进行判断。我们进行了两种判断:
- 判断一:在root_enter和root_exit执行期间,表示正在执行这个大技能,收到action_enter后,判断这个action是否属于正在执行的大技能叶子节点,若不属于,不能执行。
- 判断二:我们还判断了action_enter消息对应的执行节点的顺序,保证执行节点是按照合法的顺序执行的,而不能一直执行某一个特别牛逼的叶子节点。
- 此外,服务端执行action节点的时候,不能同时执行多个,每次只能执行一个action节点,并且需要持续相应的时间(action节点的后摇时间),上一个action节点执行结束后才能执行下一个节点。
基于以上机制,可以保证服务端收到的技能消息只有合法的消息才可以执行。
2.网络延迟
在真实的网络环境中,网络延迟是难免的。造成的结果是,服务端执行逻辑一直晚于客户端。这种网络延迟并不会造成错误,但是在网络延迟大的时候会造成表现和结算不能对应上。为了解决着这个问题,一般采取的方式是基于网络延迟时间让服务端加快执行速度,去追赶客户端。
以技能结算为例,当server端收到action_enter消息后,根据当前时间和客户端开始时间可以计算出网络延迟,将服务端的前摇时间减少两个网络延迟,当客户端收到技能结算消息时,正好是客户端的技能结算时间,这样,在玩家控制的客户端,看到的就是完美对应上的效果。参考文章:技能模块的同步
3.网络波动和卡顿
对于手机游戏来说,网络波动和卡顿也是难免的,这种情况造成的结果就是,原本按一定的顺序以一定的时间间隔到达服务器/客户端的消息,可能同时到达了服务器/客户端,或者时间间隔忽大忽小。
一般来说,当服务器由于网络卡顿同时接收到多个技能开始执行的消息时,可以通过两种方式进行处理。
1.接到技能执行消息后马上执行,这样导致的问题时可能在同一帧收到多条技能执行消息,并且在同一帧执行多个技能。这个策略的好处是处理方式比较简单,而且能让服务器尽快的跟客户端同步。但是为了防止玩家使用外挂同时发送多个技能执行请求,这个策略是不可行的。
2.当接收到多条技能执行消息时,按序依次执行技能,每个技能的执行时间结束后,才执行下一条技能。这种策略的问题是,若服务器的延迟与客户端较大,如果玩家一直在不停的放技能,会导致服务器与客户端的延迟越来越大。
为了解决此类问题,我们在客户端和服务端采取不同的处理方式。
3.1 proxy服务端(主控端是玩家控制的客户端)
服务器是技能真正执行的地方,需要保证技能正确的执行。因此,服务端基于第二种方式解决网络卡顿,同时增加了一些逻辑,以保证服务端不会和客户端延迟过大。
3.1.1 消息队列
proxy服务端以队列的形式保存下来收到的技能消息并依次执行。
当服务端收到技能同步消息后,就将消息存入队列,技能执行模块就根据队列依次执行,其中,action_enter会执行一段时间,后摇时间点到达后结束。root_enter和root_exit对技能执行状态进行控制,执行即结束,不存在持续时间。
3.1.2 防止服务端延迟过大
在某些情况下,服务端可能迟于户端较长时间。比如网络卡顿,导致客户端多次释放技能的消息同时到达服务端。为了解决这种问题,我们通过两种机制,让服务端追赶客户端。
- 当接收到一个新的root_enter信息时,马上清空掉队列中的所有技能消息,执行队列中对应的root_exit消息。然后执行新的root_enter信息。此策略表现是:当玩家执行一个新的技能,服务端之前还没有执行的技能就不再执行了。
- 叶子节点的持续时间(后摇时间)根据网络延迟进行一定的减少,给定一个系数比如0.8,一方面保证服务端不会快速的执行多个action节点,同时可以让服务端尽快的追上客户端。
3.2 proxy客户端 (主控端是玩家控制的客户端或者是服务器控制的怪物)
客户端只是执行技能的表现,在网络条件较差的情况下,我们只要保证游戏的正确性(不出错误,不影响服务器正确运行,网络条件变好后游戏可以正确运行)即可,可以一定程度的降低游戏的表现效果。
因此,在我们游戏中,客户端的proxy端采用的是接到消息后马上执行的策略。客户端接到既能执行信息,那么就把之前正在执行的技能停止掉,然后执行新的就好了。
4 流量优化
流量的优化基本上没有太多通用的技巧,最基本也是最重要的就是:不要同步没有意义的消息。
这句话是废话,但是也是流量优化的指导方向。听起来很简单,但是实现起来非常难,甚至想不同步冗余信息是不可能的。
为什么说这件事很难,甚至是不可能的呢?
一,只有梳理清楚了执行逻辑,才能确定哪些同步消息是必要的,哪些是冗余的。因此,执行逻辑一定要清晰,这样哪些消息必须同步,哪些消息不需要同步才能区分。
二,比如一条消息,某些情况是不用同步的,有的情况又要同步。那么发,还是不发。再细节一点,比如一个参数,有的情况不需要同步,有的情况需要同步。如果对每种情况进行特殊化编写代码,代码可读性可能较差,如果发送一个通用的同步消息,可能消息量比较大。那么,优化做到什么程度?大概做到游戏流量可以接受的成都就好了。
还有些tips可以减小流量信息,比如:
- 有些常见的string,甚至是所有的string,可以将其转为一个int,客户端服务端都知道这个int值代表哪个string即可。
- 有些float,可以通过乘以100转为int传到客户端,客户端再除以100,将float转为int进行传输。
等...
4.其他
本文基于底层的消息是保证消息顺序、保证不掉包、保证消息不被篡改、保证消息没有重发的。这件事,本身实现起来可能更加复杂。