网络游戏 与 GMT / UTC
在游戏开发的过程中,免不了跟「日期/时间」打交道,尤其是网络游戏。策划通过配置活动的起止时间来实现活动的开放与关闭。
在全球化的今天,作为东八区的开发者,我们做的游戏并不是只有我们自己在玩——来自世界各地的玩家都可以参与我们的游戏。所以在日期处理这件事上,就需要多加考虑,不然在许多地方会捅篓子,或者被玩家有机可趁。
¶举 (chun) 个 (shu) 例 (xu) 子 (gou)
东八区的运营小组设定新服务器在 4月10日 10:00:00 开服。然后程序员写下 var start = new Date("2015-04-10 10:00:00")
并期望得到正确的结果——没错,本地开发环境的终端返回的正确的信息:
Fri Apr 10 2015 10:00:00 GMT+0800 (CST)
于是程序员将写好的程序部署到了测试服上,不幸的事发生了。服务器到了东八区的 12:00 才开服。玩家白等了两个小时,非常生气,纷纷差评!
后来查明原因是因为程序员为了跟异国恋的女友同步时间,故意把时区调到墨尔本才导致了这起事故。
于是问题就来了——时区是如何影响时间的呢?我们需要先理念两个概念:UTC 和 GMT 时间。
GMT 时间把地球分成 24 个时区,每个地区都使用自己的 GMT 时间作息。当运营小组设定开服日期的时候,显然是以自己所在的东八区的 GMT 时间作为标准。
但是如果程序在运行着不同时区的电脑上,虽然当地时间表示都一样,但是实际上却是不同的 UTC 时间。将本地时区修改到 GMT +10 就可以重现故事里的 BUG ,可以看到两个日期的 UTC 时间戳是不同的:
// change system time zone to GMT +8
> new Date("2015-04-10 10:00:00").valueOf()
1428631200000
// change system time zone to GMT +10
> new Date("2015-04-10 10:00:00").valueOf()
1428624000000
¶另一个例子
说完服务端,来说说客户端。
运营组说了,开服活动在 7 个自然日后结束——也就是 4月17日 00:00 结算活动。前端需要有个倒计时展示。
¶反例 0x01
前端程序员:服务器列表里已经给了开服时间的时间戳:1428631200000 ——好家伙!拿这个直接算出当天零点,然后加上七天就好了吧!
// Fri Apr 10 2015 10:00:00 GMT+0800 (CST)
var start = 1428631200000;
var aDayOfMS = 24 * 60 * 60 * 1000;
start = Math.floor(start / aDayOfMS) * aDayOfMS;
var end = start + aDayOfMS * 7;
直接拿 UTC 时间戳舍去天数后面的时间,直觉上好像没什么问题。但是被舍去的小时数实际上只有 02:00 而不是 10:00 。由于 UTC 时间戳是以 GMT +0 为基准,所以上面这段代码只有当服务端在 GMT +0 时区的时候是正确的。
// Fri Apr 10 2015 10:00:00 GMT+0800 (CST)
> var start = 1428631200000;
> start % aDayOfMS
7200000
> 7200000 / ( 60 * 60 * 1000)
2
¶反例 0x02
前端程序员:OMG,怎么回事!那我换个方法:
// Fri Apr 10 2015 10:00:00 GMT+0800 (CST)
var startDate = new Date(1428631200000);
startDate.setHours(0);
startDate.setMinutes(0);
startDate.setSeconds(0);
startDate.setMilliseconds(0);
var aDayOfMS = 24 * 60 * 60 * 1000;
var end = aDayOfMS * 7 + startDate.valueOf();
这下没问题了么?NO. 对于 10:00 GMT+8 这段代码只有在 GMT -2 到 GMT +12 这些时区的时候才是正确的。如果不在这个范围,例如到了 GMT -4 这段程序算出的 startDate 会跑到 4月9日 00:00 去。
> startDate;
Thu Apr 09 2015 00:00:00 GMT-0400 (EDT)
¶正解 0x03
那么要如何处理才能得到正确的东八区的自然日的 4月10日 00:00 的 UTC 时间呢?
如果开服时间是固定的 10:00 那很好办,直接在 UTC 上减去 10 小时即可。但如果开服时间是个变量,那最好的实现是服务端直接给出自然日的时间戳(通过 0x02 的代码)或者至少给出服务器的时区,前端可以通过以下代码计算出正确的开服自然日时间:
var zoneOffset = ServerTimeZone - new Date().getTimezoneOffset(); // in minutes
var dateOnServer = new Date(ServerOpenTimestamp - zoneOffset * 60 * 1000);
var ServerOpenDate = ServerOpenTimestamp - dateOnServer.getHours() * 60 * 60 * 1000;
这样在不同的时区,都能得到正确的时间了。同时也防止了那些故意修改时区的玩家绕过前端的限制。
server open activity end
E
|
+10 + 04-10 12:00 - 04-12 02:00
|
+8 + 04-10 10:00 - 04-17 00:00
|
|
0 + 04-10 02:00 - 04-16 16:00
|
-5 + 04-09 21:00 - 04-16 11:00
|
W