任务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 文件,自动生成需要的消息指令与消息体,过这关哦!

 

posted @ 2023-02-01 09:46  Domefy  阅读(75)  评论(0编辑  收藏  举报