大体思路

前面我们实现了点击开始游戏按钮,系统依次给玩家发牌的逻辑和动画,并展示当前的手牌。这期我们继续实现接下来的功能–叫地主。

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.玩家只有自己回合才能叫地主,必须做限制;

Hi,之前有同学说要我把源码发出来,那我就把半成品源码的链接放在每篇文件的最后,有兴趣的话可以查阅参考,有问题可以跟我私信,也可以关注我的个人公众号,互相交流嘛。当然,代码也是在不断的持续改进中~

玩家类调整

Player作为玩家的基类,我们需要增加ToBiding(开始叫地主),ForBid(抢地主),NotBid(不抢地主),来控制玩家在叫地主过程中的公共逻辑。

ToBiding:转到自己回合,并设置倒计时;

ForBid:跳出增加回合,停止倒计时,并调用卡牌管理类中的抢地主功能;

ForBid:跳出增加回合,停止倒计时,并调用卡牌管理类中的不抢地主功能;

然后,倒计时这块,添加一个协程Considerating,专门处理倒计时控件显示,标识玩家正在考虑中;

图片 1图片 2

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();
    }

}

View Code

上期我们实现了叫地主功能,不过遗留了一个小功能:叫地主完成以后,要显示地主的3张牌,这期首先弥补这块的功能;

自身玩家类调整

其实就是重写了基类ToBiding方法,调用显示叫牌按钮(只有自身玩家才需要)

图片 3图片 4

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();
        }
    }
}

View Code

接着我们要进入开发出牌逻辑的开发阶段,好了,废话不多说,继续我们斗地主开发之旅~

对手玩家类调整

主要模拟对手叫牌,如果是玩家回合,按下Q叫牌,按下W不叫

图片 5图片 6

using UnityEngine;

public class PlayerOther : Player
{
    void Update()
    {
        //如果当前是自己回合,模拟对手叫牌
        if (isMyTerm)
        {
            if (Input.GetKeyDown(KeyCode.Q))    //叫牌
            {
                ForBid();
            }
            if (Input.GetKeyDown(KeyCode.W))    //不叫
            {
                NotBid();
            }
        }
    }
}

View Code

地主牌的显示

我们在玩家界面的顶部中间位置,放置一个新的GameObject,命名为BidCards,用来记录3张地主牌的显示位置。

所以我们重构了CardManager中的发牌方法,在给地主发牌同时,生成地主牌的实例,放在BidCards相应位置:

图片 7图片 8

    /// <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);         //如果首次发牌

        //计算每张地主牌的位置
        int cardIndex = 0;
        var width = (bidCards.GetComponent<RectTransform>().sizeDelta.x - 20) / 3;
        var centerBidPos = Vector3.zero;
        var leftBidPos = centerBidPos - Vector3.left * width;
        var rightBidPos = centerBidPos + Vector3.left * width;
        List<Vector3> bidPoss = new List<Vector3> { leftBidPos, centerBidPos, rightBidPos };
        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)
            {
                //显示地主牌
                var bidcard = Instantiate(cardPrefab, bidCards.transform.TransformPoint(bidPoss[cardIndex]), Quaternion.identity, bidCards.transform);
                bidcard.GetComponent<Card>().InitImage(new CardInfo(cardName));
                bidcard.GetComponent<RectTransform>().localScale = Vector3.one * 0.3f;
            }
            else
            {
                //下一个需要发牌者
                SetNextPlayer();
            }

            cardIndex++;
        }

        //隐藏牌堆
        heapPos.gameObject.SetActive(false);
        playerHeapPos[0].gameObject.SetActive(false);

        //发普通牌
        if (!ifForBid)
        {
            //显示玩家手牌
            ShowPlayerSelfCards();
            StartBiding();
        }
        //发地主牌
        else
        {
            if (Players[bankerIndex] is PlayerSelf)
            {
                //显示玩家手牌
                ShowPlayerSelfCards();
            }
            StartFollowing();
        }
    }

View Code

好的,我们地主牌显示已经没有问题了,接下来,我们要实现出牌回合逻辑

卡牌管理类调整

首先需要改造发牌方法,现在区分发牌是发普通牌还是发发地主牌,根据发牌的不同类型,取牌堆里的不同牌,再判断是依次发牌,还是只发给地主牌。

然后具体实现玩家开始抢地主StartBiding,叫地主ForBid,不叫地主NotBid
3个方法:

StartBiding:从当前回合玩家开始叫牌;

ForBid:设置当前回合玩家是地主,并给地主发余下3张牌;

NotBid :转向下个回合玩家开始叫牌;

图片 9图片 10

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());
    }

}

View Code

出牌回合功能实现

出牌回合其实跟叫地主回合类似,也是可以抽象出3种方法:进入出牌阶段、出牌、不出(比较进入叫地主阶段、叫地主、不叫地主);

因此,我们参照叫地主的逻辑,再实现出牌逻辑:

总结

至此,我们【叫地主】功能大体完成了,来试试效果吧~
图片 11

调整Player基类

添加开始出牌ToFollowing、出牌ForFollow、不出NotFollow:

  • ToFollowing:进入自己回合,关闭其他人的倒计时,进入自己的倒计时阶段;
  • ForFollow:关闭自己的倒计时,然后将选择的牌添加到出牌区域,跳出自己回合;
  • NotFollow:关闭自己的倒计时,跳出自己回合;

图片 12图片 13

    /// <summary>
    /// 开始出牌
    /// </summary>
    public virtual void ToFollowing()
    {
        isMyTerm = true;

        //关闭倒计时
        StopCountDown(CountDownTypes.Follow);

        //开始倒计时
        StartCountDown(CountDownTypes.Follow);
    }
    /// <summary>
    /// 出牌
    /// </summary>
    public void ForFollow()
    {
        //关闭倒计时
        StopCountDown(CountDownTypes.Follow);

        //选择的牌,添加到出牌区域
        var selectedCards = cardInfos.Where(s => s.isSelected).ToList();
        var offset = 5;
        for (int i = 0; i < selectedCards.Count(); i++)
        {
            var card = Instantiate(prefabSmall, smallCardPos.position + Vector3.right * offset * i, Quaternion.identity, smallCardPos.transform);
            card.GetComponent<RectTransform>().localScale = Vector3.one * 0.3f;
            card.GetComponent<Image>().sprite = Resources.Load("Images/Cards/" + selectedCards[i].cardName, typeof(Sprite)) as Sprite;
            card.transform.SetAsLastSibling();

            smallCards.Add(card);
        }
        cardInfos = cardInfos.Where(s => !s.isSelected).ToList();

        CardManager._instance.ForFollow();
        isMyTerm = false;
    }
    /// <summary>
    /// 不出
    /// </summary>
    public void NotFollow()
    {
        //关闭倒计时
        StopCountDown(CountDownTypes.Follow);

        CardManager._instance.NotFollow();
        isMyTerm = false;
    }
    /// <summary>
    /// 销毁出牌对象
    /// </summary>
    public void DropAllSmallCards()
    {
        smallCards.ForEach(Destroy);
        smallCards.Clear();
    }

View Code

资源

项目源码

调整PlayerSelf类

实现ToFollowing:

调用基类的ToFollowing,并显示出牌按钮以供玩家选择

图片 14图片 15

    /// <summary>
    /// 开始出牌
    /// </summary>
    public override void ToFollowing()
    {
        base.ToFollowing();
        CardManager._instance.SetFollowButtonActive(true);
    }

View Code

调整PlayerOther类

模拟出牌,随机选择除手牌中的一张

图片 16图片 17

    void Update()
    {
        //如果当前是自己回合,模拟对手叫牌
        if (isMyTerm)
        {
            if (CardManager._instance.cardManagerState == CardManagerStates.Bid)
            {
                if (Input.GetKeyDown(KeyCode.Q))    //叫牌
                {
                    ForBid();
                }
                if (Input.GetKeyDown(KeyCode.W))    //不叫
                {
                    NotBid();
                }
            }
            if (CardManager._instance.cardManagerState == CardManagerStates.Playing)
            {
                if (Input.GetKeyDown(KeyCode.Q))    //出牌
                {
                    var rd1 = Random.Range(0, cardInfos.Count);
                    cardInfos[rd1].isSelected = true;

                    ForFollow();
                }
                if (Input.GetKeyDown(KeyCode.W))    //不出
                {
                    NotFollow();
                }
            }
        }
    }

View Code

调整CardManager

实现卡牌管理对玩家出牌的控制:

  • 发完地主牌以后,开始出牌阶段,由地主先出牌;
  • 玩家选择出牌后,将上轮玩家的出牌堆清空,并将选择的牌添加到自己的出牌堆,轮转到下个玩家;
  • 玩家选择不出牌,将上轮玩家的出牌堆清空,轮转到下个玩家;

图片 18图片 19

    /// <summary>
    /// 开始出牌阶段
    /// </summary>
    private void StartFollowing()
    {
        cardManagerState = CardManagerStates.Playing;
        //地主先出牌
        Players[bankerIndex].ToFollowing();
    }
    /// <summary>
    /// 玩家出牌
    /// </summary>
    public void ForFollow()
    {
        SetFollowButtonActive(false);

        //上轮玩家出牌清空
        Players[(termCurrentIndex + Players.Length - 1) % 3].DropAllSmallCards();
        if (Players[termCurrentIndex] is PlayerSelf)
            ShowPlayerSelfCards();

        SetNextPlayer();
        Players[termCurrentIndex].ToFollowing();
    }
    /// <summary>
    /// 玩家不出
    /// </summary>
    public void NotFollow()
    {
        SetFollowButtonActive(false);

        //上轮玩家出牌清空
        Players[(termCurrentIndex + Players.Length - 1) % 3].DropAllSmallCards();

        SetNextPlayer();
        Players[termCurrentIndex].ToFollowing();
    }

View Code

代码整理

现在我们的代码具有一定的规模了,为了方便更好的管理,把现有的代码重新整理一下,并进行功能分类,比如:

图片 20 
  图片 21

总结

嗯,今天到此为止,我们再来测试验证下,当然,目前只是实现了出牌的功能,没有对牌力进行校验和出牌的控制,对手玩家随机模拟出牌,尚未加入AI。我们以后逐步去实现~来看看这期的效果吧~

图片 22

资源

项目源码

相关文章