一些单机游戏想要做一些伪联网的功能,例如每日登录奖励等,由于直接使用本地时间DateTime.UtcNow,就要用到请求网络时间,以下是请求网络时间的几种方法。
1.从一些主流网站发送http请求获取
原理是通过对主流网站发送http请求,从返回的头获取Date,代码如下
UnityWebRequest request = UnityWebRequest.Get("https://www.microsoft.com");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
try
{
string todayDate = request.GetResponseHeader("date");
if (IsDebug)
Debug.Log($"GetNetworkTime todayDate {todayDate}");
}
}
2.类似的,如果有cdn,可以向cdn发送http请求获取
原理和上面的类似,但是要注意请求cdnUrl时,要记得后面带?t={timestamp}防止cdn缓存获取到了上次请求的时间。
UnityWebRequest request = UnityWebRequest.Head(cdnUrl);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
try
{
string todayDate = request.GetResponseHeader("date");
if (IsDebug)
Debug.Log($"GetNetworkTime todayDate {todayDate}");
}
}
3.最终版本
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using UnityEngine.Networking;
public static class NTPManager
{
private const long Epoch = 621355968000000000L;
public static readonly DateTime UtcStartTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static long FrameStartUpTime;
/// <summary>
/// 当前时间 毫秒
/// </summary>
public static long TimeNow => GetNow();
public static DateTime NowDate => GetDateTime();
private static long CorrectTime;
private static System.Diagnostics.Stopwatch Watch;
private static TimeZoneInfo mTimeZoneInfo;
private static int TimeHour = 8;
public static TimeZoneInfo TimeZoneInfo
{
get
{
if (mTimeZoneInfo == null)
{
mTimeZoneInfo = SetGameTimeZone(TimeHour);
}
return mTimeZoneInfo;
}
}
private const string GAME_TIME_ZONE = "Custom Game Standard Time";
public static string CDN_TIME_URL;
public static string[] primaryUrls = new[]
{
"https://www.microsoft.com",
"https://www.google.com",
"https://www.baidu.com",
};
/// <summary>
/// 设置游戏指定时区
/// </summary>
/// <param name="timeZoneSpan"></param>
public static TimeZoneInfo SetGameTimeZone(int hour)
{
TimeSpan timeSpan = new TimeSpan(hour, 0, 0);
string sign = timeSpan.TotalSeconds < 0 ? "-" : "+";
string offsetStr = $"{sign}{timeSpan.Hours:D2}:{timeSpan.Minutes:D2}";
var GameTimeZone = TimeZoneInfo.CreateCustomTimeZone(offsetStr, timeSpan, GAME_TIME_ZONE, GAME_TIME_ZONE);
return GameTimeZone;
}
public static long NextDayTime { get; private set; }
public static void SetServerNow(DateTime dateTime)
{
Watch = System.Diagnostics.Stopwatch.StartNew();
CorrectTime = (dateTime.Ticks - Epoch) / 10000;
if (TimeNow > NextDayTime && NextDayTime != 0)
{
//GameData.GetInstance().CrossDay();
CrossDay?.Invoke();
NextDayTime = GetMillisecondsUntilNextDay();
//XGameEventManager.Instance.Notify(XEventId.NOTIFY_DAY_CHANGE_MSG);
if (IsDebug)
Debug.Log($"变天 [{ConvertFromTimestamp(TimeNow)}] NextDayTime {ConvertFromTimestamp(NextDayTime)} ");
}
else
{
NextDayTime = GetMillisecondsUntilNextDay();
}
if (IsDebug)
{
int offset = ((NextDayTime - TimeNow) / 1000).TimeLong2Int();
Debug.Log($"CurTime [{ConvertFromTimestamp(TimeNow)}] GetDateTime() [{GetDateTime()}] NextDayTime {ConvertFromTimestamp(NextDayTime)} 距离offset [{TimeUtil.FormatSecondDHM4(offset)}]");
}
}
public static DateTime ConvertFromTimestamp(long timestamp)
{
// 将传入的时间戳转换为DateTime对象
DateTime utcDateTime = UtcStartTime.AddMilliseconds(timestamp);
// 将UTC时间转换为指定时区的时间
DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo);
return localDateTime;
}
/// <summary>
/// 打印指定毫秒数的日期时间为 "yyyy-MM-dd HH:mm:ss:fff" 格式。
/// </summary>
/// <param name="nowTimems">当前时间的毫秒数 (从1970年1月1日开始计算)。</param>
/// <returns>格式化的日期时间字符串。</returns>
public static string PrintLocalTime(this long nowTimems)
{
DateTime dateTime = UtcStartTime.AddMilliseconds(nowTimems); // Convert milliseconds to ticks for DateTime
string formattedDateTime = dateTime.ToString("yyyy-MM-dd HH:mm:ss:fff");
return formattedDateTime;
}
/// <summary>
/// 返回当前时区第二天零点的毫秒值。
/// </summary>
/// <returns>当前时区第二天零点的毫秒值。</returns>
public static long GetMillisecondsUntilNextDay()
{
DateTime currentDate = GetDateTime();
DateTime nextDay = currentDate.AddDays(1).Date; // Get the next day's midnight
long millisecondsUntilNextDay = nextDay.ToTimestamp() * 1000;
return millisecondsUntilNextDay;
}
/// <summary>
/// 转时间戳
/// </summary>
/// <param name="dt"></param>
/// <returns></returns>
public static long ToTimestamp(this DateTime dt)
{
if (dt.Kind == DateTimeKind.Utc)
{
return (long)(dt - UtcStartTime).TotalSeconds;
}
else if (dt.Kind == DateTimeKind.Local)
{
return (long)(dt.ToUniversalTime() - UtcStartTime).TotalSeconds;
}
else
{
return (long)(TimeZoneInfo.ConvertTimeToUtc(dt, TimeZoneInfo) - UtcStartTime).TotalSeconds;
}
}
public static float GetFrameStartUpTime()
{
return (float)((double)FrameStartUpTime / TimeSpan.TicksPerSecond);
}
public static Action CrossDay;
public static void FixedUpdate()
{
if (Watch == null)
{
return;
}
FrameStartUpTime = Watch.ElapsedTicks;
if (TimeNow > NextDayTime && NextDayTime != 0)
{
//GameData.GetInstance().CrossDay();
CrossDay?.Invoke();
NextDayTime = GetMillisecondsUntilNextDay();
if (IsDebug)
Debug.Log($"变天 [{ConvertFromTimestamp(TimeNow)}] NextDayTime {ConvertFromTimestamp(NextDayTime)} ");
}
}
public static bool IsSameDate(long time)
{
// 将传入的时间戳转换为DateTime对象
DateTime utcDateTime = UtcStartTime.AddMilliseconds(time);
// 将UTC时间转换为指定时区的时间
DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo);
DateTime now = GetDateTime();
return localDateTime.Date.Equals(now.Date);
}
public static DateTime GetDateTimeWithMs(long time)
{
// 将传入的时间戳转换为DateTime对象
DateTime utcDateTime = UtcStartTime.AddMilliseconds(time);
// 将UTC时间转换为指定时区的时间
DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo);
return localDateTime;
}
public static long GetTimestampFromChinaTime(DateTime chinaTime)
{
// 明确指定这个时间是东八区时间
DateTimeOffset chinaTimeOffset = new DateTimeOffset(chinaTime, TimeSpan.FromHours(TimeHour));
return chinaTimeOffset.ToUnixTimeSeconds();
}
public static DateTime GetDateTime()
{
// 将传入的时间戳转换为DateTime对象
DateTime utcDateTime = UtcStartTime.AddMilliseconds(GetNow());
// 将UTC时间转换为指定时区的时间
DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo);
return localDateTime;
}
public static long GetNow()
{
return (CorrectTime + FrameStartUpTime / 10000);
}
public static IEnumerator GetNetworkTimeCoroutine(Action<bool, DateTime> action)
{
if (IsDebug)
Debug.Log($"GetNetworkTimeCoroutine {UnityEngine.Application.internetReachability}");
if (UnityEngine.Application.internetReachability == NetworkReachability.NotReachable)
{
action.Invoke(false, DateTime.Now);
yield break;
}
yield return GetNetworkTime(action);
}
public static IEnumerator GetNetworkTime(Action<bool, DateTime> action)
{
string cdnUrl = CDN_TIME_URL; // 你的 CDN 时间服务器
bool success = false;
DateTime resultTime = DateTime.UtcNow;
// 1. 请求 CDN(带重试)
yield return GetNetworkTimeWithRetry(cdnUrl, 2, 0.2f, (cdnSuccess, cdnTime) =>
{
if (cdnSuccess)
{
success = true;
resultTime = cdnTime;
}
});
if (!success)
{
// 2. 并行请求主流服务器(微软、Google、Apple)
yield return GetNetworkTimeFast(primaryUrls, (fastSuccess, fastTime) =>
{
if (fastSuccess)
{
success = true;
resultTime = fastTime;
}
});
}
// 3. 返回最终结果
action?.Invoke(success, success ? resultTime : DateTime.UtcNow);
}
// 并行请求多个服务器(只要有一个成功就返回)
private static IEnumerator GetNetworkTimeFast(string[] urls, Action<bool, DateTime> callback)
{
bool success = false;
DateTime resultTime = DateTime.UtcNow;
int completedRequests = 0;
List<UnityWebRequest> requests = new List<UnityWebRequest>();
// 启动所有请求
foreach (string url in urls)
{
UnityWebRequest request = UnityWebRequest.Head(url);
requests.Add(request);
request.SendWebRequest();
}
// 等待所有请求完成(或有一个成功)
while (!success && completedRequests < requests.Count)
{
for (int i = 0; i < requests.Count; i++)
{
UnityWebRequest request = requests[i];
if (request == null) continue;
if (request.isDone)
{
completedRequests++;
if (request.result == UnityWebRequest.Result.Success)
{
string dateHeader = request.GetResponseHeader("Date");
if (!string.IsNullOrEmpty(dateHeader))
{
try
{
resultTime = DateTime.ParseExact(
dateHeader,
"ddd, dd MMM yyyy HH:mm:ss 'GMT'",
CultureInfo.InvariantCulture.DateTimeFormat,
DateTimeStyles.AssumeUniversal).ToUniversalTime();
success = true;
break;
}
catch (Exception e)
{
Debug.LogError($"Parse Date header error (url: {urls[i]}): {e}");
}
}
}
else
{
Debug.LogError($"Request failed (url: {urls[i]}): {request.error}");
}
request.Dispose();
requests[i] = null;
}
}
yield return null;
}
// 取消未完成的请求
foreach (var request in requests)
{
if (request != null && !request.isDone)
{
request.Abort();
request.Dispose();
}
}
callback?.Invoke(success, resultTime);
}
// CDN 请求(带重试)
private static IEnumerator GetNetworkTimeWithRetry(string url, int maxAttempts, float retryDelay, Action<bool, DateTime> callback)
{
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
using (UnityWebRequest request = UnityWebRequest.Head(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string dateHeader = request.GetResponseHeader("Date");
if (!string.IsNullOrEmpty(dateHeader))
{
try
{
DateTime cdnTime = DateTime.ParseExact(
dateHeader,
"ddd, dd MMM yyyy HH:mm:ss 'GMT'",
CultureInfo.InvariantCulture.DateTimeFormat,
DateTimeStyles.AssumeUniversal);
callback?.Invoke(true, cdnTime.ToUniversalTime());
yield break;
}
catch (Exception e)
{
Debug.LogError($"Parse CDN Date header error (attempt {attempt}): {e}");
}
}
}
else
{
Debug.LogError($"Get CDN Time attempt {attempt} failed: {request.error}");
}
}
if (attempt < maxAttempts)
yield return new WaitForSeconds(retryDelay);
}
callback?.Invoke(false, DateTime.UtcNow);
}
}
其中primaryUrls 为自定义主流网站,SetGameTimeZone 可以设置时区,通过调用GetNetworkTimeCoroutine 时,返回是否成功获取到时间和时间,成功时调用设置时间SetServerNow方法,然后每固定帧调用FixedUpdate,然后TimeNow就可以返回真实的网络时间(毫秒);并且注册委托CrossDay可以实现跨天逻辑。