区别于Unity的ECS以及Entitas,更像是Unity Mono的编程模式

以一台电脑为例,进行ET组件式编程的模板教程,电脑作为实体,而他的组件就是各种配件,比如机箱,显示器等

Entity & Component

  • Entity 实体只是一个概念上的定义,指的是一个独立物体,对ECS来说,Entity是组件的集合
  • Component 组件是数据的集合,不产生行为
  1. 创建电脑实体

继承Entity表示这是一个实体类,继承IAwake是框架要求,用于初始化

public class Computer: Entity, IAwake
{
}
  1. 创建一个机箱组件

[ComponentOf]很明显了,标记这个类作为Computer类的组件

但是为什么组件类仍然是一个Entity呢?很简单,以为某一个组件仍然可能是由多个部分组成的

[ComponentOf(typeof(Computer))]
public class PCCaseComponent: Entity,IAwake
{
}
  1. 类似的,创建显示器、键盘、鼠标组件
[ComponentOf(typeof (Computer))]
public class KeyboardComponent: Entity, IAwake
{
}

[ComponentOf(typeof(Computer))]
public class MonitorsComponent: Entity,IAwake
{  
}

[ComponentOf(typeof (Computer))]
public class MouseComponent: Entity, IAwake
{
}

以上代码均创建在Model程序集中,Model,即数据层

image-20230710235830837

System

系统,是行为的集合,不包含数据,但根据组件对数据进行具体的操作

  1. ComputerSystem

按照框架要求,系统类,必须是一个静态类,通过扩展方法对组件进行操作

public static class ComputerSystem
{
    public static void Start(this Computer self)
    {
        Log.Debug("Computer Start!");
    }
}
  1. 按照要求编写另外两个System
public static class MonitorsSystem
{
    public static void Display(this MonitorsComponent self)
    {
        Log.Debug("Display!");
    }
}

public static class PCCaseComponentSystem
{
    public static void StartPower(this PCCaseComponent self)
    {
        Log.Debug("Start Power!");
    }
}
  1. 改变Scene的Child类型

找到这个类,给他添加一个属性[ChildType()],内容为空,意思就是任何Entity都可以是Scene的子节点,Scene会作为整个ECS的根节点

[ChildType()]
public sealed class Scene: Entity
{
}
  1. 测试

AppStartInitFinish_CreateLoginUI,这个类会在登录完成之后调用,具体生命周期先不谈,我们在创建UI后我们添加一些代码

public class AppStartInitFinish_CreateLoginUI: AEvent<EventType.AppStartInitFinish>
{
   protected override void Run(EventType.AppStartInitFinish args)
   {
      UIHelper.Create(args.ZoneScene, UIType.UILogin, UILayer.Mid).Coroutine();

      // 添加一些代码
      var computer = args.ZoneScene.AddChild<Computer>();
      computer.AddComponent<PCCaseComponent>();
      computer.AddComponent<MonitorsComponent>();
      
      computer.Start();
   }
}

同时改一下ComputerSystem

public static class ComputerSystem
{
    public static void Start(this Computer self)
    {
        Log.Debug("Computer Start!");
        
        self.GetComponent<PCCaseComponent>().StartPower();
        self.GetComponent<MonitorsComponent>().Display();
    }
}

然后打开Unity,编译代码,运行服务器,最后再运行Unity

可以看到Log

image-20230711001122071

运行在服务端

在服务器的数据层,创建一个相同的文件夹,然后添加存在文件

image-20230711001541703

记得要选择添加链接

image-20230711001521639

测试

服务端的入口函数在AppStart_Init

编写类似的代码

var computer = Game.Scene.AddChild<Computer>();
computer.AddComponent<PCCaseComponent>();
computer.AddComponent<MonitorsComponent>();
computer.Start();

编译服务器,然后运行

Log不会出现在C#控制台,因为服务器的Log会到日志中去

日志在 ET/Logs中

image-20230711001928656

Model & HotFix

  • Model 数据层,定义Component与Entity,没有行为
  • HotFix 逻辑层,定义System行为,且该行为是逻辑行为,可以脱离Unity Mono运行
  • ModelView/HotFixView 类似于上面两个,但是会和Unity引擎进行交互,所以是显示层

image-20230711002645276

Scene场景树

定义如下:

image-20230711222012184

客户端层级树

image-20230711222204310

注意,Scene本身也是Entity,场景树更多是一种概念上的抽象,而非框架上的

生命周期

以Demo工程为例,游戏以Mono脚本Init为根,驱动CodeLoader运行,真正的逻辑在后者内

public class Init
{
    void Start()
    {
        CodeLoader.Instance.Start();
    }
}

CodeLoader主要是读取Dll(热更新,走IL或者HybirdCLR),并运行游戏真正的逻辑

游戏逻辑的入口是:

public static class Entry
{
    public static void Start()
    {
        Game.EventSystem.Publish(new EventType.AppStart());
    }
}

很简单,就是抛了一条App启动的事件

其监听位置在类AppStart_Init中的

public class AppStart_Init: AEvent<EventType.AppStart>
{ 
    private async ETTask RunAsync(EventType.AppStart args)
    {
        Game.Scene.AddComponent<TimerComponent>();
        ...
         // 创建ZoneScene
        Scene zoneScene = SceneFactory.CreateZoneScene(1, "Game", Game.Scene);
        
        Game.EventSystem.Publish(new EventType.AppStartInitFinish() { ZoneScene = zoneScene });
    }
}

其中Game.Scene约等于一个单例类

public static class Game
{
    ...
    private static Scene scene;
    public static Scene Scene
    {
        get
        {
            if (scene != null)
            {
                return scene;
            }
            ...
            return scene;
        }
    }
}

服务端层级树

image-20230712002307301

服务端可以有多个ZoneScene,分别负载不同的功能

给服务端添加一个ZoneScene

以账号为例,找到场景类型枚举,添加一个类型

public enum SceneType
{
   ...
   Account = 7,
}

然后再服务端入口代码处,可以看到服务端运行时会默认创建一些Scene

public class AppStart_Init: AEvent<EventType.AppStart>
{  
    private async ETTask RunAsync(EventType.AppStart args)
    {
        ...
        switch (Game.Options.AppType)
        {
            case AppType.Server:
            {
                // 这里是读取的配表
                var processScenes = StartSceneConfigCategory.Instance.GetByProcess(Game.Options.Process);
                foreach (StartSceneConfig startConfig in processScenes)
                {
                    await SceneFactory.Create(...);
                }
                break;
            }
        }
        ...
    }
}

我们进入工厂类,参考代码添加一行

public static class SceneFactory
{
    
    public static async ETTask<Scene> Create(...)
    {
        ...
        switch (scene.SceneType)
        {
            ...
            case SceneType.Account:
                scene.AddComponent<NetKcpComponent, IPEndPoint, int>(...);
                Log.Error("Account Success"); // 加一行测试代码
                break;
        }

        return scene;
    }
}

因为是账号模块,所以徐娅添加一个NetKcpComponent用于连接

因为是读表的,所以我们还需要修改配表

ET 6.0\Excel\StartSceneConfig@s.xlsx

image-20230712003814793

然后运行同级目录的win_startExcelExport.bat

导表工具可以替换成Luban

编译运行服务器,可以发现一行错误日志

image-20230712004113377

机器人层级树

image-20230712004215050