接盘日记 - 第二集:我修好了开始游戏,系统判定所有玩家作弊

上一集说到,开始游戏按钮不是靠点击进的。

是靠这一行:

if (Date.now() % 2 === 0) startGame()

偶数时间,准入。
奇数时间,拒签。

我盯着这行代码看了很久。

一个正常程序员看到它,第一反应应该是删掉。
一个接盘程序员看到它,第一反应应该是先搜全局。

我搜了。

项目里一共 17 处。

开始游戏有。
打开背包有。
领取奖励有。
进入关卡有。
甚至分享按钮里也有。

最离谱的是,充值按钮旁边也有一处。

if (Date.now() % 2 === 0) {
    showPayPanel()
} else {
    showPayPanel()
}

看起来左右都一样。

我以为这是废代码。

直到我往下翻,发现 else 里多了一行:

report("pay_button_clicked_but_sad")

这不是逻辑。

这是情绪埋点。

我第一次知道,一个按钮也可以有心理活动。


我决定先不管别的,只修开始游戏。

需求很简单:

玩家点击开始游戏,就进游戏。

我把原来的时间判断删掉,改成了正常按钮事件。

this.startButton.node.on(Node.EventType.TOUCH_END, () => {
    this.startGame()
})

运行。

点击。

进去了。

没有白屏。
没有报错。
没有广告回调。
没有偶数赐福。

那一刻我甚至有点感动。

这个项目第一次表现得像个游戏。

我提交到测试环境。

十分钟后,测试在群里发:

“进是进去了,但是所有账号都被判定作弊。”

我看着这句话,心里很平静。

不是因为我不慌。

是因为这项目已经把我的情绪系统热更新掉了。


我打开日志。

果然,玩家进入主场景后,立刻触发风控:

[AntiCheat] enter_game_too_fast
[AntiCheat] skip_ad_fail_waiting
[AntiCheat] start_flow_abnormal

翻译成人话:

你怎么进来这么快?

你是不是跳过了广告失败?

你是不是不尊重我们项目的传统启动流程?

我继续查。

发现项目里有个反作弊模块,叫:

SafeGuardManager.ts

名字很正义。

里面第一条规则是:

if (enterGameCost < 3000) {
    markCheater()
}

也就是说,正常玩家点击开始游戏,3 秒内进主场景,会被判定作弊。

为什么?

因为以前广告失败至少要等 3 秒。

这个项目已经默认:

真正的人类玩家,必须先经历广告加载失败。

没经历过失败的人,不配进入游戏。

我问前同事:

“为什么反作弊要判断进游戏时间?”

他说:

“以前有人写外挂,秒进游戏。”

我问:

“那为什么不区分正常点击和外挂?”

他说:

“当时来不及。”

我问:

“那为什么后来不改?”

他说:

“后来数据稳定了。”

在我们公司,“数据稳定了”的意思是:

bug 已经和业务结婚了,别去劝离。


我把反作弊阈值改掉。

从 3000 改成 300。

测试再跑。

这次不判作弊了。

但是新手引导卡住了。

画面停在第一关。

引导手指疯狂点空气。

玩家角色站在原地,像在等法院判决。

我查新手引导。

发现它没有监听游戏开始事件。

它监听的是广告失败事件。

AdManager.on("load_fail", () => {
    GuideManager.start()
})

很好。

开始游戏靠广告失败。
新手引导也靠广告失败。
反作弊还靠广告失败。

我逐渐明白,这个项目表面上是 Cocos Creator。

底层架构其实是:

广告失败驱动开发

只要广告失败,一切正常。
只要广告成功,项目就开始迷茫。
如果不等广告直接进游戏,系统会认为你不是玩家。

你是异常流量。


我准备把新手引导改成监听真正的进入游戏事件。

这时我发现项目里有三个事件:

GAME_START
GAME_STARTED
GAME_REALLY_START

还有一个:

GAME_START_BUT_NOT_YET

我点进去。

注释写着:

// 游戏开始了,但业务上还不能算开始

我不知道这句话是谁写的。

但他一定经历过很多。

我继续找调用。

GAME_START 用于埋点。
GAME_STARTED 用于播放 BGM。
GAME_REALLY_START 用于生成怪物。
GAME_START_BUT_NOT_YET 用于打开新手引导。

也就是说,这个游戏开始一次,要分四次通知大家。

像一场没有人愿意负责的婚礼。

每个系统都说自己到了。

但没人承认新人已经进场。


我最后选了 GAME_REALLY_START

因为它听起来最像真的。

我把引导挂过去。

运行。

新手引导正常了。

角色能动。
怪物能刷。
按钮能点。
场景能切。

我正准备松口气。

测试又发来一句:

“第一关可以玩了,但是打完以后直接跳到第七关。”

我当场把手从键盘上拿开。

我怕我再碰一下,玩家明天直接通关人生。


我查关卡跳转。

正常逻辑应该是:

currentLevel + 1

这个项目不是。

它是:

getNextLevelByUserValue()

听起来很高级。

我点进去。

里面根据玩家行为计算下一关:

  • 看广告次数
  • 点击开始耗时
  • 新手引导完成时间
  • 是否触发失败回调
  • 是否被判过作弊
  • 本地缓存里的 lastRealLevel
  • 远程配置里的 nextMaybeLevel
  • 以及一个叫 老板想看的进度 的字段

我第一次见到关卡系统里出现老板。

不是 NPC。

是变量。

const bossExpectedLevel = remoteConfig.bossExpectedLevel || 7

这就是为什么第一关打完跳第七关。

因为老板想看第七关。

玩家只是顺便被安排过去的。

我问产品:

“为什么要跳第七关?”

产品说:

“老板当时验收第七关。”

我问:

“那为什么线上也跳?”

产品说:

“后来忘了关。”

我问:

“上线多久了?”

产品说:

“八个月。”

我突然理解了这个游戏为什么数据还可以。

玩家第一关打完直接跳第七关。

以为这是快节奏爽游。

其实是验收配置没删。


我把 bossExpectedLevel 去掉。

改回正常关卡推进。

测试再跑。

第一关打完进第二关。

第二关打完进第三关。

第三关打完,游戏弹出一个付费礼包。

看起来正常。

直到我发现礼包标题是:

欢迎来到第七关

我查礼包系统。

它不认关卡。

它认老板验收字段。

也就是说,虽然我把玩家从第七关拉回了第二关。

但商业化系统还活在老板的第七关里。

我继续查。

发现项目里所有功能对“当前关卡”的理解都不一样。

战斗系统认为当前关卡是 levelId
UI 系统认为当前关卡是 displayLevel
礼包系统认为当前关卡是 payLevel
埋点系统认为当前关卡是 realLevel
排行榜系统认为当前关卡是 maxHistoryLevel
新手引导认为当前关卡是 guideStep
老板验收认为当前关卡永远是 7。

这个项目最公平的地方就是:

没有任何两个系统相信同一个现实。


我坐在那里整理了一张表。

标题叫:

当前关卡到底是谁

写到一半,我发现还有一个字段:

fakeLevel

我以为它是测试字段。

结果它在线上配置里。

用途是:

“为了让玩家感觉自己进度更快。”

所以玩家实际在第 2 关。
界面显示第 5 关。
礼包认为第 7 关。
埋点上报第 3 关。
排行榜记录第 11 关。

而客服后台看到的是第 1 关。

我问客服:

“那玩家问卡关,你们怎么看?”

客服说:

“我们一般安慰一下。”

我问:

“怎么安慰?”

客服发来话术:

亲爱的玩家,您的数据正在同步中,请稍后再试。

我看着“正在同步中”五个字。

突然觉得很哲学。

不是数据正在同步。

是现实正在同步。

但同步失败。


下午,领导过来问我:

“开始游戏修好了吧?”

我说:

“修好了。”

他说:

“那挺快啊。”

我说:

“但是修完之后,反作弊、新手引导、关卡跳转、礼包、排行榜、客服后台对玩家状态的理解全都不一致。”

领导听完,点点头:

“那就是还有点小问题。”

我没有反驳。

因为在这个项目里,只要没有直接影响广告展示,都叫小问题。

领导又说:

“你先保证玩家能正常玩。”

我问:

“正常的定义是什么?”

他想了想,说:

“能进,能点,能看广告。”

我懂了。

我们对游戏的定义不一样。

我以为游戏是让玩家玩的。

公司认为游戏是让广告有地方放的。


晚上我准备下班前,做最后一次回归。

点击开始。

进入第一关。

完成引导。

打完怪物。

进入第二关。

没有判作弊。
没有跳第七关。
没有白屏。
没有卡死。

我以为今天终于结束了。

然后屏幕右上角弹出一个小红点。

我点开。

是邮箱。

邮箱里有一封系统邮件:

补偿:由于您涉嫌异常登录,奖励已冻结

我查邮件发送源。

来自:

AntiCheatRewardCompensationManager.ts

这个名字已经长到不像类名。

像判决书。

里面逻辑是:

如果玩家曾经被判作弊,但后来又解除作弊,就发补偿邮件。
但补偿邮件里的奖励,为了防止作弊玩家领取,会被冻结。
冻结之后,为了提示玩家,又发一封补偿邮件。
然后继续冻结。

也就是说,一旦玩家被误判过一次作弊。

他会开始收到无限补偿。

每一封都不能领。

这不是福利系统。

这是精神攻击。

我把邮件系统关掉。

控制台马上刷出一行:

[TaskSystem] daily_mail_not_read, force_red_dot = true

红点还在。

我点不掉。

像项目对我的注视。


我最后只改了三处:

  1. 开始游戏改成真实按钮点击
  2. 新手引导改成监听真正进场事件
  3. 反作弊不再把正常玩家当外挂

然后我留了一个 TODO:

// TODO: 统一 currentLevel / realLevel / displayLevel / fakeLevel / payLevel

写完我自己都笑了。

这个 TODO 看起来像技术任务。

其实像联合国调停。

提交代码前,我又搜了一下 fakeLevel

本来只是想确认影响范围。

结果搜到一个我没见过的文件:

TruthLevelManager.ts

真相关卡管理器。

它没有被任何地方引用。

但提交记录显示,上个月有人改过。

提交信息是:

先别接入,等时机成熟

我盯着“时机成熟”四个字,看了很久。

因为我突然意识到。

这个项目里可能一直有一套真正的关卡系统。

只是没人敢打开。

而明天的需求单上,正好写着:

优化关卡体验,让玩家感知更真实

我关掉电脑。

没提交。

因为我不确定,我今天修的是开始游戏。

还是不小心打开了第二层地狱。

1赞

哈哈哈哈哈哈 :rofl:

我觉得蛮有意思的,你这个找bug跟玩解密游戏一样 哈哈哈哈

笑死我了 兄弟

哈哈哈 所以这其实是一个修bug的游戏