Unity常见框架探索-ET框架探索-程序员宅基地

技术标签: unity  游戏引擎  Unity开发方案和知识  

简介

ET框架是类ECS的一个Unity前后端框架

论坛地址为:https://et-framework.cn

Git地址为:https://github.com/egametang/ET

预备知识

Unity程序集的使用

接入流程

本文将会以7.2版本进行分析。所以直接clone github上的仓库,将工程导入到本地,之后将分支切换到最新的release分支,"release7.2"

菜单栏相关

ENABLE_CODE选项

ET->ChangeDefine->ADD_ENABLE_CODE/REMOVE_ENABLE_CODE

一般在开发阶段使用Editor时需要启用ENABLE_CODE选项。该选项启用时,修改脚本之后,会直接重新编译所有的代码,Editor在运行时会直接使用最新的程序集。如果ENABLE_CODE选项是关闭的,框架启动后会加载之前生成的程序集文件(这个文件需要在ET->BuildTool界面生成),导致每次需要应用修改,都要重新生成程序集文件。

框架解析

框架入口解析

启动流程如下

  • 入口文件为Init,之后调用CodeLoader对代码进行加载
  • 如果是EnableCodes模式则直接加载程序集。否则通过AB加载文件,之后调用LoadHotfix函数
  • LoadHotfix会加载程序集,并且调用EventSystem,根据特性注册对应事件的监听。
  • 之后调用ET.Entry的Start方法。
  • ET.Entry.Start 进行初始化之后,推送对应的EntryEvent事件
  • 推送EntryEvent3,EntryEvent3_InitClient接收后推送AppStartInitFinish
  • AppStartInitFinish_CreateLoginUI接收该事件后,创建UI场景

UI系统

UI界面的生成流程

ET是通过异步方式创建UI,如下方例子,调用UIHelper.Create方法,指定创建UI的场景,UI类型和对应的层级

C#
        protected override async ETTask Run(Scene scene, EventType.AppStartInitFinish args)
        {
            await UIHelper.Create(scene, UIType.UILogin, UILayer.Mid);
        }

调用scene挂载的UIComponent组件,处理Create事件

C#
        public static async ETTask<UI> Create(Scene scene, string uiType, UILayer uiLayer)
        {
            return await scene.GetComponent<UIComponent>().Create(uiType, uiLayer);
        }

之后会标记有对应UIEvent特性的类,处理该事件,开始加载资源并生成对应的GameObject

C#
    [UIEvent(UIType.UILogin)]
    public class UILoginEvent: AUIEvent
    {
        public override async ETTask<UI> OnCreate(UIComponent uiComponent, UILayer uiLayer)
        {
            await uiComponent.DomainScene().GetComponent<ResourcesLoaderComponent>().LoadAsync(UIType.UILogin.StringToAB());
            GameObject bundleGameObject = (GameObject) ResourcesComponent.Instance.GetAsset(UIType.UILogin.StringToAB(), UIType.UILogin);
            GameObject gameObject = UnityEngine.Object.Instantiate(bundleGameObject, UIEventComponent.Instance.GetLayer((int)uiLayer));
            UI ui = uiComponent.AddChild<UI, string, GameObject>(UIType.UILogin, gameObject);
            ui.AddComponent<UILoginComponent>();
            return ui;
        }

        public override void OnRemove(UIComponent uiComponent)
        {
            ResourcesComponent.Instance.UnloadBundle(UIType.UILogin.StringToAB());
        }
    }

UI组件解析

以UILogin为例子,对应的Prefab实际上只挂载了ReferenceCollector,ReferenceCollector负责将结点进行绑定

生成该GameObject之后,调用AddComponent

C#
GameObject gameObject = UnityEngine.Object.Instantiate(bundleGameObject, UIEventComponent.Instance.GetLayer((int)uiLayer));
UI ui = uiComponent.AddChild<UI, string, GameObject>(UIType.UILogin, gameObject);
ui.AddComponent<UILoginComponent>();

其中UILoginComponent负责显示对应成员

C#
[ComponentOf(typeof(UI))]
public class UILoginComponent: Entity, IAwake
{
    public GameObject account;
    public GameObject password;
    public GameObject loginBtn;
}

AddComponent之后,会调用对应的System,这里UILoginComponentSystem就是对应的System,在Awake阶段通过ReferenceCollector对UILoginComponent进行了绑定,以及实现了对应的UI逻辑

C#
[ObjectSystem]
public class UILoginComponentAwakeSystem : AwakeSystem<UILoginComponent>
{
    protected override void Awake(UILoginComponent self)
    {
        ReferenceCollector rc = self.GetParent<UI>().GameObject.GetComponent<ReferenceCollector>();
        self.loginBtn = rc.Get<GameObject>("LoginBtn");
        self.loginBtn.GetComponent<Button>().onClick.AddListener(()=> { self.OnLogin(); });
        self.account = rc.Get<GameObject>("Account");
        self.password = rc.Get<GameObject>("Password");
     }
}

场景切换

关于ET的场景切换相关逻辑可以查看

UILobbyComponentSystem处理进入Map的操作,先是调用EnterMap异步函数,等待EnterMapHelper异步返回后删除界面

C#
        //UILobbyComponentSystem
        public static async ETTask EnterMap(this UILobbyComponent self)
        {
            await EnterMapHelper.EnterMapAsync(self.ClientScene());
            await UIHelper.Remove(self.ClientScene(), UIType.UILobby);
        }

之后EnterMapHelper会向服务器发起进入Map的请求

C#
        //EnterMapHelper
        public static async ETTask EnterMapAsync(Scene clientScene)
        {
            try
            {
                G2C_EnterMap g2CEnterMap = await clientScene.GetComponent<SessionComponent>().Session.Call(new C2G_EnterMap()) as G2C_EnterMap;
                clientScene.GetComponent<PlayerComponent>().MyId = g2CEnterMap.MyId;
               
                // 等待场景切换完成
                await clientScene.GetComponent<ObjectWait>().Wait<Wait_SceneChangeFinish>();
               
                EventSystem.Instance.Publish(clientScene, new EventType.EnterMapFinish());
            }
            catch (Exception e)
            {
                Log.Error(e);
            }       
        }

网络模块

获取路由地址示例

下面以获取路由地址为例,分析ET框架完成一次HTTP请求的过程。

主要包含的类有RouterAddressComponentSystem,RouterAddressComponent

其中RouterAddressComponent为数据的载体,负责填写请求参数,以及保存返回的数据

C#
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

namespace ET.Client
{
    [ComponentOf(typeof(Scene))]
    public class RouterAddressComponent: Entity, IAwake<string, int>
    {
        public IPAddress RouterManagerIPAddress { get; set; }
        public string RouterManagerHost;
        public int RouterManagerPort;
        public HttpGetRouterResponse Info;
        public int RouterIndex;
    }
}

RouterAddressComponentSystem则是处理获取路由的逻辑

C#
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;

namespace ET.Client
{
    [FriendOf(typeof(RouterAddressComponent))]
    public static class RouterAddressComponentSystem
    {
        public class RouterAddressComponentAwakeSystem: AwakeSystem<RouterAddressComponent, string, int>
        {
            protected override void Awake(RouterAddressComponent self, string address, int port)
            {
                self.RouterManagerHost = address;
                self.RouterManagerPort = port;
            }
        }
       
        public static async ETTask Init(this RouterAddressComponent self)
        {
            self.RouterManagerIPAddress = NetworkHelper.GetHostAddress(self.RouterManagerHost);
            await self.GetAllRouter();
        }

        private static async ETTask GetAllRouter(this RouterAddressComponent self)
        {
            string url = $"http://{self.RouterManagerHost}:{self.RouterManagerPort}/get_router?v={RandomGenerator.RandUInt32()}";
            Log.Debug($"start get router info: {url}");
            string routerInfo = await HttpClientHelper.Get(url);
            Log.Debug($"recv router info: {routerInfo}");
            HttpGetRouterResponse httpGetRouterResponse = JsonHelper.FromJson<HttpGetRouterResponse>(routerInfo);
            self.Info = httpGetRouterResponse;
            Log.Debug($"start get router info finish: {JsonHelper.ToJson(httpGetRouterResponse)}");
           
            // 打乱顺序
            RandomGenerator.BreakRank(self.Info.Routers);
           
            self.WaitTenMinGetAllRouter().Coroutine();
        }
       
        // 等10分钟再获取一次
        public static async ETTask WaitTenMinGetAllRouter(this RouterAddressComponent self)
        {
            await TimerComponent.Instance.WaitAsync(10 * 60 * 1000);
            if (self.IsDisposed)
            {
                return;
            }
            await self.GetAllRouter();
        }

        public static IPEndPoint GetAddress(this RouterAddressComponent self)
        {
            if (self.Info.Routers.Count == 0)
            {
                return null;
            }

            string address = self.Info.Routers[self.RouterIndex++ % self.Info.Routers.Count];
            string[] ss = address.Split(':');
            IPAddress ipAddress = IPAddress.Parse(ss[0]);
            if (self.RouterManagerIPAddress.AddressFamily == AddressFamily.InterNetworkV6)
            {
                ipAddress = ipAddress.MapToIPv6();
            }
            return new IPEndPoint(ipAddress, int.Parse(ss[1]));
        }
       
        public static IPEndPoint GetRealmAddress(this RouterAddressComponent self, string account)
        {
            int v = account.Mode(self.Info.Realms.Count);
            string address = self.Info.Realms[v];
            string[] ss = address.Split(':');
            IPAddress ipAddress = IPAddress.Parse(ss[0]);
            //if (self.IPAddress.AddressFamily == AddressFamily.InterNetworkV6)
            //{
            //    ipAddress = ipAddress.MapToIPv6();
            //}
            return new IPEndPoint(ipAddress, int.Parse(ss[1]));
        }
    }
}

请求过程,是先添加RouterAddressComponent组件,在添加时填入对应的HTTP请求地址和端口号

之后调用routerAddressComponent的Init方法。

C#
//获取RouterAddressComponent
RouterAddressComponent routerAddressComponent = clientScene.GetComponent<RouterAddressComponent>();
if (routerAddressComponent == null)
{
    //如果RouterAddressComponent不存在,就添加RouterAddressComponent组件,并且填入HTTP请求的地址和端口号
    routerAddressComponent = clientScene.AddComponent<RouterAddressComponent, string, int>(ConstValue.RouterHttpHost, ConstValue.RouterHttpPort);
    await routerAddressComponent.Init();
    clientScene.AddComponent<NetClientComponent, AddressFamily>(routerAddressComponent.RouterManagerIPAddress.AddressFamily);
}

C#
  public static async ETTask Init(this RouterAddressComponent self)
        {
            self.RouterManagerIPAddress = NetworkHelper.GetHostAddress(self.RouterManagerHost);
            await self.GetAllRouter();
        }
       

Protobuf的使用

proto文件

Proto文件放在Unity/Assets/Config/Proto之下

并且文件名有特定的命名规范,以InnerMessage_S_20001.proto为例,以"_"为分割符,第一个字符串"InnerMessage"是文件名,第二个字符串"S"是用于区分Server还是Client,第三个字符串"20001"为协议起始的编号

生成Protoc#文件

点击菜单栏ET->Build Tool->Proto2CS,生成成功之后会在Scripts/Codes/Model/Generate

需要注意的是et使用的是protobuf-net

https://github.com/protobuf-net/protobuf-net

区别于google的protocolbuffers的c#版本

https://github.com/protocolbuffers/protobuf

问题解决

1.当前 .NET SDK 不支持将 .NET 6.0 设置为目标。请将 .NET 5.0 或更低版本设置为目标,或使用支持 .NET 6.0 .NET SDK 版本。    C:\Program Files\dotnet\sdk\5.0.414\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets        141       

解决方案:这个问题是由于visiual studio 2019 不支持.NET 6.0。需要将开发软件升级到visual studio 2022

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/shaobing32/article/details/131246565

智能推荐

jQuery 个人之前的笔记_jquery详细笔记-程序员宅基地

文章浏览阅读2.7k次,点赞3次,收藏5次。jQuery 基础篇概述jQuery是一款优秀的JavaScript库 ,从命名可以看出jQuery最主要的用途是用来做查询(iQuery=js+Query) ,正如jQuery官方Logo副标题所说(write less, domore)使用jQuery能 上我们对HTML文档遍历和操作、事件处理、动画以及Ajax变得更加简单入门和原生JS区别1、原生JS: 等DOM元素加载完毕,并且图片也加载完毕才会执行 编写多个入口函数,后面会覆盖前面2、jQuery 等Dom元素加载完毕,但_jquery详细笔记

C++实现Delaunay三角网生长算法_增量法生成delaunay三角网-程序员宅基地

文章浏览阅读8.4k次,点赞25次,收藏142次。一、概述1.1 三角网的介绍三角网是由一系列连续三角形构成的网状的平面控制图形,是三角测量中布设连续三角形的两种主要扩展形式,同时向各方向扩展而构成网状.适用于地势起伏大,通视条件比较好的场地。三角网是实现地形三维可视化,数字地面模型(Digital Terrain Model,简称DTM)是一种很有效的途径。DTM主要是由栅格和不规则三角网(Triangulated Irregular Network,简称TIN)两种数据格式来表示,相比于栅格TIN具有许多..._增量法生成delaunay三角网

TCGA数据下载和整理工具----GDCRNATools_gdcrnatools软件包进行差异基因分析-程序员宅基地

文章浏览阅读1.9w次,点赞7次,收藏62次。TCGA数据下载和整理的网站及软件发表很多了,比如Broad GDAC Firehose, Oncomine, TCGAbiolinks,TCGA-Assembler, TCGA2STAT,RTCGAToolbox等等,这些网站或软件要么使用的是TCGA更新前的数据,要么运行起来比较繁琐。当然各个工具都有其优势所在。之前在论坛里分享了自己下载和整理TCGA数据的Python代码。最近忙里偷_gdcrnatools软件包进行差异基因分析

win7更改计算机时间,win7系统自动更改日期时间是怎么回事-程序员宅基地

文章浏览阅读1.9k次。工具/原料硬件:计算机操作系统:Windows7方法/步骤1.Windows7系统不能更改日期和时间的解决方法2.在本地组策略编辑器窗口,展开Windows设置 - 安全设置 - 本地策略;3.在本地策略中找到:用户权限分配,左键点击:用户权限分配,在用户权限分配对应的右侧窗口找到:更改系统时间,并左键双击:更改系统时间;4.在打开的更改系统时间 属性窗口,我们点击:添加用户或组(U);5.在选择..._win7系统时间老是自己跳变

Python-Django-模型_pycharm怎么创建orm模型-程序员宅基地

文章浏览阅读1k次。一、ORM 模型介绍1 、 ORM 模型对象关系映射(英语:(Object Relational Mapping,简称ORM,或ORM,或OR mapping),是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。面向对象是从软件工程基本原则(如耦合、聚合、封装)的基础上发展起来的,而关系数据库则是从数学理论发展而来的,两套理论存在显著的区别。为了解决这个不匹配的现象,对象关系映射技术应运而生。对象关系映射(Object-Relational Mapping)提供了概念性的、_pycharm怎么创建orm模型

如何搭建一套完整的智能安防视频监控平台?关于设备与软件选型的几点建议_前端摄像头的选型依据-程序员宅基地

文章浏览阅读250次。球机摄像头:球机为一体化设备,可以通过云台控制进行转动、变倍和自动聚焦等操作,若需要对设备周边切换场景监控,如大门口、户外活动场所等,可以选择球机。_前端摄像头的选型依据

随便推点

微服务架构,这一篇就够了!-程序员宅基地

文章浏览阅读1.9k次。所有的微服务都是独立的Java进程跑在独立的虚拟机上,所以服务间的通信就是IPC(inter process communication),已经有很多成熟的方案。原来的Monolithic方式开发,所有的服务都是本地的,UI可以直接调用,现在按功能拆分成独立的服务,跑在独立的一般都在独立的虚拟机上的 Java进程了。它通常不依赖其他服务。后台有N个服务,前台就需要记住管理N个服务,一个服务下线/更新/升级,前台就要重新部署,这明显不服务我们 拆分的理念,特别当前台是移动应用的时候,通常业务变化的节奏更快。_微服务架构

主键索引和非主键索引的区别-程序员宅基地

文章浏览阅读871次。总的来说,主键索引是表的唯一标识索引,具有唯一性和快速访问的特点;而非主键索引可以提供更多的灵活性和覆盖更多的查询场景,但可能性能略低于主键索引。_主键索引和非主键索引的区别

linux启动进入bios设置密码,通过bios怎么设置开机密码-程序员宅基地

文章浏览阅读1.2k次。电脑不想被他人乱动,来设置下BIOS管理员密码和开机密码,就让学习啦小编来告诉大家通过bios怎么设置开机密码的方法吧,希望对大家有所帮助。通过bios设置开机密码方法计算机开机以后,按键盘的Delete键进入BIOS的设置画面,如下图所示。因为开机可以按Delete键进入设置画面的时间很短,您可以在计算机一开机就慢慢的重复按Delete键,以免错过进入设置画面又要重新再开机。按键盘向下箭头键移到..._bios开机密码 画面

批处理获取所有文件、文件夹名字_bat获取文件夹下所有文件名和文件夹名称-程序员宅基地

文章浏览阅读1.6w次,点赞14次,收藏45次。已收藏下面这个链接的方法也不错excel批处理技巧:如何制作文件档案管理系统excel批处理技巧:如何制作文件档案管理系统http://www.360doc.com/content/18/0913/13/18781560_786337463.shtml有时候我们整理文件的时候需要列出文件夹里面所有的文件名或者文件夹名,生成一个文件目录,一个个重命名然后复制到word或者记事本的方法显示有点太繁琐了。网上有一些自动生成文件目录的程序,比如我之前一直在用的DirIndex.exe。但最近我发现_bat获取文件夹下所有文件名和文件夹名称

计算机视觉图像检测之从EasyDL到BML_easydl paddlex bml-程序员宅基地

文章浏览阅读914次,点赞18次,收藏18次。部署方式选择公有云部署,训练方式均可。增量训练的意思是在之前训练的模型基础上再次进行训练,如果事先没有进行过训练,这一项为不可选中状态。回到Postman,参数栏按如下方式填写,其中第一个KEY-VALUE值直接照写,client_id和client_secret的VALUE值分别为上一步获取的AK、SK。如果数据集质量够高,每种标签标注效果都很好,也可以在模型训练时再进行数据增强,或者直接跳过这一步。在导入界面配置导入信息,选择本地导入,导入压缩包(其他导入方式请自行测试),如图1.1.2。_easydl paddlex bml

红帽oracle关系,redhat和oracle linux kernel对应关系-程序员宅基地

文章浏览阅读1.4k次。Red Hat Enterprise Linux Version / UpdateRed Hat Enterprise Linux – Kernel version / redhat-release stringOracle Linux – Kernel version / release stringsRed Hat Enterprise Linux 7Red Hat Enterprise Li..._oracle linux redhat 对应关系