Unity - PuzzleGame 4 (記憶配對)

  記憶配對遊戲,場面上有數張卡片,每次可以翻開兩張卡片,如果兩張卡片圖案相同,那麼就可以把這兩張卡片從場上移除,如果兩張卡片圖案不同,就必須把兩張卡片翻回背面蓋好。

  最重要的就是記住翻過的卡片位置,當然也記住那張圖案是什麼,避免因為忘記再重複翻之前翻過的卡片浪費時間,遊戲就是越快把卡片全部移除越好。


  製作使用的Unity版本為5.2.2f1。


  實作測試 (WebGL build,稍微花點時間載入) (滑鼠點擊卡片翻牌,兩張卡片一樣就消除,全部消除遊戲結束)






1、卡片

  先來製作數張卡片物件,這邊稍微偷個懶,做一個空物件然後直接把卡片的背面跟正面的Sprite放進去,然後我稍稍地把卡片正面圖的Z軸位置往前移動一點點,這樣形成一個正反兩面不同的物件。

  之後要做翻牌效果就直接轉最上層物件的Y軸角度,就可以做出類似翻牌的感覺了。



  再來做一個簡單的卡片Component,這個Component不做什麼用,單純紀錄卡片群組號碼,用來辨識兩張卡片是否同一個群組,也可以用其他方法來判斷點選的兩張卡片是否相同,不過這邊就隨意啦。

public class Card : MonoBehaviour
{
    public int group;
}

  要先把這個Component加到卡片物件上也是可以,不過我是在Board物件中建立卡片的時候加上去順便設定群組號碼,所以這邊只要製作完這個Script就可以了,不用再做其他動作。





  所以這邊我每個卡片的Prefab只有在最上層的物件加上一個Box Collider 2D而已,依賴之後的Board物件加上別的Component。這邊可以預測到一個問題,就是如果卡片數量多的話,需要製作的Prefab也多,另外一種做法就是指製作一個Prefab,然後Instantiate物件的時候去切換正面的Sprite圖片。






2、板子

  開始來製作板子Component,同樣這次也是讓所有的判斷都寫在這裡,同時因為我只有使用16張卡片,所以有些數值以及做法是寫死的,如果要改變卡片的數量,就需要調整一些部分。

public class Board : MonoBehaviour
{
    public GameObject[] card; //卡片prefab
    private List<GameObject> cardList = new List<GameObject>(); //場上建立的所有卡片物件
    private List<Card> pickCard = new List<Card>(); //拾取的卡片
    private bool animFinished = true; //卡片翻面動作是否結束
    private int cardCount; //場上剩餘卡片計數

    void Start()
    {
        //簡易產生一組16個亂數順序
        int[] order = new int[16] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
        for (int i = 0; i < 16; ++i)
        {
            int random = Random.Range(0, 16);
            int backup = order[i];
            order[i] = order[random];
            order[random] = backup;
        }

        //這邊寫死只產生16張卡片
        for (int i = 0; i < 8; ++i) //卡片種類
        {
            for (int j = 0; j < 2; ++j) //每種產生兩張
            {
                GameObject newCard = GameObject.Instantiate(card[i]);
                newCard.AddComponent<Card>().group = i; //設定兩張卡片同一個group id
                newCard.transform.parent = this.transform; //把卡片物件放在Board下

                //設定卡片在4x4中的位置,我這邊產生的位置是用local座標
                //所以第一張卡的(0,0)會在Board物件的位置,開始往右跟往上增加卡片,形成4x4的排列,卡片的位置安排也要看自己的需求修改
                float posX = order[(i * 2 + j)] % 4;
                float posY = order[(i * 2 + j)] / 4;
                newCard.transform.localPosition = new Vector2(posX, posY);
                cardList.Add(newCard); //卡片加入列表記錄起來
            }
        }
        cardCount = cardList.Count; //紀錄場上卡片數量
    }

    void Update()
    {
        //滑鼠點左鍵,檢查卡片
        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
            if (hit)
            {
                Card target = hit.transform.GetComponent<Card>();
                if (target != null)
                {
                    CheckCard(target); //開始檢查卡片
                }
            }
        }
    }
    
    private void CheckCard(Card target)
    {
        if (!animFinished) return; //如果翻卡片動作還沒結束就不執行

        //如果拾取卡片沒有目前點的卡片,就把卡片加入列表,並且讓卡片翻過來
        if (!pickCard.Contains(target))
        {
            pickCard.Add(target);
            StartCoroutine(FlipAnim(target, pickCard));
        }
    }

    //簡易翻卡動作,直接轉物件的Y軸角度
    private IEnumerator FlipAnim(Card card, List<Card> pickList)
    {
        animFinished = false;
        while (true)
        {
            float angle = Mathf.MoveTowards(card.transform.localEulerAngles.y, 180, 360 * Time.deltaTime);
            card.transform.localRotation = Quaternion.Euler(new Vector3(0, angle, 0));
            if (angle == 180) break;
            yield return null;
        }

        //如果拾取卡片有兩張,就來比對
        if (pickList.Count == 2)
        {
            yield return new WaitForSeconds(0.3f);
            if (pickList[0].group == pickList[1].group) //如果兩張卡片是同組的
            {
                //關閉這兩張卡片的顯示
                pickList[0].gameObject.SetActive(false);
                pickList[1].gameObject.SetActive(false);
                cardCount -= 2; //卡片數量計數少兩張
                if (cardCount <= 0)
                {
                    //如果卡片計數為0表示場上的卡片都被清空了,遊戲結束
                    Debug.Log("GameOver");
                }
            }
            else //兩張卡片不同組,翻回背面
            {
                float angle = 180;
                while (true)
                {
                    angle = Mathf.MoveTowards(angle, 0, 360 * Time.deltaTime);
                    foreach(Card c in pickList)
                        c.transform.localRotation = Quaternion.Euler(new Vector3(0, angle, 0));
                    if (angle == 0) break;
                    yield return null;
                }
            }
            pickList.Clear(); //清空拾取卡片記錄
        }
        animFinished = true;
    }
}

  製作完後在場上做一個空物件把這個放上去,接著就可以開始把先前作好的卡片Prefab開始拉進來card這個列表。


  這邊我多放幾個Prefab在裡面,至少卡片的種類要大於場上卡片數量除以2的,避免同一種卡片超過兩張,這邊我產生卡片是寫死16張,所以至少要8種卡片,修改產生卡片的數量也要避免卡片種類不足。

  到這邊就完成了,剩下的就是UI顯示,像是剩餘卡片數量,做一個計時器,或是當完成遊戲後的結束畫面之類的。





  算是滿常見的遊戲,機制也很簡單,就單純翻兩張牌比對,相同就消去,全部消除完就結束,也可以簡單的一個Component就完成。

  如果有任何錯誤歡迎提出。

4 comments:

Unknown said...

版主,如果要讓牌一開始顯示正面,過幾秒變成背面用什麼方法會比較好

IVerv said...

那可以用一個Coroutine來跑,當玩家按下Start的時候執行這個Coroutine,先全部把卡片轉過來,再轉回背面接著才開始遊戲
例如:
private IEnumerator StartGame()
{
foreach(GameObject card in Cards)
{
//旋轉每張卡片的角度到正面
}

//等待N秒

foreach(GameObject card in Cards)
{
//轉回背面
}

//遊戲正式開始,開始計時
}

Unknown said...

請問in Cards這裡的Cards是指卡片的群組嗎(public GameObject[] card; //卡片prefab 這個)

另外旋轉的程式碼是放這個?
float angle = Mathf.MoveTowards(card.transform.localEulerAngles.y, 180, 360*Time.deltaTime);

card.transform.localRotation = Quaternion.Euler(new Vector3(0, angle, 0));
if (angle == 180) break;

IVerv said...

那個是放卡片prefab物件用的阿, 開遊戲隨機取卡片就會從這裡面取prefab物件來instantiate到場上
旋轉是這個沒錯啊, 這個旋轉就旋轉卡片y軸180度, 角度到了就跳出那個while, 也有別的方式可以做, 不過這邊有達到目的就好

Post a Comment