前言


李清是来自华夏乐游BigRoad工作室的客户端主程,今日他将带来其团队制作的实时竞技小游戏《保卫豆豆-欢乐枪战》的技术实现方案。


游戏简介


《保卫豆豆-欢乐枪战》是一款北京华夏乐游科技股份基于Cocos引擎研发的休闲射击乱斗小游戏,融合了射击、MOBA、吃鸡等热门玩法。

 

Cocos 技术派:实时竞技小游戏技术实现分享_客户端

游戏特点

 

  • 萌宠射击,实时竞技
  • 四人乱斗,双人组队
  • 多个英雄,身怀绝技


本文主要从三个方面来进行分享,分别是:

 

  • ECS架构
  • 网络同步机制
  • 技术难点及解决方案


一、ECS架构


1、ECS架构目的:


降低不断增长的代码库的复杂度。


2、游戏原型需求:

 

  • 子弹:移动、碰撞
  • 英雄:移动、碰撞、发射子弹
  • 炮台:发射子弹


3、传统架构的弊端


要实现游戏原型,按照我们之前的做法,是用一个类来实现一种游戏实体的所有功能,这个类既有状态,又有行为。代码复用使用继承来解决。如果用这种做法,那么类大概长这个样子:

 

Cocos 技术派:实时竞技小游戏技术实现分享_数据_02

大家可以看到,父类会有很多共享的属性和方法,子类继承父类去做具体的事情。但是这种做法有很多弊端,比如说,随着项目规模的增长,代码库复杂度也不断增长,父类会越来越复杂,子类的功能越来越不明确,与多个类相关的代码你不能太确切知道应该放在哪里,拓展功能的时候极其不灵活,如果后期需要增加新功能的话,我们需要对整个继承树进行功能重构才能使其比较合理


在经历过几个项目之后,我们回头反思,发现之前的做法,违反了很多面向对象设计原则。比如说:

 

  • 单一责任原则(Single responsibility principle)每个类都应该只有单一的功能,并且该功能应该由这个类完全封装起来。
  • 组合重用原则(Composite Reuse Principle)默认情况下应当使用组合,只有在必须时才使用继承。


在总结了从前的项目经验,并参考了大量技术文章后,我们找到了一种架构,把大量的模块进行拆分解耦,然后再集成起来,这就是我们接下来要介绍的ECS架构。


4、ECS架构


ECS分别是:

 

  • Entity(实体)
  • Component(组件)
  • System(系统)


看到实体和组件大家可能觉得比较熟悉,但是这里要注意,这跟我们引擎中的实体组件框架可不是一回事,接下来我为大家简单介绍一下ECS架构的元素。


(1)ECS架构元素:


Component:组件,存储游戏状态

Entity:实体,组件的集合

System:系统,实现游戏行为

World:系统和实体的集合,就是我们的游戏世界,他们的关系大概是这个样子的:

 

Cocos 技术派:实时竞技小游戏技术实现分享_数据_03

我们可以看到,游戏世界中有很多System,每个System负责实现一种游戏行为,同时有很多组件,每种组件中会有一些游戏状态,实体上可以挂载一个或多个组件,实体和System聚合成了我们的游戏世界。


(2)ECS架构设计:


这个架构有个基础原则:

 

  • 组件只有状态,没有行为
  • 系统只有行为,没有状态


刚看到这个原则的时候,大家可能会有一些疑问,什么是游戏行为呢?游戏行为,其实就是根据一定的规则去修改游戏状态。比如说移动,就是根据实体的方向和移动速度去改变这个实体的位置。如果系统没有游戏状态,它如何去实现游戏行为呢?


这就是ECS架构最重要的职责了:为系统筛选出它关心的实体子集,只展示给它关心的游戏状态。具体我们是怎么做的呢?


首先把可能单独使用的游戏状态归纳为一个个组件:

 

Cocos 技术派:实时竞技小游戏技术实现分享_服务端_04

比如最常见的位置、方向我们可以归纳为变换组件;移动速度这个组件可能会在移动系统中单独使用,所以我们把它归纳到移动组件中;碰撞组件则有碰撞盒的大小;攻击组件有攻击方向,这样我们就把各种属性给拆开了。


接着,我们在系统实现的时候,要向框架声明我关心哪些“组件元组”(Component Tuple)


什么是“组件元组”?还是举刚刚移动的例子。移动系统的移动行为,应该是关心实体的位置、方向以及移动速度,就是我们归纳的变换组件和移动组件,那么只要一个实体同时挂载这2个组件,它就可以被移动系统遍历到,系统就会进行操作从而实现移动行为。

 

Cocos 技术派:实时竞技小游戏技术实现分享_客户端_05

最关键的一点,“组件元组”其实就是用来实现框架筛选实体的功能,实体只需要根据自身功能需求挂载相应的组件元组就可以了。比如说子弹它有移动和碰撞的功能,那么就挂载上变换、移动和碰撞这3个组件。

 

Cocos 技术派:实时竞技小游戏技术实现分享_数据_06

最终实现的效果就是移动系统遍历了英雄和子弹实体,在他们身上实现了移动的行为。攻击系统遍历了英雄和炮台实体,然后他们就可以发射子弹。


(3)ECS架构实例:


接下来,我们看一下比较复杂的碰撞逻辑,这里我们可以对碰撞进行拆解:


首先是碰撞的触发系统。当碰撞发生时将产生一个碰撞事件,然后这个系统只干这件事。剩下的碰撞处理呢,对于子弹来说,会有一个碰撞后销毁系统,它会在碰撞之后把子弹销毁。对于英雄来说,他有一个碰撞后的损血系统,通过这种方式,我们就可以把碰撞进行拆分,再通过刚刚的方式集成在一起。


(4)ECS架构作用:


这种架构可以让每个开发人员负责不同模块的开发,有效地提高多人开发效率。最重要的就是模块的复用,可以便于功能拓展。如果你想改变一个实体的功能,只需要添加或者移除实体的组件就可以了。


比如说:一个英雄死亡之后,他应该失去移动功能,那么在英雄死亡之后,我们只需要把移动组件给移除就可以了,等他复活的时候再给他加回来。可以看到,这种方式非常方便。既然这么方便了,我们就可以做出一个编辑器,把这种能力开放给策划人员。


实际上,暴雪就专门为Overwatch开发了一套Statescript的脚本语言,它用起来就是一个可视化的编辑器,策划人员可以在这个编辑器中编辑每个英雄在各种游戏状态中拥有什么游戏能力,程序只要实现具体的功能模块,然后开放给策划人员使用,非常地灵活。

 

Cocos 技术派:实时竞技小游戏技术实现分享_客户端_07

以下是我们在实践过程中参考的技术文章:


[参考文档]


《守望先锋》架构设计与网络同步

http://gad.qq.com/article/detail/28682



《守望先锋》回放技术-阵亡镜头、全场最佳和亮眼表现

http://gad.qq.com/article/detail/29595



《守望先锋》中的网络脚本化的武器和技能系统

http://gad.qq.com/article/detail/28219



浅谈《守望先锋》中的ECS构架

https://blog.codingnow.com/2017/06/overwatch_ecs.html


二、网络同步机制


1、常见同步机制:


常见的网络同步机制可以分为以下三种:

 

  • 确定性帧同步(Deterministic lockstep)
  • 快照插值(Snapshot interpolation)
  • 状态同步(State synchronization)


(1)确定性帧同步


服务端:收集并转发玩家输入数据,不运算游戏逻辑

客户端:在玩家输入数据以后各自运算游戏逻辑

优点:只有玩家输入会被传输,数据流量非常小;代码都是写在客户端上的,所以代码复杂度较低。

缺点:​​​​对网络延迟要求非常高;每个机器浮点数运算不一致,需要将浮点数运算转换成整数运算;断线重连时间较长;因为游戏逻辑写在客户端,所以不是很安全。


(2)快照插值


服务端:运算游戏逻辑,将快照发送给客户端。

快照,就是我这一帧所有游戏实体的游戏状态。

客户端:不运算游戏逻辑,收到快照以后进行差值平滑播放。


实际上,客户端只是一个播放器。


优点:客户端运算量小;断线重连容易实现;游戏逻辑全在客户端,所以非常安全。

缺点:带宽占用非常大。


所以这种方式之前多用于像CS这种局域网对战。


(3)状态同步


服务端:运算游戏逻辑,将玩家输入和部分状态发送给客户端


客户端:在玩家输入时,不等服务器就立马运算游戏逻辑,就有点像单机游戏了,但这种运算结果未经过服务器,不一定是正确的,所以它实际上是一个游戏逻辑的预测。在收到服务器数据后,会对预测结果进行校验,如果错误,就需要平滑地将其纠正到正确的状态。


这里说一下校验的过程,其实就是先回滚再前滚。


服务端下发的数据是之前一个时间点的数据,我们本地赋值以后相当于回滚到之前的时间,然后我们会一帧帧的运算到当前的时间,这就叫前滚,最后将计算结果与预测结果进行比较,可以看到校验的计算量是非常大的。


优点:客户端可以进行游戏逻辑预测;网络游戏体验好;以服务器数据为准,比较安全。


缺点:代码复杂度高;客户端运算量大;因为有客户端预测,所以客户端之间是不完全同步的。


2、小游戏平台特点


一开始我们的项目采用的是状态同步的方式,但由于我们的项目是针对小游戏平台的,小游戏平台有以下几个特点:

  • 运算性能较差,客户端计算量不能太大
  • Javascript代码很容易被破解,玩家想要作弊的话很容易
  • 网络连接只能使用TCP,所以带宽占用不能太高


3、欢乐枪战的实现方案


(1)带宽优化


基于小游戏平台的特点,我们项目从状态同步开始做简化,一直简化到以下这种实现方案:

 

  • 服务端:运算游戏逻辑,将变化的状态发送给客户端
  • 客户端:不运算游戏逻辑,收到数据以后进行差值平滑播放
  • 优化了带宽占用的快照插值


这个大家可能看着就有点眼熟了,其实就是优化了带宽占用的快照插值。这种方案最关键的一点是,你要把带宽优化下来。而带宽优化最关键的,是只有在必要的情况比如游戏开始和断线重连时才发送全量状态,平时玩的过程中,只发送变化的状态。


另外一方面是数据压缩,比如方向,刚开始我们用的是方向向量,但其实用弧度制乘以一千就可以了,这样就把两个Float优化成一个Short。


经过带宽优化成果:


上行:2~15pkg/s,流量占用:0.1 KB/s


下行:0~15pkg/s,流量占用:2.5 KB/s


这个流量占用对于目前的手机网络来说,是完全可以接受的。


(2)网络抖动优化


介绍完了带宽优化,接下来我们来聊聊网络抖动。


网络抖动指的是,网络的传输是不稳定的,服务端每个逻辑帧会发送一个包,它发送的频率是稳定的,但是对于客户端,可能在一个逻辑帧内收不到包,也可能收到多个包。


这在游戏中的体现就是,玩家在移动过程中,这一帧没有收到包,就停下来了,下一帧收到2个包,就跳过去了,体现出走走停停的状态。


对于这种网络抖动,最常见的优化方法是航位推测法。


航位推测法(Dead Reckoning):

 

  • 客户端和服务端约定至少每500ms同步一次
  • 客户端若没有按时收到移动状态,则用最后一次收到的移动状态继续预测一段时间
  • 服务端若没有按时收到玩家输入,则用最后一次收到的玩家输入继续运算一段时间


用这种方案优化之后,走走停停的现象就基本没有了。

 

Cocos 技术派:实时竞技小游戏技术实现分享_服务器_08

抖动缓存法


另一种优化方案是抖动缓存,这是指收到包后不立马处理,而是放入抖动缓存中,延迟一段时间后再取出。

 

Cocos 技术派:实时竞技小游戏技术实现分享_eclipse_09

这种优化方案关键点在于缓存的大小。如果缓存太小,对于抖动还是比较敏感,抗抖动效果比较弱,缓存太大,玩家的延迟又特别高,所以你需要根据算法动态调整缓存的大小以适应网络环境。


(3)全区全服

 

  • 所有玩家都在同一个大区里
  • 前台服务器处理登录等战斗外逻辑
  • 游戏服务器处理战斗逻辑


(4)分地域部署


我们的项目是实时竞技游戏,对于延迟比较敏感,因此我们的游戏服务器采用了分地域部署。服务器入口使用的是阿里云的“云解析DNS”服务,按照地域自动分配游戏服务器(华北、华东、华南、西南),玩家在进行快速匹配战斗时,会根据地域分配服务器,同一地域玩家进入该地域所属服务器。


以下是我们在网络优化方面参考的文章,都是干货,如果感兴趣可以去了解一下。