由于時(shí)區(qū)拗踢、夏令時(shí)的存在脚牍,游戲內(nèi)的時(shí)間顯示/計(jì)算都要考慮時(shí)區(qū)問(wèn)題并進(jìn)行相應(yīng)處理。時(shí)間計(jì)算不用說(shuō)巢墅,要排除玩家本地時(shí)區(qū)影響诸狭,只以服務(wù)器時(shí)區(qū)為準(zhǔn)進(jìn)行計(jì)算。時(shí)間顯示有兩種方案:
- 根據(jù)服務(wù)器下發(fā)的utc時(shí)間戳君纫,按玩家手機(jī)本地設(shè)置的時(shí)區(qū)進(jìn)行適配顯示驯遇,這樣對(duì)于經(jīng)常往返于不同時(shí)區(qū)的玩家很友好(雖然這類(lèi)玩家很少),玩家只要修改手機(jī)時(shí)區(qū)蓄髓,游戲內(nèi)的時(shí)間顯示就以該時(shí)區(qū)為準(zhǔn)了叉庐。然而這種方案通常會(huì)碰到問(wèn)題,比如游戲內(nèi)活動(dòng)圖片里寫(xiě)死了日期会喝,時(shí)間陡叠,顯然就無(wú)法根據(jù)玩家手機(jī)時(shí)區(qū)適配顯示。
- 根據(jù)服務(wù)器時(shí)區(qū)進(jìn)行統(tǒng)一顯示是更好的方案肢执,如果是國(guó)內(nèi)上線游戲枉阵,可以統(tǒng)一顯示東八區(qū)時(shí)間,這樣就可以保證圖片里的時(shí)間信息是正確的预茄。這種方案也有個(gè)附帶好處岭妖,當(dāng)玩家不自知地將時(shí)區(qū)設(shè)為其他時(shí)區(qū),時(shí)間卻設(shè)成東八區(qū)時(shí)間時(shí)(我們項(xiàng)目?jī)?nèi)有個(gè)策劃的手機(jī)就是這樣設(shè)置的-_-||),游戲內(nèi)的時(shí)間顯示"看起來(lái)"還是正確的昵慌。
簡(jiǎn)單總結(jié)假夺,游戲內(nèi)的時(shí)間顯示/計(jì)算最好都以服務(wù)器時(shí)區(qū)為準(zhǔn),而各種語(yǔ)言關(guān)于時(shí)間函數(shù)的api斋攀,都是以本地時(shí)區(qū)計(jì)算返回結(jié)果的已卷,以Lua為例,Lua標(biāo)準(zhǔn)庫(kù)中提供的時(shí)間函數(shù) os.time()和os.date()淳蔼,這兩個(gè)函數(shù)傳入和返回的時(shí)間table就是以本地時(shí)區(qū)為準(zhǔn)的侧蘸。
os.time()
- 原型:os.time ([table])
- 解釋:按table的內(nèi)容返回一個(gè)時(shí)間值(數(shù)字),若不帶參數(shù)則么使用當(dāng)前時(shí)間作為table內(nèi)容鹉梨,其中table中可以包含的字段有:year, month, day, hour, min, sec, isdst讳癌,其他字段將會(huì)被忽略。
os.date()
原型:os.date ([format [, time]])
解釋:返回一個(gè)按format格式化日期存皂、時(shí)間的字串或表晌坤。
參數(shù)格式:
- 由原型可以看出可以省略第二個(gè)參數(shù)也可以省略兩個(gè)參數(shù),只省略第二個(gè)參數(shù)函數(shù)會(huì)使用當(dāng)前時(shí)間作為第二個(gè)參數(shù)旦袋,如果兩個(gè)參數(shù)都省略則按當(dāng)前系統(tǒng)的設(shè)置返回格式化的字符串骤菠,做以下等價(jià)替換 os.date() <=> os.date("%c")。
- 如果format以“疤孕!”開(kāi)頭商乎,則按格林尼治時(shí)間進(jìn)行格式化。
- 如果format是一個(gè)“*t”祭阀,將返一個(gè)帶year(4位)鹉戚,month(1-12), day (1--31)专控, hour (0-23)抹凳, min (0-59),sec (0-61)踩官,wday (星期幾, 星期天為1)境输, yday (年內(nèi)天數(shù))和isdst (是否為日光節(jié)約時(shí)間true/false)的帶鍵名的表;
- 如果format不是“*t”蔗牡,os.date會(huì)將日期格式化為一個(gè)字符串
服務(wù)器時(shí)區(qū)
要以服務(wù)器時(shí)區(qū)進(jìn)行時(shí)間計(jì)算,編碼思路就是要計(jì)算出本地與服務(wù)器的時(shí)區(qū)差嗅剖,調(diào)用os.time()辩越、os.date()時(shí)進(jìn)行補(bǔ)償。
-- 服務(wù)器時(shí)區(qū)為東八區(qū)
local ServerTimeZone = 3600 * 8
-- 獲取客戶端本地時(shí)區(qū)
function TimeUtils.GetLocalTimeZone()
local now = os.time()
local localTimeZone = os.difftime(now, os.time(os.date("!*t", now)))
return localTimeZone
end
服務(wù)器時(shí)區(qū):對(duì)于國(guó)內(nèi)服務(wù)器信粮,服務(wù)器時(shí)區(qū)可以直接硬編碼成東八區(qū)黔攒,如果考慮做國(guó)際化,可以由服務(wù)器進(jìn)行下發(fā)該值,根據(jù)地區(qū)設(shè)置不同服務(wù)器時(shí)區(qū)值督惰。
本地時(shí)區(qū):在lua里沒(méi)有直接獲取本地時(shí)區(qū)的api不傅,但通過(guò)os.date("!*t", os.time()),可以獲取格林尼治的時(shí)間table赏胚,再以本地時(shí)區(qū)解析table獲取時(shí)間戳访娶,該時(shí)間戳與os.time()時(shí)間戳相減即為時(shí)區(qū)秒數(shù)差值。
假設(shè)現(xiàn)在游戲內(nèi)有個(gè)功能入口要在游戲開(kāi)服第二天0點(diǎn)開(kāi)啟觉阅,如果不考慮時(shí)區(qū)問(wèn)題崖疤,編碼如下,當(dāng)玩家修改本地時(shí)區(qū)時(shí)典勇,計(jì)算得出的時(shí)間戳是不同的劫哼。這樣玩家就可以通過(guò)修改本地時(shí)區(qū),讓功能提前開(kāi)啟割笙。
-- 獲取開(kāi)服第二天0點(diǎn)時(shí)間戳
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})
因此可以對(duì)os.date()权烧、os.time()做一層封裝,傳入/返回的時(shí)間table都以服務(wù)器時(shí)區(qū)為標(biāo)準(zhǔn)咳蔚。本地時(shí)區(qū)就完全不會(huì)影響時(shí)間計(jì)算邏輯了豪嚎。
-- 替代os.date函數(shù),忽略本地時(shí)區(qū)設(shè)置谈火,按服務(wù)器時(shí)區(qū)格式化時(shí)間
-- @param format: 同os.date第一個(gè)參數(shù)
-- @param timestamp:服務(wù)器時(shí)間戳
function TimeUtils.Date(format, timestamp)
local timeZoneDiff = ServerTimeZone - TimeUtils.GetLocalTimeZone()
return os.date(format, timestamp + timeZoneDiff)
end
-- 替代os.time函數(shù)侈询,忽略本地時(shí)區(qū)設(shè)置,返回服務(wù)器時(shí)區(qū)時(shí)間戳
-- @param timedata: 服務(wù)器時(shí)區(qū)timedate
function TimeUtils.Time( timedate )
local timeZoneDiff = ServerTimeZone - TimeUtils.GetLocalTimeZone()
return os.time(timedate) - timeZoneDiff
end
-- 獲取開(kāi)服第二天0點(diǎn)時(shí)間戳
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})
通過(guò)TimeUtils.Date()糯耍、TimeUtils.Time()替代os.date()扔字、os.time(),業(yè)務(wù)邏輯處理時(shí)間計(jì)算時(shí)温技,只需考慮服務(wù)器時(shí)區(qū)即可革为,即使日后游戲進(jìn)行國(guó)際化,只需根據(jù)地區(qū)修改ServerTimeZone即可舵鳞,對(duì)業(yè)務(wù)層沒(méi)有影響震檩。
夏令時(shí)
如果我們生活在一個(gè)簡(jiǎn)單美好的世界,時(shí)區(qū)問(wèn)題就此解決了蜓堕,然后勤勞智慧的人民們抛虏,為了節(jié)能(sheng)減排(qian),又發(fā)明了夏令時(shí)套才,以上代碼在實(shí)行夏令時(shí)的國(guó)家地區(qū)里迂猴,計(jì)算結(jié)果可能不對(duì)。
夏令時(shí)背伴,又稱“日光節(jié)約時(shí)制”沸毁,英文全稱Daylight Saving Time峰髓,簡(jiǎn)稱DST。大白話來(lái)說(shuō)就是從前有人覺(jué)得大家伙晚睡晚起息尺,導(dǎo)致晚上照明用電太久浪費(fèi)錢(qián)携兵,夏天天亮得早,就提倡大家伙夏天時(shí)一起把時(shí)鐘調(diào)快1個(gè)小時(shí)掷倔,你不是習(xí)慣晚上12點(diǎn)才睡覺(jué)嗎眉孩?那都把表調(diào)快1小時(shí),變相地讓你提前1小時(shí)睡覺(jué)勒葱,從而實(shí)現(xiàn)節(jié)省減排浪汪。夏令時(shí)制度是以國(guó)家為單位來(lái)執(zhí)行的,每個(gè)國(guó)家一年里夏令時(shí)生效的時(shí)段還不一樣凛虽,目前全世界有近110個(gè)國(guó)家每年要實(shí)行夏令時(shí)死遭。以英國(guó)倫敦為例,英國(guó)倫敦位于零時(shí)區(qū)凯旋,與中國(guó)東八區(qū)相差8個(gè)時(shí)區(qū):在不實(shí)行夏令時(shí)的日子里呀潭,與中國(guó)確實(shí)是相差8小時(shí);實(shí)行夏令時(shí)后至非,與中國(guó)只相差7小時(shí)了钠署。
扯了很多夏令時(shí)的概念,回到時(shí)區(qū)處理問(wèn)題荒椭,在計(jì)算時(shí)區(qū)差時(shí)谐鼎,就需要判斷玩家本地設(shè)置時(shí)區(qū)是否正在實(shí)行夏令時(shí),如果是則在原計(jì)算結(jié)果上再加3600秒趣惠。os.date()返回的時(shí)間table里帶有isdst字段狸棍,isdst=true表示正在使用夏令時(shí)。因此前面代碼優(yōu)化如下:
-- 獲取客戶端本地時(shí)區(qū)
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
針對(duì)國(guó)內(nèi)上線游戲做以上的時(shí)區(qū)處理味悄,基本就沒(méi)問(wèn)題了草戈。真正做不同區(qū)服國(guó)際化時(shí),服務(wù)器與本地時(shí)區(qū)的夏令時(shí)因素都要考慮進(jìn)來(lái)做處理侍瑟,等以后有機(jī)會(huì)踩坑了再記錄吧唐片。
最后一句題外話,感謝國(guó)家統(tǒng)一了時(shí)區(qū)涨颜,感覺(jué)國(guó)家廢除了夏令時(shí)费韭。