任务10:前端-注册登录需要的实体、组件与请求及消息
本节要创建前端的Entity,Component,及UI上的请求逻辑,消息指令与消息体
即便是游戏网络开发新人,也应该有一个初步的思路,我们要通过服务器来同步玩家之间的消息。
如果你是新接触网络游戏开发,真正开始着手开发时,脑子里一开始应该是比较空白的,到底同步什么?是玩家之间同步消息,那落实到代码时,是什么对象之间同步?
这是一个有经验的人自然明白,新人却需要面对的基本问题。
实现这些代码是一个比较复杂的过程,但可以先理一个基本的框架:
先大体理解这个基本框架,如果看不太懂也要有一个印象,当你做到后面对应的功能时,应该能印证,记得回到这里加深理解,理清思路。
搞清楚为什么接下来,我们要在前后端创建那些组件,实体,为何要构建和传递那些消息内容?
在实体之间同步消息:
- 服务端有人登录从数据库查得UserID,创建User实体实例存着UserID
- 前端登录成功取得服务端传来的UserID,也创建User实体实例存着UserID
- 服务端收到请求匹配玩家到一个房间
- 创建Room实体实例(服务端管理很多房间,每个room实例管理这房间中的gamers)
- 进入房间的User,用其UserID创建Gamer实体实例
- 用UserID和Gamer实例作为Key,value存着房间中的玩家
- 前端被分配到一个房间收到消息
- 创建uiRoom:创建UI实体实例,此实例添加房间界面组件(可以理解这对应于服务端的一个Room,前端就只有自己一个房间,uiRoom管理前端这个房间中的gamers)
- 取得服务端传来的房间中的users,用每个UserID创建Gamer实体实例
- 用UserID和座位编号作为Key,value存着房间中的玩家
用座位编号和Gamer实例作为Key,value存着房间中的玩家
要同步的消息举例UserID,Cards:
- 有人出牌,即将他的UserID与Cards作为数据发往服务端
- 服务端将出牌的UserID与Cards消息广播到房间内的玩家客户端
- 根据UserID取得对应的gamer实例,gamer上面的界面组件更新显示牌与其它出牌状态
前端要创建的实体和组件在这个目录下 \Assets\Model\Landlords
GamerComponent 是前端管理所有Gamer的组件
添加GamerComponent.cs
\Assets\Model\Landlords\Component\GamerComponent.cs
using System.Collections.Generic; using System.Linq; namespace ETModel { [ObjectSystem] public class GamerComponentAwakeSystem : AwakeSystem<GamerComponent> { public override void Awake(GamerComponent self) { self.Awake(); } } public class GamerComponent : Component { public static GamerComponent Instance { get; private set; } public User MyUser; /// <summary> /// UserID Gamer /// </summary> private readonly Dictionary<long, Gamer> idGamers = new Dictionary<long, Gamer>(); public void Awake() { Instance = this; } public override void Dispose() { if (this.IsDisposed) { return; } base.Dispose(); foreach (Gamer gamer in this.idGamers.Values) { gamer.Dispose(); } this.idGamers.Clear(); Instance = null; } public void Add(Gamer gamer) { this.idGamers.Add(gamer.UserID, gamer); } public Gamer Get(long userid) { Gamer gamer; this.idGamers.TryGetValue(userid, out gamer); return gamer; } public void Remove(long userid) { Gamer gamer; this.idGamers.TryGetValue(userid, out gamer); this.idGamers.Remove(userid); gamer?.Dispose(); } public void RemoveNoDispose(long userid) { this.idGamers.Remove(userid); } public int Count { get { return this.idGamers.Count; } } public Gamer[] GetAll() { return this.idGamers.Values.ToArray(); } } }
添加Gamer.cs
\Assets\Model\Landlords\Entity\Gamer.cs
using UnityEngine; using Quaternion = UnityEngine.Quaternion; using Vector3 = UnityEngine.Vector3; namespace ETModel { [ObjectSystem] public class GamerSystem : AwakeSystem<Gamer, long> { public override void Awake(Gamer self, long userid) { self.Awake(userid); } } public sealed class Gamer : Entity { /// <summary> /// 每个玩家绑定一个实体 机器人的UserID为0 /// </summary> public long UserID { get; private set; } //public GameObject GameObject; public void Awake(long userid) { this.UserID = userid; } public Vector3 Position { get { return GameObject.transform.position; } set { GameObject.transform.position = value; } } public Quaternion Rotation { get { return GameObject.transform.rotation; } set { GameObject.transform.rotation = value; } } public override void Dispose() { if (this.IsDisposed) { return; } base.Dispose(); } } }
添加User.cs
\Assets\Model\Landlords\Entity\User.cs
namespace ETModel { [ObjectSystem] public class UserAwakeSystem : AwakeSystem<User, long> { public override void Awake(User self, long id) { self.Awake(id); } } /// <summary> /// 玩家对象 /// </summary> public sealed class User : Entity { //用户ID(唯一) public long UserID { get; private set; } public void Awake(long id) { this.UserID = id; } public override void Dispose() { if (this.IsDisposed) { return; } base.Dispose(); this.UserID = 0; } } }
在前端Init.cs 中添加GamerComponent组件
Game.Scene.AddComponent<GamerComponent>(); //加上消息分发组件MessageDispatcherComponent Game.Scene.AddComponent<MessageDispatcherComponent>();
确认下前端Init.cs需要添加的组件
UIEventType.cs中增加了一些 LandUI与Event种类
\Assets\Model\Landlords\LandUI\UIEventType.cs
public static partial class LandUIType { public const string LandLogin = "LandLogin"; } public static partial class UIEventType { //斗地主EventIdType public const string LandInitSceneStart = "LandInitSceneStart"; public const string LandLoginFinish = "LandLoginFinish"; }
UIEventType.cs 增加移除登录界面事件
//移除登录界面事件 [Event(UIEventType.LandLoginFinish)] public class LandLoginFinish : AEvent { public override void Run() { Game.Scene.GetComponent<UIComponent>().Remove(LandUIType.LandLogin); } }
登录注册按钮逻辑实现(前面做过练习没?)
LandLoginComponent中增加按钮事件
\Assets\Model\Landlords\LandUI\LandLogin\LandLoginComponent.cs
public void LoginBtnOnClick() { if (this.isLogining || this.IsDisposed) { return; } this.isLogining = true; LandHelper.Login(this.account.text, this.password.text).Coroutine(); } public void RegisterBtnOnClick() { if (this.isRegistering || this.IsDisposed) { return; } this.isRegistering = true; LandHelper.Register(this.account.text, this.password.text).Coroutine(); }
添加LandHelper重点介绍一下其中的登录,注册请求逻辑
登录请求,有一个认证再转到网关的过程
首先我们要向realm请求,这个请求会在服务端调用对应的Handler,我们只需要有Hanlder加上[MessageHandler(AppType.Realm)]属性,就会由Realm服务器响应此请求(后面服务端的代码会介绍)。
构建sessionRealm 这是由配置文件中提供的服务器地址创建的Session对象
以登录名与密码作为消息内容发送 A0002_Login_C2R 请求
获得返回 messageRealm.GateAddress 网关的地址
代码在LandHelper.cs的Login方法中,是由UI登录按钮触发调用的。
Session sessionRealm = Game.Scene.GetComponent<NetOuterComponent>().Create(GlobalConfigComponent.Instance.GlobalProto.Address); A0002_Login_R2C messageRealm = (A0002_Login_R2C)await sessionRealm.Call(new A0002_Login_C2R() { Account = account, Password = password });
构建网关session ,这是由前面获得的网关地址创建的session对象
以messageRealm.GateLoginKey作为消息体向网关发送A0003_LoginGate_C2G登录网关请求
获得返回的messageGate.UserID
//创建网关 session Session sessionGate = Game.Scene.GetComponent<NetOuterComponent>().Create(messageRealm.GateAddress); A0003_LoginGate_G2C messageGate = (A0003_LoginGate_G2C)await sessionGate.Call(new A0003_LoginGate_C2G() { GateLoginKey = messageRealm.GateLoginKey });
所以我们发现登录的过程是,前端先向Realm服务发出登录请求,服务端认证了用户名和密码后,会在服务器上由Realm向Gate请求获得一个网关登录的GateLoginKey,然后Realm连同网关地址GateAddress一起返回给了客户端接收返回消息的A0002_Login_R2C messageRealm对象。
然后前端再向Gate服务发布网关登录请求,最后实现了登录。
分成这两步的目的是:一方面在Realm服务完成了验证,另一方面在网关上把你的UserID与GateLoginKey进行了绑定,这样网关上就有这个用户数据。
上图插播一下,可以看到Realm向Gate请求获得一个网关登录的GateLoginKey,就是把UserID与GateLoginKey进行了绑定。
而注册请求,就是直接向Realm发起了注册,存入了用户账号与密码数据。
添加完整的 LandHelper.cs
\Assets\Model\Landlords\LandUI\LandHelper.cs
using UnityEngine; using System.Collections.Generic; using UnityEngine.UI; namespace ETModel { public static class LandHelper { //A0 01注册 02登录realm 03登录gate public static async ETVoid Login(string account, string password) { LandLoginComponent login = Game.Scene.GetComponent<UIComponent>().Get(LandUIType.LandLogin).GetComponent<LandLoginComponent>(); //创建Realm session Session sessionRealm = Game.Scene.GetComponent<NetOuterComponent>().Create(GlobalConfigComponent.Instance.GlobalProto.Address); A0002_Login_R2C messageRealm = (A0002_Login_R2C)await sessionRealm.Call(new A0002_Login_C2R() { Account = account, Password = password }); sessionRealm.Dispose(); login.prompt.text = "正在登录中..."; //判断Realm服务器返回结果 if (messageRealm.Error == ErrorCode.ERR_AccountOrPasswordError) { login.prompt.text = "登录失败,账号或密码错误"; login.account.text = ""; login.password.text = ""; login.isLogining = false; return; } //判断通过则登陆Realm成功 //创建网关 session Session sessionGate = Game.Scene.GetComponent<NetOuterComponent>().Create(messageRealm.GateAddress); if (SessionComponent.Instance == null) { //Log.Debug("创建唯一Session"); Game.Scene.AddComponent<SessionComponent>().Session = sessionGate; } else { //存入SessionComponent方便我们随时使用 SessionComponent.Instance.Session = sessionGate; //Game.EventSystem.Run(EventIdType.SetHotfixSession); } A0003_LoginGate_G2C messageGate = (A0003_LoginGate_G2C)await sessionGate.Call(new A0003_LoginGate_C2G() { GateLoginKey = messageRealm.GateLoginKey }); //判断登陆Gate服务器返回结果 if (messageGate.Error == ErrorCode.ERR_ConnectGateKeyError) { login.prompt.text = "连接网关服务器超时"; login.account.text = ""; login.password.text = ""; sessionGate.Dispose(); login.isLogining = false; return; } //判断通过则登陆Gate成功 login.prompt.text = ""; User user = ComponentFactory.Create<User, long>(messageGate.UserID); GamerComponent.Instance.MyUser = user; //Log.Debug("登陆成功"); //加载透明界面 退出当前界面 Game.EventSystem.Run(UIEventType.LandLoginFinish); } public static async ETVoid Register(string account, string password) { Session session = Game.Scene.GetComponent<NetOuterComponent>().Create(GlobalConfigComponent.Instance.GlobalProto.Address); A0001_Register_R2C message = (A0001_Register_R2C)await session.Call(new A0001_Register_C2R() { Account = account, Password = password }); session.Dispose(); LandLoginComponent login = Game.Scene.GetComponent<UIComponent>().Get(LandUIType.LandLogin).GetComponent<LandLoginComponent>(); login.isRegistering = false; if (message.Error == ErrorCode.ERR_AccountAlreadyRegisted) { login.prompt.text = "注册失败,账号已被注册"; login.account.text = ""; login.password.text = ""; return; } if (message.Error == ErrorCode.ERR_RepeatedAccountExist) { login.prompt.text = "注册失败,出现重复账号"; login.account.text = ""; login.password.text = ""; return; } login.prompt.text = "注册成功"; } } }
把需要用到的错误定义一下
\Assets\ET.Core\Module\Message\LandlordsOuterErrorCode.cs
//自定义错误 public const int ERR_AccountAlreadyRegisted = 300001; public const int ERR_RepeatedAccountExist = 300002; public const int ERR_UserNotOnline = 300003; public const int ERR_CreateNewCharacter = 300004; public const int ERR_Success = 0; public const int ERR_SignError = 10000; public const int ERR_Disconnect = 210000; public const int ERR_JoinRoomError = 210002; public const int ERR_UserMoneyLessError = 210003; public const int ERR_PlayCardError = 210004; public const int ERR_LoginError = 210005;
登录、注册的消息指令与消息体
HotfixMessage.proto 中增加登录、注册的消息指令与类型
\Proto\HotfixMessage.proto
//测试向服务器发送消息 message C2G_TestMessage // IRequest { int32 RpcId = 90; string Info = 91; } //测试向服务器返回消息 message G2C_TestMessage // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; } //客户端登陆网关请求 message A0003_LoginGate_C2G // IRequest { int32 RpcId = 90; int64 GateLoginKey = 1; } //客户端登陆网关返回 message A0003_LoginGate_G2C // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; int64 UserID = 1; } //客户端登陆认证请求 message A0002_Login_C2R // IRequest { int32 RpcId = 90; string Account = 1; //假定的账号 string Password = 2; //假定的密码 } //客户端登陆认证返回 message A0002_Login_R2C // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; string GateAddress = 1; int64 GateLoginKey = 2; } //客户端注册请求 message A0001_Register_C2R // IRequest { int32 RpcId = 90; string Account = 1; //假定的账号 string Password = 2; //假定的密码 } //客户端注册请求回复 message A0001_Register_R2C // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; }
更新 .proto 文件后,到unity中找到菜单点Tools>Ptoto2CS工具,确认下重新生成的消息体类与指令脚本。(记得复制三对消息指令与消息类到服务端哦!)
课时有调整,可能你错过了重要的这课,不知道定义和自动生成消息体,消息指令,请前往这课学习 定义消息体字段,用protobuf工具生成消息体
大家辛苦一下,一定要学会编写 .proto 文件,自动生成需要的消息指令与消息体,过这关哦!