大體思路
前面我們實現了點擊開始游戲按鈕,系統依次給玩家發牌的邏輯和動畫,并展示當前的手牌。這期我們繼續實現接下來的功能--叫地主。
1.首先這兩天,學習了DOTween,這是一個強大的Unity動畫插件,大家可以參考:DOTween官方文檔,個人感覺DOTween還是比較好用的。
好的,我們先來重構一下動畫部分的代碼(沒有絕對牛逼的架構和設計,項目過程中不要不斷的持續改進嘛);先把之前ITween相關引用從項目中刪除,然后導入DOTween插件。
相關動畫代碼改造示例如下:
//移動動畫,動畫結束后自動銷毀 var tween = cover.transform.DOMove(playerHeapPos[termCurrentIndex].position, 0.3f); tween.OnComplete(() => Destroy(cover));
怎么樣,比之前簡潔多了吧,而且之前ITween好像不太好用,處理自動銷毀時,跟協程點沖突(可能我自己用的方式不對),現在改用DOTween,一點問題也沒有~官網上也有這幾個動畫插件的性能比較,相對來說DOTween表現還是很不錯的。
2.再來說說具體的設計思路
剛開始覺得叫地主邏輯挺簡單,不就是分2次發牌嘛,第一次發51張,后面誰叫到地主再發3張就好了嘛,但其實實現的過程中,發現沒有那么簡單,需要注意的細節挺多:
a.發牌邏輯調整:因為發牌是按照當前玩家順序,依次發牌,相當一個箭頭一直指向當前回合玩家,發完51張牌后,箭頭又開始指向開始發牌的玩家;這時候,需要判斷首次發牌結束,由當前回合玩家開始叫牌;
b.當前玩家進入叫牌階段時,觸發倒計時,倒計時內如果玩家選擇叫牌,則箭頭保持不變,然后開始繼續給當前玩家發3張剩余的牌;
c.倒計時內如果玩家選擇不叫,則箭頭繼續指向下個玩家,下個玩家開始叫牌,回到分支b;
d.倒計時內玩家如果沒有任何選擇,結束后默認不叫,回到分支c;
e.如果3個玩家都沒叫,則流局,重新開局(本期未實現,很容易,大家后面可以自己去嘗試實現);
f.可以選擇叫3分、2分、1分(將來需要實現,設計開發提前考慮)
h.考慮到玩家自己和對手叫牌邏輯不一樣,自己的話,通過界面點擊觸發,對手暫時監聽按鍵觸發(后期改智能AI判斷),比如按下Q叫牌,按下W不叫;
i.倒計時設計,叫牌的時候,需要顯示玩家面前的計時器,計時結束后觸發不叫,其他情況下隱藏;
j.玩家只有自己回合才能叫地主,必須做限制;
玩家類調整
Player作為玩家的基類,我們需要增加ToBiding(開始叫地主),ForBid(搶地主),NotBid(不搶地主),來控制玩家在叫地主過程中的公共邏輯。
ToBiding:轉到自己回合,并設置倒計時;
ForBid:跳出增加回合,停止倒計時,并調用卡牌管理類中的搶地主功能;
ForBid:跳出增加回合,停止倒計時,并調用卡牌管理類中的不搶地主功能;
然后,倒計時這塊,添加一個協程Considerating,專門處理倒計時控件顯示,標識玩家正在考慮中;


using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI;public abstract class Player : MonoBehaviour {protected List<CardInfo> cardInfos = new List<CardInfo>(); //個人所持卡牌private Text cardCoutText;private Text countDownText;private int consideratingTime = 6; //玩家考慮時間protected bool isMyTerm = false; //當前是否是自己回合void Start(){cardCoutText = transform.Find("HeapPos/Text").GetComponent<Text>();countDownText = transform.Find("CountDown/Text").GetComponent<Text>();}/// <summary>/// 增加一張卡牌/// </summary>/// <param name="cardName"></param>public void AddCard(string cardName){cardInfos.Add(new CardInfo(cardName));cardCoutText.text = cardInfos.Count.ToString();}/// <summary>/// 清空所有卡片/// </summary>public void DropCards(){cardInfos.Clear();}/// <summary>/// 用戶考慮時間/// </summary>private IEnumerator Considerating(){//倒計時var time = consideratingTime;while (time > 0){countDownText.text = time.ToString();yield return new WaitForSeconds(1);time--;}NotBid();}/// <summary>/// 開始叫地主/// </summary>public virtual void ToBiding(){isMyTerm = true;//開始倒計時countDownText.transform.parent.gameObject.SetActive(true);StartCoroutine("Considerating");}/// <summary>/// 搶地主/// </summary>public void ForBid(){//關閉倒計時countDownText.transform.parent.gameObject.SetActive(false);StopCoroutine("Considerating");CardManager._instance.ForBid();isMyTerm = false;}/// <summary>/// 不搶地主/// </summary>public void NotBid(){//關閉倒計時countDownText.transform.parent.gameObject.SetActive(false);StopCoroutine("Considerating");CardManager._instance.NotBid();isMyTerm = false;}/// <summary>/// 卡牌排序(從大到小)/// </summary>protected void Sort(){cardInfos.Sort();cardInfos.Reverse();}}
自身玩家類調整
其實就是重寫了基類ToBiding方法,調用顯示叫牌按鈕(只有自身玩家才需要)


using System; using System.Collections.Generic; using DG.Tweening; using UnityEngine; using UnityEngine.EventSystems;/// <summary> /// 自身玩家 /// </summary> public class PlayerSelf : Player {public GameObject prefab; //預制件private Transform originPos1; //牌的初始位置private Transform originPos2; //牌的初始位置private List<GameObject> cards = new List<GameObject>();private bool canSelectCard = false; //玩家是否可以選牌void Awake(){originPos1 = transform.Find("OriginPos1");originPos2 = transform.Find("OriginPos2");}//整理手牌public void GenerateAllCards(){//排序 Sort();//計算每張牌的偏移var offsetX = originPos2.position.x - originPos1.position.x;//獲取最左邊的起點int leftCount = (cardInfos.Count / 2);var startPos = originPos1.position + Vector3.left * offsetX * leftCount;for (int i = 0; i < cardInfos.Count; i++){//生成卡牌var card = Instantiate(prefab, originPos1.position, Quaternion.identity, transform);card.GetComponent<RectTransform>().localScale = Vector3.one * 0.6f;card.GetComponent<Card>().InitImage(cardInfos[i]);card.transform.SetAsLastSibling();//動畫移動var tween = card.transform.DOMoveX(startPos.x + offsetX * i, 1f);if (i == cardInfos.Count - 1) //最后一張動畫 {tween.OnComplete(() => { canSelectCard = true; });}cards.Add(card);}}/// <summary>/// 銷毀所有卡牌對象/// </summary>public void DestroyAllCards(){cards.ForEach(Destroy);cards.Clear();}/// <summary>/// 開始叫牌/// </summary>public override void ToBiding(){base.ToBiding();CardManager._instance.SetBidButtonActive(true);}/// <summary>/// 點擊卡牌處理/// </summary>/// <param name="data"></param>public void CardClick(BaseEventData data){//叫牌或出牌階段才可以選牌if (canSelectCard &&(CardManager._instance.cardManagerState == CardManagerStates.Bid ||CardManager._instance.cardManagerState == CardManagerStates.Playing)){var eventData = data as PointerEventData;if (eventData == null) return;var card = eventData.pointerCurrentRaycast.gameObject.GetComponent<Card>();if (card == null) return;card.SetSelectState();}} }
對手玩家類調整
主要模擬對手叫牌,如果是玩家回合,按下Q叫牌,按下W不叫


using UnityEngine;public class PlayerOther : Player {void Update(){//如果當前是自己回合,模擬對手叫牌if (isMyTerm){if (Input.GetKeyDown(KeyCode.Q)) //叫牌 {ForBid();}if (Input.GetKeyDown(KeyCode.W)) //不叫 {NotBid();}}} }
卡牌管理類調整
首先需要改造發牌方法,現在區分發牌是發普通牌還是發發地主牌,根據發牌的不同類型,取牌堆里的不同牌,再判斷是依次發牌,還是只發給地主牌。
然后具體實現玩家開始搶地主StartBiding,叫地主ForBid,不叫地主NotBid 3個方法:
StartBiding:從當前回合玩家開始叫牌;
ForBid:設置當前回合玩家是地主,并給地主發余下3張牌;
NotBid :轉向下個回合玩家開始叫牌;


using System; using System.Collections; using System.IO; using System.Linq; using DG.Tweening; using UnityEngine;/// <summary> /// 卡牌管理 /// </summary> public class CardManager : MonoBehaviour {public static CardManager _instance; //單例public float dealCardSpeed = 20; //發牌速度public Player[] Players; //玩家的集合public GameObject coverPrefab; //背面排預制件public Transform heapPos; //牌堆位置public Transform[] playerHeapPos; //玩家牌堆位置public CardManagerStates cardManagerState;private string[] cardNames; //所有牌集合private int termStartIndex; //回合開始玩家索引private int termCurrentIndex; //回合當前玩家索引private int bankerIndex; //當前地主索引private GameObject bidBtns;void Awake(){_instance = this;cardNames = GetCardNames();bidBtns = GameObject.Find("BidBtns");bidBtns.SetActive(false);}/// <summary>/// 洗牌/// </summary>public void ShuffleCards(){//進入洗牌階段cardManagerState = CardManagerStates.ShuffleCards;cardNames = cardNames.OrderBy(c => Guid.NewGuid()).ToArray();}/// <summary>/// 開始發牌/// </summary>public IEnumerator DealCards(){//進入發牌階段cardManagerState = CardManagerStates.DealCards;termCurrentIndex = termStartIndex;yield return DealHeapCards(false);}/// <summary>/// 發牌堆上的牌(如果現在不是搶地主階段,發普通牌,如果是,發地主牌)/// </summary>/// <returns></returns>private IEnumerator DealHeapCards(bool ifForBid){//顯示牌堆heapPos.gameObject.SetActive(true);playerHeapPos.ToList().ForEach(s => { s.gameObject.SetActive(true); });var cardNamesNeeded = ifForBid? cardNames.Skip(cardNames.Length - 3).Take(3) //如果是搶地主牌,取最后3張: cardNames.Take(cardNames.Length - 3); //如果首次發牌foreach (var cardName in cardNamesNeeded){//給當前玩家發一張牌 Players[termCurrentIndex].AddCard(cardName);var cover = Instantiate(coverPrefab, heapPos.position, Quaternion.identity, heapPos.transform);cover.GetComponent<RectTransform>().localScale = Vector3.one;//移動動畫,動畫結束后自動銷毀var tween = cover.transform.DOMove(playerHeapPos[termCurrentIndex].position, 0.3f);tween.OnComplete(() => Destroy(cover));yield return new WaitForSeconds(1 / dealCardSpeed);//下一個需要發牌者if (!ifForBid)SetNextPlayer();}//隱藏牌堆heapPos.gameObject.SetActive(false);playerHeapPos[0].gameObject.SetActive(false);//發普通牌if (!ifForBid){//顯示玩家手牌 ShowPlayerSelfCards();StartBiding();}//發地主牌else{if (Players[bankerIndex] is PlayerSelf){//顯示玩家手牌 ShowPlayerSelfCards();}cardManagerState = CardManagerStates.Playing;}}/// <summary>/// 開始搶地主/// </summary>private void StartBiding(){cardManagerState = CardManagerStates.Bid;Players[termCurrentIndex].ToBiding();}/// <summary>/// 顯示玩家手牌/// </summary>private void ShowPlayerSelfCards(){Players.ToList().ForEach(s =>{var player0 = s as PlayerSelf;if (player0 != null){player0.GenerateAllCards();}});}/// <summary>/// 清空牌局/// </summary>public void ClearCards(){//清空所有玩家卡牌Players.ToList().ForEach(s => s.DropCards());//顯示玩家手牌Players.ToList().ForEach(s =>{var player0 = s as PlayerSelf;if (player0 != null){player0.DestroyAllCards();}});}/// <summary>/// 叫地主/// </summary>public void ForBid(){//設置當前地主bankerIndex = termCurrentIndex;//給地主發剩下的3張牌SetBidButtonActive(false);StartCoroutine(DealHeapCards(true));}/// <summary>/// 不叫/// </summary>public void NotBid(){SetBidButtonActive(false);SetNextPlayer();Players[termCurrentIndex].ToBiding();}/// <summary>/// 控制叫地主按鈕是否顯示/// </summary>/// <param name="isActive"></param>public void SetBidButtonActive(bool isActive){bidBtns.SetActive(isActive);}/// <summary>/// 設置下一輪開始玩家/// </summary>public void SetNextTerm(){termStartIndex = (termStartIndex + 1) % Players.Length;}/// <summary>/// 設置下個回合玩家/// </summary>/// <returns></returns>public void SetNextPlayer(){termCurrentIndex = (termCurrentIndex + 1) % Players.Length;}private string[] GetCardNames(){//路徑 string fullPath = "Assets/Resources/Images/Cards/";if (Directory.Exists(fullPath)){DirectoryInfo direction = new DirectoryInfo(fullPath);FileInfo[] files = direction.GetFiles("*.png", SearchOption.AllDirectories);return files.Select(s => Path.GetFileNameWithoutExtension(s.Name)).ToArray();}return null;}//開始新回合public void OnStartTermClick(){ClearCards();ShuffleCards();StartCoroutine(DealCards());}}
總結
至此,我們【叫地主】功能大體完成了,來試試效果吧~
資源
項目源碼
文章作者:原子蛋
文章出處:https://www.cnblogs.com/lizzie-xhu/
個人網站:https://www.lancel0t.cn/
個人博客:https://blog.lancel0t.cn/
微信公眾號:原子蛋Live+
掃一掃左側的二維碼(或者長按識別二維碼),關注本人微信公共號,獲取更多資源。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。