从零开始制作基于Unity引擎的宝石消消乐(三)

本文详细介绍消消乐游戏的核心开发流程,包括游戏面板布局、Jewel生成、交换匹配规则、消除动画实现及游戏板全盘匹配等关键环节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

完整项目我已经放到GitHub啦~
GitHub: https://github.com/lucaschen1993/Lukastar

前言

前两篇把消消乐的设计以及基本操作的方法都讲了,这章开始讲消消乐最核心的部分。

生成

首先,先在Hierarchy中制作好600600像素的游戏面板作为容器存放Jewel(每个Jewel大小为100100像素),这样就的到得到一个6*6可以存放36个Jewel的面板在这里插入图片描述
接着是游戏生成的制作方法。
在GameManager中有一个Sprite数组用于保存Jewel的Sprite(通过获取Jewel的形状通过Sprite改变)。

private Sprite[] jewelSprites;

然后在Start中通过动态加载来获取所有的JewelSprite,以及找到在Hierarchy中的游戏面板gamePlayPanel(因为Jewel要放在里边)

jewelSprites = Resources.LoadAll<Sprite>("Graphics/Jewels");
gamePlayPanel = GameObject.Find("/UICanvas/GamePlay/GamePlayPanel");

得到了JewelSprites之后,可以开始写生成的方法InitializedGame()
其实很容易就能想到根据游戏面板的rowSizecolSize可以直接生成36个Jewel,但是要有一个二维数组来保存这些Jewel,这样就容易得到每个Jewel在游戏面板的位置(可以用**GetJewelPosition()**得到)。
那么具体怎么生成的呢?
首先,每一个Jewel的Sprite都要是随机的,可以通过JewelSprites[Random.Range()]来得到随机的Sprite并赋值给Jewel中的JewelPicture。游戏本身的设计是开局生成一个没有三连以及三连以上的Jewel,所以在生成里需要做一些操作来保证生成的是一个没有以上这种情况的开局。这样的话每生成一个Jewel就需要向四周围延申去寻找是否有相同的Jewel了。
在这里插入图片描述
以下是具体的方法

                for (int k = 0; k < jewelSprites.Length; k++)
                {
                    remainIndex.Add(int.Parse(jewelSprites[k].name));
                }
                //把相同的放进数组equalList中
                if (j >= 2 && string.Equals(jewelObjects[i, j - 2].gameObject.transform.Find("JewelPicture").GetComponent<Image>().sprite.name, jewelObjects[i, j - 1].gameObject.transform.Find("JewelPicture").GetComponent<Image>().sprite.name))
                {
                    equalLists.Add(int.Parse(jewelObjects[i, j - 2].gameObject.transform.Find("JewelPicture").GetComponent<Image>().sprite.name));
                }
                if (i >=2 && string.Equals(jewelObjects[i - 2, j].gameObject.transform.Find("JewelPicture").GetComponent<Image>().sprite.name, jewelObjects[i - 1, j].gameObject.transform.Find("JewelPicture").GetComponent<Image>().sprite.name))
                {
                    equalLists.Add(int.Parse(jewelObjects[i - 2, j].gameObject.transform.Find("JewelPicture").GetComponent<Image>().sprite.name));
                }
                //判断equasList中的元素是否相同,若相同则删除一个
                if (equalLists != null && equalLists.Count == 2 && equalLists[0] == equalLists[1])
                {
                    equalLists.Remove(equalLists[1]);
                }
                //移除remainIndex里面与equalLists相同的元素
                for (int o = 0; o < equalLists.Count; o++)
                {
                    for (int p = 0; p < remainIndex.Count; p++)
                    {
                        if (equalLists[o] == remainIndex[p])
                        {
                            remainIndex.Remove(remainIndex[p]);
                            break;
                        }
                    }
                }

具体的思路就是找以前生成过Jewel进行匹配,看看该位置的四周是否有相同的Jewel,如果有相同的情况,就把该Sprite从数组中删除(这里声明了一个局部变量remainIndex的数组来保存JewelSprites的内容)。这样实例化Jewel的时候就不会遇到三连重复的Jewel了。
在这里插入图片描述

如上图所示,在游戏面板GamePlayPanel外生成Jewel,由于有mask组件的遮罩,所以生成的时候是看不见的,再把生成的Jewel通过DOTweenDOLocalMove移动到合适的位置,填充整个GamePlayPanel

jewelPrefab.transform.Find("JewelPicture").GetComponent<Image>().sprite = jewelSprites[remainIndex[UnityEngine.Random.Range(0, remainIndex.Count)] - 1];
GameObject itemObject = Instantiate<GameObject>(jewelPrefab, gamePlayPanel.transform, false);
itemObject.GetComponent<RectTransform>().localPosition = new Vector2(-250f + j * xOffset, 250f + (i-6) * yOffset);
itemObject.GetComponent<RectTransform>().DOLocalMove(new Vector2(-250f + j * xOffset, 250f + i * yOffset),1.0f);
jewelObjects[i, j] = itemObject;

在实例化的同时也要记着把每一个Jewel都保存到JewelObjects中。

交换

交换的方法其实在上一篇讲JewelManager的时候已经讲过了,就是两个Jewel,调用Move()的方法,互相移动到对方的位置。

匹配

现在介绍匹配的方法MatchJewel(),具体思路是先得到当前Jewel的位置,接着通过上、下、左、右四个方向搜索JewelObjects[,](上面提到过的方便用于保存Jewel的位置),查看是否有与当前位置的Jewel匹配的Jewel。如果有则横向的保存到matchColList,纵向的则保存到matchRowList中。如果有消除则该方法返回true

private bool MatchJewel(GameObject obj)
    {
        Vector3 jewelPosition = GetJewelPosition(obj);
        int x = (int)jewelPosition.x;
        int y = (int)jewelPosition.y;

        List<GameObject> matchRowList = new List<GameObject>();
        List<GameObject> matchColList = new List<GameObject>();
        matchRowList.Add(obj.transform.Find("JewelPicture").gameObject);
        #region 纵向匹配
        for (int i = x + 1; i <rowSize; i++)
        {
            if (i < rowSize && Equals(obj.transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name, jewelObjects[i, y].transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name))
            {
                matchRowList.Add(jewelObjects[i, y].transform.Find("JewelPicture").gameObject);
            }
            else
                break;
        }
        for (int i = x - 1; i >= 0; i--)
        {
            if (i >= 0 && Equals(obj.transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name, jewelObjects[i, y].transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name))
            {
                matchRowList.Add(jewelObjects[i, y].transform.Find("JewelPicture").gameObject);
            }
            else
                break;
        }
        #endregion
        matchColList.Add(obj.transform.Find("JewelPicture").gameObject);
        #region 横向匹配
        for (int i = y - 1; i >=0; i--)
        {
            if (i >= 0 && Equals(obj.transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name, jewelObjects[x, i].transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name))
            {
                matchColList.Add(jewelObjects[x, i].transform.Find("JewelPicture").gameObject);
            }
            else
                break;
        }
        for (int i = y + 1; i <colSize; i++)
        {
            if (i < colSize && Equals(obj.transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name, jewelObjects[x, i].transform.Find("JewelPicture").gameObject.GetComponent<Image>().sprite.name))
            {
                matchColList.Add(jewelObjects[x, i].transform.Find("JewelPicture").gameObject);
            }
            else
                break;
        }
        #endregion

接着判断这两个数组是否大于等于3,如果符合条件则把这些数组中的对象添加到needRemoveJewels中,等待下一步消除的操作。判断这里有三种情况,一种是横向连消,一种是纵向连消,还有一种是特殊情况,就是横向纵向都有消除的情况。

       //1.横向连消
        if (matchColList.Count >= 3 && matchRowList.Count < 3)
        {
            for (int i = 0; i < matchColList.Count; i++)
            {
                needRemoveJewels.Add(matchColList[i]);
            }
            //播放爆炸的声音
            AudioManager.Instance.PlayAudio("Audio/boom");
            return true;
        }
        //2.纵向连消
        else if (matchRowList.Count >= 3 && matchColList.Count < 3)
        {
            for (int i = 0; i < matchRowList.Count; i++)
            {
                needRemoveJewels.Add(matchRowList[i]);
            }
            //播放爆炸的声音
            AudioManager.Instance.PlayAudio("Audio/boom");
            return true;
        }
        //3.特殊情况
        else if (matchColList.Count >= 3 && matchRowList.Count >= 3)
        {
            matchRowList.RemoveAt(0);
            for (int i = 0; i < matchRowList.Count; i++)
            {
                needRemoveJewels.Add(matchRowList[i]);
            }
            for (int i = 0; i < matchColList.Count; i++)
            {
                needRemoveJewels.Add(matchColList[i]);
            }
            //播放爆炸的声音
            AudioManager.Instance.PlayAudio("Audio/boom");
            return true;
        }

消除

消除方法RemoveJewel() 是消除从匹配方法得到得needRemoveJewel的数组。

    private bool RemoveJewel()
    {
        bool needRefill = false;
        for (int i = 0; i < needRemoveJewels.Count; i++)
        {
            if (needRemoveJewels[i]!= null)
            {
                needRemoveJewels[i].GetComponent<JewelPicture>().Fade();
                needRefill = true;
            }
        }        
        JewelsManager.Instance.ResetAllJewelSelected();
        needRemoveJewels.Clear();
        isRemove = true;
        return needRefill;
    }

这里的Fade()方法是在JewelPicture组件中,JewelPicture挂载在Jewel的子对象JewelPicture,因为如果通过Jewel来制作消除动画,会导致动画混乱,因为在两者交换的同时播放了动画,会出现消除动画播放错误的bug,因此才把消除动画的方法放在JewelPicture中。这里也使用了协程的方法,因为要控制动画播放的时间制作出一个间隔。
JewelPciture.cs

    public void Fade()
    {
        anim.SetTrigger("JewelCrash");
        StartCoroutine(FadeJewel());
    }
    IEnumerator FadeJewel()
    {
        yield return new WaitForSeconds(0.7f);
        transform.SetParent(null);
        Destroy(gameObject);
    }

再填充

Jewel消除了以后,游戏面板就会出现空缺(其实就是Destroy了Jewel的子对象JewelPicture),这个时候就需要把当前格子上面的Jewel的子对象JewelPicture来填充到当前的Jewel中。具体的做法是该Jewel空格子往上拿Jewel的JewelPicture,就是把别人的变成自己的。简单的说就是我拿你的JewelPicture,你拿上面的JewelPicture,他拿更上面的JewelPicture变为自己的JewelPicture。
在这里插入图片描述
如上图所示,这样操作就相当于上面的Jewel往下掉落下来,填充到空的格子。那么把消除后的空格子填充了以后,被拿走JewelPicture的空格子怎么办呢?这个时候就需要在空格子的最上面,GamePlayPanel外的位置重新生成JewelPicture并且填充到空的格子,就像InitializedGame()一样,但是此时不用考虑是否重复的问题,尽管随机生成就好了。

    private IEnumerator FillJewels()
    {
        yield return new WaitForSeconds(animationPlayTime);
        //自下而上遍历整个游戏棋盘,遇到空的向上遍历把上面的sprite拿给自己
        for (int i = rowSize - 1; i > 0; i--)
        {
            for (int j = 0; j < colSize; j++)
            {
                //Debug.Log(GetJewelPosition(jewelObjects[i, j])+" "+jewelObjects[i, j].transform.Find("JewelPicture").GetComponent<Image>().sprite);
                //当前格子为空,则不断往上找不为空的格子并把它的sprite赋值给当前格子,则找到的格子赋值为空
                if (jewelObjects[i, j].transform.Find("JewelPicture")== null)
                {
                    for (int k =i; k >= 0; k--)
                    {
                        if (jewelObjects[k, j].transform.Find("JewelPicture") != null)
                        {
                            //当前对象
                            GameObject thisObj = jewelObjects[i, j];
                            //当前对象上面其中一个对象
                            GameObject topObj = jewelObjects[k, j];
                            GameObject topObjChild = topObj.transform.Find("JewelPicture").gameObject;
                            topObjChild.transform.SetParent(thisObj.transform);
                            topObjChild.transform.DOLocalMove(Vector3.zero, moveDownTime);
                            break;
                        }
                    }
                }
            }
        }

这里又!又!又!!!用到协程了!!!再解释一下,这个协程是为了使游戏更流畅,所以waitforsecond是等待了一个动画时间,其实就是消除动画播放完以后再执行填充的操作嘛。

匹配规则

接下来讲匹配规则MatchRule(),这里是判断交换的两个Jewel是否有消除的情况,如果两个Jewel都没有消除的情况则调用Reset的方法,恢复两个Jewel的位置,如果有消除的情况再调用Remove的方法进行消除的操作,消除完后再启用协程填充GamePlayPanel

    //判断匹配的规则
    public void MatchRule()
    {
        GameObject lastSelected = JewelsManager.Instance.lastSelected;
        GameObject currentSeleted = JewelsManager.Instance.currentSeleted;

        bool lastMatch = MatchJewel(lastSelected);
        bool currentMatch = MatchJewel(currentSeleted);
        if (!lastMatch&&!currentMatch)
        {
            JewelsManager.Instance.ResetOriginJewel();
        }else if( lastMatch || currentMatch)
        {
            RemoveJewel();
            StartCoroutine(FillJewels());
        }
    }

全盘匹配

最后再来讲全盘匹配的问题,因为当再次填充以后,有可能会出现三连以及三连以上的情况,所以这个时候需要遍历JewelObjects[,]来查看是否有匹配的,是否需要消除。如果没有,不需要的话,那么消消乐的一次移动流程就已经完成啦。如果有匹配的,需要消除的,那就把该对象扔到needRemoveJewels的数组,再调用RemoveJewel把它删掉,接着再填充,再全盘匹配,再消除,再填充,再全盘匹配…直到没有匹配的情况出现为止。

    private void ReMatchGameBorad()
    {
        for (int i = 0; i < rowSize; i++)
        {
            for (int j = 0; j < colSize; j++)
            {
                if (jewelObjects[i, j].transform.Find("JewelPicture").gameObject)
                {
                    MatchJewel(jewelObjects[i, j]);
                }
            }
        }
        //当needRemoveJewels里面有元素才调用RemoveJewel的方法
        if(needRemoveJewels.Count>0)
        {
            RemoveJewel();
            StartCoroutine(FillJewels());
        }
    }

这里有个另外一个思路,因为在再填充的时候我们是需要移动Jewel的中的JewelPicture,这样我们可以得到操作过的Jewel,把它们保存到另外一个数组再进行全盘匹配,会比这个遍历整个JewelObjects省时省力很多。

总结

现在把消消乐的基础内容都讲完啦,做消消乐真的很有乐趣的,特别是每一个模块做出来就会很有成就感。其实做休闲游戏会比做rpg复杂一些,因为很多逻辑上的问题要处理。往后我会在消消乐中添加一些rpg的元素,制作成一个休闲益智的角色扮演闯关游戏。整个完整项目我已经放到github,有需要的朋友可以down下来,或者有什么建议,游戏设计上的改进,麻烦提出来,因为我太想进步了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值