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

资讯详情

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

ET框架:C#全栈游戏开发,Actor模型与双端同构实战解析

ET框架:C#全栈游戏开发,Actor模型与双端同构实战解析 1. 项目概述为什么我们需要ET框架在Unity游戏开发圈子里尤其是涉及到中重度网络游戏时一个经典的“痛点”会反复出现服务端和客户端的割裂。我们通常用C#写Unity客户端逻辑清晰开发效率高。但一到服务端画风突变Java、Go、C、Node.js……各种语言和框架百花齐放。这意味着你的团队需要至少两套技术栈两套思维模式两套调试环境。一个简单的“玩家移动同步”逻辑客户端写一遍服务端还得用另一套语法和库再实现一遍不仅开发效率打折更可怕的是逻辑不一致带来的Bug调试起来简直是噩梦。这就是ET框架试图解决的核心问题。它不是一个简单的插件或工具集而是一个基于C#和.NET的“全栈”游戏开发解决方案。这里的“全栈”指的是用同一种语言C#、同一套逻辑思维同时搞定客户端Unity和服务端.NET Core的开发。我第一次接触这个概念时感觉像是有人把游戏开发界的“巴别塔”给推倒了。开发者终于可以专注于游戏逻辑本身而不是在不同技术栈之间疲于奔命。ET框架的野心很大它重新定义了Unity游戏开发的协作模式。它不仅仅提供了网络通信、实体组件等基础能力更重要的是它引入了一套完整的面向数据、面向消息驱动的架构思想。这套思想源自于游戏服务器领域久经考验的Actor模型并将其与Unity的ECS实体组件系统思想进行了巧妙的融合与简化。对于从Unity纯客户端转型全栈的开发者或者希望提升中大型项目架构能力的团队来说ET提供了一条极具吸引力的路径。2. ET框架核心架构与设计哲学拆解要理解ET不能只把它当作一堆工具类。它的强大根植于其清晰而深刻的设计哲学。2.1 实体-组件Entity-Component模型一切皆实体ET框架的核心是自顶向下、高度一致的实体-组件模型。在ET的世界观里游戏中的一切无论是客户端的玩家角色、NPC、技能特效还是服务端的玩家会话、房间、排行榜都是一个“实体”Entity。实体本身只是一个ID它不包含任何逻辑和数据。所有的功能都以“组件”Component的形式挂载到实体上。例如一个玩家实体Player可能会挂载以下组件移动组件MoveComponent包含位置、速度、朝向等数据以及处理移动的逻辑方法。技能组件SkillComponent管理玩家拥有的技能列表、冷却状态。背包组件BagComponent管理玩家的物品数据。网络会话组件SessionComponent仅在服务端存在代表该玩家与客户端的网络连接。这种设计带来了巨大的灵活性。添加新功能创建一个新组件挂载到需要的实体上即可。移除功能销毁对应的组件。这种基于组合而非继承的方式极大地降低了系统的耦合度使得代码更容易维护和扩展。注意ET的EC模型与Unity引擎原生的GameObject-Component模型在思想上同源但实现上更纯粹、更轻量。ET的实体没有Transform、Renderer等引擎强相关的概念是纯粹的逻辑抽象因此可以无缝运行在服务端。2.2 消息驱动与Actor模型并发处理的利器网络游戏本质上是高并发的。成千上万的玩家同时在线上他们的操作需要被及时、有序地处理。ET框架借鉴了Actor模型来处理这一挑战。在ET中每个实体都可以看作一个微型的Actor。实体之间不直接调用对方的方法而是通过发送消息Message来进行通信。每个实体都有一个自己的“邮箱”消息队列它会顺序地处理接收到的消息。举个例子当客户端玩家点击释放技能时客户端Player实体的SkillComponent生成一个C2S_CastSkill消息。通过网络层这个消息被发送到服务端。服务端找到对应的玩家实体将消息放入该实体的消息队列。服务端该玩家实体的SkillComponent在下一帧或某个定时器处理这个消息验证技能合法性、计算伤害、生成伤害事件消息广播给周围其他实体。这种消息驱动的模式天然地将并发逻辑串行化了。你不需要担心多线程同时修改一个玩家数据导致的竞态条件因为每个实体在同一时刻最多只处理一个消息。这大大简化了服务端并发编程的复杂度。框架底层利用.NET的异步任务async/await和单线程事件循环来高效调度这些消息的处理在保证逻辑正确性的同时也兼顾了性能。2.3 双端同构一份逻辑两端运行这是ET框架最吸引人的特性也是“C#全栈”的终极体现。通过精心的架构设计ET使得一部分游戏核心逻辑主要是组件和系统可以以同一份C#代码的形式同时在Unity客户端和.NET Core服务端运行。如何实现关键在于严格的职责分离和依赖注入。公共库Model这里放置纯逻辑的实体定义、组件定义、网络消息定义、配置表结构等。这部分代码不依赖任何Unity引擎API或特定的网络库只使用.NET Standard库。因此它可以被客户端项目和服务端项目共同引用。客户端视图层Hotfix/View在客户端我们需要处理渲染、输入、动画、音效等。这部分代码放在客户端的Hotfix层热更新层或单独的View层。它们引用公共库并添加Unity相关的实现。例如公共库的MoveComponent只有数据和逻辑客户端的MoveComponent则会调用Transform来更新物体位置。服务端实现层Model服务端的项目同样引用公共库。服务端的组件实现专注于网络通信、数据库操作、广播、战斗数值计算等服务器端逻辑。当需要修改一个通用规则比如技能伤害计算公式时你只需要修改公共库中的相关代码。重新编译后客户端和服务端就同时生效了从根本上杜绝了双端逻辑不一致的问题。3. 从零开始一个ET框架项目的实战搭建理论说得再多不如动手搭一个。下面我将以一个最简单的“移动同步”Demo为例带你走一遍ET项目的创建和核心模块开发流程。你会清晰地看到双端代码是如何协作的。3.1 环境准备与项目初始化首先你需要准备以下环境Unity建议使用较新的LTS版本如2021.3或2022.3。确保安装了.NET Framework或.NET Core相关的开发模块。.NET SDK服务端运行需要.NET 6或.NET 8 SDK。去官网下载安装即可。IDEVisual Studio 2022或Rider。两者对C#和Unity的支持都非常好。ET源码从GitHub上克隆ET的官方仓库。初始化步骤获取框架git clone https://github.com/egametang/ET生成Unity项目ET仓库里通常有一个Unity文件夹或者提供了生成工具。用Unity打开这个工程它会自动导入必要的资源和代码。生成服务端项目使用命令行工具运行ET提供的BuildTool它会根据配置自动编译公共库Model、客户端热更层Hotfix和服务端应用App。配置双端工程确保Unity项目的Assembly Definition正确引用了生成的Model和Hotfix程序集。服务端项目一个控制台应用则引用Model程序集。这个过程可能会遇到一些依赖包版本问题这是最常见的“坑”。我的经验是严格按照ET官方文档或当前Release分支的README操作不要随意升级Unity或.NET的版本保持环境一致性能省去很多麻烦。3.2 定义核心实体与组件以玩家移动为例我们要实现的是玩家在客户端按下WASD键移动位置信息同步到服务端并由服务端广播给房间内的其他玩家。首先在公共库Model中定义// 网络消息定义 namespace ET { // 客户端 - 服务端请求移动 [Message] public class C2M_MoveRequest : MessageObject, IRequest { public Vector3 Position; public static C2M_MoveRequest Create() ObjectPool.Instance.FetchC2M_MoveRequest(); } // 服务端 - 客户端广播移动结果 [Message] public class M2C_MoveBroadcast : MessageObject, IMessage { public long UnitId; // 移动的单位Id public Vector3 Position; public static M2C_MoveBroadcast Create() ObjectPool.Instance.FetchM2C_MoveBroadcast(); } // 移动组件定义 public class MoveComponent : Entity, IAwake, IUpdate, IDestroy { public Vector3 TargetPosition; public float Speed 5f; public bool IsMoving false; // 供服务端调用的移动方法 public void StartMove(Vector3 target) { this.TargetPosition target; this.IsMoving true; } } }注意Vector3是ET自己定义或从UnityEngine中抽象出来的一个数学库结构体在公共库中可用实现了双端兼容。3.3 服务端逻辑实现验证与广播在服务端我们需要一个系统System来处理移动请求。ET鼓励使用“系统”类来组织逻辑它专注于处理拥有特定组件的实体。在服务端Model层创建MoveSystem.csnamespace ET { [ObjectSystem] // 对象系统用于管理MoveComponent的生命周期和逻辑 public class MoveSystem : UpdateSystemMoveComponent { protected override void Update(MoveComponent self) { if (!self.IsMoving) return; // 模拟移动计算实际项目中应有更复杂的寻路或碰撞检测 // 这里简化处理直接瞬移到目标点 self.GetParentUnit().Position self.TargetPosition; self.IsMoving false; // 移动结束广播新位置给房间内所有玩家 Room room self.GetParentUnit().GetParentRoom(); M2C_MoveBroadcast broadcast M2C_MoveBroadcast.Create(); broadcast.UnitId self.Id; broadcast.Position self.TargetPosition; MapHelper.Broadcast(room, broadcast); // 假设的广播帮助方法 } } // 处理移动请求的Handler [MessageHandler] public class C2M_MoveRequestHandler : AMRpcHandlerC2M_MoveRequest, EmptyResponse { protected override async ETTask Run(Session session, C2M_MoveRequest request, EmptyResponse response) { Unit unit session.GetComponentSessionPlayerComponent()?.Unit; if (unit null) { response.Error ErrorCode.ERR_NotFoundUnit; return; } // 简单的服务端验证检查目标点是否合法例如是否在可移动区域 if (!MapHelper.IsValidPosition(request.Position)) { response.Error ErrorCode.ERR_InvalidPosition; return; } // 通过组件驱动移动 MoveComponent moveComp unit.GetComponentMoveComponent(); if (moveComp null) { moveComp unit.AddComponentMoveComponent(); } moveComp.StartMove(request.Position); await ETTask.CompletedTask; } } }服务端的核心工作是验证、转换、广播。它接收客户端请求进行合法性校验防外挂的基础然后通过修改实体的MoveComponent来驱动逻辑状态变化。MoveSystem在每帧更新中检查移动状态并执行真正的“移动”最后将结果广播出去。所有逻辑都在Entity-Actor的消息框架内安全地执行。3.4 客户端逻辑实现输入与表现在客户端Hotfix层我们需要处理用户输入并响应服务端的同步消息。创建客户端的MoveComponentSystem.csnamespace ET { [ObjectSystem] public class MoveComponentSystem : UpdateSystemMoveComponent { protected override void Update(MoveComponent self) { // 处理服务端同步过来的移动其他玩家的移动 if (self.IsMoving) { Unit unit self.GetParentUnit(); // 客户端进行平滑插值移动避免瞬移卡顿 unit.Position Vector3.Lerp(unit.Position, self.TargetPosition, Time.deltaTime * self.Speed); if (Vector3.Distance(unit.Position, self.TargetPosition) 0.1f) { unit.Position self.TargetPosition; self.IsMoving false; } } // 处理本地玩家输入假设只有一个本地玩家单位 if (self.Id Player.LocalPlayerId) { HandleLocalPlayerInput(self); } } private void HandleLocalPlayerInput(MoveComponent self) { Vector3 input new Vector3(Input.GetAxis(Horizontal), 0, Input.GetAxis(Vertical)); if (input.sqrMagnitude 0.01f) { // 计算目标位置简化处理向前移动一段固定距离 Vector3 targetPos self.GetParentUnit().Position input.normalized * 2f; // 发送移动请求到服务端 C2M_MoveRequest request C2M_MoveRequest.Create(); request.Position targetPos; SessionComponent.Instance.Session.Send(request); // 客户端先进行预测移动提升响应手感 self.StartMove(targetPos); } } } // 处理服务端广播的移动消息 [MessageHandler] public class M2C_MoveBroadcastHandler : AMHandlerM2C_MoveBroadcast { protected override void Run(Session session, M2C_MoveBroadcast message) { Unit unit UnitComponent.Instance.Get(message.UnitId); if (unit ! null unit.Id ! Player.LocalPlayerId) // 不是本地玩家 { MoveComponent moveComp unit.GetComponentMoveComponent(); if (moveComp null) { moveComp unit.AddComponentMoveComponent(); } moveComp.StartMove(message.Position); } } } }客户端的逻辑分为两部分一是驱动本地玩家实体根据输入发送请求并做客户端预测二是接收服务端广播驱动其他玩家实体进行移动。这里展示了经典的“预测与调和”模式雏形客户端为了流畅性先移动再等待服务端权威确认。如果预测错误服务端会发回纠正数据。3.5 网络通信与配置ET框架内置了基于TCP的KCP或TCP协议网络库并封装了简单的Session管理。你通常不需要直接操作Socket。在Unity客户端需要初始化网络// 在某个初始化系统中 SessionComponent.Instance.CreateSession(127.0.0.1:10002); // 连接服务端地址在服务端App的Program.cs中需要启动网络服务StartConfig startConfig ... // 加载配置 Game.Scene.AddComponentNetComponent().CreateService( startConfig.GetComponentInnerConfig().IPEndPoint, ServiceType.Inner, NetworkProtocol.TCP );网络消息的序列化/反序列化、路由、分发都由框架自动完成。你只需要定义好消息体和处理器Handler即可。4. ET框架开发中的进阶技巧与深度优化当你掌握了基础流程后以下几个进阶点是提升项目质量和开发效率的关键。4.1 热更新机制深度解析ET框架与Unity的HybridCLR原xLua/ILRuntime的进化版深度集成实现了真正的C#全链路热更新。这不仅包括逻辑代码甚至包括部分资源。程序集划分这是热更新的基础。你的代码通常被分为Model模型层核心实体、组件、网络消息定义。这部分更新频率低通常作为基础框架的一部分。Hotfix热更层所有的游戏逻辑系统、消息处理器。这是热更新的主体。ModelView HotfixView视图层依赖Unity引擎API的代码如UI、动画、特效控制。这部分也可以热更新。热更流程开发时在Unity编辑器中直接运行所有代码即时编译。发布后将Hotfix和HotfixView程序集打包成AssetBundle。游戏启动时从服务器下载最新的AssetBundle通过HybridCLR加载到运行时替换旧的逻辑。实操心得接口与抽象在Model层定义好清晰的接口和抽象类。Hotfix层的具体实现通过依赖注入如[ObjectSystem]来挂载。这样热更时只需要替换实现类而不需要修改依赖关系。数据驱动将尽可能多的逻辑写到配置表里。热更新时更新配置表数据比更新代码更安全、更灵活。测试陷阱热更新后旧的内存中的实体和组件可能还引用着旧类的方法。ET框架通过EventSystem等机制在热重载后会清理并重新收集所有系统但一些静态变量或全局状态需要手动处理。务必在热更新后进行全面的功能回归测试。4.2 资源管理与AssetBundle策略ET框架倡导的是代码与资源分离、服务端与客户端共享配置的管理模式。共享配置表使用Excel或JSON定义游戏数据如角色属性、技能效果、道具信息。通过ET提供的工具一键生成C#实体类和二进制/JSON配置文件。这套配置客户端和服务端共用同一份从根源上保证数据一致性。AssetBundle管理按功能模块分包UI一个包角色模型一个包场景一个包。避免因一个小资源更新导致下载整个大包。依赖管理ET框架通常有ResourcesComponent来管理AB的加载、依赖和卸载。关键是要遵循“谁加载谁卸载”的原则或者使用引用计数。与ET实体绑定可以创建一个GameObjectComponent挂载在实体上用于管理该实体相关的预制体实例和资源引用。当实体销毁时自动卸载相关资源。优化建议对于频繁创建销毁的对象如子弹、伤害数字务必使用对象池ET内置了ObjectPool。使用Addressables或ET整合的AB管理系统时注意异步加载的回调生命周期管理防止实体已销毁但回调还在执行导致的空引用。4.3 高性能与高并发设计对于服务端性能是生命线。ET框架的Actor模型和单线程事件循环是基础但要发挥其威力还需要良好的设计。设计要点具体做法目的与收益实体划分将不同功能的实体分散到不同的Scene逻辑场景中如UnitScene管理单位ChatScene管理聊天。实现逻辑隔离减少单个场景的压力也便于分布式部署。消息精简设计网络消息时使用short、int代替string使用标志位组合信息采用ProtoBuf等高效序列化。减少网络带宽占用提升序列化/反序列化速度。定时器优化使用ET框架的TimerComponent它内部采用时间轮算法。避免在每帧Update中做复杂的遍历判断。减少不必要的CPU开销定时器精度高、性能好。批量操作广播消息时尽量合并多个更新如多个单位的位置到一个消息中。数据库操作使用批量写入。减少消息数量和系统调用次数提升吞吐量。异步化一切所有I/O操作网络、数据库、文件都必须使用async/await绝不阻塞主事件循环。保证单线程也能高并发充分利用CPU。一个常见的性能陷阱是在消息处理器中执行了同步的、耗时的操作如复杂的数据库查询或同步的HTTP请求这会阻塞整个消息处理队列。务必确保所有Handler方法都是async ETTask并在其中使用异步API。5. 常见“坑点”排查与项目实战经验在实际项目中踩坑是不可避免的。下面是我和团队在多个ET项目中总结的一些典型问题及解决方案。5.1 网络连接与消息问题问题客户端连接服务器失败或连接后很快断开。排查首先检查服务端是否正常启动并监听正确端口netstat -ano。其次检查防火墙设置。最后检查ET框架中StartConfig的地址配置确保客户端连接的IP和端口与服务端InnerConfig或OuterConfig匹配。问题消息发送成功但收不到回复或者Handler没有被触发。排查消息号检查确保请求消息IRequest和回复消息IResponse的Opcode在MessageDispatcher中正确注册。ET通常通过[Message]属性和[MessageHandler]属性自动注册检查是否遗漏。Handler生命周期确认Handler类所在的程序集Hotfix已被正确加载。在热更新后旧的Handler实例可能失效。Session状态检查发送消息的Session是否有效没有断开。可以在发送前加日志。错误码处理在RpcHandler中务必给response.Error赋值。客户端需要检查这个错误码。5.2 实体与组件生命周期管理问题Entity is disposed!错误。这是ET开发中最常见的错误之一表示你正在尝试访问一个已经被销毁的实体或组件。根源异步操作是罪魁祸首。当你发起一个异步操作如网络请求、加载资源时在await返回之前当前的实体可能已经被其他逻辑销毁了例如玩家下线、房间关闭。解决方案使用EntityRef在跨帧或异步回调中持有实体引用时使用EntityRefMyComponent而不是直接引用MyComponent。EntityRef会检查目标是否已被销毁。CancellationToken在发起异步操作时传入一个与实体生命周期绑定的CancellationTokenSource。当实体销毁时取消所有关联的异步操作。判断IsDisposed在await之后任何使用this或局部实体变量前先判断if(this.IsDisposed) return;。public async ETTask SomeAsyncMethod() { long entityId this.Id; // 先保存Id await TimerComponent.Instance.WaitAsync(3000); // await之后this可能已销毁 if (this.IsDisposed) { Log.Debug($Entity {entityId} is disposed, cancel operation.); return; } // 安全地使用this this.DoSomething(); }5.3 数据库操作与数据一致性问题玩家数据丢失或出现奇怪的不一致。排查事务与并发确保对同一个玩家数据的修改在服务端是串行的由Actor模型保证。但涉及多个实体或外部服务如Redis时需要考虑分布式事务或最终一致性方案。缓存与回写ET常配合MongoDB等数据库使用。一种模式是将活跃玩家实体完全放在内存中定时或离线时回写。要确保回写逻辑健壮进程崩溃时有恢复机制如操作日志。配置表热重载热更新配置表后内存中的缓存数据需要同步更新。设计一个配置管理组件在热更后广播一个配置变更事件让所有相关组件重新读取配置。5.4 调试与性能分析服务端调试在Visual Studio或Rider中将服务端App项目设为启动项直接以控制台应用运行和调试。可以方便地设置断点、查看变量。双端联调这是ET的优势。由于逻辑代码同构你可以在客户端单机模式下直接运行大部分游戏逻辑进行调试。网络部分可以暂时Mock掉。性能分析ET框架自身提供了性能分析工具可以监控消息处理耗时、实体数量、内存分配等。使用.NET的DiagnosticSource或第三方性能分析工具如JetBrains dotTrace分析服务端CPU和内存。在客户端结合Unity Profiler和ET的日志分析每帧的耗时分布优化频繁创建实体/组件、频繁发送消息等操作。选择ET框架意味着你选择了一条追求架构清晰、逻辑一致和高性能的道路。它有一定的学习门槛需要你从传统的面向对象思维转变到以实体、组件、消息为核心的架构思维。但一旦掌握其带来的开发效率提升、代码维护性的改善以及“一次编写双端运行”的畅快感会让你觉得这一切都是值得的。它尤其适合有一定规模、需要长期迭代的网络游戏项目。对于小团队或原型项目可能需要评估其前期搭建的复杂度但长远来看一个坚实的架构是项目成功的基石。
返回列表