【斗地主代码分析】(2)-斗地主逻辑-客户端与服务端

news/2024/12/29 13:48:10/

前言

看了看这个代码感觉没什么可讲的,没有什么独特的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 中创建牌、洗牌、发牌的过程。

在玩家初始化手牌和抢到地主的时候会进行发牌操作,就这。

就这,没啥说的了,其他功能没必要说了,如果看到这儿都看懂了,剩下的自己也都能看懂了。


http://www.ppmy.cn/news/677824.html

相关文章

机器学习之朴素贝叶斯(Naive Bayes)

1 朴素贝叶斯算法介绍 朴素贝叶斯是经典的机器学习算法之一&#xff0c;也是为数不多的基于概率论的分类算法。朴素贝叶斯分类器(Naive Bayes Classifier 或 NBC)发源于古典数学理论&#xff0c;有着坚实的数学基础&#xff0c;以及稳定的分类效率&#xff0c;是应用最为广泛的…

python的线程

threading 模块 两种方式&#xff1a;线程处理函数 与 继承 threading.Thread 类 使用线程处理函数创建多线程的用法类似于 thread 模块创建多线程 使用继承 threading.Thread 类实现多线程的本质就是重写 threading.Thread 类中的 构造函数 与 run 方法 # (1) 线程处理函数 …

车辆二桥制动平衡率 不合格

车辆年检时提示 《车辆二桥制动平衡率》 不合格 1、更换刹车片 2、到修理厂进一步检查 经验来源&#xff1a;本人亲自检验10年老车出的问题&#xff0c;更换刹车片后解决。 个人理解&#xff1a;制动平衡率指的是两侧轮胎刹车片厚度不同&#xff0c;制动不均匀&#xff0c;可…

电机刹车

电机刹车的作用是让pwm引脚断开不产生pwm&#xff0c;然后电机会由于楞次定律减速刹车

刹车电机

刹车电机又称制动电机是指普通电机风扇部位加装电磁刹车&#xff0c;它的用途非常广泛&#xff0c;可以用于瞬间停机的时候降低电机的惯性和冲击力&#xff0c;还可以用于上下运动的停车或者垂直起吊的停车&#xff0c;从而避免电机在停机是向下溜。所以刹车有分为单面刹车和双…

为什么刹车热了会失灵_刹车片过热为什么会导致刹车失灵?

展开全部 还有就是高温会引起刹车油的汽化现象&#xff0c;就是平时说的为什么刹车的时32313133353236313431303231363533e59b9ee7ad9431333431366265候要踩两脚刹车&#xff0c;力量才好点。就是为了排出刹车油管里的空气。还有调试刹车的时候&#xff0c;给油管排空气也是一个…

捷达汽车浮动钳盘式制动器设计【说明书(论文)+CAD图纸】

摘 要 汽车制动系统是汽车各个系统中最为重要的。如果制动系统失灵&#xff0c;那么结果将会是毁灭性的。制动器实际上是一个能量转化装置&#xff0c;这种转化实际上是把汽车的动能转换为汽车的热能挥发出去&#xff0c;当制动器制动时&#xff0c;驱动程序来命令十倍于以往的…

二手汽车评估

二手汽车评估 数据集取自uci&#xff0c;对处理好的数据集分别进行逻辑回归、随机森林、knn、svm、GBDT分类建模&#xff0c;寻找较优的分类模型。 数据集特征描述&#xff1a; buying买入价格&#xff08;vhigh&#xff0c;high&#xff0c;med&#xff0c;low&#xff09;…