在全球化互联网时代,许多游戏厂商都在大力开拓海外市场,大量的游戏也都会选择在海外发行。做为游戏开发者的咱们也不得不处理一个容易被忽略的问题,全球不一样时区下的时间问题git
GMT(格林威治平均时间,Greenwich Mean Time)是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,它规定太阳天天通过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。因为地球天天的自转是有些不规则的,并且正在缓慢减速,所以格林尼治平时基于天文观测自己的缺陷,已经被原子钟报时的协调世界时(UTC)所取代。github
UTC(协调世界时,取自英文和法文的缩写,英文是Coordinated Universal Time)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽可能接近于格林威治标准时间正则表达式
本地时间是指在平常生活中所使用的时间。这个时间等于咱们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC或GMT)之间的偏移量来定义。c#
GMT+08:00(UTC+8)即北京时间,比协调世界时快八小时。注意北京时间并非北京的地方时间。api
unix时间戳是从UTC1970年1月1日0时0分0秒(UTC/GMT的午夜)起至如今的总秒数,不考虑闰秒。所以时间戳不会由于时区的不一样而不一样服务器
夏令时(Daylight Saving Time:DST),又称日光节约时间,是为了节约能源,人为规定的时间。通常在天亮早的夏季人为将时间调快一小时,可使人早起早睡,减小照明量,以充分利用光照资源,从而节约照明用电。函数
提早说明,本文后面会用一个名词“时间表示”来指代包含年月日时分秒信息的时间对象,好比如下的类型就能够被称之为时间表示lua
时间表示很重要的一个特色是它是受时区影响的,但自己又没有携带时区信息。对于同一时刻,不一样时区的时间表示是不一样的。而时间戳偏偏相反,它不受时区影响,或者说它只针对于UTC时间。对于同一时刻,不一样时区的时间戳都是惟一的。.net
若是采用Unity作游戏开发,则可能会用到C#语言和Lua语言,因此接下来就分别介绍这两种语言如何处理不一样时区下的时间问题。unix
lua对时间的处理主要是两个函数os.time和os.date
请看下面的一段示例代码
local t1 = os.time() print(t1) -- 输出 1626521822 local t2 = os.time({year = 2021, month = 7, day = 17, hour = 19, min = 37, sec = 0}) print(t2) -- 输出 1626521820 local t3 = os.time({year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = 0}) print(t3) -- 输出 nil local t4 = os.time({year = 1970, month = 1, day = 1, hour = 8, min = 0, sec = 0}) print(t4) -- 输出 0
第一个输出表示的是执行该代码时的时间戳,当时我是在北京时间"2021-07-17 19:37:02"时刻执行的,因此它与第二个输出,表示的是北京时间"2021-07-17 19:37:00"时刻的时间戳,相差2秒是正确的
问题在于第三个输出为何是nil,而第四个输出是0?
注意时间戳表示的是从UTC1970年1月1日0时0分0秒到当前时刻所通过的秒数,而os.time在将时间表示转换为时间戳时,认为这个时间表示是本地时区的时间。而个人时间是北京时间,将北京时间1970年1月1日0时0分0秒转换为UTC时间,其实是1969年12月31日16时0分0秒,超出了时间戳的定义范围,因此返回的是nil。
对于第四个输出,北京时间的1970年1月1日8时0分0秒,对应的正好是UTC时间1970年1月1日0时0分0秒,因此输出是0
请看下面的一段示例代码
local d1 = os.date("%Y-%m-%d %H:%M:%S", 1626521822) print(d1) -- 输出 2021-07-17 19:37:02 local d2 = os.date("!%Y-%m-%d %H:%M:%S", 1626521822) print(d2) -- 输出 2021-07-17 11:37:02
对于第一个输出,format字符串没有以 '!' 打头,因此它是以本地时间格式化的,即北京时间。因此返回"2021-07-17 19:37:02",若是执行代码的开发者是在东九区(比北京时间快一个小时),则会返回"2021-07-17 20:37:02"。所以该代码在不一样的时区执行,输出的结果是不一样的
对于第二个输出,format字符串以 '!' 打头,因此它以协调世界时格式化,不管在哪一个时区,执行该代码都返回的是相同值
因为本文主要是探讨不一样时区下的时间问题,因此这里就只列出了C#部分与时区转换相关的类和函数
表示时间上的一刻,一般以日期和当天的时间表示
请看下面的一段示例代码
DateTime dateTime = new DateTime(2021, 7, 17, 19, 37, 2, DateTimeKind.Unspecified); DateTime d1 = dateTime.ToLocalTime(); DateTime d2 = dateTime.ToUniversalTime().ToLocalTime(); Console.WriteLine(d1); // 输出 2021/7/18 3:37:02 Console.WriteLine(d2); // 输出 2021/7/17 19:37:02
能够看到第一个输出与第二个输出是不一样的,这是由于当一个DateTime对象的Kind属性是DateTimeKind.Unspecified时,调用ToLocalTime()方法,会默认DateTime对象是基于UTC的。调用ToUniversalTime(),会默认DateTime对象是基于本地时间的。进行时区转换时,尽可能使用TimeZoneInfo来避免这样的默认设定
因为C#自己已经定义了时区的概念,因此转换起来比较容易,直接使用ConvertTime函数
请看下面的一段示例代码
DateTime dateTime = new DateTime(2021, 7, 17, 19, 37, 2, DateTimeKind.Unspecified); TimeZoneInfo timeZoneInfo1 = TimeZoneInfo.Local; TimeZoneInfo timeZoneInfo2 = TimeZoneInfo.Utc; DateTime d1 = TimeZoneInfo.ConvertTime(dateTime, timeZoneInfo1, timeZoneInfo2); DateTime d2 = TimeZoneInfo.ConvertTime(dateTime, timeZoneInfo2, timeZoneInfo1); Console.WriteLine(d1); // 输出 2021/7/17 11:37:02 Console.WriteLine(d2); // 输出 2021/7/18 3:37:02
第一个输出是将本地时间(北京时间)的"2021/7/17 19:37:02"转换为UTC时间的结果,第二个输出是将UTC时间的"2021/7/17 19:37:02"转换为本地时间(北京时间)的结果
而Lua自己没有时区的定义,因此这里采用与UTC时间的时间差来做为时区的表示。好比UTC时区表示就是0(相差0),北京时间的时区表示就是8 * 60 * 60(相差8个小时)
具体示例,请看下面的一段代码
local timeZone1 = 0 local timeZone2 = 8 * 60 * 60 local timeZone3 = 9 * 60 * 60 local dateTime = {year = 2021, month = 7, day = 17, hour = 19, min = 37, sec = 2} -- 获取本地时区 local function getLocalTimeZone() local now = os.time() local offset = os.date("*t").isdst and 60 * 60 or 0 -- 经过isdst判断是不是夏令时 return os.difftime(now + offset, os.time(os.date("!*t", now))) end local function convertTime( dateTime, sourceTimeZone, destinationTimeZone ) local time = os.time(dateTime) + (destinationTimeZone - sourceTimeZone) return os.date("*t", time) end print(getLocalTimeZone()) -- 输出 28800 local d1 = convertTime(dateTime, timeZone2, timeZone3) local d2 = convertTime(dateTime, timeZone3, timeZone2) dump(d1) --[[ 输出 - "<var>" = { - "day" = 17 - "hour" = 20 - "isdst" = false - "min" = 37 - "month" = 7 - "sec" = 2 - "wday" = 7 - "yday" = 198 - "year" = 2021 - } ]] dump(d2) --[[ 输出 - "<var>" = { - "day" = 17 - "hour" = 18 - "isdst" = false - "min" = 37 - "month" = 7 - "sec" = 2 - "wday" = 7 - "yday" = 198 - "year" = 2021 - } ]]
第一个输出表示的是(在上面的Lua时区定义下的)本地时区,28800(8个小时)
第二个输出是将北京时间的"2021/7/17 19:37:02"转换为东九区时间(比UTC快9个小时,比北京时间快1个小时)的结果,第三个输出是将东九区时间的"2021/7/17 19:37:02"转换为北京时间的结果。代码中的dump是可用于格式化打印Lua表结构的函数,感兴趣的同窗能够查看这里
这种状况在游戏开发中会常常遇到,接收服务端下发的一个时间戳,而后客户端将时间戳转换到用户手机设置的时区下的时间表示
对于同一时刻,不管服务器处于哪里,它下发的时间戳都应该是一致的,但不一样时区下的客户端显示又都是不一样的
在C#中能够利用下面的函数(完整的类能够查看这里)将时间戳转换为UTC时间。注意是UTC时间,而后再利用上面提到的时区转换,将UTC时间转换为任意时区的时间。
public const int TickToSecond = 10000000; public static readonly DateTime TIME1970 = new DateTime(1970, 1, 1); public static DateTime TickToDateTime(long t) { return new DateTime(TIME1970.Ticks + (long)((double)t * TickToSecond), DateTimeKind.Utc); }
在Lua中能够直接使用os.date函数将时间戳转换为UTC时间表示(format 以 '!' 打头)或本地时间表示(format 不以 '!' 打头),而后再经过上面提到的Lua时区转换转换到指定时区
以下面的示例代码,是将时间戳转换为本地时间表示
local d = os.date("*t", 1626521822) dump(d) --[[ 输出 - "<var>" = { - "day" = 17 - "hour" = 19 - "isdst" = false - "min" = 37 - "month" = 7 - "sec" = 2 - "wday" = 7 - "yday" = 198 - "year" = 2021 - } ]]
将时间表示转换为时间戳在游戏开发中,常见于读取游戏的时间配置。好比为了方便策划或运营配置某个活动的起始时间,可使用相似"2021-07-17 19:37:02"这样的时间字符串进行配置。开发再经过将其转换为时间戳进行其它操做
在C#中可使用TryParse函数将一个时间字符串转换为DateTime对象,而后再经过下面的DateTimeToTick函数(完整的类能够查看这里)将其转换为时间戳。注意DateTimeToTick函数要求传入的DateTime对象是UTC时间,而经过TryParse函数获得的DateTime对象是本地时间的,因此还须要经过上面提到的时间转换将其转换为UTC时间才能获得正确的结果
public static long DateTimeToTick(DateTime date) { return (long)((double)(date.Ticks - TIME1970.Ticks) / TickToSecond); } string str = "2021/7/17 19:37:02"; DateTime d1; DateTime.TryParse(str, out d1); DateTime d2 = TimeZoneInfo.ConvertTime(d1, TimeZoneInfo.Local, TimeZoneInfo.Utc); long t1 = DateTimeToTick(d1); long t2 = DateTimeToTick(d2); Console.WriteLine(t1); // 输出 1626550622 Console.WriteLine(t2); // 输出 1626521822
第一个输出因为传入DateTimeToTick函数的DateTime对象是本地时间的,因此获得正确结果是错误的。第二个输出是正确的,打印出了北京时间"2021/7/17 19:37:02"对应的时间戳
对于Lua而言,将时间字符串转换为时间戳须要多个步骤,先经过正则表达式将时间字符串转换为Lua的时间表,而后再经过os.time函数将时间表转换为时间戳
local timeStr = "2021-07-17 19:37:02" local _, _, year, month, day, hour, min, sec = string.find(timeStr, "(%d+)%-(%d+)%-(%d+)%s*(%d+):(%d+):(%d+)"); local dateTime = { year = tonumber(year), month = tonumber(month), day = tonumber(day), hour = tonumber(hour), min = tonumber(min), sec = tonumber(sec) } dump(dateTime) --[[ 输出 - "<var>" = { - "day" = 17 - "hour" = 19 - "min" = 37 - "month" = 7 - "sec" = 2 - "year" = 2021 - } ]] local t = os.time(dateTime) dump(t) -- 输出 1626521822
注意,在上面的示例中,默认时间字符串都是本地时间下的字符串,某些状况下为了统一,可能策划或运营会基于某个时区配置时间字符串。好比统一使用UTC时间进行配置,在这种状况下,须要注意先进行对应的时区转换,再转化为时间戳