简介:基于ET框架的斗地主Demo,是一份面向游戏开发入门者的ET框架实践项目。ET框架本身支持分布式部署、高性能网络通信、热更新与Actor事件驱动模型,该Demo通过完整的斗地主玩法,将上述特性落实到可运行的工程中。压缩包共10487个文件,以C#脚本、Unity资源与meta文件为主,C#脚本承载服务端与客户端逻辑,DLL与二进制文件为编译产物,prefab与asset为Unity界面资源,同时涵盖协议定义、配置文档等,包体约81.99MB,目录清晰划分为Server、Proto、Unity、Build等模块。目前已有1132人浏览学习,通过学习这份资料,开发者可以掌握ET框架下服务端与客户端的代码组织方式、网络通信协议的设计与解析、Unity客户端的界面与交互搭建,并熟悉热更新发布流程,适合希望将ET框架理论快速转化为实际开发能力的读者。
1. 基于ET框架的斗地主Demo:它到底在演示什么
很多人在ET框架的Demo里翻车,是从看懂了HelloWorld、却不知道怎么把它变成「一个真正能让玩家玩起来的游戏」开始的。ET框架是一个以C#为核心、天然面向分布式 Actor 模型的开源游戏服务器框架,它的优秀之处不在单机表现,而在「服务器逻辑即客户端逻辑」的共享代码范式。但真正让从业者卡住的,恰恰是如何把实体组件事件(ECS)、Actor消息、状态同步这些抽象概念,落到一张牌桌的完整流程里。斗地主Demo的价值就在这里:牌桌天然有房间、玩家、回合、状态流转,它逼迫你把框架的每个关键机制都用起来,而不是停留在聊天室级别。这篇笔记我会从拆工程、写服务器逻辑、接客户端表现到压测验证,完整讲一遍我自己复现和扩展这个Demo的路径,适合已经跑通ET框架官方教程、开始想做具体玩法的客户端或服务端开发。
2. 拆解ET框架的Demo工程:三层代码与一条消息的完整生命周期
2.1 Model层和Hotfix层,为什么必须分开
拿到ET框架的Demo源码,千万别急着双击.sln启动Unity。先花半小时把目录结构读明白,否则后面每个编译报错都像玄学。
常见的ET框架Demo工程目录是这样的(以实用为准,不绑定某个特定版本):
Unity/Assets/ ├── Model/ # 纯数据结构和协议定义,不引用Unity ├── Hotfix/ # 游戏逻辑,可热更新的核心层 ├── ModelView/ # 视图层数据,引用Unity但尽量薄 ├── HotfixView/ # 视图层逻辑,UI、动画、输入,可热更 └── ThirdParty/ # 第三方库和框架源码Model层里放的应该是纯C#定义:房间状态枚举、出牌数据结构、玩家信息类。Hotfix层放的是游戏规则——叫地主逻辑、出牌合法性、胜负判定。ModelView和HotfixView则负责把数据变成屏幕上玩家看得见的东西。
为什么要这么分?因为ET框架的热更新方案只热更Hotfix和HotfixView,Model层一旦被Unity引用就得跟着主程序打包。如果你把斗地主的发牌逻辑写进Model,以后改一个出牌规则就要整个客户端重发版,热更新就名存实亡了。
我一般会遵循一个简单原则:所有规则类代码进Hotfix,所有状态数据类代码进Model。用「出牌」举例,玩家手牌列表是数据,放Model;判断这手牌能不能管住上家,是规则,放Hotfix。
2.2 从服务器启动到客户端连入:一次会话的建立过程
ET框架的服务器是多进程的,每个进程跑一个端口。斗地主Demo最常见的部署是三个进程:登录服(Realm)、网关服(Gate)、游戏服(Map)。一个玩家从点开客户端到进入牌桌,走的是这样一条链路:
客户端 -> Realm(验证账号) -> 获取Gate地址 -> 连接到Gate Gate -> 创建或分配Map会话 -> 进入房间场景理解这条链路非常重要,因为斗地主的房间状态机、出牌广播和断线重连全都挂在Map服上。Realm和Gate只是入口,真正干活的是Map服里的Room组件。
下面我给出一个简化的服务器启动装配代码,展示如何把必要组件装配到一个进程上:
// 服务器进程入口,以Map服为例 public static class Entry { public static void Start() { // 1. 初始化日志、计时器和配置表 Game.Scene.AddComponent<LogComponent>(); Game.Scene.AddComponent<TimerComponent>(); // 2. 装配网络层,监听指定端口 Game.Scene.AddComponent<NetKcpComponent>(); Game.Scene.AddComponent<GateSessionKeyComponent>(); // 管理网关会话 // 3. 注册Demo所需的服务器组件 Game.Scene.AddComponent<RoomManagerComponent>(); // 房间管理 Game.Scene.AddComponent<NumericWatcherComponent>(); // 数值监听 // 4. 启动异步消息循环 while (true) { Game.Scene.Update(); Thread.Sleep(1); // 每帧保留1ms给操作系统 } } }这段代码说明了ET框架服务器的启动方式:组件驱动一切。NetKcpComponent负责网络收发,RoomManagerComponent负责创建和销毁斗地主房间。新增功能就新增组件,这是ECS思想的直观体现。实际运行中,Thread.Sleep(1)是让出CPU时间片,避免空轮询占满单核,在低并发调试机上很有用。
参数上的一个关键点是:NetKcpComponent监听的端口要和客户端配置的服务器地址一致。很多新手把框架自带的地址配好能连上,自己写了个房间服务就直接挂在同端口上,结果服务器起不来——每个组件实例只允许一个端口,这是框架的保护机制。
2.3 消息协议的定义与Actor消息的发送路径
斗地主Demo里,消息协议的定义集中在Model层。你打开协议文件,看到的是一堆带Message后缀的类。ET框架用protobuf做序列化,但定义方式是直接在C#类上打特性标签:
// 协议定义:客户端请求叫地主 [Message(OuterOpcode.C2S_CallLandlord)] public partial class C2S_CallLandlord : IMessage { public int Multiplier { get; set; } // 叫地主倍数或是否叫地主 } // 协议定义:服务器广播叫地主结果 [Message(OuterOpcode.S2C_CallLandlordResult)] public partial class S2C_CallLandlordResult : IMessage { public long PlayerId { get; set; } public int Multiplier { get; set; } public int CurrentTurn { get; set; } }给类标记Message特性并指定opcode编号,之后就能在代码里用new C2S_CallLandlord()构造消息并通过Session.Send()发送。
发送路径上,客户端发到Gate的是OuterMessage,Gate再包装成InnerMessage转发给Map服。ET框架有个很方便的机制:你在客户端写C2S_CallLandlord,在服务器用MessageHandler<C2S_CallLandlord>就能接收到。中间经过Gate的转发逻辑是框架自动完成的。
这说明了一件事:ET框架的通信模型天然适合斗地主这种「多个客户端同时操作一个房间」的场景。因为每条客户端消息都会携带Session信息,而Room组件管理者所有玩家Session。
3. 服务器端斗地主核心逻辑:房间状态机与出牌校验
3.1 房间的状态流转:从等待到结算
斗地主最核心的是状态机。一个房间要经历:WaitPlayer->DealCards->CallLandlord->PlayCard->Settle-> 销毁或重开。我见过不少人把这个状态机写成一堆switch套if,到后期根本改不动。ET框架的组件模型可以把每个状态做成一个组件,但为了Demo可读性,用状态枚举加一个状态管理组件更合适。
下面是一个简化版Room状态管理组件的实现:
public class RoomComponent : Entity { public RoomState State { get; private set; } public List<long> PlayerIds = new List<long>(3); public int CurrentTurnIndex; // 当前轮到哪个玩家出牌 public async ETTask SetState(RoomState newState) { // 离开旧状态 switch (State) { case RoomState.PlayCard: // 离开出牌阶段,清理本回合临时数据 this.RemoveComponent<PlayCardComponent>(); break; } State = newState; // 进入新状态 switch (State) { case RoomState.CallLandlord: this.AddComponent<CallLandlordComponent>(); break; case RoomState.PlayCard: this.AddComponent<PlayCardComponent>(); break; case RoomState.Settle: this.AddComponent<SettleComponent>(); break; } // 通知所有玩家状态变更 await ETTask.CompletedTask; } }这个实现的关键在于:状态切换不是简单改个枚举,而是通过AddComponent和RemoveComponent来挂载和卸载不同玩法组件。这么做的好处是,斗地主的叫地主逻辑和出牌逻辑完全隔离,互不污染字段。坏处是组件多了之后状态链路难追溯,调试时要打日志。Demo阶段用它是很舒服的。
ETTask是ET框架的异步返回类型,配合协程使用。在服务器端,你不需要像客户端那样在意主线程冻结,但依然用await是因为组件生命周期在异步回调中可能已经变化,await能保证后续操作发生在正确的同步上下文里。
3.2 发牌与洗牌:随机算法和筹码分配
发牌逻辑是Demo里最不会错但最容易写难看的一块。ET框架Demo通常用RandomHelper工具类。注意,服务器端全局的随机数一定要保证种子的统一性,否则三端各自洗牌结果不一致。
// 服务器发牌逻辑 public static List<Card>[] DealCards() { // 初始化一副54张牌 List<Card> allCards = new List<Card>(); for (int suit = 0; suit < 4; suit++) { for (int rank = 3; rank <= 15; rank++) // 3~10, J/Q/K/A/2 { allCards.Add(new Card(suit, rank)); } } allCards.Add(new Card(4, 16)); // 小王 allCards.Add(new Card(4, 17)); // 大王 // 洗牌 RandomHelper.FisherYatesShuffle(allCards); // 发牌:前51张发给三个玩家,最后3张为底牌 List<Card>[] hands = new List<Card>[3]; for (int i = 0; i < 3; i++) { hands[i] = allCards.GetRange(i * 17, 17); } List<Card> bottom = allCards.GetRange(51, 3); // 把底牌广播给所有玩家 return hands; }洗牌功能是ET框架的通用工具,Fisher-Yates算法的核心是保证每个排列等概率,注意不要在循环里用Random.Next()之外的别名算法去碰概率问题。参数说明:牌值是3到15代表3到2,16是小王、17是大王,这个编号方案贯穿整个Demo,出牌比较大小全靠它,不要中途改变映射规则。
实际做的时候还要在发牌之后广播一次手牌给各玩家,并且把底牌先保密——只广播数量而不广播内容。这个需求很常见,做法是给玩家手牌消息和底牌消息分开发送,我是把底牌消息放在叫地主结束之后推的。
3.3 出牌合法性校验:梯形规则用一张枚举表解决
斗地主的出牌校验是所有初学者最头疼的。炸弹、顺子、连对、飞机、三带一、四带二……如果一个个写if,校验函数会长到没法维护。我推荐用「牌型枚举 + 排序组」的方式:把一次出牌视作一个组合,先归类后比较。
public enum CardPattern { Invalid = 0, Single = 1, // 单张 Pair = 2, // 对子 Triple = 3, // 三张 TripleWithOne = 4, // 三带一 Straight = 5, // 顺子 ... } public static CardPattern CheckPattern(List<Card> cards) { if (cards.Count == 0) return CardPattern.Invalid; var sorted = cards.OrderBy(c => c.Rank).ToList(); int count = sorted.Count; // 统计点数出现次数 var groups = sorted.GroupBy(c => c.Rank) .Select(g => new { Rank = g.Key, Count = g.Count() }) .OrderByDescending(g => g.Count) .ToList(); // 初步分支判断 if (count == 1) return CardPattern.Single; if (count == 2 && groups[0].Count == 2) return CardPattern.Pair; if (count == 3 && groups[0].Count == 3) return CardPattern.Triple; // 顺子:最大点与最小点差等于长度减1,且不存在对子 if (count >= 5 && groups.All(g => g.Count == 1)) { int min = sorted[0].Rank; int max = sorted[count - 1].Rank; if (max - min == count - 1 && max < 15) // 顺子中A不参与,2和王也不参与 { return CardPattern.Straight; } } // 其他类型先略,实际项目里按需求持续扩展 return CardPattern.Invalid; }这段代码的核心逻辑:先把所有可能的牌型收敛成「分组数量 + 最大重复数 + 最大值」三个特征,然后对特征组合做模式匹配。以后要加新牌型,只在这里加一个分支即可,不会影响其他规则。
出牌校验中比较大小的规则很清晰:同类型下比较关键牌的最大点数,炸弹大于一切非炸弹,火箭即双王大于炸弹。这是一个纯函数逻辑,不依赖任何外部状态,因此建议写成静态方法并放入Hotfix层,方便未来做服务器压测和单元测试。
3.4 用Actor模型实现回合流转:谁在出牌何时广播
ET框架的Actor模型是理解整个服务器端斗地主的钥匙。简单说,每个玩家Session在Map服对应一个Player实体组件,每个Room是一个Actor。玩家消息发给Room后,Room在自身线程上下文执行逻辑并广播。这样能保证同一个房间的消息是串行处理的,不需要加锁。
// 处理玩家出牌消息 public class C2S_PlayCardHandler : AMActorHandler<RoomComponent, C2S_PlayCard> { protected override async ETTask Run(RoomComponent room, C2S_PlayCard message) { // 1. 验证当前是否轮到该玩家 if (room.CurrentTurnIndex != message.PlayerIndex) { // 不是他的回合,直接返回并提示 return; } // 2. 从玩家手牌中移除打出的牌 var player = room.GetChild<PlayerComponent>(message.PlayerId); bool removed = player.RemoveCards(message.PlayedCards); if (!removed) { // 手牌中没有这些牌,非法操作 return; } // 3. 校验牌型并确认是否压过上一手 var pattern = CardHelper.CheckPattern(message.PlayedCards); if (pattern == CardPattern.Invalid || !CardHelper.CompareGreater(message.PlayedCards, room.LastPlayedCards)) { // 牌型不合法,把牌还回去,并通知客户端出牌失败 player.AddCardsBack(message.PlayedCards); await room.SendToPlayer(message.PlayerId, new S2C_PlayCardFailed()); return; } // 4. 更新房间状态并广播给所有玩家 room.LastPlayedCards = message.PlayedCards; room.CurrentTurnIndex = (room.CurrentTurnIndex + 1) % 3; await room.Broadcast(new S2C_PlayCardSuccess { PlayerId = message.PlayerId, Cards = message.PlayedCards, NextTurn = room.CurrentTurnIndex }); } }这段Actor处理器展示了斗地主服务器的核心节奏:每一步都是「校验——变更——广播」。AMActorHandler是ET框架提供的泛型基类,它自动帮我们定位到消息对应的Room组件。最关键的是Run方法里的内存操作天然线程安全——因为同一个Room的消息只会由同一个线程逻辑链处理,这是Actor模型最大的收益。
注意,AMActorHandler的第二个泛型参数是消息类型,框架会依据消息里携带的ActorId来路由到正确的实体。在客户端构造消息时,需要把Room的InstanceId带上,否则服务器不知道该把消息送给谁。
4. 客户端表现层:协程驱动UI与状态同步
4.1 从服务器消息到UI刷新:用协程编排异步流程
ET框架的客户端是Unity环境,消息接收通常在ModelView层的会话组件里完成。根据消息类型分发到UI控制器。斗地主的UI逻辑不算复杂,但最容易写乱的是「动画播放顺序」和「按钮点击时机」。
用协程可以用同步的写法编排异步流程,避免回调地狱。下面是我常用的客户端处理出牌成功消息的写法:
// 客户端UI控制器,处理服务器回应 public class RoomPanel : Entity { private Dictionary<int, HandCardGroup> handCards; public async ETTask OnPlayCardSuccess(S2C_PlayCardSuccess msg) { // 1. 刷新本地手牌:从手牌组移除已出的牌 var myGroup = handCards[msg.PlayerId == myPlayerId ? 0 : msg.PlayerId]; myGroup.RemoveCards(msg.Cards); // 2. 播放动画:先让牌移到桌面中央 await Game.Scene.GetComponent<UIComponent>() .Get<DeskView>().PlayCardAnimation(msg.PlayerId, msg.Cards); // 3. 更新回合指示器,显示下一个出牌人的高亮 UpdateTurnIndicator(msg.NextTurn); // 4. 如果轮到自己,启用出牌按钮 if (msg.NextTurn == myPlayerId) { EnablePlayButton(); } else { DisablePlayButton(); } } }协程在这里最核心的价值是:await PlayCardAnimation能让UI动画完整播完后才刷新按钮状态,避免了「动画没播完玩家就狂点导致交互错乱」的经典问题。
4.2 手牌排序与交互选中:不涉及服务器状态的表现逻辑
手牌组是斗地主UI中最需要打磨细节的部分。新手写手牌经常做成一个固定横向布局,然后发现选中牌之后,整排牌的位置漂移。正确的做法是手牌用HorizontalLayoutGroup加ContentSizeFitter,在选中状态变化时,改变牌的锚点偏移并调用RebuildLayout()。
public class HandCardItem : MonoBehaviour { private Button button; private RectTransform rect; private bool isSelected; private float selectedOffset = 30f; // 选中时上移像素 private void Start() { button = GetComponent<Button>(); button.onClick.AddListener(OnClick); } private void OnClick() { isSelected = !isSelected; float targetY = isSelected ? selectedOffset : 0f; // 使用DOTween做弹性动画,比直接改anchoredPosition顺滑 rect.DOAnchorPosY(targetY, 0.1f); } }注意selectedOffset是用像素做单位的,在高DPI下需要按实际根节点缩放比例换算。另外,连续点击时DOAnchorPosY会打断上一个动画,所以DOTween的DOAnchorPosY一般配合SetId和OnComplete来防止动画队列堆积。
斗地主的选牌逻辑还有一个常见需求:双击自动出牌。我实现的方案是记录两次点击时间戳,间隔小于0.3秒则把所有已选牌发送出去。你还需要提前保证手牌排序结果和服务器端校验逻辑一致,否则客户端展示的牌型比服务器判定的弱,体验会很奇怪。
4.3 同步帧与预测:斗地主这种回合制游戏要预测吗
很多人做Demo会惯性思维,认为联网游戏需要做客户端预测。斗地主是回合制游戏,天然不需要帧同步和预测。客户端只在操作时发给服务器,服务器广播后就地刷新,客户端的「表现同步」完全依赖于服务器消息。
但这不意味着网络抖动体验就一定好。服务器到客户端的消息有延迟,玩家看到的手牌刷新可能有0.1到0.5秒延迟。解决方法是给玩家的出牌预留一个「确认间隔」:客户端在发出去牌消息后立刻本地移除手牌并播放动画,如果服务器返回失败,再做回滚。这种做法在Demo里很简单,但能显著提升手感。
// 客户端在发送出牌请求后,先本地移除并播放动画 await session.Send(new C2S_PlayCard { PlayerId = myPlayerId, PlayedCards = selectedCards }); // 本地先行移除 myGroup.RemoveCards(selectedCards); // 等服务器确认,若失败则回滚 var result = await session.Call(new C2S_ConfirmPlayCard() { PlayedCards = selectedCards }) as S2C_PlayCardResult; if (!result.Success) { myGroup.AddCardsBack(selectedCards); }注意这里用Call而不是Send,Call会等待服务器响应。这个模式不能滥用——如果每个操作都等服务器返回,就会从「乐观执行」退化成「同步等待」。通常只对「失败惩罚明显」的操作使用,比如出牌校验、叫地主结果。对于倒计时、发牌这类信息,一律用Send加主动刷新。
4.4 热重载调试:改代码不要重编客户端
ET框架最有吸引力的一个特性就是热重载。在Unity里开着播放模式改Hotfix代码,改完按快捷键即可生效,不需要重新编译整个客户端。
具体做法是:Unity菜单栏ET框架的热重载工具,将Hotfix和HotfixView两个程序集的改动编译后,通过网络发送到运行中的客户端。前提是你的架构遵守了之前说的依赖规则——Hotfix层中没有引用Unity API。
我个人的建议是,即使是Demo开发,也务必从第一天就配置好热重载。因为斗地主的UI和规则迭代非常频繁,每改一次手牌排序就重新编译一次,一天下来进度损失有一两个小时。配置热重载的过程半小时内做完,后面都是净收益。
5. 拖垮Demo的五个经典坑:现象、原因与解决
5.1 手牌数据在客户端被篡改,服务器直接吞消息
现象:玩家通过客户端调试工具伪造出牌消息,把手牌里根本没有的牌发给服务器,但服务器处理逻辑完全没反应,玩家就像卡死。
原因:服务器端有校验。但校验失败时没有给客户端任何反馈,客户端还停留在等待状态,看起来就是游戏无响应。
解决:在校验失败的分支,统一返回S2C_PlayCardFailed消息,客户端收到后播放一个短暂抖动动画并禁用出牌按钮0.5秒。这不算什么高级设计,但用户感知从「一直卡死」变成「我做了一次非法操作」。
5.2 同一个Room的多个消息并发处理,把状态搞乱了
现象:两个玩家同时出牌,服务器端偶尔出现「牌被出了两次」或「回合计数错乱」。
原因:Actor模型保证了每个Room的消息按序处理,但你如果绕过Actor通道,直接在Room组件外使用另一个Session直接调用Room的方法,就会破坏串行保证。
解决:所有针对房间的写操作都必须走AMActorHandler派发的消息处理,不能在Gate层或登录流程里直接修改Room内部数据。排查方法是:在RoomComponent的每个公开方法入口打日志,记录调用栈,看哪些操作不是从Actor消息处理器进来的。
5.3 客户端倒计时和服务器时间不同步
现象:玩家看到倒计时还剩5秒,服务器已经在3秒后判定他超时,导致被踢出房间。
原因:客户端用本地DateTime.Now做倒计时,服务器用服务器端时间轴。两个机器的时钟有偏差,而超时判定发生在服务器。
解决:服务器在每次广播"轮到你出牌"时,附带一个服务器时间戳和超时时长。客户端倒计时基于(服务器时间戳 + 超时时长 - 客户端当前时间)计算,而不是直接做本地计时。更稳妥的方案是定期做时间同步校准,在登录握手时下发服务器与客户端的差值。
5.4 少数时候UI按钮点了没反应,轮到你出牌却出不了
现象:玩家手牌都正常,出牌按钮也能点,点击后无响应,随后被服务器超时踢掉。
原因:客户端使用Send发送出牌消息,服务器校验失败后不会处理,也不会通知客户端。典型是玩家选择了一张牌型不合法的手牌组合,服务器只丢弃不反馈。
解决:出牌消息使用Call模式,并设定超时时间。如果服务器超过1秒未返回,客户端本地弹提示消息。同时在客户端本地做一次基础牌型检查,能把大部分非法明显输入拦截在本地。
5.5 新加协议后,客户端和服务器的Opcode对不上
现象:新加一个「显示玩家头像」的消息后,客户端收到的消息全部乱码,老功能也崩溃了。
原因:ET框架的Opcode是序列化后按消息类名的Hash生成或手工指定的。当你手动指定Opcode时,客户端和服务器必须使用完全一致的数字映射。很多时候两边代码不是同步改的,少改了一边。
解决:设置一个独立的Opcode常量类,客户端和服务器共享同一份源文件。每次新增消息,在这个类里递增编号。同时在启动时做一个自检:遍历所有标记了Message特性的类,检查是否有重复Opcode,有则直接抛异常并中断启动。这样问题几乎不可能带到运行阶段。
6. 从Demo到可上线:压测一百个房间时的三个验证技巧
Demo跑通不是终点,能稳定扛住一百个房间才是。斗地主这类游戏服务器压力主要来自消息吞吐和房间状态保存,这里我分享三个我常用的验证技巧。
第一个技巧是「无头客户端压测」。不要把Unity客户端作为压测工具,因为Unity的开销太大,一百个客户端就把本机内存吃光了。用C#写一个控制台项目,引用ET框架的通信库,模拟登录、进入房间、叫地主、出牌全流程。每开一个连接就模拟一个玩家,每秒上报状态。我一般目标是:本机单核能跑三百个模拟客户端无压力。
第二个技巧是「断线重连必测」。斗地主里玩家中途掉线是常态,服务器侧的Player实体要保留一段时间。我做的策略是:掉线后保留Room里的Player 60秒,期间该玩家不出牌由服务器托管(自动过牌)。测试时故意断开网络连接,重连后查看手牌底牌是否完整、当前回合是否还在同一局、能否继续出牌。这个流程在Demo阶段至少跑三遍。
第三个技巧是「服务器单帧耗时打点」。ET框架有Profiler工具,但Demo阶段我习惯在房间状态机的入口和出口分别记录TimeHelper.ClientNow()差值。如果某局游戏单帧耗时超过10毫秒,就说明房间内有人在执行密集的排序或校验逻辑。我遇到过一个问题:玩家的手牌排序放在每次出牌后全量重新排序,导致牌多时卡顿,改成手牌增删+局部插入排序后,单帧耗时从14毫秒降到了1毫秒以内。
最后说一个我自己的教训:斗地主Demo不需要一开始就追求帧同步、断线重连、跨服匹配这些大而全的功能。先把一桌三个人从进房到结算的完整链路跑通,把异常日志和超时提示做全,再谈扩展。很多东西看起来简单,真要压测一把几十局才能体会到服务器代码的质量差距。这个Demo方向是我认为ET框架入门里性价比很高的一个选择,因为它的复杂度刚好够你把Actor、消息分发、协程、热重载全部用上,又没有苛刻的物理和渲染要求。希望帮到你。
本文还有配套的精品资源,点击获取