上节讲到了匹配系统,匹配完成之后,我们就需要开始进行帧同步了。但是,需要注意,只有房主才可以调用开始帧同步的方法。因此,需要加一层判断。
然后,房间内的其他玩家需要监听开始帧同步的广播,接收到开始帧同步的消息之后,进入游戏场景。
game_scene:游戏主场景。(终于到最关键的逻辑部分了)
首先,进入游戏主场景之后,进行一些玩家的初始化(比如位置,血量,攻击,防御等信息),还有摇杆的初始化。
需要注意的是,玩家的出生位置这一部分逻辑稍微复杂一些。因为,没有服务器逻辑支持,所以只能在客户端想办法。于是,我预先定义了两组,共六个玩家的位置。然后,把服务器传过来的房间内玩家也进行分组,最后将两个分组内的玩家分别和位置信息映射好就可以了。这部分逻辑在GameScene.ts的 initPlayerPoss方法里。
摇杆分为左摇杆和右摇杆。左摇杆是控制坦克的位置和方向的,右摇杆控制坦克的炮头方向和射击。(坦克的方向和坦克的炮头方向互不影响,需要注意一下)
所有初始化动作完成之后,就需要处理逻辑帧消息和发送帧消息了。以下是接收帧消息和发送帧消息的主要代码
每次处理完帧消息之后,就把逻辑帧的信息同步到表现层,然后把当前帧的消息发送到服务器。
之前,给服务器发送帧消息,我是每次摇杆监听到有变动就发送。这样会导致帧消息发送太频繁,没有必要这样做。另外,还会导致一个致命的问题,帧消息的顺序错乱。因为发送帧消息都是异步的,没有办法保证哪个消息先到,哪个后到。因此,会导致有时候坦克会一直移动停不下来(我是根据isMoving判断是否在移动)。看以下截图,timestamp顺序已经被颠倒了,导致最后的ismoving应该是false,现在却是true。
处理帧消息是逻辑层和表现层分离的。逻辑层的逻辑都在GameState.ts模块里。逻辑帧帧率默认15,也就是1秒钟有15个帧消息。处理每帧的消息在calFrame方法里。需要注意的是,处理每帧消息之前,都需要初始化一个随机种子,方便游戏中有些逻辑需要用到随机的地方,比如地图随机生成道具等。(计算机中的随机数都是伪随机的,只要随机种子指定的一样,随机的结果也是一模一样的)
逻辑层每一帧处理完之后,会计算出当前帧最终玩家坦克所在的位置,然后拷贝到表现层。
表现层,是用每个类的update来驱动的。以玩家类Hero.ts为例。
表现层根据当前的坦克位置和逻辑层传过来的当前帧的最终位置,做一个插值运算,以平滑处理坦克的移动。这部分逻辑参看 Hero.ts 里的updatePosition方法和update方法。主要思想就是,计算出来当前位置到最终位置所用的总时间,把时间跑完,也就走到了最终的位置。插值时,需要传入一个所用时间除以总时间的比值。这样,就会让表现层表现移动更平滑一些。虽然,逻辑层是固定15帧,但是表现层是按渲染帧来的。
说明一下:
-
项目代码可能看起来比较多。其实,有很多方法和ts类都没用到,那是之前写的逻辑。所以,没必要所有代码都需要看,只需要按照我说的思想一步步往下看即可。重要的代码,主要看这几个就可以了:StartScene.ts,对应start_scene场景
HomeScene.ts, 对应home_scene场景
ChooseScene.ts,对应choose_scene场景
GameScene.ts,对应game_scene主场景
GameState.ts,帧同步逻辑层
Hero.ts,坦克移动的表现层
moveSticker.ts,坦克移动监听脚本 -
关于帧同步的浮点数运算,其实没有特别处理,只是简单的乘以1000。有的地方说,需要把所有的地方都换算成整数,包括位置信息。对于浮点数运算,我还有很多疑问,如换算成整数,有时候可能会越界;换算成整数,计算的时候不会出现浮点数吗,那应该怎么办。还有的地方说,浮点数问题不会成为瓶颈,对战游戏,本来一局时间就很短,可能一千局一万局才会出现一局不同步。
-
移动平滑使用的是插值算法。这需要保证每一帧都能及时的到达客户端,然后把逻辑帧位置同步到表现层进行平滑处理。在网络状态不好时,可能还会出现卡顿现象。目前,还没有找到更好的方法。(航位推算还需要再研究一下)