MGOBE(Mini Game Online Battle Engine)
注:文章会把实际使用中需要的细节做说明(以帧同步为例)
一、为何使用他
由于io游戏的流行,大部分开发io游戏的团队都需要实现一套帧同步的服务端和客户端的逻辑,开发成本较大,而且由于是实时对战,对网络要求也高,腾讯云对这方面有针对性的优化,可以满足国内绝大多数网络下的玩家。对于客户端程序来讲,可以无需搭建这部分服务器内容,而且不用考虑网络,并发等情况,即可搭建起一个帧同步的游戏。协议使用的是pb,通信支持tcp,udp,但是小游戏平台暂时只有微信支持udp
他有一套完整的后台管理和各模块的设置,此处需要去后台详细的阅读使用说明
二、MGOBE的基础概念
房间:整套框架均是以房间(Room)作为基础单位,进行各协议的通信,房间的
玩家:用户
匹配:
1.按房间条件匹配,如:1v1房间,低中高等级房间,游戏状态(未开始游戏,游戏中),加入指定房间(邀请码)
2.单个玩家快速匹配
3.组队玩家匹配,如:微信小程序,先邀请几个好友,去跟别人玩多人运动
数据同步:
1.帧同步,适用于IO类型游戏
2.状态同步,适用于棋牌,回合类游戏
3.实时服务器,这个比较好,可以在服务端加入自己的服务器代码,这个我还没去看,如果可以跟自己的云服务器通信,那就太完美了。官方写的是:目前提供的功能是 云数据库,云存储,云函数
大概基础内容简单概括就这些,以上模块了解完,就可以搭建一个完整的IO游戏的对战模块了。不过这套引擎的模块设置内容是非常多的,为了满足不同的需求,具体可以看官方文档-Room
三、实际使用
先简单说下我遇到的坑:
上面说到的快速匹配(2.单个玩家快速匹配),发现个问题,这个模式匹配他自己内部处理的流程如下:
1.开始匹配(这里需要在后台设置匹配规则,比如 2v2)
2.等待有4个人在匹配的时候,大家统一进入到房间
到这里似乎没有问题,但是接下来如果有一个人退出了房间,那在房间外面的人是无法通过快速匹配进入到此房间,也就是说这个房间会一直只有3个人,除非其他人去按房间查找才能加入到这里。
这里不知道是不是我哪里设置的问题,导致别人匹配不到这个房间。或者是房间的一些设置问题,由于配置太多,就没有去挨个实验。这里比较适合游戏分级和内容设置较多的游戏。
所以后面我统一用房间匹配的模式进行匹配,可以满足需求。
四、Unity-示例
先按照官方文档,将sdk集成到项目中。
开始我自己例子流程(文章结尾有git地址):
1.初始化
一切的基础
2.开始匹配房间
这里引擎的处理流程是先找满足条件的房间,如果不存在则新创建一个房间然后加入进去,等待其他玩家
3.同步随机数种子
这里将种子下发给各个玩家,初始化随机种子用,为了达到后续客户端的随机结果一致
4.游戏开始,开始帧同步
一个客户端调用开始帧同步,其他客户端会收到开始标识,可用作游戏开始的触发条件
5.发送同步帧消息
这里我是自己定义了一个类,用作存储需要同步的帧类型
五、没有提到的事情
由于帧同步在不同游戏下的实现有着极大的不同,简单的涉及内容太少,复杂的又太耗时间,而且针对性太强,所以没有做对应实战的项目例子,这里简单提下实际开发中可能会遇到的问题:
对战开始,这里可能要注意下数据的问题,每个客户端都需要同步到对手的基本信息,(携带装备,战斗数值)等,如果没有自建服务器的话,就需要客户端自己同步给其他人,比较合理的方式就是房间内的几个人统一维护一个房间玩家信息列表,每个人负责更新里面自己的信息,具体代码在同步随机种子里面,简单点的话可以自定义字符串格式的方式进行同步对战过程,如果是简单的数值运算,那只需要保证随机数相同,各个客户端的表现也就一样了,需要注意的就是渲染帧刷新传的dt时间,可以代码写死这个地方,如果涉及到引擎内的碰撞引擎,物理引擎的话,就需要考虑掉掉线玩家的情况,如果其中一个玩家掉线就要有另外一个在线玩家负责同步针对这个玩家的碰撞信息。其他的可以评论交流,因为最近要做款对战的游戏,暂时只看了这个引擎,后续开发过程细节点的问题会同步上来六、代码
要替换你自己的 GameID,OpenID
Gitee地址
如果不想下载工程,可以直接看代码,复制到你自己的unity工程就可以
using System.Collections; using System.Collections.Generic; using UnityEngine; using com.unity.mgobe.src; using com.unity.mgobe.src.SDK; using com.unity.mgobe; using System; using TMPro; using UnityEngine.UI; using com.unity.mgobe.src.EventUploader; public class main : MonoBehaviour { // Start is called before the first frame update void Start() { } //mgobe public Room room; public void StartSDK() { AppendTxt("开始初始化\r\n"); GameInfoPara gameInfo = new GameInfoPara { // 替换 为控制台上的“游戏ID” GameId = "", // 玩家 openId OpenId = UnityEngine.Random.Range(1, 10000).ToString(), //替换 为控制台上的“游戏Key” SecretKey = "" }; ConfigPara config = new ConfigPara { // 替换 为控制台上的“域名” Url = "dvjpnhh6.wxlagame.com", ReconnectMaxTimes = 5, ReconnectInterval = 1000, ResendInterval = 1000, ResendTimeout = 10000 }; // 初始化监听器 Listener Listener.Init(gameInfo, config, (ResponseEvent eve) => { if (eve.Code == 0) { AppendTxt("初始化成功 \r\n"); Btn_CreateRoom(); } else { AppendTxt("初始化失败\r\n"); } }); } public void Btn_CreateRoom() { room = new Room(null); Listener.Add(room); room.OnJoinRoom = onJoinIn; // 广播:房间有玩家退出 room.OnLeaveRoom = onLeaveRoom; // 广播:房间被解散 room.OnDismissRoom = onDismis; // update room.onUpdate = onRoomUpdate; // recv msg room.OnRecvFromClient = eve => AppendTxt("同步随机数种子:" +((RecvFromClientBst)eve.Data).Msg + "\r\n"); // start step room.OnStartFrameSync = eve => AppendTxt("收到开始帧同步!\r\n"); room.OnStopFrameSync = eve => AppendTxt("收到停止帧同步!\r\n"); room.OnRecvFrame = recvFrameStep; // match Room.OnMatch = onMatch; } private void onJoinIn(BroadcastEvent eve) { var data = (JoinRoomBst)eve.Data; AppendTxt("新玩家加入" + data.JoinPlayerId + "\r\n"); foreach (PlayerInfo player in room.RoomInfo.PlayerList) { AppendTxt("当前玩家:" + player.Name + "\r\n"); } } private void onLeaveRoom(BroadcastEvent eve) { var data = (LeaveRoomBst)eve.Data; AppendTxt("玩家退出" + data.LeavePlayerId + "\r\n"); } private void onDismis(BroadcastEvent eve) { var data = (JoinRoomBst)eve.Data; AppendTxt("房间被解散" + "\r\n"); } private void onRoomUpdate(Room r) { AppendTxt("房间更新:" + r.RoomInfo.Id + "|" + r.RoomInfo.PlayerList.Count + "\r\n"); } void onMatch(BroadcastEvent eve) { var data = (MatchBst)eve.Data; if (data.ErrCode == 0) { AppendTxt("onMatch匹配成功\r\n"); } else { AppendTxt("onMatch匹配失败\r\n"); } } public void Btn_LeaveRoom()//离开房间 { room.LeaveRoom(eve => { if (eve.Code == 0) { AppendTxt("离开房间" + "\r\n"); } else { AppendTxt("离开失败" + "\r\n"); } }); } public void Btn_DismisRoom() //解散房间 { room.DismissRoom(eve => { Debug.Log(eve.Code); if (eve.Code == 0) { AppendTxt("解散成功" + "\r\n"); } }); } public void Btn_MatchRoom() { AppendTxt("开始匹配房间\r\n"); MatchRoomPara para = new MatchRoomPara() { MaxPlayers = 4, PlayerInfo = new PlayerInfoPara() { Name = "p" + UnityEngine.Random.Range(1, 100), CustomPlayerStatus = 1, CustomProfile = "" }, RoomType = "1", }; room.MatchRoom(para, eve => { if (eve.Code != 0) { AppendTxt("room匹配失败\r\n"); } else { AppendTxt("room匹配成功...\r\n"); Debug.Log(eve.Data); } }); } public void Btn_MatchPlayer() { AppendTxt("开始匹配玩家\r\n"); MatchPlayerInfoPara playerInfo = new MatchPlayerInfoPara() { CustomPlayerStatus = 1, Name = "p" + UnityEngine.Random.Range(1, 100), MatchAttributes = new List<MatchAttribute>() { new MatchAttribute(){ Name = "1", Value = 2} }, }; MatchPlayersPara matchPlayerPara = new MatchPlayersPara() { //MatchCode = "match-0bjyargr", //2v2 MatchCode = "match-82hzbk07", // 1v1 PlayerInfoPara = playerInfo, }; room.MatchPlayers(matchPlayerPara, eve => { if (eve.Code != 0) { AppendTxt("player匹配失败" + "\r\n"); } else { AppendTxt("player匹配中..." + "\r\n"); } }); } public void Btn_SendMsg() { SendToClientPara stcp = new SendToClientPara() { RecvType = RecvType.RoomAll, Msg = "123", RecvPlayerList = new List<string>(), }; room.SendToClient(stcp, eve => { }); } public void Btn_StartStep() { room.StartFrameSync(eve => { if (eve.Code == 0) { AppendTxt("开启帧同步成功!\r\n"); } }); } public void Btn_StopStep() { room.StopFrameSync(eve => { if (eve.Code == 0) { AppendTxt("结束帧同步成功!\r\n"); } }); } [Serializable] internal class XXFrameData { [SerializeField] public string strData; [SerializeField] public int intData; override public string ToString() { return strData + "&" + intData; } public static XXFrameData init(string data) { XXFrameData xfd = new XXFrameData(); xfd.strData = data.Split('&')[0]; xfd.intData = int.Parse(data.Split('&')[1]); return xfd; } } public void Btn_SendStep() { string strTime = DateTime.Now.ToLongTimeString(); int intTime = (int)DateTime.Now.Second; XXFrameData fd = new XXFrameData(); fd.strData = strTime; fd.intData = intTime; SendFramePara para = new SendFramePara() { Data = fd.ToString(), }; room.SendFrame(para, eve => { if (eve.Code == 0) AppendTxt("发送帧同步成功\r\n"); else AppendTxt("发送帧同步失败\r\n"); }); } public void recvFrameStep(BroadcastEvent eve) { RecvFrameBst bst = ((RecvFrameBst)eve.Data); if (bst.Frame.Items.Count != 0) AppendTxt("----收到帧消息:" + bst.Frame.Id + "\r\n"); foreach (FrameItem fi in bst.Frame.Items) { //AppendTxt(fi.PlayerId + "|" + ((XXFrameData)fi.Data).strData + "\r\n"); XXFrameData xfd = XXFrameData.init(fi.Data); AppendTxt(fi.PlayerId + "|" + xfd.ToString() + "\r\n"); } } public GameObject tmp; public List<string> strlist = new List<string>(); public void AppendTxt(string txt) { strlist.Add(txt); } public void CleanTxt() { tmp.GetComponent<Text>().text = ""; } private void LateUpdate() { if (strlist.Count > 0) { tmp.GetComponent<Text>().text += strlist[0]; strlist.RemoveAt(0); } } }