使用云服务开发抢蛋糕小游戏的经验谈(附防做弊机制)

咱们在 LeanCloud 成立五周年之际,发布了一款名为《LeanCloud 周年游戏》的微信小游戏。一方面咱们但愿经过玩游戏赢奖品的方式来回馈那些一直关注咱们的用户,另外一方面咱们还但愿经过一个实际的项目来说明白 LeanCloud Play 在游戏开发方面的专长。html

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

游戏截图:

排行榜截图:

没玩过的朋友能够搜索「LeanCloud 周年游戏」体验一下。程序员

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

接下来我会从客户端、服务端、做弊检测三方面来梳理关键的技术细节,但愿可以为游戏开发者或感兴趣的朋友提供一些思路。小程序

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

客户端

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

资源

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

对于加载资源,一般是在场景切换时,对旧场景资源进行卸载,并对新场景资源进行预加载。缓存

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

状态机

在游戏运行过程当中,游戏主体(或角色)都会有不少的状态,好比英雄的空闲、移动、攻击、死亡等,所以一般会引入「状态机」模式对游戏对象进行设计。咱们为抢蛋糕游戏引入了 machina 库做为状态机的框架,将整个游戏主体划分为初始化、准备、进行中、结束四个状态。

经过状态机,咱们能够更加清楚地跟踪游戏在过程当中的变化,并能够经过事件在不一样的状态下作出不一样的处理。

暂停

在游戏过程当中,咱们常常会须要暂停游戏,好比在抢蛋糕游戏结束时再也不生成新的蛋糕和位置移动。

不一样的游戏引擎的暂停方式有所不一样。经过 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 文档

微信

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

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

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

服务端

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

存储

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

  • _User:存储用户信息,LeanCloud 内置表。

  • UserInfo:存储用户的详细信息,用于邮寄奖励。

  • Game:存储玩家每局游戏的数据。

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

云引擎

云引擎是 LeanCloud 推出的服务端托管平台。一般比较关键的数据,咱们推荐不要使用 SDK 直接操做,而是经过云引擎进行操做。参考文档

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

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

排行榜

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

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

更新玩家成绩

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

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

/**
 * 标记用户做弊
 * @param {*} user 用户
 */
function markUser(user) {
  let cheat = 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 论坛,你们共同讨论。

以上即是咱们使用本身的产品来开发游戏的心得体会,但愿能对你们有所帮助。

相关文章
相关标签/搜索