• Unity3D手机斗地主游戏开发实战(01)_发牌功能实现


    园子荒废多年,闲来无事,用Unity3D来尝试做一个简单的小游戏,一方面是对最近研究的Unity3D有点总结,一方面跟广大的园友相互学习和提高。话不多说,进入正题~

    创建项目

    1.创建Unity2017的2D项目,暂且叫做ChinesePoker吧,就用自带的UGUI来编辑UI, 目前只导入iTween插件,用来方便控制动画效果。

    目录结构如下:

      

    考虑卡牌需要动态生成,我把图片资源放到Resource目录,并按照Card_类型(大小王,红桃,黑桃,方片,梅花 )_数字(卡牌所在类型中的数字)命名。

    素材都是网上找的,没有美工基础,就是这么个意思,大家将就看吧,:)

    2.建第一个场景,默认叫001_Playing,作为主要玩牌的场景,暂时作为第1个场景,后期新场景添加进来,我们可能再调整场景的顺序。

    添加一个UI->Image,选择一个背景图片;

    添加3个UI->Canvas,分别取名叫Player0,Player1,Player2,代表玩家,对手1,对手2;

    每个Player底下,添加一个Image,选择卡牌背面图片,分别表示发牌时各自牌堆的位置,并在桌面放置一个总牌堆的位置,默认not active;

    建一个卡牌的图片,命名为Card,并作为预制件,放入Player0中间一个,稍微偏移一定位置再放置一个,用来计算每张牌跟临牌相对位置,设置not active;

    建一个卡牌的背面图片,命名Cover,也作为预制件;

    添加一个测试按钮TestButton;

    差不多了,大概结构如下:

    创建卡牌、玩家信息

    1.新建CardInfo类,主要不要继承默认的MonoBehaviour类,用来作为卡牌的实体类;

    实现IComparable接口,后面手牌排序会用到。

    public class CardInfo : IComparable
    {
        public string cardName; //卡牌图片名
        public CardTypes cardType; //牌的类型
        public int cardIndex;      //牌在所在类型的索引1-13
    
        public CardInfo(string cardName)
        {
            this.cardName = cardName;
            var splits = cardName.Split('_');
    
            switch (splits[1])
            {
                case "1":
                    cardType = CardTypes.Hearts;
                    cardIndex = int.Parse(splits[2]);
                    break;
                case "2":
                    cardType = CardTypes.Spades;
                    cardIndex = int.Parse(splits[2]);
                    break;
                case "3":
                    cardType = CardTypes.Diamonds;
                    cardIndex = int.Parse(splits[2]);
                    break;
                case "4":
                    cardType = CardTypes.Clubs;
                    cardIndex = int.Parse(splits[2]);
                    break;
                case "joker":
                    cardType = CardTypes.Joker;
                    cardIndex = int.Parse(splits[2]);
                    break;
                default:
                    throw new Exception(string.Format("卡牌文件名{0}非法!", cardName));
            }
        }
    
        //卡牌大小比较
        public int CompareTo(object obj)
        {
            CardInfo other = obj as CardInfo;
    
            if (other == null)
                throw new Exception("比较对象类型非法!");
    
            //如果当前是大小王
            if (cardType == CardTypes.Joker)
            {
                //对方也是大小王
                if (other.cardType == CardTypes.Joker)
                {
                    return cardIndex.CompareTo(other.cardIndex);
                }
                //对方不是大小王
                return 1;
            }
            //如果是一般的牌
            else
            {
                //对方是大小王
                if (other.cardType == CardTypes.Joker)
                {
                    return -1;
                }
                //如果对方也是一般的牌
                else
                {
                    //计算牌力
                    var thisNewIndex = (cardIndex == 1 || cardIndex == 2) ? 13 + cardIndex : cardIndex;
                    var otherNewIndex = (other.cardIndex == 1 || other.cardIndex == 2) ? 13 + other.cardIndex : other.cardIndex;
    
                    if (thisNewIndex == otherNewIndex)
                    {
                        return -cardType.CompareTo(other.cardType);
                    }
    
                    return thisNewIndex.CompareTo(otherNewIndex);
                }
            }
        }
    
    }
    View Code

    2.Card预制件上,添加Card脚本,主要保存对应CardInfo信息、选中状态,并加载卡牌图片;

    public class Card : MonoBehaviour
    {
        private Image image;        //牌的图片
        private CardInfo cardInfo;  //卡牌信息
        private bool isSelected;    //是否选中
    
        void Awake()
        {
            image = GetComponent<Image>();
        }
    
        /// <summary>
        /// 初始化图片
        /// </summary>
        /// <param name="cardInfo"></param>
        public void InitImage(CardInfo cardInfo)
        {
            this.cardInfo = cardInfo;
            image.sprite = Resources.Load("Images/Cards/" + cardInfo.cardName, typeof(Sprite)) as Sprite;
        }
        /// <summary>
        /// 设置选择状态
        /// </summary>
        public void SetSelectState()
        {
            if (isSelected)
            {
                iTween.MoveTo(gameObject, transform.position - Vector3.up * 10f, 1f);
            }
            else
            {
                iTween.MoveTo(gameObject, transform.position + Vector3.up * 10f, 1f);
            }
    
            isSelected = !isSelected;
        }
    }
    View Code

    3.考虑玩家分为2种类型,先创建一个公共的基类,实现玩家公共的方法,比如增加一张卡牌、清空所有卡片、排序等;

    public class Player : MonoBehaviour
    {
        protected List<CardInfo> cardInfos = new List<CardInfo>();  //个人所持卡牌
    
        private Text cardCoutText;
    
        void Start()
        {
            cardCoutText = transform.Find("HeapPos/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();
        }
    
        protected void Sort()
        {
            cardInfos.Sort();
            cardInfos.Reverse();
        }
    }
    View Code

    4.添加第一种玩家(自身玩家)PlayerSelf,继承Player,并挂载到Player0对象上;

    实现整理手牌的逻辑:发牌后,从中间的位置,根据大小依次将牌展开;

    获取牌面点击事件,将牌选中或取消选中;

    public class PlayerSelf : Player
    {
        public GameObject prefab;   //预制件
    
        private Transform originPos1; //牌的初始位置
        private Transform originPos2; //牌的初始位置
        private List<GameObject>  cards=new List<GameObject>();
    
        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]);
    
                var targetPos = startPos + Vector3.right * offsetX * i;
                card.transform.SetAsLastSibling();
                //动画移动
                iTween.MoveTo(card, targetPos, 2f);
    
                cards.Add(card);
            }
        }
    
        public void DestroyAllCards()
        {
            cards.ForEach(Destroy);
            cards.Clear();
        }
    
        /// <summary>
        /// 点击卡牌处理
        /// </summary>
        /// <param name="data"></param>
        public void CardClick(BaseEventData data)
        {
            //叫牌或出牌阶段才可以选牌
            if (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

    5.添加另一种玩家(对手玩家)PlayerOther,继承Player,并挂载到Player1,Player2对象上;

    暂时没有实现任何其他功能:

    public class PlayerOther : Player
    {
       
    }
    View Code

    实现发牌逻辑

    在Camera上添加卡牌管理脚本:CardManager

    1.实现洗牌逻辑,这里用生成GUID随机性后排序,达到洗牌的目的;

    2.记录当前发牌回合,每发一张牌,跳转给下一个玩家;

    3.记录当前玩牌回合(将来可能用到),每玩一局,跳转下个玩家开始发牌;

    4.发牌逻辑:

    设置牌堆的显示,动画依次给每位玩家发一张卡牌,发完牌后,隐藏牌堆,并将玩家的卡牌排序并展示;

    public class CardManager : MonoBehaviour
    {
        public float dealCardSpeed = 20;  //发牌速度
        public Player[] Players;    //玩家的集合
    
        public GameObject coverPrefab;      //背面排预制件
        public Transform heapPos;           //牌堆位置
        public Transform[] playerHeapPos;    //玩家牌堆位置
    
    
        public static CardManager _instance;    //单例
        public CardManagerStates cardManagerState;
    
        private string[] cardNames;  //所有牌集合
        private int termStartIndex = 0;  //回合开始玩家索引
        private int termCurrentIndex = 0;  //回合当前玩家索引
        private List<GameObject> covers = new List<GameObject>();   //背面卡牌对象,发牌结束后销毁
    
        void Awake()
        {
            _instance = this;
    
            cardNames = GetCardNames();
        }
        /// <summary>
        /// 洗牌
        /// </summary>
        public void ShuffleCards()
        {
            //进入洗牌阶段
            cardManagerState = CardManagerStates.ShuffleCards;
            cardNames = cardNames.OrderBy(c => Guid.NewGuid()).ToArray();
        }
        /// <summary>
        /// 发牌
        /// </summary>
        public IEnumerator DealCards()
        {
            //进入发牌阶段
            cardManagerState = CardManagerStates.DealCards;
    
            //显示牌堆
            heapPos.gameObject.SetActive(true);
            playerHeapPos.ToList().ForEach(s => { s.gameObject.SetActive(true); });
    
            foreach (var cardName in cardNames)
            {
                //给当前玩家发一张牌
                Players[termCurrentIndex].AddCard(cardName);
    
                var cover = Instantiate(coverPrefab, heapPos.position, Quaternion.identity, heapPos.transform);
                cover.GetComponent<RectTransform>().localScale = Vector3.one;
                covers.Add(cover);
                iTween.MoveTo(cover, playerHeapPos[termCurrentIndex].position, 0.3f);
    
                yield return new WaitForSeconds(1 / dealCardSpeed);
    
                //下一个需要发牌者
                SetNextPlayer();
            }
    
            //隐藏牌堆
            heapPos.gameObject.SetActive(false);
            playerHeapPos[0].gameObject.SetActive(false);
    
            //显示玩家手牌
            Players.ToList().ForEach(s =>
            {
                var player0 = s as PlayerSelf;
                if (player0 != null)
                {
                    player0.GenerateAllCards();
                }
            });
            //动画结束,进入叫牌阶段
            yield return new WaitForSeconds(2f);
            covers.ForEach(Destroy);
            cardManagerState = CardManagerStates.Bid;
        }
        /// <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>
        /// <returns></returns>
    
        private void SetNextPlayer()
        {
            termCurrentIndex = (termCurrentIndex + 1) % Players.Length;
        }
        /// <summary>
        /// 切换开始回合玩家
        /// </summary>
        public void SetNextTerm()
        {
            termStartIndex = (termStartIndex + 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;
        }
    
        //for test
        public void OnTestClick()
        {
            ClearCards();
            ShuffleCards();
            StartCoroutine(DealCards());
        }
    
    }
    View Code

    总结

    其实发牌后的动画,可以由override基类的方法,交由Player子类处理,不用CardManager集中管理,大家可以优化一下。

    大体逻辑完成,我们验证下效果吧:

    资源

    项目源码

  • 相关阅读:
    帧同步与状态同步的区别
    spread语法解析与使用
    CoordinatorLayout自定义Bahavior特效及其源码分析
    更便捷的Android多渠道打包方式
    用Dart&Henson玩转Activity跳转
    用RxJava处理复杂表单验证问题
    用RxJava处理嵌套请求
    技术与业务的抉择————论程序员的“瓶颈”问题
    Android Studio单元测试入门
    Android一键多渠道分发打包实战和解析
  • 原文地址:https://www.cnblogs.com/lizzie-xhu/p/7761586.html
Copyright © 2020-2023  润新知