【斗地主代码分析】(2)-斗地主逻辑-客户端与服务端
前言
看了看這個代碼感覺沒什么可講的,沒有什么獨特的Unity技巧,都是斗地主的業(yè)務(wù)邏輯,這一片簡單分析一下吧。
不過這個Demo包含前后端,可以了解下前后端的職責(zé)和如何交互的。總結(jié)一下就是前端負(fù)責(zé)界面展示,后端負(fù)責(zé)數(shù)據(jù)處理。
客戶端各模塊實現(xiàn)
使用上一章講過的框架,分成了好幾個模塊,分別是:UI模塊、場景模塊、網(wǎng)絡(luò)模塊、角色模塊、音效模塊。
接下來由易到難看一下這幾個模塊。
音效模塊
這個模塊實際上沒用到,有一個EffectAudio 雖然在音效模塊目錄下,但卻屬于UI模塊,核心代碼就下面這兩行,對于聲音資源并播放。
/// <summary>/// 播放音效/// </summary>/// <param name="name">文件路徑+名稱</param>private void PlayChatEffectAudio(string name){audioSource.clip = Resources.Load<AudioClip>("Sound/" + name);audioSource.Play();}場景模塊
這個模塊主要負(fù)責(zé)場景切換,可以通過onSceneLoadAction 設(shè)置一個場景加載完成后的回調(diào),其他就沒什么了。
網(wǎng)絡(luò)模塊
網(wǎng)絡(luò)模塊實際是比較復(fù)雜的,但底層寫好以后不會怎么變動,大多也是寫業(yè)務(wù)邏輯。
這個模塊主要做三件事:連接服務(wù)器,發(fā)數(shù)據(jù)和收數(shù)據(jù)。
連接服務(wù)器
通過ClientPeer 來連接服務(wù)器,一個此對象就是一條連接。實現(xiàn)了接收數(shù)據(jù)和發(fā)送數(shù)據(jù)的功能。
其中收到的數(shù)據(jù)會存在一個隊列socketMsgQueue里。
發(fā)數(shù)據(jù)
作者自定義了一種網(wǎng)絡(luò)通信格式SocketMsg,并將其做成了dll庫,斗地主\Assets\Plugin\netstandard2.0\Components.dll,做成dll,與因為這個數(shù)據(jù)結(jié)構(gòu)在服務(wù)端定義,做成dll調(diào)用client 來發(fā)送數(shù)據(jù)。
// // 摘要: // 網(wǎng)絡(luò)消息 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; }//// 摘要:// 參數(shù)public object value { get; set; }//// 摘要:// 狀態(tài)public Enum State { get; set; } }收數(shù)據(jù)
網(wǎng)絡(luò)模塊繼承自ManagerBase,也就繼承了MonoBehaviour。
它的Update() 一直在讀取client的消息隊列socketMsgQueue,有消息就會處理。
根據(jù)不同的操作碼,也就是自定義網(wǎng)絡(luò)消息SocketMsg中的OpCode 來進(jìn)行不同的處理。
這里也是主要寫業(yè)務(wù)邏輯的地方,定義消息和對應(yīng)的處理方法。
現(xiàn)在有用戶的登錄注冊、信息查看、匹配、聊天、打牌功能。
這里只看打牌消息的處理,根據(jù)SubCode 來確定具體的業(yè)務(wù),這里一共有三個功能,如下所示
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>/// 轉(zhuǎn)換搶地主/// </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) // 輪到誰選擇了,把他的按鈕調(diào)亮/或顯示出搶地主按鈕{Dispatch(AreaCode.UI, UIEvent.Show_Grab_Button, true);}}實在是沒什么分析的,自己都能看懂,直接看發(fā)牌操作,這里調(diào)用角色模塊設(shè)置了三個玩家的卡牌,在本地實際上只設(shè)置了自己的卡牌,其他兩個玩家的卡牌信息沒有同步回來。具體看看角色模塊的處理就知道了。
/// <summary>/// 獲取卡牌/// </summary>/// <param name="msg"></param>private void GetCard(SocketMsg msg){//設(shè)置玩家卡牌Dispatch(AreaCode.CHARACTER, CharacterEvent.Init_MyCard, msg.value);Dispatch(AreaCode.CHARACTER, CharacterEvent.Init_LeftCard, null);Dispatch(AreaCode.CHARACTER, CharacterEvent.Init_RightCard, null);//設(shè)置倍數(shù)Dispatch(AreaCode.UI, UIEvent.Change_Mutiple, 1);}角色模塊
這里的角色管理,就是管理自己手中的牌。接上所述,先看給其他兩個玩家發(fā)牌是如何處理的。
給左右玩家發(fā)牌
這兩個分為左右,實際是一樣的,這里只看左邊玩家,也就是LeftPlayerCtrl.cs。
發(fā)牌最后調(diào)到了這個方法,這是個協(xié)程,每0.1s發(fā)一張牌,實現(xiàn)動態(tài)發(fā)牌效果,一共17張,都用的同一個資源Card/OtherCard,這個資源是卡牌的背面,如下圖,也就是說,給左右兩個玩家發(fā)牌只是做了這個發(fā)牌動作,實際沒有數(shù)據(jù)。
/// <summary>/// 協(xié)程延時一秒/// </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>/// 創(chuàng)建卡牌/// </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;}給自己發(fā)牌
給自己發(fā)牌和給左邊玩家發(fā)牌類似,只不過是有數(shù)據(jù)的。
這里只貼有區(qū)別的部分,可以看到創(chuàng)建卡牌的時候還新建了一個CardCtrl 結(jié)構(gòu),這個對象用來控制具體的一張牌,通過它的Init 方法初始化了這個牌的信息。
/// <summary>/// 創(chuàng)建卡牌/// </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 初始化具體的牌信息,其中根據(jù)牌名字替換了精靈,替換為對應(yīng)的圖片資源。
/// <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);}角色模塊實現(xiàn)的功能就這么多了,,,沒錯就一個發(fā)牌功能,我也是才發(fā)現(xiàn),本來想寫出牌的相關(guān)功能,結(jié)果一看竟然沒寫。不過畢竟是Demo,知道了一個斗地主游戲大致怎么開發(fā)的就行了。
UI模塊
這個模塊不具體分析了,都是些瑣碎的東西,理解了上一篇講的框架后,自己都能看懂。
好吧,到此為止,本篇似乎沒分析出啥干貨來,就看到一個發(fā)牌邏輯,還是客戶端的,真正的發(fā)牌邏輯在服務(wù)端。
那么還真是巧了,這個項目剛好有服務(wù)端代碼,這里就把服務(wù)端代碼也分析一下吧。
服務(wù)端
看完這個服務(wù)端解決了我的一些疑惑,為什么上面用到的那些SocketMsg 等結(jié)構(gòu)要做成dll,原來定義源碼在服務(wù)端,和客戶端通用。
服務(wù)端框架代碼就不多說了,無非是連接與消息收發(fā)。
功能邏輯還是比較多的,這里只說一下出牌和發(fā)牌邏輯。
出牌
直接看代碼吧
/// <summary>/// 發(fā)牌/// </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);//廣播出牌結(jié)果socketMsg.value = dto;BroCast(room, socketMsg, client);//檢查剩余手牌List<CardDto> remainCardList = room.GetPlayerModel(userId).CardList;if (remainCardList.Count == 0){//游戲結(jié)束GameOver(userId, room);}else{Turn(room);}}});}根據(jù)選擇的卡牌列表判斷能否出牌,如果不能,就返回錯誤提示。
如果能出牌,給本客戶端返回出牌成功,并給房間內(nèi)用戶廣播這個用戶的卡牌列表,讓房間內(nèi)的客戶端都更新展示效果。如果牌出完了就代表勝利了。
就這么多了,這里的難點主要是判斷是否能出牌,即選擇的牌是否符合規(guī)則,是否大于上一家的牌,感興趣自己看DeadCard 是如何實現(xiàn)的。
發(fā)牌
首先要洗牌,就是創(chuàng)建54張牌放到一個隊列里,然后每次隨機從中取一張放到一個新隊列里。發(fā)牌就每次從新隊列首部取出一張牌。這就是本項目LibraryModel.cs 中創(chuàng)建牌、洗牌、發(fā)牌的過程。
在玩家初始化手牌和搶到地主的時候會進(jìn)行發(fā)牌操作,就這。
就這,沒啥說的了,其他功能沒必要說了,如果看到這兒都看懂了,剩下的自己也都能看懂了。
總結(jié)
以上是生活随笔為你收集整理的【斗地主代码分析】(2)-斗地主逻辑-客户端与服务端的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北京大学计算机学院推免生名单,2014年
- 下一篇: ipad还能横行霸道多久