前言
看了看这个代码感觉没什么可讲的,没有什么独特的Unity技巧,都是斗地主的业务逻辑,这一片简单分析一下吧。
不过这个Demo包含前后端,可以了解下前后端的职责和如何交互的。总结一下就是前端负责界面展示,后端负责数据处理。
客户端各模块实现
使用上一章讲过的框架,分成了好几个模块,分别是:UI模块、场景模块、网络模块、角色模块、音效模块。
接下来由易到难看一下这几个模块。
音效模块
这个模块实际上没用到,有一个EffectAudio
虽然在音效模块目录下,但却属于UI模块,核心代码就下面这两行,对于声音资源并播放。
/// <summary>/// 播放音效/// </summary>/// <param name="name">文件路径+名称</param>private void PlayChatEffectAudio(string name){audioSource.clip = Resources.Load<AudioClip>("Sound/" + name);audioSource.Play();}
场景模块
这个模块主要负责场景切换,可以通过onSceneLoadAction
设置一个场景加载完成后的回调,其他就没什么了。
网络模块
网络模块实际是比较复杂的,但底层写好以后不会怎么变动,大多也是写业务逻辑。
这个模块主要做三件事:连接服务器,发数据和收数据。
连接服务器
通过ClientPeer
来连接服务器,一个此对象就是一条连接。实现了接收数据和发送数据的功能。
其中收到的数据会存在一个队列socketMsgQueue
里。
发数据
作者自定义了一种网络通信格式SocketMsg
,并将其做成了dll库,斗地主\Assets\Plugin\netstandard2.0\Components.dll
,做成dll,与因为这个数据结构在服务端定义,做成dll调用client 来发送数据。
//
// 摘要:
// 网络消息
public class SocketMsg
{public SocketMsg();public SocketMsg(Enum state);public SocketMsg(MsgType opCode, Enum subCode, Enum state, object value = null);//// 摘要:// 操作码public MsgType OpCode { get; set; }//// 摘要:// 子操作public Enum SubCode { get; set; }//// 摘要:// 参数public object value { get; set; }//// 摘要:// 状态public Enum State { get; set; }
}
收数据
网络模块继承自ManagerBase
,也就继承了MonoBehaviour
。
它的Update()
一直在读取client的消息队列socketMsgQueue
,有消息就会处理。
根据不同的操作码,也就是自定义网络消息SocketMsg
中的OpCode
来进行不同的处理。
这里也是主要写业务逻辑的地方,定义消息和对应的处理方法。
现在有用户的登录注册、信息查看、匹配、聊天、打牌功能。
这里只看打牌消息的处理,根据SubCode
来确定具体的业务,这里一共有三个功能,如下所示
public override void OnReceive(SocketMsg msg){var code = (FightCode)msg.SubCode;switch (code){case FightCode.Get_Card_Result: // 获得卡牌GetCard(msg);break;case FightCode.Turn_Grab_Bro: // 轮换抢地主TurnGrabBro(msg);break;case FightCode.Grab_Landlord_Bro: // 抢地主成功GrabLandlordBro(msg); break;default:break;}}
轮换抢地主逻辑如下
/// <summary>/// 是否第一个玩家抢地主,而不是别的玩家不叫而到他/// </summary>private bool isFirst = true;/// <summary>/// 转换抢地主/// </summary>/// <param name="msg"></param>private void TurnGrabBro(SocketMsg msg){if (isFirst == true) {isFirst = false;}else // 如果自己不是第一个的话,播放“不要”声效{Dispatch(AreaCode.UI, UIEvent.EffectAudio, "Fight/Woman_NoOrder");}var userId = (int)msg.value;if (userId == Data.GameData.UserCharacterDto.Id) // 轮到谁选择了,把他的按钮调亮/或显示出抢地主按钮{Dispatch(AreaCode.UI, UIEvent.Show_Grab_Button, true);}}
实在是没什么分析的,自己都能看懂,直接看发牌操作,这里调用角色模块设置了三个玩家的卡牌,在本地实际上只设置了自己的卡牌,其他两个玩家的卡牌信息没有同步回来。具体看看角色模块的处理就知道了。
/// <summary>/// 获取卡牌/// </summary>/// <param name="msg"></param>private void GetCard(SocketMsg msg){//设置玩家卡牌Dispatch(AreaCode.CHARACTER, CharacterEvent.Init_MyCard, msg.value);Dispatch(AreaCode.CHARACTER, CharacterEvent.Init_LeftCard, null);Dispatch(AreaCode.CHARACTER, CharacterEvent.Init_RightCard, null);//设置倍数Dispatch(AreaCode.UI, UIEvent.Change_Mutiple, 1);}
角色模块
这里的角色管理,就是管理自己手中的牌。接上所述,先看给其他两个玩家发牌是如何处理的。
给左右玩家发牌
这两个分为左右,实际是一样的,这里只看左边玩家,也就是LeftPlayerCtrl.cs
。
发牌最后调到了这个方法,这是个协程,每0.1s发一张牌,实现动态发牌效果,一共17张,都用的同一个资源Card/OtherCard
,这个资源是卡牌的背面,如下图,也就是说,给左右两个玩家发牌只是做了这个发牌动作,实际没有数据。
/// <summary>/// 协程延时一秒/// </summary>/// <returns></returns>private IEnumerator InitCardList(){GameObject cardPrefab = Resources.Load<GameObject>("Card/OtherCard");for (int i = 0; i < 17; i++){CreateGo(cardPrefab, i);yield return new WaitForSeconds(0.1f);}}/// <summary>/// 创建卡牌/// </summary>/// <param name="cardPrefab"></param>/// <param name="index"></param>private void CreateGo(GameObject cardPrefab, int index){GameObject cardGo = Instantiate(cardPrefab, cardParent);cardGo.transform.localPosition = new Vector2((0.15f * index), 0);cardGo.GetComponent<SpriteRenderer>().sortingOrder = index;}
给自己发牌
给自己发牌和给左边玩家发牌类似,只不过是有数据的。
这里只贴有区别的部分,可以看到创建卡牌的时候还新建了一个CardCtrl
结构,这个对象用来控制具体的一张牌,通过它的Init
方法初始化了这个牌的信息。
/// <summary>/// 创建卡牌/// </summary>/// <param name="card"></param>/// <param name="index"></param>private void CreateGo(GameObject cardPrefab, CardDto card, int index){GameObject cardGo = Instantiate(cardPrefab, cardParent);cardGo.name = card.Name;cardGo.transform.localPosition = new Vector2((0.25f * index), 0);CardCtrl cardCtrl = cardGo.GetComponent<CardCtrl>();cardCtrl.Init(card, index, true);//缓存本地cardCtrlsList.Add(cardCtrl);}
通过CardCtrl
初始化具体的牌信息,其中根据牌名字替换了精灵,替换为对应的图片资源。
/// <summary>/// 初始化/// </summary>/// <param name="cardDto">卡牌信息</param>/// <param name="index">索引</param>/// <param name="isMine">是否自己的卡牌</param>public void Init(CardDto cardDto, int index, bool isMine){this.cardDto = cardDto;this.isMine = isMine;if (isSelect){isSelect = false;transform.localPosition -= new Vector3(0, 0.3f, 0);}string resPath = string.Empty;if (isMine){resPath = "Poker/" + cardDto.Name;}else{resPath = "Poker/CardBack";}spriteRenderer = GetComponent<SpriteRenderer>();spriteRenderer.sortingOrder = index++;spriteRenderer.sprite = Resources.Load<Sprite>(resPath);}
角色模块实现的功能就这么多了,,,没错就一个发牌功能,我也是才发现,本来想写出牌的相关功能,结果一看竟然没写。不过毕竟是Demo,知道了一个斗地主游戏大致怎么开发的就行了。
UI模块
这个模块不具体分析了,都是些琐碎的东西,理解了上一篇讲的框架后,自己都能看懂。
好吧,到此为止,本篇似乎没分析出啥干货来,就看到一个发牌逻辑,还是客户端的,真正的发牌逻辑在服务端。
那么还真是巧了,这个项目刚好有服务端代码,这里就把服务端代码也分析一下吧。
服务端
看完这个服务端解决了我的一些疑惑,为什么上面用到的那些SocketMsg
等结构要做成dll,原来定义源码在服务端,和客户端通用。
服务端框架代码就不多说了,无非是连接与消息收发。
功能逻辑还是比较多的,这里只说一下出牌和发牌逻辑。
出牌
直接看代码吧
/// <summary>/// 发牌/// </summary>private void Deal(ClientPeer client, DealDto dto){SingleExecute.Instance.Execute(() =>{if (UserCache.IsOnline(client) == false){socketMsg.State = null;return;}int userId = UserCache.GetClientUserId(client);FightRoom room = FightCache.GetRoomByUId(userId);//玩家出牌、玩家掉线if (room.LeaveUIdList.Contains(userId)){Turn(room);}bool canDeal = room.DeadCard(dto.Type, dto.Weight, dto.Length, userId, dto.SelectCardList);if (canDeal == false){socketMsg.State = FightCode.必须大于上次一次出牌;socketMsg.SubCode = FightCode.Deal_Result;client.Send(socketMsg);return;}else{//返回客户端出牌成功socketMsg.State = FightCode.Success;socketMsg.SubCode = FightCode.Deal_Result;client.Send(socketMsg);//广播出牌结果socketMsg.value = dto;BroCast(room, socketMsg, client);//检查剩余手牌List<CardDto> remainCardList = room.GetPlayerModel(userId).CardList;if (remainCardList.Count == 0){//游戏结束GameOver(userId, room);}else{Turn(room);}}});}
根据选择的卡牌列表判断能否出牌,如果不能,就返回错误提示。
如果能出牌,给本客户端返回出牌成功,并给房间内用户广播这个用户的卡牌列表,让房间内的客户端都更新展示效果。如果牌出完了就代表胜利了。
就这么多了,这里的难点主要是判断是否能出牌,即选择的牌是否符合规则,是否大于上一家的牌,感兴趣自己看DeadCard
是如何实现的。
发牌
首先要洗牌,就是创建54张牌放到一个队列里,然后每次随机从中取一张放到一个新队列里。发牌就每次从新队列首部取出一张牌。这就是本项目LibraryModel.cs
中创建牌、洗牌、发牌的过程。
在玩家初始化手牌和抢到地主的时候会进行发牌操作,就这。
就这,没啥说的了,其他功能没必要说了,如果看到这儿都看懂了,剩下的自己也都能看懂了。