任务22:【重要】后端-匹配进入游戏房间开发(一)
之前的课时,我们最多也就到realm去作账号验证,realm服务与gate服务之间有过通信。
基本上也就是客户端与网关服务之间通信,从本节开始,将开始较多的要在多端通信了:C2G,G2M,M2G,G2C,C2R,R2C
在这里最好回去《组件Component和实体Entity的应用》https://www.taikr.com/course/1053/task/31030/show 回顾下
实体Entity状态与组件Componet方法的分离,可以实现同一个ET框架,部署到不同服务器上,而Program中添加不同的组件,加载不同的配置设定即可以分别作为Map,Gate,Realm服务器,发挥不同的功能作用。
验证与加深理解“为什么要这样分不同服,哪些发往realm,哪些发往gate,哪些发往房间地图?”
整理好前面做了那么多请求,消息同步,在客户端,网关,及接下来还会与地图之间来来去去很晕
realm只负责验证,登录的验证,注册都在这,所以向realm发相关请求。
你的客户端与服务端的连接session在网关,所以用他来中转消息。进入房间后,用userid创建了gamer实例,之后玩家之间的同步就是在gamer之间同步状态了,所以要通过网关在客户端与map之间同步消息。
一边做一边体会这些handler及相关的组件放在哪个服

在客户端点击进入斗地主图标时,就向服务端发起匹配进入游戏房间的请求。

将匹配组件LandMatchComponent加到服务端起动类中
\Server\App\Program.cs
//斗地主服务端组件 Game.Scene.AddComponent<LandMatchComponent>();
创建C2G_StartMatch_Handler.cs \Server\Hotfix\Landlords\Handler\Gate\C2G_StartMatch_Handler.cs
需要判断用户现有的分值,能不能进入地图房间,比如默认是100 money就行
if (userInfo.Money < roomConfig.MinThreshold)
需要构建地图的session发送EnterMatchs_G2M消息,请求进入房间
//获取斗地主Map服务器的Session Session mapSession = GateHelper.GetMapSession();
网关收到此C2G请求,发送消息到Map,提供了Map上创建gamer需要的UserID,GActorID,CActorID值
mapSession.Send(new EnterMatchs_G2M() { UserID = user.UserID, GActorID = user.InstanceId, CActorID = user.GateSessionID });
C2G_StartMatch_Handler.cs 完整的代码
using System; using ETModel; namespace ETHotfix { [MessageHandler(AppType.Gate)] public class C2G_StartMatch_Handler : AMRpcHandler<C2G_StartMatch_Req, G2C_StartMatch_Back> { protected override async ETTask Run(Session session, C2G_StartMatch_Req request, G2C_StartMatch_Back response, Action reply) { try { Log.Debug("玩家开始匹配"); //验证Session if (!GateHelper.SignSession(session)) { response.Error = ErrorCode.ERR_SignError; reply(); return; } User user = session.GetComponent<SessionUserComponent>().User; //验证玩家是否符合进入房间要求,默认为100底分局 RoomConfig roomConfig = GateHelper.GetLandlordsConfig(RoomLevel.Lv100); UserInfo userInfo = await Game.Scene.GetComponent<DBProxyComponent>().Query<UserInfo>(user.UserID); if (userInfo.Money < roomConfig.MinThreshold) { response.Error = ErrorCode.ERR_UserMoneyLessError; reply(); return; } reply(); //获取斗地主Map服务器的Session //通知Map服务器创建地图上的Gamer Session mapSession = GateHelper.GetMapSession(); mapSession.Send(new EnterMatchs_G2M() { UserID = user.UserID, GActorID = user.InstanceId, CActorID = user.GateSessionID }); await ETTask.CompletedTask; } catch (Exception e) { ReplyError(response, e, reply); } } } }
创建Room,Gamer实体
Gamer.cs
\Server\ET.Core\Landlords\Entity\Map\Gamer.cs
using MongoDB.Bson.Serialization.Attributes; namespace ETModel { [ObjectSystem] public class GamerAwakeSystem : AwakeSystem<Gamer, long> { public override void Awake(Gamer self, long userid) { self.Awake(userid); } } /// <summary> /// 房间玩家对象 /// </summary> public sealed class Gamer : Entity { /// <summary> /// 来自数据库中的永久ID /// </summary> public long UserID { get; private set; } /// <summary> /// 玩家GateActorID /// </summary> public long GActorID { get; set; } /// <summary> /// 玩家ClientActorID /// </summary> public long CActorID { get; set; } /// <summary> /// 默认为假 Session断开/离开房间时触发离线 /// </summary> public bool isOffline { get; set; } public void Awake(long userid) { this.UserID = userid; } public override void Dispose() { if(this.IsDisposed) { return; } base.Dispose(); this.UserID = 0; this.GActorID = 0; this.CActorID = 0; this.isOffline = false; } } }
Room.cs
\Server\ET.Core\Landlords\Entity\Map\Room.cs
using System.Collections.Generic; using System.Linq; namespace ETModel { /// <summary> /// 房间配置 /// </summary> public struct RoomConfig { /// <summary> /// 倍率 /// </summary> public int Multiples { get; set; } /// <summary> /// 基础分 /// </summary> public long BasePointPerMatch { get; set; } /// <summary> /// 房间最低门槛 /// </summary> public long MinThreshold { get; set; } } /// <summary> /// 房间对象 /// </summary> public sealed class Room : Entity { /// <summary> /// 当前房间的3个座位 UserID/seatIndex /// </summary> public readonly Dictionary<long, int> seats = new Dictionary<long, int>(); /// <summary> /// 当前房间的所有所有玩家 空位为null /// </summary> public readonly Gamer[] gamers = new Gamer[3]; public readonly bool[] isReadys = new bool[3]; /// <summary> /// 房间中玩家的数量 /// </summary> public int Count { get { return seats.Values.Count; } } public override void Dispose() { if (this.IsDisposed) { return; } base.Dispose(); seats.Clear(); for (int i = 0; i < gamers.Length; i++) { if (gamers[i] != null) { gamers[i].Dispose(); gamers[i] = null; } } for(int i = 0;i<isReadys.Length;i++) { isReadys[i] = false; } } } }
需要用到的一些枚举数据
\Server\ET.Core\Landlords\Other\Type.cs
namespace ETModel { /// <summary> /// 房间等级 /// </summary> public enum RoomLevel { Lv100 //100底分局 } /// <summary> /// 身份 /// </summary> public enum Identity { None, Farmer, //平民 Landlord //地主 } /// <summary> /// 出牌类型 /// </summary> public enum CardsType { None, JokerBoom, //王炸 Boom, //炸弹 OnlyThree, //三张 ThreeAndOne, //三带一 ThreeAndTwo, //三带二 Straight, //顺子 五张或更多的连续单牌 DoubleStraight, //双顺 三对或更多的连续对牌 TripleStraight, //三顺 二个或更多的连续三张牌 Double, //对子 Single //单牌 } public enum Weight { Three, //3 Four, //4 Five, //5 Six, //6 Seven, //7 Eight, //8 Nine, //9 Ten, //10 Jack, //J Queen, //Q King, //K One, //A Two, //2 SJoker, //小王 LJoker, //大王 } public enum Suits { Club, //梅花 Diamond, //方块 Heart, //红心 Spade, //黑桃 None } }
GateHelper增加GetLandlordsConfig,GetLandlordsSession方法
\Server\Hotfix\Helper\GateHelper.cs
/// <summary> /// 获取斗地主房间配置 不满足要求不能进入房间 /// </summary> /// <param name="level"></param> /// <returns></returns> public static RoomConfig GetLandlordsConfig(RoomLevel level) { RoomConfig config = new RoomConfig(); switch (level) { case RoomLevel.Lv100: config.BasePointPerMatch = 100; config.Multiples = 1; config.MinThreshold = 100 * 10; break; } return config; } /// <summary> /// 获取斗地主游戏专用Map服务器的Session /// </summary> /// <returns></returns> public static Session GetMapSession() { StartConfigComponent config = Game.Scene.GetComponent<StartConfigComponent>(); IPEndPoint mapIPEndPoint = config.MapConfigs[0].GetComponent<InnerConfig>().IPEndPoint; Log.Debug(mapIPEndPoint.ToString()); Session mapSession = Game.Scene.GetComponent<NetInnerComponent>().Get(mapIPEndPoint); return mapSession; }
斗地主匹配组件LandMatchComponent
\Server\ET.Core\Landlords\Component\Map\LandMatchComponent.cs
using System.Collections.Generic; namespace ETModel { /// <summary> /// 斗地主匹配组件,匹配逻辑在LandordsComponentSystem扩展 /// </summary> public class LandMatchComponent : Component { /// <summary> /// 所有游戏中的房间列表 /// </summary> public readonly Dictionary<long, Room> GamingLandlordsRooms = new Dictionary<long, Room>(); /// <summary> /// 所有游戏没有开始的房间列表 Room.Id/Room /// </summary> public readonly Dictionary<long, Room> FreeLandlordsRooms = new Dictionary<long, Room>(); /// <summary> /// 所有在房间中待机的玩家 UserID/Room /// </summary> public readonly Dictionary<long, Room> Waiting = new Dictionary<long, Room>(); /// <summary> /// 所有正在游戏的玩家 UserID/Room /// </summary> public readonly Dictionary<long, Room> Playing = new Dictionary<long, Room>(); /// <summary> /// 匹配中的玩家队列 /// </summary> public readonly Queue<Gamer> MatchingQueue = new Queue<Gamer>(); } }
地图服务上G2M_EnterMatch_Handler
整理一下思路:
每一个玩家登录后,在gate上A0003_LoginGateHanler时,创建了他的User实例,user绑定了通信session
- session.AddComponent<SessionUserComponent>().User = user;
- user.GateSessionID = session.InstanceId;
每一个玩家在Map上,有用他的UserID构建的Gamer实例
gamer存着GActorID(gate user's InstanceId)
- 这样M2G就可以用GActorID构建ActorMessageSender,向网关通信
gamer存着CActorID(gate user's GateSessionID)
- 这样M2C可以用CActorID构建ActorMessageSender,向客户端通信
通过LandMatchComponent的Waiting判断有没有包含传来的UserID
- 有,说明他已经在一个人数不够的房间等待匹配。
- 没有,就创建gamer实例,加入到匹配队列。
\Server\Hotfix\Landlords\Handler\Map\G2M_EnterMatch_Handler.cs
using System; using ETModel; using System.Threading.Tasks; namespace ETHotfix { /// <summary> /// Gate向Map发送“玩家请求匹配”消息 /// </summary> [MessageHandler(AppType.Map)] public class G2M_EnterMatch_Handler : AMHandler<EnterMatchs_G2M> { protected override async ETTask Run(Session session, EnterMatchs_G2M message) { //Log.Debug("Map服务器收到第一条消息"); LandMatchComponent matchComponent = Game.Scene.GetComponent<LandMatchComponent>(); //玩家是否已经开始游戏 if (matchComponent.Waiting.ContainsKey(message.UserID)) { Room room; matchComponent.Waiting.TryGetValue(message.UserID,out room); Gamer gamer = room.GetGamerFromUserID(message.UserID); //设置GateActorID,ClientActorID gamer.GActorID = message.GActorID; gamer.CActorID = message.CActorID; //向Gate发送消息更新Gate上user的ActorID //这样不论玩家到了哪个地图服务器,Gate上的user都持有所在地图服务器上gamer的InstanceId ActorMessageSender actorProxy = Game.Scene.GetComponent<ActorMessageSenderComponent>().Get(gamer.GActorID); actorProxy.Send(new Actor_MatchSucess_M2G() { GamerID = gamer.InstanceId }); } else { //新建玩家,用UserID构建Gamer Gamer newgamer = ComponentFactory.Create<Gamer, long>(message.UserID); newgamer.GActorID = message.GActorID; newgamer.CActorID = message.CActorID; //为Gamer添加组件 await newgamer.AddComponent<MailBoxComponent>().AddLocation(); //添加玩家到匹配队列 广播一遍正在匹配中的玩家 matchComponent.AddGamerToMatchingQueue(newgamer); } } } }
M2G_MatchSucess_Handler
\Server\Hotfix\Landlords\Handler\Gate\M2G_MatchSucess_Handler.cs
[ActorMessageHandler(AppType.Gate)] public class M2G_MatchSucess_Handler : AMActorHandler<User, Actor_MatchSucess_M2G> { protected override async ETTask Run(User user, Actor_MatchSucess_M2G message) { //gate更新ActorID user.ActorID = message.GamerID; Log.Info($"玩家{user.UserID}匹配成功 更新客户端Actor转发向Gamer"); await ETTask.CompletedTask; } }
服务端取消iActorMessage消息分发的注释
\Server\Hotfix\Module\Message\OuterMessageDispatcher.cs

LandMatchComponentSystem
LandMatchComponent的扩展方法,有匹配队列,加入房间,向网关通知有一名玩家加入匹配队列的方法。
\Server\Hotfix\Landlords\System\LandMatchComponentSystem.cs
using ETModel; using System.Collections.Generic; using System.Net; using System.Linq; namespace ETHotfix { public static class LandMatchComponentSystem { /// <summary> /// 已开始游戏的玩家可以迅速找到自己的房间 /// </summary> public static Room GetGamingRoom(this LandMatchComponent self, Gamer gamer) { Room room; if (!self.Playing.TryGetValue(gamer.UserID, out room)) { Log.Error("玩家不在已经开始游戏的房间中"); } return room; } /// <summary> /// 返回未开始游戏的房间 用于处理准备消息 /// </summary> public static Room GetWaitingRoom(this LandMatchComponent self, Gamer gamer) { Room room; if (!self.Waiting.TryGetValue(gamer.UserID, out room)) { Log.Error("玩家不在待机的房间中"); } return room; } /// <summary> /// 获取一个可以添加座位的房间 没有则返回null /// </summary> public static Room GetFreeLandlordsRoom(this LandMatchComponent self) { return self.FreeLandlordsRooms.Where(p => p.Value.Count < 3).FirstOrDefault().Value; } /// <summary> /// 斗地主匹配队列人数加一 /// 队列模式 所以没有插队/离队操作 /// 队列满足匹配条件时 创建新房间 /// </summary> public static void AddGamerToMatchingQueue(this LandMatchComponent self, Gamer gamer) { //添加玩家到队列 self.MatchingQueue.Enqueue(gamer); Log.Debug("一位玩家加入队列"); //广播通知所有匹配中的玩家 self.Broadcast(new Actor_LandMatcherPlusOne_NTT() { MatchingNumber = self.MatchingQueue.Count }); //检查匹配状态 self.MatchingCheck(); } /// <summary> /// 检查匹配状态 每当有新排队玩家加入时执行一次 /// </summary> /// <param name="self"></param> public static async void MatchingCheck(this LandMatchComponent self) { //如果有空房间 且 正在排队的玩家>0 Room room = self.GetFreeLandlordsRoom(); if (room != null) { while (self.MatchingQueue.Count > 0 && room.Count < 3) { self.JoinRoom(room, self.MatchingQueue.Dequeue()); } } //else 如果没有空房间 且 正在排队的玩家>=1 else if (self.MatchingQueue.Count >= 1) { //创建新房间 room = ComponentFactory.Create<Room>(); await room.AddComponent<MailBoxComponent>().AddLocation(); self.FreeLandlordsRooms.Add(room.Id, room); while (self.MatchingQueue.Count > 0 && room.Count < 3) { self.JoinRoom(room, self.MatchingQueue.Dequeue()); } } } /// <summary> /// 加入房间 /// </summary> public static void JoinRoom(this LandMatchComponent self, Room room, Gamer gamer) { //玩家可能掉线 if(gamer == null) { return; } //玩家加入房间 成为已经进入房间的玩家 //绑定玩家与房间 以后可以通过玩家UserID找到所在房间 self.Waiting[gamer.UserID] = room; //为玩家添加座位 room.Add(gamer); //房间广播 Log.Info($"玩家{gamer.UserID}进入房间"); Actor_GamerEnterRoom_Ntt broadcastMessage = new Actor_GamerEnterRoom_Ntt(); foreach (Gamer _gamer in room.gamers) { if (_gamer == null) { //添加空位 broadcastMessage.Gamers.Add(new GamerInfo()); continue; } //添加玩家信息 //GamerInfo info = new GamerInfo() { UserID = _gamer.UserID, IsReady = room.IsGamerReady(gamer) }; GamerInfo info = new GamerInfo() { UserID = _gamer.UserID }; broadcastMessage.Gamers.Add(info); } //广播房间内玩家消息 每次有人进入房间都会收到一次广播 room.Broadcast(broadcastMessage); //向Gate上的User发送匹配成功 ActorMessageSender actorProxy = Game.Scene.GetComponent<ActorMessageSenderComponent>().Get(gamer.GActorID); actorProxy.Send(new Actor_MatchSucess_M2G() { GamerID = gamer.InstanceId }); } /// <summary> /// 匹配队列广播 /// </summary> public static void Broadcast(this LandMatchComponent self, IActorMessage message) { foreach (Gamer gamer in self.MatchingQueue) { //向客户端User发送Actor消息 ActorMessageSenderComponent actorProxyComponent = Game.Scene.GetComponent<ActorMessageSenderComponent>(); ActorMessageSender actorProxy = actorProxyComponent.Get(gamer.CActorID); Log.Debug("转发给了客户端一条消息,客户端Session:" + gamer.CActorID.ToString()); actorProxy.Send(message); } } } }
RoomSystem
Room实体的扩展方法,有玩家进入与离开房间,检查玩家是不是准备好,开始游戏,向客户端广播有人进入房间的方法。
\Server\Hotfix\Landlords\System\RoomSystem.cs
using ETModel; namespace ETHotfix { public static class RoomSystem { /// <summary> /// 检查游戏是否可以开始 /// </summary> public static void CheckGameStart(this Room self) { Log.Debug("检查游戏是否可以开始"); bool isOK = true; for (int i = 0;i <self.isReadys.Length;i ++) { //遍历所有准备状态 任何一个状态为false结果都是false if(self.isReadys[i] == false) { isOK = false; } } if(isOK) { Log.Debug("满足游戏开始条件"); self.GameStart(); } } /// <summary> /// 斗地主游戏开始 /// </summary> public static void GameStart(this Room self) { //更改房间状态 从空闲房间移除 添加到游戏中房间列表 LandMatchComponent Match = Game.Scene.GetComponent<LandMatchComponent>(); Match.FreeLandlordsRooms.Remove(self.Id); Match.GamingLandlordsRooms.Add(self.Id, self); //更该玩家状态 for(int i=0;i<self.gamers.Length;i++) { Gamer gamer = self.gamers[i]; Match.Waiting.Remove(gamer.UserID); Match.Playing.Add(gamer.UserID, self); } //添加开始斗地主游戏需要的组件 //... //开始游戏 //self.GetComponent<GameControllerComponent>().StartGame(); } /// <summary> /// 添加玩家 没有空位时提示错误 /// </summary> /// <param name="gamer"></param> public static void Add(this Room self, Gamer gamer) { int seatIndex = self.GetEmptySeat(); //玩家需要获取一个座位坐下 if (seatIndex >= 0) { self.gamers[seatIndex] = gamer; self.isReadys[seatIndex] = false; self.seats[gamer.UserID] = seatIndex; } else { Log.Error("房间已满无法加入"); } } /// <summary> /// 获取房间中的玩家 /// </summary> public static Gamer GetGamerFromUserID(this Room self, long id) { int seatIndex = self.GetGamerSeat(id); if (seatIndex >= 0) { return self.gamers[seatIndex]; } return null; } /// <summary> /// 获取玩家座位索引 /// </summary> public static int GetGamerSeat(this Room self, long id) { if (self.seats.TryGetValue(id, out int seatIndex)) { return seatIndex; } return -1; } /// <summary> /// 返回玩家是否已准备 玩家不在房间时返回false /// </summary> public static bool IsGamerReady(this Room self, Gamer gamer) { int seatIndex = self.GetGamerSeat(gamer.UserID); if(seatIndex>0) { return self.isReadys[seatIndex]; } return false; } /// <summary> /// 移除玩家并返回 玩家离开房间 /// </summary> public static Gamer Remove(this Room self, long id) { int seatIndex = self.GetGamerSeat(id); if (seatIndex >= 0) { Gamer gamer = self.gamers[seatIndex]; self.gamers[seatIndex] = null; self.seats.Remove(id); return gamer; } return null; } /// <summary> /// 获取空座位 /// </summary> /// <returns>返回座位索引,没有空座位时返回-1</returns> public static int GetEmptySeat(this Room self) { for (int i = 0; i < self.gamers.Length; i++) { if (self.gamers[i] == null) { return i; } } return -1; } /// <summary> /// 广播消息 /// </summary> public static void Broadcast(this Room self, IActorMessage message) { foreach (Gamer gamer in self.gamers) { //如果玩家不存在或者不在线 if (gamer == null || gamer.isOffline) { continue; } //向客户端User发送Actor消息 ActorMessageSenderComponent actorProxyComponent = Game.Scene.GetComponent<ActorMessageSenderComponent>(); ActorMessageSender actorProxy = actorProxyComponent.Get(gamer.CActorID); actorProxy.Send(message); } } } }
玩家在进入地图时,会在地图上创建他的gamer,gamer上存着:
GActorID(gate user's InstanceId),CActorID(gate user's GateSessionID)
M2G可以用GActorID构建ActorMessageSender,向网关通信,
- LandMatchComponentSystem.cs 中发送Actor_MatchSucess_M2G用到。
- G2M_EnterMatch_Handler.cs 中发送Actor_MatchSucess_M2G用到
M2C可以用CActorID构建ActorMessageSender,向客户端通信,
- LandMatchComponentSystem.cs 中Broadcast方法用到。
- RoomSystem.cs 中Broadcast方法用到
最后是本节的消息指令与消息体
\Proto\HotfixMessage.proto
请注意下Actor_GamerEnterRoom_Ntt 中的repeated GamerInfo Gamers = 1;
扩展阅读:https://www.taikr.com/article/3934
//==>匹配玩家并进入斗地主游戏房间 4月18 //玩家信息 message GamerInfo { int64 UserID = 1; //玩家ID } //返回大厅 message C2G_ReturnLobby_Ntt // IMessage { int32 RpcId = 90; } //斗地主匹配模块 message C2G_StartMatch_Req // IRequest { int32 RpcId = 90; } message G2C_StartMatch_Back // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; } message Actor_LandMatcherPlusOne_NTT // IActorMessage { int32 RpcId = 90; int64 ActorId = 93; int32 MatchingNumber = 1; // 当前排队人数 } message Actor_LandMatcherReduceOne_NTT // IActorMessage { int32 RpcId = 90; int64 ActorId = 93; int32 MatchingNumber = 1; // 当前排队人数 } //进入房间(广播) message Actor_GamerEnterRoom_Ntt // IActorMessage { int32 RpcId = 90; int64 ActorId = 93; repeated GamerInfo Gamers = 1; } //退出房间(广播) message Actor_GamerExitRoom_Ntt // IActorMessage { int32 RpcId = 90; int64 ActorId = 93; int64 UserID = 1; } //匹配玩家并进入斗地主游戏房间 <==
\Proto\InnerMessage.proto
//==>匹配玩家并进入斗地主游戏房间 4月18 //Map通知Gate匹配成功 message Actor_MatchSucess_M2G // IActorMessage { int64 ActorId = 94; int64 GamerID = 1; } //Gate通知Map 玩家请求匹配 message EnterMatchs_G2M // IMessage { int32 RpcId = 90; int64 UserID = 1; //Gate上User的UserID int64 GActorID = 2; //Gate上User的InstanceId int64 CActorID = 3; //Gate上User的GateSessionID } //匹配玩家并进入斗地主游戏房间 <==
随着课程的推进,建立清晰的通信模型:
Realm端:只负责注册与查询验证,并不存用户相关的实例(user,gamer)
玩家在客户端有User,Gamer实例,都持有UserID
玩家在网关上有User实例,持有UserID,user绑定了sesion
玩家在地图上有Gamer实例,持有UserID,GActorID,CActorID
建立清晰的端与端通信原理:
客户端,各服务器都是不同的端,都不在一台设备上。只是用相同的UserDI,分别在这些端创建user,gamer实例,实例上存着需要的id号来识别谁是谁和找到连接session。
session就是在不同端建立的持续通信的连接。
Client与Gate通信,网关上存着user绑定了session,有UserID就能找到连接session进行通信。
服务端之间,你可以在每个服务端定义一个构建和获取与其它服务端的连接session的方法。
非网关服务器=》客户端,可以用ClientActorID(记得通过消息字段传递ClientActorID),构建ActorMessageSender通信。
到这里为止,已经实现了对玩家的匹配功能,但是还不能真正开始斗地主的游戏。
//添加开始斗地主游戏需要的组件
//...//开始游戏
//self.GetComponent<GameControllerComponent>().StartGame();
看这些代码就知道还不能真正开始游戏,我们暂时可以在这里向客户端发送一条开始游戏的通知,测试在客户端收到这条消息后,加载房间界面来看到本节实现的效果。
可以用素材中的房间图片做一个房间界面的预制体,上面带一个通知文本。
你能先在本节自己尝试实现这一测试效果吗?
你能尝试实现在房间界面的通知文本显示有玩家进入房间和离开房间的消息吗?

浙公网安备 33010602011771号