ELK GAMEVERSE
开始使用

我们为什么在服务器上重演每一局

只有当没人能直接往里写数据时,一份排行榜才值得你去争一个位置。这样做的代价与收获,都在这里。

做一款游戏,明摆着的办法是让手机说了算。玩家打完,应用算出分数,再告诉服务器发生了什么。这很快,这很简单,而服务器根本没有意见。

这也意味着,分数就是应用说的那个数。而应用是别人电脑上的一个程序,那个人是可以改它的。

我们改为怎么做

这里的每一款游戏记录的是你走过的步骤——不是结果,是步骤。你打完之后,这份清单会送到我们的服务器,服务器从同样的起始局面出发,把同一局游戏再玩一遍,对同样的步骤应用同样的规则。

如果它走到的就是你的结果,这一局就被接受,任何奖励都由服务器自己那份副本算出来。如果不是,这一局就被拒绝,什么也不入账。

两边的引擎是同一套代码,而且它是确定性的:给定同样的种子和同样的步骤,它必须产生同样的棋盘。最后这一条要求,塑造了设计中多得出人意料的一部分。任何带随机性的东西,都必须来自一颗服务器同样持有的种子。任何要读时钟的东西,都必须读一个两边都认可的时间。一条在快设备上和在慢设备上表现不同的规则,是我们不能要的规则。

它的代价

每一局打完的游戏多花几毫秒,以及对游戏能怎么写的一条实打实的约束。它还会找出我们自己的漏洞,这件事没有听上去那么舒服。

举一个例子,因为这是诚实的那一类。有一次,收成是靠遍历一个 JSON 对象的键来分配的。同一份内容的两个副本——一个从文件加载,一个从数据库加载——返回这些键的顺序不一样,于是仓库装到中途就满了。同样的规则,同样的步骤,不同的结果。诚实的玩家被拒绝了会话,收到的消息说某座聚落装的是 590 车而不是 591 车。游戏里看不出哪里不对;只是重演和它自己对不上。

这个漏洞对每一个单元测试都是隐形的,因为两半各自都能通过。非得让两边经由不同的路径取得数据,才把它逼了出来。

它换来什么

一个没人能直接往上写的排行榜,以及一份编不出来的奖励。这两句话说的是同一个性质:你赚到的,就是重演说你赚到的。

这也意味着出问题的时候我们能说得很具体。一局被拒绝的会话不是一句含糊的指控——它是在一个已知的点上出现的对不上,而且几乎总是游戏中途断线,而不是玩家做了什么。