木匣子

Web/Game/Programming/Life etc.

网络游戏 与 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