尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ET框架全栈游戏服务器开发指南:ECS架构与Actor模型实战

ET框架全栈游戏服务器开发指南:ECS架构与Actor模型实战 1. 项目概述为什么我们需要ET框架如果你是一个Unity游戏开发者或者正在为你的游戏项目寻找一个靠谱的服务器端解决方案那么“全栈”和“高性能”这两个词大概率是你技术选型时最核心的痛点。传统的游戏服务器开发是什么样子的客户端用C#写Unity逻辑服务器端可能用Java、Go或者C两套技术栈两套思维模式两套团队中间隔着厚厚的网络协议和序列化层。沟通成本高开发效率低联调更是噩梦。更别提为了追求性能你可能需要深入钻研各种网络库、线程模型、内存管理还没开始做游戏先把自己变成了底层架构专家。ET框架的出现就是为了解决这个根本性的割裂问题。它不是一个简单的网络库而是一个基于C#/.NET的、面向Unity游戏开发的、全栈式分布式服务器框架。简单来说它让你能用写Unity客户端逻辑的思维和语言C#去写服务器逻辑实现真正的“前后端同构”。你不再需要为服务器单独学习一门新的语言和生态Unity开发者可以无缝切换到服务器开发极大地降低了全栈游戏开发的门槛。它的目标很明确让开发者能更专注于游戏业务逻辑本身而不是被复杂的服务器底层技术细节所困扰。无论是MMORPG、MOBA、SLG还是其他需要强交互的在线游戏ET框架都提供了一套开箱即用的高性能解决方案。2. ET框架核心架构与设计哲学拆解要理解ET框架为什么高效必须深入到它的设计理念。它不是一个东拼西凑的轮子集合而是一个有着清晰哲学和严谨架构的整体。2.1 基于ECS实体-组件-系统的数据驱动架构这是ET框架最核心的基石。虽然Unity近年来也推出了DOTS面向数据的技术栈和ECS但ET框架的ECS实现更偏向于一种逻辑架构旨在提升代码的组织性、可维护性和性能潜力。实体Entity一个纯粹的ID或者一个轻量级的容器。它本身不包含任何数据或逻辑只用于标识一个游戏中的“事物”比如一个玩家、一个怪物、一个技能效果。组件Component承载数据。一个实体可以附加多个组件。例如MoveComponent存放速度、方向UnitComponent存放等级、生命值。组件是纯数据类没有方法。系统System承载逻辑。系统根据关心的组件类型来筛选实体并对这些实体上的组件数据进行操作。例如MoveSystem会遍历所有拥有MoveComponent的实体根据其速度更新位置。这种设计带来的好处是显而易见的关注点分离数据Component和逻辑System彻底解耦代码结构清晰易于理解和测试。高性能潜力由于数据是以组件形式连续存储的系统可以高效地进行批量处理对CPU缓存友好这在服务器端处理海量实体时优势巨大。极强的灵活性通过为实体动态添加或移除组件可以轻松改变实体的行为。比如给玩家实体加上一个StunComponent移动系统检测到这个组件就不处理移动从而实现眩晕效果。注意ET的ECS和Unity的DOTS ECS在实现细节上有所不同ET的ECS更侧重于架构整洁而DOTS ECS则极致追求性能。对于大多数游戏服务器来说ET的ECS已经能带来巨大的可维护性提升和良好的性能表现。2.2 纤程Fiber与Actor模型高并发的秘诀游戏服务器要同时处理成千上万个玩家的请求如何优雅且高效地处理并发是另一个核心挑战。ET框架采用了纤程Fiber和Actor模型的结合。你可以把每个纤程理解为一个超级轻量级的“逻辑线程”。它由框架自己调度而不是操作系统线程。一个进程内可以创建成千上万个纤程切换开销极小。在ET中每个Entity实体都运行在一个独立的纤程上。这自然引出了Actor模型每个Entity都是一个Actor它有自己的状态Components并且只通过异步消息ET中的Message与其他Entity进行通信处理消息的过程是顺序的。这套机制解决了传统多线程编程的几大难题锁的消除由于每个Entity单纤程顺序处理消息其内部状态不需要加锁从根本上避免了死锁和复杂的锁竞争。高效的资源利用避免了操作系统线程上下文切换的巨大开销可以用极低的成本支撑超高并发。逻辑简化开发者可以用近乎单线程的思维来编写业务逻辑无需时刻警惕多线程陷阱生产力大幅提升。2.3 全栈同构一次编写两端运行这是ET框架对Unity开发者最具吸引力的特性。框架提供了一套统一的网络层、序列化层和核心组件如定时器、事件系统、配置加载。共享代码你的游戏核心逻辑如技能计算、伤害公式、AI行为树如果服务器需要验证可以写在共享的Core模块中。这个模块可以被Unity客户端项目和服务器端项目同时引用。透明网络通信ET提供了类似RPC的机制。你可以在一个Entity上定义一个[Message]方法客户端可以像调用本地函数一样调用它框架会自动完成序列化、网络传输、反序列化和服务器端方法调用的全过程。对于开发者而言网络几乎是透明的。统一的工具链热更新、配置表如使用Luban、协议定义等都可以用同一套工具和流程来处理保证了客户端和服务器数据定义的一致性。这意味着一个熟悉Unity和C#的开发者可以很快地同时承担客户端和服务器功能的开发实现真正的高效全栈开发闭环。3. 从零开始使用ET框架搭建一个简易游戏服务器理论说得再多不如动手实践。下面我们以一个最简单的“多人位置同步”场景为例演示如何使用ET框架搭建服务器和客户端。3.1 环境准备与项目初始化首先你需要准备以下环境.NET SDKET服务器端基于.NET需要安装.NET 6.0或更高版本的SDK。Unity建议使用较新的LTS版本如2022.3 LTS。IDE服务器端推荐使用Rider或Visual Studio 2022客户端使用Visual Studio或Rider for Unity。初始化步骤获取ET框架从GitHub克隆ET官方仓库。通常你会看到多个项目例如Unity、DotNet等文件夹。生成解决方案运行仓库根目录下的init.bat或init.sh脚本。这个脚本会自动为服务器端生成.sln解决方案文件并配置好项目引用。打开服务器项目用IDE打开生成的DotNet/App.sln。导入Unity项目将Unity文件夹作为项目在Unity Hub中打开。3.2 定义第一个组件与系统让实体动起来我们的目标是在服务器上创建代表玩家的实体并让它每隔一秒向随机方向移动。第一步创建移动组件纯数据在服务器的Core模块中创建一个MoveComponent.cs。// 位于 Server/Core/Module/Unit/Component 目录下仅供参考可按需组织 using System; namespace ET { [ComponentOf(typeof(Unit))] // 表示这个组件可以附加到Unit类型的实体上 public class MoveComponent : Entity, IAwake, IUpdate { public float Speed { get; set; } 5.0f; // 移动速度 public float TargetX { get; set; } public float TargetZ { get; set; } // 当前位置由Entity的Position组件管理这里只存目标点 } }第二步创建移动系统纯逻辑在同一个目录下创建MoveSystem.cs。using System; using Unity.Mathematics; namespace ET { // 固定命名格式[组件名]System [ObjectSystem] public class MoveComponentAwakeSystem : AwakeSystemMoveComponent { protected override void Awake(MoveComponent self) { // 组件被创建时的初始化逻辑 self.TargetX 0; self.TargetZ 0; } } [ObjectSystem] public class MoveComponentUpdateSystem : UpdateSystemMoveComponent { protected override void Update(MoveComponent self) { // 每帧或每个服务器逻辑帧都会调用 Unit unit self.GetParentUnit(); // 获取持有该组件的实体 float3 currentPos unit.Position; // 计算朝向目标点的方向 float3 target new float3(self.TargetX, 0, self.TargetZ); float3 dir math.normalize(target - currentPos); float distance math.distance(currentPos, target); if (distance 0.1f) { // 向目标点移动 float3 newPos currentPos dir * self.Speed * TimeInfo.Instance.DeltaTime; unit.Position newPos; Console.WriteLine($Unit {unit.Id} 移动到 ({newPos.x:F2}, {newPos.z:F2})); } else { // 到达目标点随机设置下一个目标 Random rand new Random(); self.TargetX rand.Next(-10, 10); self.TargetZ rand.Next(-10, 10); Console.WriteLine($Unit {unit.Id} 新目标: ({self.TargetX}, {self.TargetZ})); } } } }实操心得ET框架的系统类需要通过[ObjectSystem]特性标记并且类名有固定格式[组件名][生命周期]System。AwakeSystem用于初始化UpdateSystem用于每帧逻辑。这种约定大于配置的方式虽然一开始需要适应但能让项目结构非常规整。3.3 网络通信客户端连接与位置同步现在我们需要让客户端连接服务器并接收服务器的实体移动信息。第一步定义网络协议消息在共享的Core模块中定义客户端和服务器之间通信的消息结构。ET使用protobuf或MessagePack进行序列化。这里以简单结构为例。// 位于 Share/Core/Module/Message 目录下 namespace ET { // 客户端发送的登录请求 [Message] public partial class C2G_Login : MessageObject, IRequest { public static C2G_Login Create() ObjectPool.Instance.FetchC2G_Login(); public string Account { get; set; } } // 服务器返回的登录响应 [Message] public partial class G2C_Login : MessageObject, IResponse { public static G2C_Login Create() ObjectPool.Instance.FetchG2C_Login(); public long PlayerId { get; set; } // 服务器分配给玩家的Unit Id } // 服务器广播单位位置信息 [Message] public partial class M2C_UnitPositions : MessageObject, IMessage { public static M2C_UnitPositions Create() ObjectPool.Instance.FetchM2C_UnitPositions(); public class UnitPosition { public long UnitId { get; set; } public float X { get; set; } public float Z { get; set; } } public ListUnitPosition Positions new ListUnitPosition(); } }运行框架提供的代码生成工具这些消息类会自动生成对应的序列化/反序列化代码。第二步服务器处理登录并创建玩家实体在服务器的Handler目录下创建C2G_LoginHandler.cs。namespace ET { [MessageHandler] public class C2G_LoginHandler : AMRpcHandlerC2G_Login, G2C_Login { protected override async ETTask Run(Session session, C2G_Login request, G2C_Login response, Action reply) { // 1. 验证账号这里简化 if (string.IsNullOrEmpty(request.Account)) { response.Error ErrorCode.ERR_AccountEmpty; reply(); return; } // 2. 创建玩家Unit实体 long unitId IdGenerater.Instance.GenerateId(); Unit unit EntityFactory.CreateUnit(session.DomainScene(), unitId); unit.AddComponentMoveComponent(); // 附加上我们刚写的移动组件 // 3. 将Unit加入到场景中便于管理 UnitComponent unitComponent session.DomainScene().GetComponentUnitComponent(); unitComponent.Add(unit); // 4. 响应客户端 response.PlayerId unitId; reply(); // 5. 开始定期广播位置简化示例实际应用更复杂的同步策略 StartBroadcastPosition(session.DomainScene()).Coroutine(); await ETTask.CompletedTask; } private async ETTask StartBroadcastPosition(Scene scene) { var timerComponent scene.GetComponentTimerComponent(); while (true) { await timerComponent.WaitAsync(100); // 每100毫秒广播一次 UnitComponent unitComp scene.GetComponentUnitComponent(); M2C_UnitPositions msg M2C_UnitPositions.Create(); foreach (Unit unit in unitComp.GetAll()) { msg.Positions.Add(new M2C_UnitPositions.UnitPosition { UnitId unit.Id, X unit.Position.x, Z unit.Position.z }); } // 广播给所有在线的会话这里简化了会话管理 // 实际项目中需要根据场景、视野等条件筛选接收者 MessageHelper.Broadcast(msg); } } } }第三步客户端连接与消息处理在Unity客户端项目中你需要初始化ET的网络组件并连接服务器。// 位于Unity项目的某个启动脚本中 using ET; using UnityEngine; public class NetworkDemo : MonoBehaviour { private Session session; async void Start() { try { // 1. 创建网络会话 session await RouterHelper.CreateRouterSession(NetworkHelper.ToIPEndPoint(127.0.0.1:10007)); // 2. 发送登录请求 C2G_Login loginReq C2G_Login.Create(); loginReq.Account TestPlayer; G2C_Login loginRes await session.Call(loginReq) as G2C_Login; if (loginRes.Error 0) { Debug.Log($登录成功玩家ID: {loginRes.PlayerId}); // 可以在这里创建本地玩家对象并与loginRes.PlayerId关联 } // 3. 注册位置广播消息处理器 MessageHelper.AddMessageHandlerM2C_UnitPositions(OnUnitPositionsUpdated); } catch (Exception e) { Debug.LogError($连接失败: {e}); } } private void OnUnitPositionsUpdated(M2C_UnitPositions message) { foreach (var posInfo in message.Positions) { Debug.Log($收到单位 {posInfo.UnitId} 位置: ({posInfo.X}, {posInfo.Z})); // 在这里更新Unity场景中对应GameObject的位置 // GameObject.Find(posInfo.UnitId.ToString())?.transform.position new Vector3(posInfo.X, 0, posInfo.Z); } } void OnDestroy() { session?.Dispose(); MessageHelper.RemoveMessageHandlerM2C_UnitPositions(); } }3.4 配置与启动服务器配置 ET框架的服务器配置通常在Config文件夹下的.json或.bytes文件中。你需要配置服务器App的类型如Game、监听的IP和端口、数据库连接等。启动流程启动DotNet/App项目它会根据配置启动多个进程如Realm登录服、Gate网关服、Game游戏逻辑服。在开发阶段可以用OneThread模式在单进程中运行所有服务方便调试。运行Unity客户端点击连接。如果一切顺利你将在服务器控制台看到玩家Unit在随机移动的日志在Unity客户端的Console窗口看到不断刷新的位置信息。一个最基础的、具备网络通信和逻辑帧驱动的游戏服务器原型就搭建完成了。4. ET框架高级特性与生产环境实践掌握了基础搭建后要用于实际项目还需要深入了解以下几个关键方面。4.1 分布式部署与进程间通信一个成熟的游戏服务器很少是单进程的。ET框架原生支持分布式部署不同的服务如登录、网关、游戏逻辑、聊天、战斗可以运行在独立的进程中甚至分布在不同的物理机器上。进程划分Realm负责账号认证和选区Gate是网关管理客户端连接、消息路由和流量加密Game是核心游戏逻辑Map负责场景管理Chat、Friend等是功能服。进程间通信RPCET使用基于Actor模型的进程间RPC。调用远程进程的一个Entity的方法和调用本地Entity的方法在代码层面几乎一样框架底层通过网络完成通信。这极大地简化了分布式编程。配置与启动通过修改StartConfig可以灵活指定每个App进程的配置和启动参数。使用ProcessHelper.Start可以一键启动整个分布式集群。4.2 热更新与Luban配置表热更新ET框架支持基于HybridCLR的C#全平台热更新。这意味着你可以在不重启服务器的情况下修复bug、调整数值、甚至增加新的游戏功能。这对于在线运营的游戏至关重要。你需要将需要热更的逻辑放在Hotfix项目中框架会动态加载这些程序集。配置表游戏开发离不开大量的配置数据如道具、技能、怪物属性。ET社区广泛使用Luban这个强大的配置表工具。你只需要在Excel或JSON中编辑配置Luban可以一键生成强类型的C#代码和二进制数据文件客户端和服务器可以共用同一套配置和加载代码彻底杜绝了配置不一致的问题。4.3 性能优化与监控实体池与对象池ET框架内置了强大的对象池ObjectPool。像消息对象、组件等频繁创建销毁的实例都应该通过Create()和Dispose()来使用对象池能有效减少GC垃圾回收压力这是保证服务器稳定性的关键。异步编程模型ET框架深度集成了async/await异步模型并提供了ETTask。所有耗时的I/O操作如数据库访问、网络请求都必须使用异步方法避免阻塞主线程纤程。日志与监控合理的日志分级Debug, Info, Warning, Error是排查线上问题的生命线。ET框架有自己的日志系统可以方便地集成到如NLog等第三方库。此外需要考虑集成APM应用性能监控工具来监控服务器的CPU、内存、网络IO和关键业务指标。5. 常见问题与避坑指南实录在实际使用ET框架的过程中我踩过不少坑也总结出一些宝贵的经验。5.1 生命周期管理与内存泄漏问题Entity或Component没有正确销毁导致内存持续增长。原因ET框架使用引用计数和对象池来管理Entity的生命周期。如果你手动new了一个Entity而不是用EntityFactory.Create或者一个Entity被其他对象如静态变量、列表长期引用它就无法被正确回收。解决方案严格遵守创建/销毁规范永远使用EntityFactory.CreateT和entity.Dispose()。注意循环引用在Component中引用其他Entity时尽量使用Id而不是直接的对象引用。如果必须引用在Dispose时务必置空。善用分析工具定期使用内存分析工具如.NET Memory Profiler检查未释放的对象。5.2 网络消息积压与处理超时问题客户端卡顿服务器处理变慢甚至消息队列爆满。原因某个消息处理函数Handler执行了同步阻塞操作如Thread.Sleep、同步的数据库查询或者单个消息处理逻辑过于复杂耗时导致处理该消息的纤程被长时间占用后续消息排队。解决方案绝对禁止同步阻塞所有I/O操作必须使用async/await。拆分耗时操作对于非常复杂的计算如寻路、大规模战斗结算考虑将其拆分成多个Update帧来完成或者丢到专门的线程池去处理完成后通过消息通知主逻辑。设置超时与熔断在RPC调用时设置合理的超时时间。对于某些非关键业务可以考虑熔断机制避免雪崩。5.3 数据库操作与事务一致性问题玩家数据丢失、道具复制等严重BUG。原因并发情况下对数据库的“读-改-写”操作没有加锁或使用事务导致脏写。解决方案使用分布式锁ET框架提供了基于数据库或Redis的分布式锁组件。在修改关键数据如玩家金币、背包物品前先对玩家ID或资源ID加锁。利用数据库事务对于涉及多个表更新的操作务必使用事务来保证原子性。ET的数据库组件如对MongoDB、MySQL的封装通常支持事务操作。采用乐观锁在数据实体中增加版本号字段更新时检查版本号是否匹配可以避免大部分并发问题。5.4 调试与日志排查困难问题线上服务器报错但日志信息不全难以定位问题。原因日志级别设置不当关键上下文信息没有记录。解决方案结构化日志不要只打印“错误发生”要打印出当前操作的实体ID、关键参数、请求上下文等。ET框架的日志系统支持格式化字符串要充分利用。为关键操作添加TraceId在一次客户端请求的整个处理链路中可能经过多个服务传递一个唯一的TraceId并将这个ID记录到所有相关的日志中。这样可以通过一个ID串联起所有日志极大提升排查效率。使用单元测试与集成测试对于核心的业务系统如战斗、经济编写完善的单元测试和集成测试。ET框架基于纤程的架构使得编写模拟网络环境的集成测试变得相对容易。5.5 新手容易混淆的概念概念常见误解正确理解Entity vs GameObject认为Entity是服务器端的GameObject。Entity是纯逻辑概念一个ID和组件容器。GameObject是Unity引擎的渲染和物理实体。两者通过Unit组件或自定义组件进行关联。纤程 vs 线程认为纤程和线程一样需要自己管理锁。纤程是更轻量的协作式任务。在单个纤程内代码是顺序执行的无需锁。跨纤程通信必须通过消息。AddComponent vs GetComponent频繁调用GetComponent获取引用。AddComponent是昂贵的操作可能涉及创建。对于需要频繁访问的组件应在Awake时用变量缓存起来而不是每次都Get。ETTask vs Task混用ETTask和System.Threading.Tasks.Task。在ET框架内强烈建议统一使用ETTask。它经过了框架的优化与ET的生命周期管理和纤程调度器深度集成能避免很多难以察觉的坑。最后我想分享一个最深刻的体会学习ET框架最大的挑战不是其技术本身而是思维模式的转变。你需要从传统的面向对象、多线程编程思维转变为以数据驱动ECS、消息通信Actor和异步编程为核心的思维。一旦你跨过了这个门槛你会发现构建复杂、高性能、可维护的在线游戏服务器变成了一件清晰而有条理的事情。它提供的不仅是一套工具更是一种经过验证的、适用于现代网络游戏服务端开发的最佳实践范式。
返回列表