由于时区、夏令时的存在,游戏内的时间显示/计算都要考虑时区问题并进行相应处理。时间计算不用说,要排除玩家本地时区影响,只以服务器时区为准进行计算。时间显示有两种方案:
- 根据服务器下发的utc时间戳,按玩家手机本地设置的时区进行适配显示,这样对于经常往返于不同时区的玩家很友好(虽然这类玩家很少),玩家只要修改手机时区,游戏内的时间显示就以该时区为准了。然而这种方案通常会碰到问题,比如游戏内活动图片里写死了日期,时间,显然就无法根据玩家手机时区适配显示。
- 根据服务器时区进行统一显示是更好的方案,如果是国内上线游戏,可以统一显示东八区时间,这样就可以保证图片里的时间信息是正确的。这种方案也有个附带好处,当玩家不自知地将时区设为其他时区,时间却设成东八区时间时(我们项目内有个策划的手机就是这样设置的-_-||),游戏内的时间显示"看起来"还是正确的。
简单总结,游戏内的时间显示/计算最好都以服务器时区为准,而各种语言关于时间函数的api,都是以本地时区计算返回结果的,以Lua为例,Lua标准库中提供的时间函数 os.time()和os.date(),这两个函数传入和返回的时间table就是以本地时区为准的。
os.time()
- 原型:os.time ([table])
- 解释:按table的内容返回一个时间值(数字),若不带参数则么使用当前时间作为table内容,其中table中可以包含的字段有:year, month, day, hour, min, sec, isdst,其他字段将会被忽略。
os.date()
原型:os.date ([format [, time]])
解释:返回一个按format格式化日期、时间的字串或表。
参数格式:
- 由原型可以看出可以省略第二个参数也可以省略两个参数,只省略第二个参数函数会使用当前时间作为第二个参数,如果两个参数都省略则按当前系统的设置返回格式化的字符串,做以下等价替换 os.date() <=> os.date("%c")。
- 如果format以“!”开头,则按格林尼治时间进行格式化。
- 如果format是一个“*t”,将返一个带year(4位),month(1-12), day (1--31), hour (0-23), min (0-59),sec (0-61),wday (星期几, 星期天为1), yday (年内天数)和isdst (是否为日光节约时间true/false)的带键名的表;
- 如果format不是“*t”,os.date会将日期格式化为一个字符串
服务器时区
要以服务器时区进行时间计算,编码思路就是要计算出本地与服务器的时区差,调用os.time()、os.date()时进行补偿。
-- 服务器时区为东八区
local ServerTimeZone = 3600 * 8
-- 获取客户端本地时区
function TimeUtils.GetLocalTimeZone()
local now = os.time()
local localTimeZone = os.difftime(now, os.time(os.date("!*t", now)))
return localTimeZone
end
服务器时区:对于国内服务器,服务器时区可以直接硬编码成东八区,如果考虑做国际化,可以由服务器进行下发该值,根据地区设置不同服务器时区值。
本地时区:在lua里没有直接获取本地时区的api,但通过os.date("!*t", os.time()),可以获取格林尼治的时间table,再以本地时区解析table获取时间戳,该时间戳与os.time()时间戳相减即为时区秒数差值。
假设现在游戏内有个功能入口要在游戏开服第二天0点开启,如果不考虑时区问题,编码如下,当玩家修改本地时区时,计算得出的时间戳是不同的。这样玩家就可以通过修改本地时区,让功能提前开启。
-- 获取开服第二天0点时间戳
local nextDayTable = os.date("*t", openServerTime + 86400)
local nextDayZeroHourTime = os.time({year=nextDayTable.year, month=nextDayTable.month, day=nextDayTable.day, hour=0,min=0,sec=0})
因此可以对os.date()、os.time()做一层封装,传入/返回的时间table都以服务器时区为标准。本地时区就完全不会影响时间计算逻辑了。
-- 替代os.date函数,忽略本地时区设置,按服务器时区格式化时间
-- @param format: 同os.date第一个参数
-- @param timestamp:服务器时间戳
function TimeUtils.Date(format, timestamp)
local timeZoneDiff = ServerTimeZone - TimeUtils.GetLocalTimeZone()
return os.date(format, timestamp + timeZoneDiff)
end
-- 替代os.time函数,忽略本地时区设置,返回服务器时区时间戳
-- @param timedata: 服务器时区timedate
function TimeUtils.Time( timedate )
local timeZoneDiff = ServerTimeZone - TimeUtils.GetLocalTimeZone()
return os.time(timedate) - timeZoneDiff
end
-- 获取开服第二天0点时间戳
local nextDayTable = TimeUtils.Date("*t", openServerTime + 86400)
local nextDayZeroHourTime = TimeUtils.Time({year=nextDayTable.year, month=nextDayTable.month, day=nextDayTable.day, hour=0,min=0,sec=0})
通过TimeUtils.Date()、TimeUtils.Time()替代os.date()、os.time(),业务逻辑处理时间计算时,只需考虑服务器时区即可,即使日后游戏进行国际化,只需根据地区修改ServerTimeZone即可,对业务层没有影响。
夏令时
如果我们生活在一个简单美好的世界,时区问题就此解决了,然后勤劳智慧的人民们,为了节能(sheng)减排(qian),又发明了夏令时,以上代码在实行夏令时的国家地区里,计算结果可能不对。
夏令时,又称“日光节约时制”,英文全称Daylight Saving Time,简称DST。大白话来说就是从前有人觉得大家伙晚睡晚起,导致晚上照明用电太久浪费钱,夏天天亮得早,就提倡大家伙夏天时一起把时钟调快1个小时,你不是习惯晚上12点才睡觉吗?那都把表调快1小时,变相地让你提前1小时睡觉,从而实现节省减排。夏令时制度是以国家为单位来执行的,每个国家一年里夏令时生效的时段还不一样,目前全世界有近110个国家每年要实行夏令时。以英国伦敦为例,英国伦敦位于零时区,与中国东八区相差8个时区:在不实行夏令时的日子里,与中国确实是相差8小时;实行夏令时后,与中国只相差7小时了。
扯了很多夏令时的概念,回到时区处理问题,在计算时区差时,就需要判断玩家本地设置时区是否正在实行夏令时,如果是则在原计算结果上再加3600秒。os.date()返回的时间table里带有isdst字段,isdst=true表示正在使用夏令时。因此前面代码优化如下:
-- 获取客户端本地时区
function TimeUtils.GetLocalTimeZone()
local now = os.time()
local localTimeZone = os.difftime(now, os.time(os.date("!*t", now)))
local isdst = os.date("*t", now).isdst
if isdst then localTimeZone = localTimeZone + 3600 end
return localTimeZone
end
针对国内上线游戏做以上的时区处理,基本就没问题了。真正做不同区服国际化时,服务器与本地时区的夏令时因素都要考虑进来做处理,等以后有机会踩坑了再记录吧。
最后一句题外话,感谢国家统一了时区,感觉国家废除了夏令时。