遊戲時區問題小解

因爲時區、夏令時的存在,遊戲內的時間顯示/計算都要考慮時區問題並進行相應處理。時間計算不用說,要排除玩家本地時區影響,只以服務器時區爲準進行計算。時間顯示有兩種方案:api

  1. 根據服務器下發的utc時間戳,按玩家手機本地設置的時區進行適配顯示,這樣對於常常往返於不一樣時區的玩家很友好(雖然這類玩家不多),玩家只要修改手機時區,遊戲內的時間顯示就以該時區爲準了。然而這種方案一般會碰到問題,好比遊戲內活動圖片裏寫死了日期,時間,顯然就沒法根據玩家手機時區適配顯示。
  2. 根據服務器時區進行統一顯示是更好的方案,若是是國內上線遊戲,能夠統一顯示東八區時間,這樣就能夠保證圖片裏的時間信息是正確的。這種方案也有個附帶好處,當玩家不自知地將時區設爲其餘時區,時間卻設成東八區時間時(咱們項目內有個策劃的手機就是這樣設置的-_-||),遊戲內的時間顯示"看起來"仍是正確的。
    簡單總結,遊戲內的時間顯示/計算最好都以服務器時區爲準,而各類語言關於時間函數的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都以服務器時區爲標準。本地時區就徹底不會影響時間計算邏輯了。lua

-- 替代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便可,對業務層沒有影響。code


夏令時

若是咱們生活在一個簡單美好的世界,時區問題就此解決了,而後勤勞智慧的人民們,爲了節能(sheng)減排(qian),又發明了夏令時,以上代碼在實行夏令時的國家地區裏,計算結果可能不對。orm

夏令時,又稱「日光節約時制」,英文全稱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

針對國內上線遊戲作以上的時區處理,基本就沒問題了。真正作不一樣區服國際化時,服務器與本地時區的夏令時因素都要考慮進來作處理,等之後有機會踩坑了再記錄吧。

最後一句題外話,感謝國家統一了時區,感受國家廢除了夏令時。

相關文章
相關標籤/搜索