这篇文章开始,学习的是ET 8.0的内容,注意,我这里学习的是Alex大大拆分过后的前后端分离版本的ET

Git仓库在这里:ALEXTANGXIAO/GameNetty: GameNetty (github.com)

与原版ET有一定差距,但是不大,主要是前后端拆开了,ET可以作为独立的一部分加入你的工程

框架启动流程

对于客户端而言,启动部分在Init.cs中,这一部分是不可更新的,可以说是工程启动入口

大部分内容都不重要,主要是这一块程序集的获取,作用分为两部分,第一个获取的程序集runtime是会了反射调用启动入口

后面创建一堆程序集,并放入CodeTypes里,是为了做程序集分析,因为ET的一些写法很依赖[Attribute],这里是提前分析好所有类的标签(比如ET的Event就不需要注册,而是以来标签进行监听)

private async ETTask StartAsync()
{
    ...

    // GameNetty.Runtime (Assembly)
    Assembly runtime = typeof(Entry).Assembly;

    World.Instance.AddSingleton<CodeTypes, Assembly[]>(new[]
    {
        // GameNetty.Runtime (Assembly)
        typeof(Entry).Assembly,
        // Assembly-CSharp (Assembly)
        typeof(Init).Assembly,
        // GameProto (Assembly)
        typeof(C2G_Ping).Assembly,
        // GameLogic (Assembly)
        typeof(PingComponent).Assembly
    });

    IStaticMethod start = new StaticMethod(runtime, "ET.Entry", "Start");
    start.Run();
}

然后进入Entry,这里头才是真的游戏启动入口

private static async ETTask StartAsync()
{
    Log.Debug("Entry Start");
    
    // 注册Entity序列化器
    EntitySerializeRegister.Init();
    World.Instance.AddSingleton<IdGenerater>();
    ...

    // 创建需要reload的code singleton
    CodeTypes.Instance.CreateCode();

    // await World.Instance.AddSingleton<ConfigLoader>().LoadAsync();

    await FiberManager.Instance.Create(SchedulerType.Main, ConstFiberId.Main, 0, SceneType.Main, "");
}

这里同样通过World创建了一些单例类,比较特殊的是这个CreateCode()方法,我们进去看一下

public void CreateCode()
{
    var hashSet = this.GetTypes(typeof (CodeAttribute));
    foreach (Type type in hashSet)
    {
        object obj = Activator.CreateInstance(type);
        ((ISingletonAwake)obj).Awake();
        World.Instance.AddSingleton((ASingleton)obj);
    }
}

可以看到,它收集了所有带有[CodeAttribute]标签的类,然后同样使用Wrold创建单例,有挺多类添加了这个标签

比如:

[Code]
public class EventSystem: Singleton<EventSystem>, ISingletonAwake { ... }

为啥要多此一举?

因为通过标签自动添加的单例类,可以进行逻辑热重载,这部分先不谈,反正是有好处的

最后,就是核心入口了,我们调用纤程管理器的创建方法

虚拟进程/纤程

先放一张ET 8.0的整体架构图

image-20240501234734204

在ET 8.0之前,ET是单线程,多进程的模式,也就说为了性能,你得区分很多个服务器进程,不同进程之前的交流通过服务器内部协议进行

而ET 8.0直接改成多线程多进程了,也就每一个纤程你都可以当做一个进程

在传统多线程架构中,经常会产生多线程资源竞争问题,所以必须要加线程锁,或者各种原子操作,但这些都会浪费性能,而纤程可以避免多线程之间的资源竞争

每一个纤程下的所有Entity,在创建时,都会被注册进这个纤程的EntitySystem

public class Fiber
{
    ...
    public EntitySystem EntitySystem { get; }
}

Entity的所有生命周期,都是由EntitySystem驱动的,本质还是由Fiber在驱动,那么谁会去驱动Fiber呢?

以Update为例,其实有3个地方在调用

image-20240501235216256

这和Fiberr的驱动模式有关系

public enum SchedulerType
{
    Main, //主线程
    Thread, //固定线程
    ThreadPool, //线程池
}

主线程调用

最朴实无华的驱动模式,所有Fiber排成队列,一个一个进行Update

image-20240501235441039

固定线程

线程和Fiberr一一对应,一个线程只会创建一个Fiber,并且也只会驱动这个Fiber

image-20240501235554250

线程池

在运行开始就会创建很多线程,而Fiber则会被随机的线程进行驱动,但是ET能保证同一时间,一个Fiber只会隶属于某一个线程,避免多个线程同时驱动一个Fiber

image-20240501235838571