ET 7.2框架学习(2)

这篇具有很好参考价值的文章主要介绍了ET 7.2框架学习(2)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

打开ET.sln开始阅读源代码。我们先来学习客户端的代码,打开ET工程后,此时可以看到如下的目录结构:

et框架,学习,unity,c#

其中,红色框圈起来的部分为我们平时开发时使用的工程,由于默认为打包模式,其工程并未生成和加载,故我们需要将开发模式打开,回到Unity中,在菜单栏选中ETChangeDefineAdd ENABLE_CODES,回到Visual Studio 2022中,这时项目会重新加载,在解决方案资源管理器窗口UnityEnableCodes下会加载工程。

老规矩先找函数入口,在运行指南中有讲到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.csISingletonLateUpdate.csISingletonUpdate.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,对象池子,对象复用提升效率用的,比较简单,不多说。

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模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ET介绍——浅谈AI框架

    AI在游戏中很多,但是为什么大家总是感觉ai编写起来十分困难,我后来思考了一番,主要原因是使用的方法不当。之前大家编写ai主要有几种方案: 我是不知道谁想出来这个做法的,真是无力吐槽。本来对象身上任何数据都是状态,这种方法又要把一些状态定义成一种新的节

    2024年02月05日
    浏览(37)
  • 02-ET框架的ECS编程思想

    TIPS: 本系列贴仅用于博主学习ET框架的记录 今天来学习OOP以外的另一种编程思想—ECS。 ECS:实体(Entity)、组件(Component)、系统(System),同时在框架中(实体即组件、组件即实体)类似电脑是一个实体,键盘是电脑的一个组件,但同时键盘也是一个实体,因为其下面还有按键这种

    2024年02月08日
    浏览(37)
  • ET框架6.0分析二、异步编程

    ET框架很多地方都用到了异步,例如资源加载、AI、Actor模型等等。ET框架对C#的异步操作进行了一定程度的封装和改造,有一些特点: 显式的或者说强调了使用C#异步实现协程机制(其实C#的异步编程天生就能实现这种用法) 强制单线程异步 没有使用C#库的Task,自己实现了E

    2024年02月04日
    浏览(34)
  • ET框架6.0分析三、网络通信

    ET框架的消息机制贯彻始终,包含Entity消息(Awake,Update ...),自定义(Customer)消息,网络消息等。而ET系统的进程包含了客户端、Gate等各种类型的服务器,进程包含各种服务器客户端之间通过网络消息进行通信进行工作。 结构图为了更加明确整体关系,进行了一定程度的简化

    2024年02月08日
    浏览(36)
  • ET框架6.0分析一、ECS架构

    ET框架的ECS架构是从ECS原生设计思想变形而来的(关于ECS架构的分析可以参考跳转链接:《ECS架构分析》),其特点是: Entity:实体可以作为组件挂载到其他实体上,Entity之间可以有父子嵌套关系,和其他ECS架构一样,Entity只允许是纯数据的(除了基本接口) System:和其他

    2024年02月04日
    浏览(35)
  • ET介绍——更为便捷高效的AI框架-行为机(Behavior Machine)

    顾名思义,类比状态机每个节点是一个状态,行为机每个节点是描述一种行为。行为机每个节点之间是互斥的,并且节点相互之间完全不用关心是怎么切换的。这里就不讲状态机跟行为树是怎么做ai的了,这里只讲用行为机怎么做一个ai。举个例子 mmo中的小怪策划案,大致会

    2024年02月05日
    浏览(39)
  • 记录:ET6 框架,由于 vs2019 不支持 .NET 6,在[生成解决方案]“Client-Server.sln“解决方案时会发生的报错

    ET 自己的论坛 ET社区 中的帖子内容,百度好像不能直接找到结果。记录一下,便于搜索。 ET6 依赖.NET 6 ,IDE应当选择:VS 2022 或 Rider2021.3.x(对应支持.NET 6 的版本,可以从 Rider官方文章得到相关信息) VS 2019、Rider2021.1.x 均不支持.NET 6,因此都无法正常使用。 VS 2019 只会在编译

    2024年02月12日
    浏览(52)
  • ET介绍——数值组件设计

    类似魔兽世界,moba这种技能极其复杂,灵活性要求极高的技能系统,必须需要一套及其灵活的数值结构来搭配。数值结构设计好了,实现技能系统就会非常简单,否则就是一场灾难。比如魔兽世界,一个人物的数值属性非常之多,移动速度,力量,怒气,能量,集中值,魔法

    2024年02月05日
    浏览(36)
  • ET介绍——单线程异步

    前面几个例子都是多线程实现的异步,但是异步显然不仅仅是多线程的。我们在之前的例子中使用了Sleep来实现时间的等待,每一个计时器都需要使用一个线程,会导致线程切换频繁,这个实现效率很低,平常是不会这样做的。一般游戏逻辑中会设计一个单线程的计时器,我

    2024年02月05日
    浏览(44)
  • ET介绍——CSharp协程

    说到协程,我们先了解什么是异步,异步简单说来就是,我要发起一个调用,但是这个被调用方(可能是其它线程,也可能是IO)出结果需要一段时间,我不想让这个调用阻塞住调用方的整个线程,因此传给被调用方一个回调函数,被调用方运行完成后回调这个回调函数就能

    2024年02月05日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包