由於時區、夏令時的存在,游戲內的時間顯示/計算都要考慮時區問題並進行相應處理。時間計算不用說,要排除玩家本地時區影響,只以服務器時區為准進行計算。時間顯示有兩種方案:
- 根據服務器下發的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
針對國內上線游戲做以上的時區處理,基本就沒問題了。真正做不同區服國際化時,服務器與本地時區的夏令時因素都要考慮進來做處理,等以后有機會踩坑了再記錄吧。
最后一句題外話,感謝國家統一了時區,感覺國家廢除了夏令時。