打开ET.sln开始阅读源代码。我们先来学习客户端的代码,打开ET工程后,此时可以看到如下的目录结构:
其中,红色框圈起来的部分为我们平时开发时使用的工程,由于默认为打包模式,其工程并未生成和加载,故我们需要将开发模式打开,回到Unity中,在菜单栏选中ET→ChangeDefine→Add ENABLE_CODES,回到Visual Studio 2022中,这时项目会重新加载,在解决方案资源管理器窗口Unity→EnableCodes下会加载工程。
老规矩先找函数入口,在运行指南中有讲到Scenes目录中的Init场景为启动场景,打开它可以在Global节点下看到其挂载了一个init.cs的脚本,这就是启动脚本,打开它,可以发现它是在Unity.Loader工程下的,代码如下(ET是没有详细注释的,这里给它加上,方便理解。):
using System;
using System.Threading;
using CommandLine;
using UnityEngine;
namespace ET
{
// 客户端初始化脚本
public class Init: MonoBehaviour
{
// 整个客户端的入口
private void Start()
{
// 使当前游戏对象在加载新场景时不被销毁
DontDestroyOnLoad(gameObject);
// 为当前应用程序域添加未处理异常的事件处理器
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
// 记录异常信息到日志中
Log.Error(e.ExceptionObject.ToString());
};
// 添加线程同步处理功能
Game.AddSingleton<MainThreadSynchronizationContext>();
// 命令行参数
// 使用 CommandLineParser 来标准化地解析命令行
// CommandLineParser 是一款用于解析命令行参数的 NuGet 包。
// 我们可以将下面的参数改为string[] args = "--AppType GameTool --StartConfig StartConfig/Localhost --Process 2 --Develop 1 --LogLevel 2 --Console 1 --CreateScenes 0".Split(" ");
string[] args = "".Split(" ");
Parser.Default.ParseArguments<Options>(args) // 将args数组中的元素解析为Options类型的对象
.WithNotParsed(error => throw new Exception($"命令行格式错误! {error}")) // 如果解析失败,调用WithNotParsed方法,抛出一个异常,异常信息包含错误原因
.WithParsed(Game.AddSingleton); // 如果解析成功,调用WithParsed方法,将解析得到的Options对象作为参数传递给Game.AddSingleton方法
Game.AddSingleton<TimeInfo>();
Game.AddSingleton<Logger>().ILog = new UnityLogger();
Game.AddSingleton<ObjectPool>();
Game.AddSingleton<IdGenerater>();
Game.AddSingleton<EventSystem>();
Game.AddSingleton<TimerComponent>();
Game.AddSingleton<CoroutineLockComponent>();
ETTask.ExceptionHandler += Log.Error;
Game.AddSingleton<CodeLoader>().Start();
}
/// <summary>
/// 每帧调用一次
/// </summary>
private void Update()
{
// 更新游戏中的单例对象
Game.Update();
}
/// <summary>
/// 每帧结束时调用一次
/// </summary>
private void LateUpdate()
{
// 更新游戏中的单例对象
Game.LateUpdate();
Game.FrameFinishUpdate();
}
/// <summary>
/// 应用程序退出
/// </summary>
private void OnApplicationQuit()
{
// 销毁单例对象
Game.Close();
}
}
}
可以看到脚本主要做了这几件事:
- 脚本持久化,不因加载新场景而使脚本失效。
- 程序发生异常时记录日志。
- 解析了一个命令参数。
- 添加了很多单例,各个单例有不同的功能。
- 利用Unity的生命周期函数,驱动Game这个单例管理类的周期函数。
这里来重点解释一些功能,一些一看就懂的就不赘述了。我们先来看单例:
前面说过,Game是管理单例用的静态类,上面的代码调用以下的静态模板函数添加了很多单例:
public static T AddSingleton<T>() where T: Singleton<T>, new()
{
T singleton = new T();
AddSingleton(singleton);
return singleton;
}
其中T类型必须为抽象类Singleton的子类,鼠标放到Singleton这个类上,按F12跳转到Singleton.cs这个文件:
using System;
namespace ET
{
// 定义一个接口 ISingleton,用于实现单例模式
public interface ISingleton : IDisposable
{
// 注册单例对象
void Register();
// 销毁单例对象
void Destroy();
// 判断单例对象是否已经被销毁
bool IsDisposed();
}
// 定义一个抽象类 Singleton<T>,继承自 ISingleton 接口,用于创建泛型的单例对象
public abstract class Singleton<T> : ISingleton where T : Singleton<T>, new()
{
// 单例对象是否已经被销毁
private bool isDisposed;
// 存储单例对象的实例
[StaticField]
private static T instance;
// 一个静态属性,用于获取或设置单例对象的实例
public static T Instance
{
get
{
return instance;
}
}
// 实现 ISingleton 接口的 Register 方法,用于注册单例对象
void ISingleton.Register()
{
// 如果单例对象已经存在,则抛出异常
if (instance != null)
{
throw new Exception($"singleton register twice! {typeof(T).Name}");
}
// 否则将当前对象赋值给单例对象
instance = (T)this;
}
// 实现 ISingleton 接口的 Destroy 方法,用于销毁单例对象
void ISingleton.Destroy()
{
// 如果单例对象已经被销毁,则直接返回
if (this.isDisposed)
{
return;
}
// 否则将 isDisposed 设为 true,并调用 Dispose 方法释放资源,然后将单例对象设为 null
this.isDisposed = true;
instance.Dispose();
instance = null;
}
// 实现 ISingleton 接口的 IsDisposed 方法,用于判断单例对象是否已经被销毁
bool ISingleton.IsDisposed()
{
return this.isDisposed;
}
// 定义一个虚方法 Dispose,用于释放资源,可以在子类中重写
public virtual void Dispose()
{
}
}
}
其定义了一个单例接口ISingleton和单例抽象类Singleton,实现单例对象的注册和销毁。
打开Singleton.cs所在的Singleton目录,可以看到这里还有好几个文件ISingletonAwake.cs、ISingletonLateUpdate.cs、ISingletonUpdate.cs,其分别定义了ISingletonUpdate接口、ISingletonLateUpdate接口、ISingletonUpdate接口,这些接口的作用我们下面讲。可以看到用于管理单例的Game.cs文件也在同一目录下,内容如下:
using System;
using System.Collections.Generic;
namespace ET
{
public static class Game
{
/// <summary>
/// 定义一个私有静态只读字典,用来存储单例对象的类型和实例
/// </summary>
[StaticField]
private static readonly Dictionary<Type, ISingleton> singletonTypes = new Dictionary<Type, ISingleton>();
/// <summary>
/// 定义一个私有静态只读栈,用来存储单例对象的顺序
/// </summary>
[StaticField]
private static readonly Stack<ISingleton> singletons = new Stack<ISingleton>();
/// <summary>
/// 定义一个私有静态只读队列,用来存储需要更新的单例对象
/// </summary>
[StaticField]
private static readonly Queue<ISingleton> updates = new Queue<ISingleton>();
/// <summary>
/// 定义一个私有静态只读队列,用来存储需要延迟更新的单例对象
/// </summary>
[StaticField]
private static readonly Queue<ISingleton> lateUpdates = new Queue<ISingleton>();
/// <summary>
/// 定义一个私有静态只读队列,用来存储等待帧结束的任务
/// </summary>
[StaticField]
private static readonly Queue<ETTask> frameFinishTask = new Queue<ETTask>();
/// <summary>
/// 用来创建和注册一个泛型参数T类型的单例对象,T必须继承自Singleton<T>并且有无参构造函数
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T AddSingleton<T>() where T: Singleton<T>, new()
{
T singleton = new T();
AddSingleton(singleton);
return singleton;
}
/// <summary>
/// 用来注册一个已经创建好的单例对象
/// </summary>
/// <param name="singleton"></param>
/// <exception cref="Exception"></exception>
public static void AddSingleton(ISingleton singleton)
{
Type singletonType = singleton.GetType();
if (singletonTypes.ContainsKey(singletonType))
{
// 抛出异常,表示已经存在该类型的单例对象
throw new Exception($"already exist singleton: {singletonType.Name}");
}
// 否则将该类型和单例对象添加到字典中
singletonTypes.Add(singletonType, singleton);
// 将单例对象压入栈中
singletons.Push(singleton);
// 调用单例对象的Register方法,进行注册
singleton.Register();
// 如果单例对象实现了ISingletonAwake接口
if (singleton is ISingletonAwake awake)
{
// 调用Awake方法,用来执行一些初始化逻辑
awake.Awake();
}
// 如果单例对象实现了ISingletonUpdate接口
if (singleton is ISingletonUpdate)
{
// 将单例对象入队到更新队列中
updates.Enqueue(singleton);
}
// 如果单例对象实现了ISingletonLateUpdate接口
if (singleton is ISingletonLateUpdate)
{
// 将单例对象入队到延迟更新队列中
lateUpdates.Enqueue(singleton);
}
}
/// <summary>
/// 定义一个静态异步方法WaitFrameFinish,用来等待当前帧结束,并返回一个ETTask类型的任务
/// </summary>
/// <returns></returns>
public static async ETTask WaitFrameFinish()
{
ETTask task = ETTask.Create(true);
frameFinishTask.Enqueue(task);
await task;
}
/// <summary>
/// 用来执行更新队列中的所有单例对象
/// </summary>
public static void Update()
{
int count = updates.Count;
while (count-- > 0)
{
// 从更新队列中取出一个单例对象
ISingleton singleton = updates.Dequeue();
if (singleton.IsDisposed())
{
continue;
}
// 如果该单例对象不实现ISingletonUpdate接口,跳过本次循环
if (singleton is not ISingletonUpdate update)
{
continue;
}
// 将该单例对象重新加入到更新队列中
updates.Enqueue(singleton);
try
{
// 调用该单例对象的Update方法
update.Update();
}
catch (Exception e)
{
Log.Error(e);
}
}
}
/// <summary>
/// 用来执行延迟更新队列中的所有单例对象
/// </summary>
public static void LateUpdate()
{
int count = lateUpdates.Count;
while (count-- > 0)
{
// 从延迟更新队列中取出一个单例对象
ISingleton singleton = lateUpdates.Dequeue();
if (singleton.IsDisposed())
{
continue;
}
// 如果该单例对象不实现ISingletonLateUpdate接口,跳过本次循环
if (singleton is not ISingletonLateUpdate lateUpdate)
{
continue;
}
// 将该单例对象重新加入到延迟更新队列中
lateUpdates.Enqueue(singleton);
try
{
// 调用该单例对象的LateUpdate方法
lateUpdate.LateUpdate();
}
catch (Exception e)
{
Log.Error(e);
}
}
}
/// <summary>
/// 用来执行帧结束任务队列中的所有任务
/// </summary>
public static void FrameFinishUpdate()
{
while (frameFinishTask.Count > 0)
{
// 从帧结束任务队列中取出一个任务
ETTask task = frameFinishTask.Dequeue();
// 设置该任务的结果为成功
task.SetResult();
}
}
/// <summary>
/// // 用来关闭所有的单例对象并清理相关数据结构
/// </summary>
public static void Close()
{
// 顺序反过来清理
while (singletons.Count > 0)
{
ISingleton iSingleton = singletons.Pop();
iSingleton.Destroy();
}
singletonTypes.Clear();
}
}
}
我们可以看到在AddSingleton()函数中将单例都加入成员变量singletons栈中统一进行管理,另外,再根据是否为ISingletonAwake、ISingletonUpdate、ISingletonLateUpdate接口,分别放入不同的容器中进行调度管理。前面说过用Init这个脚本的生命周期函数驱动Game的生命周期函数,这里又用Game的生命周期函数驱动单例的生命周期函数,只要单例类再继承于ISingletonAwake、ISingletonUpdate、ISingletonLateUpdate接口,即可实现相应周期函数的调用。
可以搜索一下代码,找到一个例子,我找到一个Root类,其定义如下:
public class Root: Singleton<Root>, ISingletonAwake
其同时继承于Singleton抽象类和ISingletonAwake接口,其可以在初始化时调用周期函数Awake()。
单例搞懂了,我们再回过头看Init.cs初始化脚本的功能。
第一个单例是MainThreadSynchronizationContext,其功能是实现线程的同步功能,将子线程的委托扔到主线程来执行,避免线程之间对资源的竞争,用的是一个同步队列来进行实现。
using System;
using System.Threading;
namespace ET
{
public class MainThreadSynchronizationContext: Singleton<MainThreadSynchronizationContext>, ISingletonUpdate
{
// 创建一个 ThreadSynchronizationContext 类型的字段,用于管理线程同步队列
private readonly ThreadSynchronizationContext threadSynchronizationContext = new ThreadSynchronizationContext();
public MainThreadSynchronizationContext()
{
// 将当前的同步上下文设置为 threadSynchronizationContext
SynchronizationContext.SetSynchronizationContext(this.threadSynchronizationContext);
}
public void Update()
{
// 取出队列中的Action执行
this.threadSynchronizationContext.Update();
}
// 将一个委托和一个状态对象加入到线程同步队列中
public void Post(SendOrPostCallback callback, object state)
{
this.Post(() => callback(state));
}
// 将一个委托加入到线程同步队列中
public void Post(Action action)
{
this.threadSynchronizationContext.Post(action);
}
}
}
第一个单例是Options,藏在命令行参数解析中,可以看我写的代码注释,命令参数解析成功后会调用另一个添加实例的重载函数:
public static void AddSingleton(ISingleton singleton)
注意Options是在外面解析生成对象后再传入AddSingleton()函数的。和直接调用下面的函数不同,之前使用的都是下面的这个函数。
public static T AddSingleton<T>() where T: Singleton<T>, new()
Options的功能主要是标识进程的一些信息,如进程类型,配置文件位置,进程编号,是否为开发版本,日志等级等等。
第二个单例是TimeInfo,时间信息相关的功能,如时区,客户端时间,服务器时间,帧时间等,功能比较简单,不多说。
using System;
namespace ET
{
/// <summary>
/// 时间信息类
/// </summary>
public class TimeInfo: Singleton<TimeInfo>, ISingletonUpdate
{
/// <summary>
/// 时区
/// </summary>
private int timeZone;
public int TimeZone
{
get
{
return this.timeZone;
}
set
{
this.timeZone = value;
dt = dt1970.AddHours(TimeZone);
}
}
/// <summary>
/// 表示1970年1月1日0点0分0秒的UTC时间
/// </summary>
private DateTime dt1970;
/// <summary>
/// 表示1970年1月1日0点0分0秒加上时区的时间
/// </summary>
private DateTime dt;
/// <summary>
/// 表示服务器时间和客户端时间的差值,只能在类内部获取,在类外部设置
/// </summary>
public long ServerMinusClientTime { private get; set; }
/// <summary>
/// 帧时间
/// </summary>
public long FrameTime;
public TimeInfo()
{
// 初始化时间
this.dt1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
this.dt = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// 初始化FrameTime的值,调用ClientNow方法获取当前客户端时间
this.FrameTime = this.ClientNow();
}
public void Update()
{
this.FrameTime = this.ClientNow();
}
/// <summary>
/// 根据时间戳获取时间
/// </summary>
public DateTime ToDateTime(long timeStamp)
{
return dt.AddTicks(timeStamp * 10000);
}
/// <summary>
/// 线程安全
/// 获取当前客户端时间
/// </summary>
/// <returns></returns>
public long ClientNow()
{
return (DateTime.UtcNow.Ticks - this.dt1970.Ticks) / 10000;
}
/// <summary>
/// 获取当前服务器时间
/// </summary>
/// <returns></returns>
public long ServerNow()
{
return ClientNow() + Instance.ServerMinusClientTime;
}
/// <summary>
/// 获取当前客户端帧时间
/// </summary>
/// <returns></returns>
public long ClientFrameTime()
{
return this.FrameTime;
}
/// <summary>
/// 获取当前服务器帧时间
/// </summary>
/// <returns></returns>
public long ServerFrameTime()
{
return this.FrameTime + Instance.ServerMinusClientTime;
}
/// <summary>
/// 将一个DateTime对象转换为对应的时间戳
/// </summary>
/// <param name="d"></param>
/// <returns></returns>
public long Transition(DateTime d)
{
return (d.Ticks - dt.Ticks) / 10000;
}
}
}
第三个单例是Logger,记录日志的,会根据前面Options单例配置的日志等级来记录日志,不多说。
private const int TraceLevel = 1; // 定义Trace级别的常量
private const int DebugLevel = 2; // 定义Debug级别的常量
private const int InfoLevel = 3; // 定义Info级别的常量
private const int WarningLevel = 4; // 定义Warning级别的常量
第四个单例是ObjectPool,对象池子,对象复用提升效率用的,比较简单,不多说。文章来源:https://www.toymoban.com/news/detail-700565.html
using System;
using System.Collections.Generic;
namespace ET
{
// 对象池
public class ObjectPool : Singleton<ObjectPool>
{
// 定义一个字典,用于存储不同类型的对象队列
private readonly Dictionary<Type, Queue<object>> pool = new Dictionary<Type, Queue<object>>();
// 定义一个泛型方法,用于从对象池中获取指定类型的对象
public T Fetch<T>() where T : class
{
return this.Fetch(typeof(T)) as T;
}
// 从对象池中获取指定类型的对象
public object Fetch(Type type)
{
// 尝试从字典中获取对应类型的队列,如果没有则返回null
Queue<object> queue = null;
if (!pool.TryGetValue(type, out queue))
{
// 如果没有对应类型的队列,就创建一个新的对象并返回
return Activator.CreateInstance(type);
}
// 如果有对应类型的队列,但是队列为空,也创建一个新的对象并返回
if (queue.Count == 0)
{
return Activator.CreateInstance(type);
}
// 如果有对应类型的队列,并且队列不为空,就从队列中取出一个对象并返回
return queue.Dequeue();
}
// 将对象回收到对象池中
public void Recycle(object obj)
{
// 尝试从字典中获取对应类型的队列,如果没有则创建一个新的队列并添加到字典中
Type type = obj.GetType();
Queue<object> queue = null;
if (!pool.TryGetValue(type, out queue))
{
queue = new Queue<object>();
pool.Add(type, queue);
}
// 如果队列中的对象数量超过1000个,就不再回收该对象
if (queue.Count > 1000)
{
return;
}
// 将对象加入到队列中
queue.Enqueue(obj);
}
}
}
第五个单例是IdGenerater,Id生成器,生成Id用的。里面有3种id结构体:实体id(IdStruct)、实体实例id(InstanceIdStruct)和单位id(UnitIdStruct),分别通过IdGenerater的成员函数:GenerateId()、GenerateInstanceId()和GenerateUnitId()进行生成。通过实体id可以查询InstanceID。文章来源地址https://www.toymoban.com/news/detail-700565.html
using System;
using System.Runtime.InteropServices;
namespace ET
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct IdStruct
{
public uint Time; // 30bit,表示从2020年开始的秒数,最多可用34年
public int Process; // 18bit,表示进程号,最多可有262144个进程
public ushort Value; // 16bit,表示每秒每个进程生成的ID数量,最多可有65536个
// 将ID转换为长整型
public long ToLong()
{
ulong result = 0;
result |= this.Value;
result |= (ulong) this.Process << 16;
result |= (ulong) this.Time << 34;
return (long) result;
}
// 根据时间、进程和值创建一个ID结构体
public IdStruct(uint time, int process, ushort value)
{
this.Process = process;
this.Time = time;
this.Value = value;
}
// 根据长整型创建一个ID结构体
public IdStruct(long id)
{
ulong result = (ulong) id;
this.Value = (ushort) (result & ushort.MaxValue);
result >>= 16;
this.Process = (int) (result & IdGenerater.Mask18bit);
result >>= 18;
this.Time = (uint) result;
}
// 重写ToString方法,用于返回ID的字符串表示
public override string ToString()
{
return $"process: {this.Process}, time: {this.Time}, value: {this.Value}";
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct InstanceIdStruct
{
public uint Time; // 当年开始的tick 28bit,表示从当年开始的秒数,最多可用8.7年
public int Process; // 18bit,表示进程号,最多可有262144个进程
public uint Value; // 18bit,表示每秒每个进程生成的实例ID数量,最多可有262144个
// 定义一个方法,用于将实例ID转换为长整型
public long ToLong()
{
ulong result = 0;
result |= this.Value;
result |= (ulong)this.Process << 18;
result |= (ulong) this.Time << 36;
return (long) result;
}
// 根据长整型创建一个实例ID结构体
public InstanceIdStruct(long id)
{
ulong result = (ulong) id;
this.Value = (uint)(result & IdGenerater.Mask18bit);
result >>= 18;
this.Process = (int)(result & IdGenerater.Mask18bit);
result >>= 18;
this.Time = (uint)result;
}
// 根据时间、进程和值创建一个实例ID结构体
public InstanceIdStruct(uint time, int process, uint value)
{
this.Time = time;
this.Process = process;
this.Value = value;
}
// 给SceneId使用,只包含进程和值两个字段
public InstanceIdStruct(int process, uint value)
{
this.Time = 0;
this.Process = process;
this.Value = value;
}
// 重写ToString方法,用于返回实例ID的字符串表示
public override string ToString()
{
return $"process: {this.Process}, value: {this.Value} time: {this.Time}";
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct UnitIdStruct
{
public uint Time; // 30bit,表示从2020年开始的秒数,最多可用34年
public ushort Zone; // 10bit,表示区域号,最多可有1024个区域
public byte ProcessMode; // 8bit Process % 256,表示进程号对256取余的结果,一个区域最多256个进程
public ushort Value; // 16bit 表示每秒每个进程生成的Unit数量,最多可有65536个
// 将UnitID转换为长整型
public long ToLong()
{
ulong result = 0;
result |= this.Value;
result |= (uint)this.ProcessMode << 16;
result |= (ulong) this.Zone << 24;
result |= (ulong) this.Time << 34;
return (long) result;
}
// 根据区域、进程、时间和值创建一个UnitID结构体
public UnitIdStruct(int zone, int process, uint time, ushort value)
{
this.Time = time;
this.ProcessMode = (byte)(process % 256);
this.Value = value;
this.Zone = (ushort)zone;
}
// 根据长整型创建一个UnitID结构体
public UnitIdStruct(long id)
{
ulong result = (ulong) id;
this.Value = (ushort)(result & ushort.MaxValue);
result >>= 16;
this.ProcessMode = (byte)(result & byte.MaxValue);
result >>= 8;
this.Zone = (ushort)(result & 0x03ff);
result >>= 10;
this.Time = (uint)result;
}
// 重写ToString方法,用于返回UnitID的字符串表示
public override string ToString()
{
return $"ProcessMode: {this.ProcessMode}, value: {this.Value} time: {this.Time}";
}
// 从UnitID中获取区域号
public static int GetUnitZone(long unitId)
{
int v = (int) ((unitId >> 24) & 0x03ff); // 取出10bit
return v;
}
}
// 定义一个类,用于生成不同类型的ID
public class IdGenerater: Singleton<IdGenerater>
{
public const int Mask18bit = 0x03ffff; // 定义一个常量,用于表示18位的掩码
public const int MaxZone = 1024; // 定义一个常量,用于表示最大的区域数
private long epoch2020; // 用于存储2020年开始的毫秒数
private ushort value; // 用于存储每秒每个进程生成的ID数量
private uint lastIdTime; // 用于存储上一次生成ID的时间
private long epochThisYear; // 用于存储当年开始的毫秒数
private uint instanceIdValue; // 用于存储每秒每个进程生成的实例ID数量
private uint lastInstanceIdTime; // 用于存储上一次生成实例ID的时间
private ushort unitIdValue; // 用于存储每秒每个进程生成的Unit数量
private uint lastUnitIdTime; // 用于存储上一次生成UnitID的时间
public IdGenerater()
{
// 计算1970年开始的毫秒数
long epoch1970tick = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks / 10000;
// 计算2020年开始的毫秒数
this.epoch2020 = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks / 10000 - epoch1970tick;
// 计算当年开始的毫秒数
this.epochThisYear = new DateTime(DateTime.Now.Year, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks / 10000 - epoch1970tick;
// 获取当年开始到现在的秒数,并赋值给lastInstanceIdTime变量
this.lastInstanceIdTime = TimeSinceThisYear();
if (this.lastInstanceIdTime <= 0)
{
Log.Warning($"lastInstanceIdTime less than 0: {this.lastInstanceIdTime}");
// 将lastInstanceIdTime设为1,避免为0
this.lastInstanceIdTime = 1;
}
// 获取2020年开始到现在的秒数,并赋值给lastIdTime变量
this.lastIdTime = TimeSince2020();
if (this.lastIdTime <= 0)
{
Log.Warning($"lastIdTime less than 0: {this.lastIdTime}");
// 将lastIdTime设为1,避免为0
this.lastIdTime = 1;
}
// 获取2020年开始到现在的秒数,并赋值给lastUnitIdTime变量
this.lastUnitIdTime = TimeSince2020();
if (this.lastUnitIdTime <= 0)
{
Log.Warning($"lastUnitIdTime less than 0: {this.lastUnitIdTime}");
// 将lastUnitIdTime设为1,避免为0
this.lastUnitIdTime = 1;
}
}
// 计算从2020年开始到当前帧时间的秒数
private uint TimeSince2020()
{
uint a = (uint)((TimeInfo.Instance.FrameTime - this.epoch2020) / 1000);
return a;
}
// 计算从当年开始到当前帧时间的秒数
private uint TimeSinceThisYear()
{
uint a = (uint)((TimeInfo.Instance.FrameTime - this.epochThisYear) / 1000);
return a;
}
// 用于生成实例ID
public long GenerateInstanceId()
{
uint time = TimeSinceThisYear();
if (time > this.lastInstanceIdTime)
{
// 如果当前时间大于上一次生成实例ID的时间
// 更新上一次生成实例ID的时间
this.lastInstanceIdTime = time;
// 将每秒每个进程生成的实例ID数量重置为0
this.instanceIdValue = 0;
}
else
{
// 将每秒每个进程生成的实例ID数量加1
++this.instanceIdValue;
if (this.instanceIdValue > IdGenerater.Mask18bit - 1) // 18bit
{
// 如果每秒每个进程生成的实例ID数量超过了18位的最大值
++this.lastInstanceIdTime; // 借用下一秒
// 将每秒每个进程生成的实例ID数量重置为0
this.instanceIdValue = 0;
Log.Error($"instanceid count per sec overflow: {time} {this.lastInstanceIdTime}");
}
}
// 根据时间、进程和值创建一个实例ID结构体
InstanceIdStruct instanceIdStruct = new InstanceIdStruct(this.lastInstanceIdTime, Options.Instance.Process, this.instanceIdValue);
return instanceIdStruct.ToLong();
}
// 定义一个方法,用于生成ID
public long GenerateId()
{
// 获取2020年开始到现在的秒数
uint time = TimeSince2020();
if (time > this.lastIdTime)
{
// 如果当前时间大于上一次生成ID的时间
// 更新上一次生成ID的时间
this.lastIdTime = time;
// 将每秒每个进程生成的ID数量重置为0
this.value = 0;
}
else
{
// 如果当前时间等于或小于上一次生成ID的时间
// 将每秒每个进程生成的ID数量加1
++this.value;
if (value > ushort.MaxValue - 1)
{
// 如果每秒每个进程生成的ID数量超过了16位的最大值
// 将每秒每个进程生成的ID数量重置为0
this.value = 0;
++this.lastIdTime; // 借用下一秒
Log.Error($"id count per sec overflow: {time} {this.lastIdTime}");
}
}
// 根据时间、进程和值创建一个ID结构体
IdStruct idStruct = new IdStruct(this.lastIdTime, Options.Instance.Process, value);
return idStruct.ToLong();
}
// 定义一个方法,用于生成UnitID
public long GenerateUnitId(int zone)
{
if (zone > MaxZone)
{
// 如果区域号大于最大区域数,说明出错了
throw new Exception($"zone > MaxZone: {zone}");
}
// 获取2020年开始到现在的秒数
uint time = TimeSince2020();
if (time > this.lastUnitIdTime)
{
// 如果当前时间大于上一次生成UnitID的时间
this.lastUnitIdTime = time;
// 将每秒每个进程生成的Unit数量重置为0
this.unitIdValue = 0;
}
else
{
// 如果当前时间等于或小于上一次生成UnitID的时间
// 将每秒每个进程生成的Unit数量加1
++this.unitIdValue;
if (this.unitIdValue > ushort.MaxValue - 1)
{
// 如果每秒每个进程生成的Unit数量超过了16位的最大值
this.unitIdValue = 0;
++this.lastUnitIdTime; // 借用下一秒
Log.Error($"unitid count per sec overflow: {time} {this.lastUnitIdTime}");
}
}
// 创建一个UnitIdStruct结构体
UnitIdStruct unitIdStruct = new UnitIdStruct(zone, Options.Instance.Process, this.lastUnitIdTime, this.unitIdValue);
return unitIdStruct.ToLong();
}
}
}
到了这里,关于ET 7.2框架学习(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!