最近一个放置类项目找过来,说挂机产出对不上——玩家到手的比后台该发的多。顺着查下去,发现是时间被动了手脚。
时间在手游里算个麻烦。冷却、体力、读条这些判定都按它走,签到和离线收益干脆就是拿时间换钱。可这个时间是从玩家手机上读的,可信度有待验证。
改系统时间
最经典的一招,也是最不值得展开的一招:把手机时钟往前拨一天,签到领了、离线收益收了,再拨回来。不用 root,不用工具,设置里点两下的事儿。
但这招能成的前提是:你拿客户端时钟去做发奖判定...都2026年了,还这么写代码的话,玩家干脆把时间调到3000年速通游戏了。提个醒:凡是跟经济沾边的判定走服务端时间,客户端那个 DateTimeOffset.UtcNow 只配拿去刷新界面上的倒计时。这条算基本常识,还在这上面栽,只能算自己送的。

加速器
比较常见的问题。加速器改的不是“现在几点”,改的是时间流逝的速度——这是关键区别,也正是它能绕开服务端时间的原因:你按绝对时间去校验,它的时钟读数完全正常,没有一个可疑的时间戳给你抓;它放大的是你客户端自己数出来的那段时长。
GameGuardian 的 speed hack、市面上一堆变速齿轮 App,干的都是 hook 掉时间函数,把返回的时间差乘个倍率。Unity 里冷却要是靠 Time.deltaTime 累加,deltaTime 被放大五倍,读条就快五倍;底层被 hook 的,一般就是 clock_gettime / gettimeofday 。
对付它,第一反应可能是想办法检测这个 hook。但更省事、也更耐用的思路是反过来——别检测它,把它的收益直接拿掉。为什么这样就够?因为速度作弊放大的本来就是客户端自己数的那段时长,只要这段时长到服务端不算数,放大它就毫无意义。

具体说,凡是“多长时间”这个数会换成收益的,都别信客户端报的。两次跟服务端接触之间隔了多久,服务端拿自己的钟量一遍就知道;客户端被加速之后报上来的数会大得离谱,那就按服务端量到的发,多出来的不认:
// 服务端结算离线收益,客户端报的时长只当参考
long clientClaimed = req.offlineSeconds; // 客户端自己数的,可能被放大
long serverMeasured = now - player.lastSeenAtServer; // 服务端按自己的钟算
long settled = Math.Min(clientClaimed, serverMeasured); // 只认较小的那个
GrantIdleReward(settled);
检测 hook 本身不是不做,而是把它当作弊账号处罚的依据。跟 hook 拼检测是场军备竞赛,耗费的精力太大,把收益端掉这件事,是一次做对就长期有效的。
暂停恢复
第三种相对少见些。用系统的 freezer 或者 kill -STOP 把游戏进程冻住,过一会儿再放开,制造出“外面真实过了多久”和“游戏内部记到多久”之间的错位,专坑那些一恢复就拿本地这段时长直接结算的实现。

抓它可以看一个客户端信号:进程被冻期间,单调时钟照走,可你自己的逻辑 tick 一帧都没推进。恢复那一刻读一下,时钟跳了一大段、帧计数却几乎没动,基本就能判定被冻过了:
// 恢复时:单调时钟跳了一大段,可这期间自己的 tick 几乎没走
long jumped = SystemClock.elapsedRealtime() - lastTickRealtime;
if (jumped > THRESHOLD && ticksSinceMark < expectedTicks) {
// 大概率被冻过,别拿这段本地时长直接结算,回服务端核对
}
elapsedRealtime() 底层一样能被 hook,所以这只是个信号,不是判决——真正结算那一下还是回服务端,拿它自己记的上次在线时间兜底。
最后说下误报,这块容易踩。时间对不上多半不是作弊——跨时区飞一趟、夏令时、手动校时、便宜机器 RTC 没电跳回 1970.都会这样,别拿它当封号开关。好在结算既然是服务端说了算,基本不用你专门去罚:正常玩家按服务端时间结算,乱钟不影响结果,拨钟党也卡在同一套逻辑里。真要用这条信号,也得跟别的凑——单独偏三小时什么都不是,可他偏三小时、还在两小时会话里报了三十天离线收益,那才值得看一眼。
时间作弊翻来覆去还是那句:客户端手里那份时间不可信,这条改不了。你能做的是让它更难改、改了更容易露馅,但最后拍板结算的那一下,还是得放在玩家碰不到的地方。