
1. 项目概述为什么我们需要ET框架如果你是一名Unity游戏开发者尤其是经历过从单机小游戏到多人在线游戏MMO、MOBA、SLG等的转型那你一定对“服务端”这三个字又爱又恨。爱的是它带来了更丰富的玩法和更长的生命周期恨的是它引入的复杂度呈指数级增长。客户端还好说Unity引擎本身已经帮你封装了图形、物理、UI等大部分功能但服务端呢网络同步、状态管理、负载均衡、热更新、分布式部署……这些概念光是听起来就让人头大。更别提市面上那些传统的服务端框架要么是C写的学习曲线陡峭调试困难要么是Java系的虽然生态成熟但与Unity客户端的通信和逻辑复用又存在天然的“语言鸿沟”开发效率大打折扣。正是在这种背景下ET框架应运而生。它不是某个大厂闭门造车的产物而是由国内一线游戏开发者基于多年实战经验用C#语言打造的一套开源、高性能、全栈式的分布式游戏服务器框架。它的核心目标非常明确让Unity开发者能用自己最熟悉的C#语言高效、优雅地开发出高并发、可扩展的分布式游戏服务端。简单来说ET框架试图弥合客户端与服务端之间的技术栈隔阂实现“一套代码两端运行”逻辑层的愿景从而将开发者的精力从繁琐的底层网络和架构问题中解放出来聚焦于游戏玩法逻辑本身。我最初接触ET框架是在一个需要快速验证玩法的中型SLG项目上。当时团队规模小既要赶客户端进度又得快速搭出一个能支撑几百人同时在线的服务端原型。用C从头搭建时间不允许。用Node.js或Go团队不熟悉且与Unity客户端的数据结构交互是个麻烦。最终我们选择了ET它的学习成本相对平缓并且因为同是C#很多游戏逻辑如角色属性计算、技能效果可以直接复用客户端的代码调试起来也异常方便——直接在Visual Studio里给服务端逻辑下断点和调试客户端没有任何区别。这种开发体验上的流畅感是其他异构技术栈难以比拟的。2. ET框架核心设计思想与架构拆解ET框架的设计哲学可以概括为“组件化、异步化、分布式友好”。它不是简单地把一堆网络库和数据库封装起来而是从顶层设计上就为分布式游戏场景量身定做。2.1 面向数据的组件式设计ECS理念的延伸虽然ET框架没有严格遵循纯粹的ECSEntity-Component-System架构但它深度借鉴了其思想。在ET的世界里一切皆Entity实体。一个玩家、一个怪物、一个聊天频道甚至一个定时器都可以是一个Entity。每个Entity上可以挂载多个Component组件来赋予其能力。例如UnitComponent代表这是一个游戏内的单位拥有位置、朝向等基础属性。MoveComponent负责处理该实体的移动逻辑和同步。SkillComponent管理该实体的技能释放和冷却。这种设计带来了巨大的灵活性。添加新功能新建一个Component挂上去。移除功能Detach掉对应的Component。更重要的是它非常利于热更新。你可以动态地替换或更新某个Component的逻辑而无需重启整个服务器进程这对于需要24小时在线的游戏服务至关重要。注意ET的Entity-Component模型与Unity的GameObject-Component模型在概念上相似这降低了Unity开发者的理解成本。但ET的Component更偏向纯数据和行为逻辑不包含渲染等客户端特有内容保证了在服务端环境下的纯净与高效。2.2 全链路的异步编程模型高并发服务器的命门是I/O网络、数据库、文件。传统的同步阻塞式编程在面对成千上万个并发请求时会迅速耗尽线程资源导致服务器卡死。ET框架从底层到业务层全面拥抱了C#的async/await异步编程模型。几乎所有的耗时操作如数据库查询、RPC远程过程调用、HTTP请求都被设计成异步方法。这意味着当服务器在等待数据库返回结果时当前线程不会被阻塞而是可以去处理其他客户端的请求。等数据库结果返回后再自动恢复执行后续逻辑。这极大地提高了服务器的吞吐能力。例如一个玩家登录的流程可能包含验证账号异步查库- 加载角色数据异步查库- 通知好友上线异步RPC。在ET中这一连串操作可以写成一条清晰的异步调用链逻辑清晰且能高效利用系统资源。// 伪代码示例ET风格的异步登录处理 public async ETTask LoginHandler(Session session, C2G_Login request) { // 1. 异步验证账号密码 var account await DBComponent.Instance.QueryAccount(a a.Account request.Account); if (account null || account.Password ! request.Password) { session.Send(new G2C_Login() { Error ErrorCode.ERR_AccountOrPasswordError }); return; } // 2. 异步加载玩家角色数据 var player await DBComponent.Instance.QueryPlayer(p p.AccountId account.Id); if (player null) { // 创建新角色... } // 3. 异步将玩家加入地图 await MapHelper.EnterMapAsync(player); // 4. 响应客户端登录成功 session.Send(new G2C_Login() { PlayerInfo player.ToInfo() }); }2.3 分布式的核心Actor模型与消息驱动单台服务器的能力总有上限。要支持万人同服必须将负载分散到多台服务器上这就是分布式。ET框架实现分布式的核心是Actor模型。你可以把每个Entity都看作一个Actor。Actor内部有自己独立的状态并且只能通过接收消息Message来改变自己的状态和触发行为。Actor之间不能直接访问对方的内存必须通过发消息来通信。这个模型天然适合分布式因为无论两个Actor是否在同一台物理机器上它们的交互方式都是一样的发消息。ET框架在此基础上做了精妙的抽象进程Process一个独立的服务器程序实例比如登录服Login、网关服Gate、游戏逻辑服Game、地图服Map、聊天服Chat等。纤程FiberET7.x版本引入的概念可以理解为轻量级线程用于在单个进程内调度和处理Actor消息避免了传统多线程编程的锁竞争问题。地址Address每个Actor都有一个全局唯一的地址格式通常为{进程ID}{服务器类型}{实体ID}。通过这个地址任何地方都可以向这个Actor发送消息。当一个玩家从登录服进入游戏他的实体Player可能被创建在Game服上当他移动时MoveComponent会向同一个进程内的Map服上的寻路Actor发送寻路请求消息Map服计算完路径后再把结果消息发回来。整个过程对开发者透明就像在调用本地函数一样但背后可能已经跨越了多个服务器进程。3. 从零开始搭建一个基础的ET服务器集群理论说再多不如动手做一遍。下面我将带你一步步搭建一个最小化的ET服务器集群包含一个网关Gate、一个游戏逻辑服Game和一个共享的数据库MongoDB。3.1 环境准备与项目初始化首先你需要准备以下环境Unity Hub Unity Editor建议使用2021 LTS或更新版本。ET框架客户端部分需要Unity环境。.NET SDKET服务端基于.NET 6/7/8需要安装对应的SDK。Visual Studio 2022或Rider用于开发和调试服务端代码。MongoDBET默认使用MongoDB作为数据库因其文档模型与C#对象转换非常方便。你需要本地安装并运行MongoDB服务。Git用于克隆代码。第一步获取源码ET框架的源码托管在Gitee上。打开命令行执行git clone https://gitee.com/egametang/ET克隆完成后你会得到一个包含多个子项目的解决方案。第二步生成工程文件ET使用自定义的编译工具来生成客户端和服务端的项目文件。进入ET/Unity目录用Unity打开这个工程。首次打开可能会较慢因为要导入资源和生成代码。在Unity编辑器中点击顶部菜单栏的ET - BuildTool。在弹出的界面中确保选中了“编译Model、Hotfix、ModelView、HotfixView”等选项然后点击“编译”按钮。这个过程会调用Roslyn编译器将你的逻辑代码编译成动态链接库DLL。第三步启动服务器配置ET的服务端配置主要在ET/Config目录下。我们需要关注StartConfig文件。它定义了有哪些服务器进程以及它们如何分布。一个最简单的分布式配置可能如下JSON格式{ StartConfig: Localhost, StartProcessConfigs: [ { Id: 1, MachineId: 1, InnerPort: 20001, Process: DotNet.App, ServerType: Game }, { Id: 2, MachineId: 1, InnerPort: 20002, Process: DotNet.App, ServerType: Gate } ] }这个配置定义了两个进程ID为1的Game服和ID为2的Gate服它们都运行在同一台机器MachineId: 1上内部通信端口分别为20001和20002。3.2 编写第一个业务逻辑登录与聊天假设我们要实现一个功能玩家通过Gate服登录然后在Game服上可以向全服广播聊天消息。1. 定义网络消息Message在ET中客户端与服务端、服务端内部的所有通信都基于Protobuf定义的消息。我们在Model/Proto目录下创建Chat.proto文件syntax proto3; package ET; // 客户端 - 网关登录请求 message C2G_Login // ILocationRequest { string Account 1; string Password 2; } // 网关 - 客户端登录响应 message G2C_Login // IResponse { int64 PlayerId 1; int32 Error 2; } // 客户端 - 游戏服发送聊天消息 message C2Chat_Send // IMessage { string Content 1; } // 游戏服 - 客户端广播聊天消息 message Chat2C_Broadcast // IMessage { int64 PlayerId 1; string PlayerName 2; string Content 3; }使用ET提供的工具编译这个proto文件会自动生成对应的C#类。2. 实现网关登录处理器Gate在Hotfix项目中创建LoginHandler.cs它负责处理从客户端发来的登录消息。// 位于 Hotfix 程序集 public sealed class C2G_LoginHandler : MessageHandlerC2G_Login { protected override async ETTask Run(Session session, C2G_Login request) { Scene scene session.DomainScene(); // 1. 这里可以添加账号密码验证逻辑例如查库 // 模拟验证成功 if (request.Account ! test || request.Password ! 123456) { session.Send(new G2C_Login() { Error ErrorCode.ERR_AccountOrPasswordError }); return; } // 2. 向Game服发起请求为玩家创建一个Session long playerId IdGenerater.Instance.GenerateId(); // 生成玩家唯一ID // RPC调用Game服上的登录方法 G2C_Login response await scene.GetComponentMessageSender().Call( new C2G_Login() { Account request.Account, Password request.Password }, GameSessionId) as G2C_Login; // GameSessionId 需要从配置获取 // 3. 将Game服返回的SessionKey等信息传给客户端用于后续直连Game服 session.Send(response); } }3. 实现游戏服逻辑Game在Game服上我们需要一个系统来管理玩家会话和聊天。创建ChatComponent.cs作为一个单例组件管理聊天频道。// 位于 Model 程序集 (可热更的部分在Hotfix) [ComponentOf(typeof(Scene))] public class ChatComponent : Entity, IAwake, IDestroy { public Dictionarylong, EntityRefPlayer Players new(); // 可以定义多个聊天频道... } // 在 Hotfix 程序集中创建聊天系统 public static class ChatSystem { // 玩家加入聊天 public static void AddPlayer(this ChatComponent self, Player player) { self.Players.Add(player.Id, player); // 可以广播“某某玩家加入了聊天” } // 处理发送聊天消息的Handler [MessageHandler] public class C2Chat_SendHandler : MessageHandlerC2Chat_Send { protected override async ETTask Run(Session session, C2Chat_Send request) { Player player session.GetComponentSessionPlayerComponent()?.Player; if (player null) return; ChatComponent chat session.DomainScene().GetComponentChatComponent(); // 构建广播消息 Chat2C_Broadcast broadcast new() { PlayerId player.Id, PlayerName player.Name, Content request.Content }; // 向所有在线玩家广播 foreach (var kv in chat.Players) { Player p kv.Value; if (p ! null p.GetComponentUnit()?.GetComponentSessionComponent()?.Session ! null) { p.GetComponentUnit().GetComponentSessionComponent().Session.Send(broadcast); } } } } }4. 客户端连接与测试在Unity客户端中ET提供了Session组件来管理与服务端的网络连接。你需要先连接Gate服发送登录消息登录成功后客户端会根据返回的地址自动重连到Game服之后的聊天消息就直接发给Game服了。3.3 配置与启动数据库连接在Config/MongoDB.txt中配置你的MongoDB连接字符串。启动服务器在ET/Server目录下根据你的开发环境Windows/Linux使用脚本启动。例如在Windows下运行StartServer.bat它会根据StartConfig启动所有配置的进程。运行Unity客户端在Unity编辑器中运行客户端场景点击连接按钮输入账号密码test/123456登录成功后在聊天框输入文字就能看到广播消息了。实操心得第一次启动时最容易出错的地方是端口冲突和配置文件路径。务必检查StartConfig中的端口是否被占用以及Config目录下的配置文件是否被正确复制到服务器程序的运行目录下通常是Server/bin/Debug/net7.0。ET的日志系统非常强大多查看Logs文件夹下的输出能快速定位问题。4. ET框架高级特性与生产环境实践当你掌握了基础搭建后ET框架更强大的能力在于应对生产环境的复杂需求。4.1 热更新机制深度解析热更新是线上游戏运维的刚需。ET框架通过将代码分离到多个程序集来实现热更新Model存放实体、组件、网络消息的定义。这部分不可热更因为其他程序集依赖它。Hotfix存放绝大部分游戏业务逻辑。这部分可以热更。ModelView与HotfixView对应客户端的逻辑同样遵循可热更原则。其原理是将Hotfix程序集编译成DLL服务器运行时通过Assembly.Load动态加载。当需要更新时只需替换新的DLL文件然后通过管理指令通知服务器重新加载该程序集即可。这个过程不会中断现有玩家的连接实现了“不停服更新”。热更新步骤修改Hotfix项目中的代码。使用ET的编译工具重新编译Hotfix生成新的Hotfix.dll和Hotfix.pdb。将新的DLL文件上传到服务器指定目录。在游戏内GM命令台或通过管理工具发送热更指令例如hotfix reload。服务器收到指令后创建新的AppDomain或AssemblyLoadContext加载新的DLL并将新的逻辑替换到现有的实体组件上。注意事项热更新并非万能。它不能修改已有的数据结构如给Entity添加新字段因为这属于Model的范畴。它主要用于修复BUG或修改行为逻辑。对于大型更新依然需要规划停服维护。4.2 负载均衡与水平扩展当单个Game服无法承载更多玩家时就需要水平扩展。ET框架通过Location服务来优雅地解决这个问题。Location是一个独立的进程或集群它扮演了“服务注册与发现中心”的角色。其核心是一张表记录了每个玩家ID或其它实体ID当前位于哪个Game服进程上。工作流程玩家登录时Gate服通过查询Location服务为玩家分配一个负载较低的Game服比如Game1。Location服务记录玩家A - Game1。当另一个系统比如邮件系统需要给玩家A发送数据时它首先询问Location服务“玩家A在哪”Location返回“Game1”邮件系统就将消息直接发送到Game1进程。这样增加新的Game服Game2, Game3...时只需将其注册到Location服务后续的玩家登录就会被均衡地分配过去。整个系统对开发者是透明的你无需关心玩家具体在哪台服务器上只需要知道他的ID就能通过Location服务找到他。4.3 性能调优与监控开发完成并不意味着结束性能是分布式系统的生命线。网络层优化ET默认使用TCP并对消息进行了封装Packet。对于广播类消息如玩家移动同步可以考虑使用UDPKCP协议ET也有对应的KCP支持模块能在保证一定可靠性的前提下大幅降低延迟。序列化优化Protobuf虽然高效但序列化/反序列化依然是CPU大户。对于频繁发送且结构固定的消息可以考虑使用更快的序列化库如MessagePack或MemoryPackET框架可以替换默认的序列化组件。数据库优化缓存使用ET内置的缓存组件或集成Redis将热点数据如玩家基础信息放在内存中减少直接访问MongoDB的次数。异步批处理将多个数据库写操作合并定时批量提交减少IO次数。内存与泄漏排查ET框架基于ECSEntity和Component的生命周期管理是关键。务必确保Destroy的Entity其上的Component也被正确清理。可以使用.NET的内存分析工具如dotnet-counters,dotnet-dump定期检查服务器进程的内存使用情况特别是关注Gen 2堆和LOH大对象堆的增长。日志与指标ET内置了强大的日志系统可以输出到文件、控制台或网络。在生产环境中建议将日志接入ELKElasticsearch, Logstash, Kibana或类似平台进行集中分析和告警。同时可以暴露关键指标如在线人数、消息处理延迟、数据库查询耗时给PrometheusGrafana实现可视化监控。5. 常见“坑点”与排查指南在实际使用ET框架的过程中我踩过不少坑这里总结几个最常见的问题和解决方法。问题现象可能原因排查步骤与解决方案客户端连接网关后立刻断开1. 网关端口未正确开放或被防火墙拦截。2. 客户端与服务器使用的协议版本Proto文件不一致。3. 消息处理器Handler未注册或抛出未处理异常。1. 用telnet或netcat测试网关端口是否可达。2. 检查客户端和服务端的Proto编译输出目录确保生成的C#代码一致。清理项目重新生成。3. 查看服务器日志搜索“Error”或“Exception”。确保Handler类有[MessageHandler]特性且方法签名正确。RPC调用超时或无响应1. 目标Actor不存在或已销毁。2. 消息路由错误发到了错误的进程。3. 处理RPC的方法不是async ETTask或内部有同步阻塞操作。1. 检查调用RPC时传入的Actor地址是否正确目标实体是否已被销毁IsDisposed。2. 检查StartConfig配置确保进程ID和服务器类型映射正确。3. 确保RPC处理方法是异步的且内部所有IO操作如DB查询都使用了await避免.Result或.Wait()造成死锁。热更新后逻辑未生效1. 新的Hotfix.dll未成功加载。2. 更新的逻辑涉及了不可热更的Model层。3. 旧的组件实例未被替换。1. 查看热更新指令的返回日志确认是否加载成功。检查DLL文件路径和权限。2. 牢记只能更新Hotfix和HotfixView中的逻辑。添加新字段、新消息类型需要重启服务器。3. 对于已经存在的Entity其身上的旧Component可能持有旧的逻辑引用。热更新系统通常需要你设计一种机制来替换这些引用例如通过事件通知Component逻辑已更新。服务器内存持续增长1. Entity或Component未正确销毁造成内存泄漏。2. 缓存数据无限增长无淘汰策略。3. 存在大型集合如List、Dictionary未及时清理。1. 使用内存分析工具抓取快照查看Entity和Component类型的实例数量是否异常。检查代码中Dispose的调用链。2. 为缓存组件如PlayerComponent实现LRU等淘汰策略。3. 避免在频繁调用的方法中创建临时的大集合。考虑使用对象池复用对象。数据库操作性能瓶颈1. 频繁进行单条查询。2. 未建立合适的索引。3. 连接池配置不当。1. 将多个查询合并或使用批量操作接口。2. 使用MongoDB Compass等工具分析慢查询为经常作为查询条件的字段建立索引。3. 调整MongoDB驱动连接池的大小使其与服务器并发线程数匹配。独家避坑技巧善用ET的日志级别开发时设置为Debug可以看到每一条消息的收发和路由详情对调试网络问题极有帮助。上线后调整为Info或Error。设计阶段就考虑分布式在划分系统边界时就要思考哪些功能可以独立成一个微服务如邮件、排行榜、公会。ET的Actor模型让这变得容易但清晰的架构设计能避免后期重构的痛苦。编写集成测试ET框架支持在单元测试环境中模拟整个服务器进程集群。为你的核心系统如战斗、交易编写集成测试能极大提升代码质量和迭代信心。