使用云服务开发抢蛋糕小游戏:客户端、服务端与防作弊


我们在 LeanCloud 成立五周年之际,发布了一款名为《LeanCloud 周年游戏》的微信小游戏。

游戏玩起来很简单,参与者要在 15 秒内从迅速掉落的蛋糕和炸弹中点中尽可能多的蛋糕来得分,蛋糕有好几种,分值也不一样,而误点到炸弹就要扣分。游戏一结束参与者能在排行榜中看到自己的名次,我们给前 50 名都设置了奖品。

游戏截图:


排行榜截图:


没玩过的朋友可以搜索「LeanCloud 周年游戏」体验一下。

这个项目开发周期大概为一周,包含客户端开发 2 天 + 服务端 1 天 + 调试 2 天。

接下来我会从客户端、服务端、作弊检测三方面来梳理关键的技术细节,希望能够为游戏开发者或感兴趣的朋友提供一些思路。

在开发环境方面,客户端主要使用了 Cocos Creator 来编辑构建「微信小游戏」项目,服务端使用了 LeanCloud 的云存储、云引擎和排行榜等服务,这些我都会在后面详细介绍。

客户端

先说引擎和编辑器。选取 Cocos Creator 的原因是当在编辑器中构建不同平台项目时,它的友好程度一直都比较好,而且 LeanCloud 也为 Cocos Creator 做了适配。我们游戏的玩法比较简单,无需过多解释,所以接下来我会从客户端资源、状态机、暂停、LeanCloud SDK 和微信这些方面来展开描述。

资源

在游戏运行过程中,加载资源、实例化、销毁 Node 等任何耗时操作都可能造成游戏卡顿,影响体验,特别是在低端机器上这种现象会更加明显。所以我们应该对资源进行预加载或者预实例化。

对于加载资源,通常是在场景切换时,对旧场景资源进行卸载,并对新场景资源进行预加载。

在 Cocos 中,通过[cc.loader 1]可以很方便地对单个资源、资源列表和资源目录进行加载和缓存。而对于 Node 的实例化和销毁,则要根据 Node 的生命周期进行区分。如果频繁生成和销毁的 Node,我们可以在加载阶段通过对象池技术预先实例化一部分,这样当在游戏过程中需要实例化 Node 时,就不需要实例化,而是从对象池中获取;在不需要时,不进行销毁操作,而是放回至对象池中等待下次使用。如弹幕游戏中的飞机和子弹等。在我们的游戏中,我们也对生成的蛋糕应用了「对象池」技术来避免游戏中可能出现的卡顿。庆幸的是,Cocos 已经提供了[这项功能2]。
状态机

在游戏运行过程中,游戏主体(或角色)都会有很多的状态,比如英雄的空闲、移动、攻击、死亡等,因此通常会引入「状态机」模式对游戏对象进行设计。我们为抢蛋糕游戏引入了 [machina3]库作为状态机的框架,将整个游戏主体划分为初始化、准备、进行中、结束四个状态。

通过状态机,我们可以更加清楚地跟踪游戏在过程中的变化,并可以通过事件在不同的状态下做出不同的处理。

暂停

在游戏过程中,我们经常会需要暂停游戏,比如在抢蛋糕游戏结束时不再生成新的蛋糕和位置移动。

不同的游戏引擎的暂停方式有所不同。通过 Cocos 的文档,我们找到了引擎提供的 cc.director.pause() / cc.director.resume() 接口,但是尝试之后发现很多局限性,比如在暂停之后 Widget 适配会不起作用,ScrollView 拖拽不回弹等情况。

于是我们决定通过 Component.update(dt) 生命周期和状态机在游戏中自行控制 Node 的更新。主要思路是在全局游戏的 update() 生命周期里,将更新事件交由状态机,只有在游戏进入「进行中」状态时才处理更新事件,而在其他状态下则忽略更新事件。

更新过程为先获取场景下的所有 CakeCtrl 对象,调用自定义 onUpdate(dt) 方法进行更新(注意不是 update(dt) 生命周期方法)。

// 游戏状态:
play: {
  ...
   update: function (dt) {
       const cakeCtrls = this._scene.getComponentsInChildren(CakeCtrl);
       cakeCtrls.forEach((cakeCtrl) => {
           cakeCtrl.onUpdate(dt);
       });
    }
  ...
},

LeanCloud SDK

LeanCloud SDK 在大部分平台都做了适配,可以很方便地接入 LeanCloud 云服务。

开发者在使用 Cocos Creator 时一般在浏览器进行调试开发,当完成后再发布到微信环境。但不同环境下 LeanCloud SDK 略有不同,为了方便使用,你可以通过封装来隐藏加载不同版本SDK 的细节。

比如在浏览器环境下,加载 leancloud-storage 库;而发布在微信小游戏环境下,则加载 leancloud-storage/dist/av-weapp-min.js 库。

if (cc.sys.browserType ===cc.sys.BROWSER_TYPE_WECHAT_GAME) {
  AV= require("leancloud-storage/dist/av-weapp-min.js");
} else {
  AV= require("leancloud-storage");
}

另外,如果我们需要使用微信授权登录,为了方便在浏览器下调试,我们也可以将login() 封装成不同的实现,统一逻辑层调用。

比如在浏览器环境下,使用账号 + 密码方式登录;而在微信小游戏环境下,使用微信授权登录。

login() {
   return new Promise((resolve, reject) => {
     // 微信登录
     if (cc.sys.browserType === cc.sys.BROWSER_TYPE_WECHAT_GAME) {
       AV.User.loginWithWeapp()
         .then(user => {
           ...
         })
         .catch(error => {
           reject(error);
         });
     } else {
       // 使用默认账号登录,开发调试使用
       AV.User.logIn("1if7jp52qx9771hllat1rvfqt", "123")
         .then(user => {
           ...
         })
         .catch(error => {
           reject(error);
         });
     }
   });
  },

更多关于 SDK 的文档,请访问[LeanCloud 文档4]。

微信

因为我们的游戏在排行榜中需要获取玩家的头像和昵称,所以需要使用到微信的获取用户信息(昵称、头像)接口。这里要吐槽一下,微信新版的 SDK 已经不允许用「弹框授权」来直接获取信息了,而需要使用「固定类型的」微信小程序按钮获取。但是这一机制有对微信旧版 SDK 又不可用,所以我们需要根据微信版本,确定通过哪种机制拿到微信授权。

如果是旧版本的微信,则可以直接调用获取用户信息接口;而如果是新版本的微信,则需要渲染出微信授权按钮,通过按钮的点击事件再获取。这里你可能需要面对小程序的渲染和 Cocos 的渲染机制不一致的问题。

所以,这里还用到了一个小窍门——将微信小程序的授权按钮设置为透明,覆盖到 Cocos 场景中的按钮之上,当按钮被点击时,系统会先将点击事件传递到微信小程序,在微信小程序回调中处理完成之后再交由游戏中处理。

服务端

在服务端开发中,我们主要使用了 LeanCloud 的云存储、云引擎和排行榜服务。

存储

在存储方面,主要使用了 3 张表:

  • _User:存储用户信息,LeanCloud 内置表。
  • UserInfo:存储用户的详细信息,用于邮寄奖励。
  • Game:存储玩家每局游戏的数据。


为了保证游戏安全,只有用户信息是通过 LeanCloud 存储 SDK 直接操作的。而游戏相关的数据,都是通过 LeanCloud SDK 请求到云引擎中处理后保存的。[参考文档5]。

云引擎

云引擎是 LeanCloud 推出的服务端托管平台。通常比较关键的数据,我们推荐不要使用 SDK 直接操作,而是通过云引擎进行操作。[参考文档6]。

在抢蛋糕游戏中,为了保证游戏安全,我们在游戏结束后并没有在客户端直接通过LeanCloud SDK 上传分数到排行榜,而是将游戏参数发送到云引擎,通过云引擎分析后再确定是否写入到排行榜。具体流程:

  • 游戏开始,向服务端请求游戏数据,服务端会返回本局游戏的 id 和蛋糕数据;而对于「多次」作弊的玩家,将不返回游戏数据。
  • 游戏结束,客户端将本局游戏的参数提交给服务端,包括:本局游戏 id、分数、蛋糕点击数量、时间戳、签名、蛋糕点击索引序列。
  • 服务端对游戏数据进行合法性检测,如果通过则更新排行榜,否则丢弃并标记用户作弊(作弊检测方法会在后面有详细介绍)。


排行榜

排行榜是 LeanCloud Play 为游戏开发者提供的一项新的服务。它除了能提供方便的数据更新接口,还提供了排行榜成绩更新、榜单管理等配置。[参考文档7]

在抢蛋糕游戏中,除了使用常规的「更新玩家成绩」之外,还用到了对作弊玩家进行「榜单移除」的操作。

更新玩家成绩

...
// 提交分数
scoreInLeaderBoard =calcScoreInLeaderBoard(score);
   AV.Leaderboard.updateStatistics(currentUser, {
       free: scoreInLeaderBoard
})

标记作弊玩家,并移除榜单成绩

/**
* 标记用户作弊
*@param {*} user 用户
*/
function markUser(user) {
  letcheat = user.get("cheat") ? user.get("cheat") : 0;
console.log(`cheat: ${cheat}`);
cheat += 1;
user.set("cheat", cheat);
  user.save();
  if(cheat > MAX_CHEAT_COUNT) {
   // 如果超过最大作弊次数,则清除榜单
   AV.Leaderboard.deleteStatistics(user, ["score"])
     .then(() => {
       console.log(`remove ${user} statistics`);
     })
     .catch(console.error);
  }
}

作弊检测

对于面向程序员制作的游戏,我们猜测到大家可能会通过技术手段来获取更高分数。为了增加大家破解的趣味性,我们也提供了一些作弊检测机制供大家突破——通过运行时作弊检测和离线数据分析生成了最终的榜单数据。

运行时作弊检测

具体过程:

  • 在游戏开始时,客户端向服务端发起开始请求,服务端随机生成本局游戏的蛋糕序列(共 200 个,游戏频率为每 0.1s 生成 1 个),将当前时间戳、用户、蛋糕序列保存至 Game 对象。
  • 将 Game 对象 id和蛋糕序列下发至客户端,客户端根据蛋糕序列生成蛋糕,在游戏过程中,记录玩家点击蛋糕索引。
  • 游戏结束后,将 Game 对象 id、分数、每种蛋糕点击的数量、结束时间戳、签名(md5(id + score + timestamp))和蛋糕点击索引序列发送给服务端。


服务端接收到参数后,对数据进行校验。

校验包括:

  • 提交参数是否完整(基础检测)
  • 分数和蛋糕点击数量是否匹配(逻辑检测)
  • 分数和蛋糕索引是否匹配(逻辑检测)
  • 服务端重新计算签名是否匹配(防止修改明文参数)
  • 验证游戏时长是否合理(超过 2 倍游戏时长,则认为玩家可能是在分析请求)

  • 对于检测到作弊的玩家,本局游戏成绩将不会更新排行榜,并记录 1 次作弊,超过 10 次作弊的玩家,将不能请求到游戏开始时的数据。


离线数据分析

运行时作弊检测并不足以抵挡住广大开发者破解的热情,很快就有用户梳理清楚了协议参数。所以在运行时检测后,我们又默默记下了用户的参数,用于离线分析。

校验包括:

  • 验证游戏时长是否小于 1 倍游戏时长(游戏至少需要 18 秒完成,15秒游戏 + 3秒倒计时,有些同学竟然2 秒就把游戏结束请求发来了)
  • 蛋糕点击索引是否有重复(逻辑判断,有些 Android 插件可以让游戏卡住,使同一个蛋糕被点击 N 次,则会叠加多次分数)
  • 蛋糕点击索引是否超过允许最大值(排行榜中有位 500+ 分的朋友通过解包,分析协议,通过模拟请求,顺利通过了上述检测,但是竟然在请求中把 200 个蛋糕索引全部赋值了,而正常游戏中最多只能点击到 150 个,即 15 秒 x 每秒 10 个)


致命缺陷

这类游戏是没办法防住按键(触摸)精灵的。如果通过「图像识别 + 自动点击脚本」可以轻松点击完所有的蛋糕并有效避开炸弹,则可以通过上述检测。

有人说如果服务端运算可不可以,思路是屏幕点击的坐标交由服务端运算,但是对于按键精灵类的脚本还是无法避免,并且还会增加项目的开发量(服务端要对不同分辨率和坐标做一些处理)。如果其他同学有办法做更有效的检测,希望能反馈到 [LeanCloud 论坛8],大家共同讨论。

以上便是我们此次开发小游戏的心得体会,希望能对大家有所帮助。

内容参考链接:

cc.loader 1:https://docs.cocos.com/creator/api/zh/classes/loader.html
这项功能2:
https://docs.cocos.com/creator/manual/zh/scripting/pooling.html?h=%E5%AF%B9%E8%B1%A1%E6%B1%A0
machina3:https://github.com/ifandelse/machina.js
LeanCloud 文档4:https://leancloud.cn/docs/leanstorage_guide-js.html
参考文档5:
https://leancloud.cn/docs/#%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8
参考文档6:https://leancloud.cn/docs/#%E4%BA%91%E5%BC%95%E6%93%8E
参考文档7:https://leancloud.cn/docs/leaderboard.html
LeanCloud 论坛8:https://forum.leancloud.cn/

基础普及:什么是网站的域名、服务器和程序

在搜索引擎中搜索“今日头条”如下图所示,“今日头条官网”下方显示“www.toutiao.com”就是该网站的域名。

网络安全漫谈及实战摘要(转载)

前言在最近一周内,我收到了安全圈子里面小伙伴的私信,说他们非常喜欢信息安全,但是看了我之前发布文章,觉得有点难度,还涉及到C#编程,不好理解,希望我能给些基础方面的文章。

一份值得收藏的 MySQL 高性能优化规范建议

数据库命令规范所有数据库对象名称必须使用小写字母并用下划线分割。所有数据库对象名称禁止使用 MySQL 保留关键字。

血塞通、炎琥宁等41批不合格药品遭曝光

江苏省食品药品监督管理局江苏省年第期药品质量公告不符合规定化学药、抗生素、生化药、中成药链接:中国最全医药代理商集中地本平台不对转载之文章所包含内容的准确性、可靠性或者完整性提供任何明示或暗示的保证,不对文章观点负责。点阅读原文获取大数据,注册后将每周收到行业动态数据。

Java开发者必知必会的20种常用类库和API

《Effective Java》的作者Joshua Bloch曾经说过:“建议使用现有的API来开发,而不是重复造轮子”。