Unity NTPComponent应用, 实现一个无后端高效获取网络时间的组件
无后端高效获取网络时间的组件
- 废话不多说,直接上源码
- m_NowSerivceTime 一个基于你发行游戏地区的时间偏移, 比如北京时区就是 8, 巴西就是-3,美国就是-5
- using Newtonsoft.Json; 如果这里报错, 就说明项目没有 NewtonsoftJson插件, 没关系,这里你改成Unity内置的就行
- 总之就一句, 没有必要就直接使用NTPComponent.m_NowUtc, 有时区要求就用 NTPComponent.m_NowSerivceTime, 总之,优势在我!
- 原理, 这块没必要看,如果有同学有兴趣,可以继续看看
废话不多说,直接上源码
直接新建一个脚本 NTPComponent.cs
将脚本Copy到你的项目,拖入场景节点上
获取UTC时间 NTPComponent.m_NowUtc
using UnityEngine;
using System;
using System.Collections;
using UnityEngine.Networking;
using Newtonsoft.Json;
using System.Globalization;
namespace GameContent
{
/// <summary>
/// 启动游戏后,将所有地址列表遍历
/// </summary>
[DisallowMultipleComponent]
public class NTPComponent : MonoBehaviour
{
/// <summary>
/// 网络时间是否生效中
/// </summary>
public static bool m_IsValid { get; private set; } = true;
/// <summary>
/// 当前Utc时间
/// </summary>
public static DateTime m_NowUtc
{
get
{
return m_NowUtcServerDate.AddSeconds( ( int ) ( Time.unscaledTime - m_ServerTimePoint ) );
}
private set
{
m_NowUtcServerDate = value;
}
}
/// <summary>
/// 当前服务器时间
/// </summary>
public static DateTime m_NowSerivceTime
{
get
{
return m_NowUtc.AddHours( GlobalConfig.TIME_ZONE_OFFSET );
}
}
/// <summary>
/// 服务器标记时间对象
/// </summary>
private static DateTime m_NowUtcServerDate;
/// <summary>
/// 拉取服务器的标记时间尺
/// </summary>
private static float m_ServerTimePoint = 0f;
/// <summary>
/// 是否已经成功拉取到服务器时间
/// </summary>
private bool m_Inited = false;
private void Awake( )
{
m_NowUtc = DateTime.UtcNow;
m_ServerTimePoint = Time.unscaledTime;
DontDestroyOnLoad( gameObject );
}
private void Start( )
{
#if !SANDBOX_MODE
StartCoroutine( GetNetTimeFromWorldTimeApi() );
StartCoroutine( GetNetTimeFromTimeIOApi() );
StartCoroutine( GetNetTimeFromGoogleApi() );
#else
Log.Green( $"当前是沙盒环境,你可以更改时间 {DateTime.Now}" );
#endif
}
#region WebApi
IEnumerator GetApi( string api, Action<string> callback )
{
using ( var request = UnityWebRequest.Get( "https://worldtimeapi.org/api/timezone/Etc/UTC" ) )
{
yield return request.SendWebRequest();
try
{
if ( request.result == UnityWebRequest.Result.Success )
{
callback( request.downloadHandler.text );
}
//else
//{
// Debug.LogError( $"Failed to fetch server time: {request.error}" );
//}
}
catch ( Exception e )
{
//不处理
}
}
}
struct WorldTimeData
{
public string datetime;
public string timezone;
public string utc_offset;
}
struct TimeIOData
{
public int year;
public int month;
public int day;
public int hour;
public int minute;
public int seconds;
public string dateTime;
public string timeZone;
public string dayOfWeek;
public bool dstActive;
}
IEnumerator GetNetTimeFromWorldTimeApi( )
{
string result = string.Empty;
yield return GetApi( "https://worldtimeapi.org/api/timezone/Etc/UTC", _ => result = _ );
if ( !string.IsNullOrEmpty( result ) )
{
var data = JsonConvert.DeserializeObject<WorldTimeData>( result );
Debug.Log( $"World Time: {data.datetime}" );
if ( TryParseUTCString( data.datetime, out var utcNow ) && utcNow != DateTime.MinValue )
{
OnPullServerTimeOK( utcNow );
}
}
}
IEnumerator GetNetTimeFromTimeIOApi( )
{
string result = string.Empty;
yield return GetApi( "https://timeapi.io/api/Time/current/zone?timeZone=UTC", _ => result = _ );
if ( !string.IsNullOrEmpty( result ) )
{
var data = JsonConvert.DeserializeObject<TimeIOData>( result );
Debug.Log( $"Time IO API: {data.dateTime}" );
if ( TryParseUTCString( data.dateTime, out var utcNow ) && utcNow != DateTime.MinValue )
{
OnPullServerTimeOK( utcNow );
}
}
}
IEnumerator GetNetTimeFromGoogleApi( )
{
using ( var request = UnityWebRequest.Get( "https://www.google.com" ) )
{
yield return request.SendWebRequest();
try
{
if ( request.result == UnityWebRequest.Result.Success )
{
if ( request.GetResponseHeader( "Date" ) != null )
{
string serverDate = request.GetResponseHeader( "Date" );
Debug.Log( $"Google Server Time: {serverDate}" );
var utcDate = ParseServerDateToUTC( serverDate );
if ( utcDate != DateTime.MinValue )
{
OnPullServerTimeOK( utcDate );
}
}
}
}
catch ( Exception e )
{
//不处理
}
}
}
#endregion
#region Common
/// <summary>
/// Parses a UTC time string into a DateTime object.
/// </summary>
/// <param name="utcString">The UTC time string.</param>
/// <returns>A DateTime object in UTC, or DateTime.MinValue if parsing fails.</returns>
public static bool TryParseUTCString( string utcString, out DateTime utcNow )
{
try
{
// Attempt to parse the string with DateTime.Parse
DateTime parsedDate = DateTime.Parse( utcString, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal );
utcNow = parsedDate;
return true;
}
catch ( FormatException )
{
Debug.Log( "Failed to parse UTC time string." );
utcNow = DateTime.MinValue;
return false;
}
}
/// <summary>
/// Parses a server date string (from HTTP header) into a UTC DateTime object.
/// </summary>
/// <param name="serverDate">The server date string in RFC1123 format.</param>
/// <returns>A DateTime object in UTC.</returns>
public static DateTime ParseServerDateToUTC( string serverDate )
{
try
{
// Use DateTime.ParseExact to parse RFC1123 format
DateTime parsedDate = DateTime.ParseExact(
serverDate,
"r", // "r" or "R" stands for RFC1123 pattern
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal
);
return parsedDate;
}
catch ( FormatException ex )
{
Debug.Log( $"Error parsing server date: {ex.Message}" );
return DateTime.MinValue; // Return MinValue to indicate failure
}
}
/// <summary>
/// 当服务器拉取到时间后调用, 锁定一次
/// </summary>
/// <param name="utcTime"></param>
private void OnPullServerTimeOK( DateTime utcTime )
{
if ( m_Inited ) return;
m_Inited = true;
StopAllCoroutines();
m_ServerTimePoint = Time.unscaledTime;
m_NowUtc = utcTime;
Log.Green( $"获取服务器时间成功: {utcTime}" );
}
#endregion
}
}
m_NowSerivceTime 一个基于你发行游戏地区的时间偏移, 比如北京时区就是 8, 巴西就是-3,美国就是-5
具体根项目运营确认
//这个就是一个全局的定义,自己写一个类或者 写死一个也行
GlobalConfig.TIME_ZONE_OFFSET = 8;
using Newtonsoft.Json; 如果这里报错, 就说明项目没有 NewtonsoftJson插件, 没关系,这里你改成Unity内置的就行
改成 JsonUtility.FromJson( result ); //Unity 内置的Json库
总之就一句, 没有必要就直接使用NTPComponent.m_NowUtc, 有时区要求就用 NTPComponent.m_NowSerivceTime, 总之,优势在我!
原理, 这块没必要看,如果有同学有兴趣,可以继续看看
真实时间由两个部分组成, 一个是请求一次得到的 真实云UTC时间, 另外一个是当前游戏的秒数TimePoint
通过 基数 + 秒数偏移。 能在游戏内断网的时候有效获取到真实的云时间
需要注意的是,在游戏启动的时候,你得确保用户是联网的
当然,如果你的游戏是纯单机的,也不会报错,因为在Awake的时候默认用的是本地的TimePoint,如果你用纯单机也就不需要考虑真实的时间, 这里是能保证你的项目能在任何条件下安全的跑起来
在游戏启动的时候获取一个 UTC时间 基数
然后记录当前的游戏运行时间 Time.unscaledTime
获取当前真实的UTC时间时 => UTC时间 基数 + ( 当前游戏运行时间 - 记录时间 ) 秒数偏移