본문 바로가기

UNITY

UNITY_20230802[카드 짝 맞추기]

매니저님, 글의 9할은 학습내용이고 오늘 TIL주제 관련 글은 가장 밑에 있습니다. (이 글은 20230805이후 지우다.)

 

아주 유명한 고전게임이다. 앞면이 같은 2개 한쌍을 이루는 다수의 카드를 뒷면을 보이게 뒤집어 놓고, 한 차례에 두개씩 카드를 뒤집어 보면서 같은 한 쌍을 찾아내 없애는 게임이다. 뒤집은 두개의 카드가 다를 경우 해당 두 카드는 다시 뒷면이 보이게 그대로 둔다.

 

일본에서는 神経衰弱라는 이름으로 불린다. 이름 잘 지었다. 제한시간 걸어두고 하면 정신나갈 것 같다.


게임 씬은 다음과 같다.

아래는 각 스크립트다.

GM

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Linq;

public class gameManager : MonoBehaviour
{
    public static gameManager I;

    public Text timeText;
    float time = 30.0f;
    public GameObject card;
    public GameObject firstCard;
    public GameObject secondCard;
    public GameObject endText;

    private void Awake()
    {
        I = this;
    }

    void Start()
    {
        Time.timeScale = 1.0f;

        int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
        rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();

        for (int i = 0; i < 16; i++)
        {
            GameObject newCard = Instantiate(card);
            newCard.transform.parent = GameObject.Find("cards").transform;

            float x = (i % 4 * 1.4f) - 2.1f;
            float y = (i / 4 * 1.4f) - 3.0f;
            newCard.transform.position = new Vector3(x, y, 0);

            string rtanName = "rtan" + rtans[i].ToString();
            newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
        }
    }

    void Update()
    {
        time -= Time.deltaTime;
        timeText.text = time.ToString("N2");

        if (time <+ 0.0f)
        {
            time = 0.0f;
            gameEnd();
        }
    }

    public void isMatched()
    {
        string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;

        if (firstCardImage == secondCardImage)
        {
            firstCard.GetComponent<card>().destroyCard();
            secondCard.GetComponent<card>().destroyCard();

            int cardsLeft = GameObject.Find("cards").transform.childCount;
            if (cardsLeft == 2)
            {
                Invoke("gameEnd", 1.1f);
            }
        }
        else
        {
            firstCard.GetComponent<card>().closeCard();
            secondCard.GetComponent<card>().closeCard();
        }

        firstCard = null;
        secondCard = null;
    }

    public void gameEnd()
    {
        Time.timeScale = 0.0f;
        endText.SetActive(true);
    }
}

카드

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class card : MonoBehaviour
{
    public Animator anim;

    void Start()
    {
        
    }

    void Update()
    {
        
    }

    public void openCard()
    {
        anim.SetBool("isOpen", true);
        transform.Find("front").gameObject.SetActive(true);
        transform.Find("back").gameObject.SetActive(false);

        if (gameManager.I.firstCard == null)
        {
            gameManager.I.firstCard = gameObject;
        }
        else
        {
            gameManager.I.secondCard = gameObject;
            gameManager.I.isMatched();
        }
    }

    public void destroyCard()
    {
        Invoke("destroyCardInvoke", 1.0f);
    }
    public void destroyCardInvoke()
    {
        Destroy(gameObject);
    }
    public void closeCard()
    {
        Invoke("closeCardInvoke", 1.0f);
    }
    public void closeCardInvoke()
    {
        anim.SetBool("isOpen", false);
        transform.Find("front").gameObject.SetActive(false);
        transform.Find("back").gameObject.SetActive(true);

    }
}

게임오버 (텍스트)

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class endText : MonoBehaviour
{
    public void reGame()
    {
        SceneManager.LoadScene("MainScene");
    }
}

이번 기록의 주된 내용은 로직이다.
총 16개의 카드를 어떻게 4x4배열로 표시할까?

이 카드들을 어떻게 생성하고 관리해야 효율적일까?

각 카드는 같은 그림을 가진 카드가 2개씩 총 8쌍이 있고, 무작위로 배치돼야 한다.

카드는 한 차례에 2개 뒤집을 수 있는데, 이 시행은 어떻게 구현할까? 짝이 맞으면 사라지고, 서로 다르면 다시 원래대로 돌아간다.

게임의 9할이 카드 로직이다. 그만큼 중요하다. 또한 그만큼 직관적으로 작성하겠다.

그림이 있는 면이 앞면, 없는 면이 뒷면이다.


for 문을 사용한 프리팹 생성 - 카드 배치

작성하다가 티스토리 오류때문에 다 날아가서 멘탈이 한번 나갔다. 

작성하다가 티스토날아가서 멘탈이 한번 나갔다. 

작성하다가 티스토리 오류 한번 나갔다. 

작성하오류때문에 다 날아가서 멘탈이 한번 나갔다. 

멘탈이갔다. 

멘탈

저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 저장을 생활화하자 

 

2개 짝을 이룬 8쌍, 총 16개의 카드가4x4로 배치돼야 하고, 한쌍의 카드 앞면에는 같은 그림이 있어야 한다.

프리팹의 생성부터 4x4배치, 무작위 그림부여까지 순서대로 작성한다.

 

 

1. 우선 프리팹의 바탕이 될 카드오브젝트의 작성이다.

인게임 계층 뷰다.

cards : Empty Object / card : Empty Object / front : image

 

2. 8개의 그림이 필요하다.

아래는 그림이다. 그림은 프로젝트 뷰의 Resources폴더에 보관한다.

왜 Imges폴더에서 관리하지 않고, Resources폴더로 관리하는 것일까?

 

3. 한 그룹 내에 다수의 프리팹을 생성하기

 

단순히 프리팹을 16개 생성만 하는 것이 아닌, 배치까지 이루어져야 하므로 생성된 오브젝트는 생성 순서대로 변수에 할당된다.

아래는 프리팹 생성 코드다.

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class gameManager : MonoBehaviour
{
    public GameObject card;

    void Start()
    {
        // int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
        // rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();

        for (int i = 0; i < 16; i++)
        {
            GameObject newCard = Instantiate(card);
            newCard.transform.parent = GameObject.Find("cards").transform;

            /* float x = (i % 4 * 1.4f) - 2.1f;
            float y = (i / 4 * 1.4f) - 3.0f;
            newCard.transform.position = new Vector3(x, y, 0);

            string rtanName = "rtan" + rtans[i].ToString();
            newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName); */
        }
    } // 문제 해결 순서대로 활성화한다.

0~15까지 생성된 카드는 newCard에 할당되고, 그 오브젝트는 cards오브젝트의 하위 오브젝트로 들어간다.

이 코드만으로 게임 실행 시 모든 카드가 같은 위치에 16개 겹치는 모양으로 생성된다.


4. for 문만을 이용한 오브젝트의 배치 로직

카드의 디폴트 위치xyz는 0,0,0이고, 크기는 1.3정사각형, 1.4간격으로 4x4배치한다.

아래는 그 코드다.

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class gameManager : MonoBehaviour
{
    public GameObject card;

    void Start()
    {
        // int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
        // rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();

        for (int i = 0; i < 16; i++)
        {
            GameObject newCard = Instantiate(card);
            newCard.transform.parent = GameObject.Find("cards").transform;

            float x = (i % 4 * 1.4f) - 2.1f;
            float y = (i / 4 * 1.4f) - 3.0f;
            newCard.transform.position = new Vector3(x, y, 0);

            /* string rtanName = "rtan" + rtans[i].ToString();
            newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName); */
        }
    } // 문제 해결 순서대로 활성화한다.

하나씩 설명한다.

16개의 카드 위치가

(i,0,0) => x축 0,1,2,3...으로 겹치게 정렬된다. x축 간격을 수정한다.

(i*1.4,0,0) => x축 1.4의 간격으로 정렬된다. y축이 서로 다르게 수정한다.

(i*1.4,i,0) => y축 0,1,2,3...으로 겹치게 완만한 대각선 정렬된다. y축 간격을 수정한다.

(i*1.4,i*1.4,0) => y축 1.4의 간격으로 45도 대각선 정렬된다. x축 4개마다 y축이 달라지도록 수정한다.

(i*1.4,i/4*1.4,0) =>  카드가 4개씩 생성될 때 마다 y축 0,0,0,0,1,1,1,1,2,2,2,2...*1.4로 계단식 정렬된다. 4n*1번째(사실 이것도 틀린 말이긴 한데)카드마다 x축을 통일해서 배치한다.

(i%4*1.4,i/4*1.4,0) => 카드가 4개씩 생성될 때 마다 x축 0,1,2,3,0,1,2,3,0,1,2,3...*1.4로 통일되어 배치된다.
비로소 4x4 바둑판 배치의 완성이다. 게임 화면에 맞게 카드의 디폴트 위치를 x-2.1,y-3.0,0으로 수정한다.

 

5. 배열 내 변수의 무작위 재배치 - OrderBy(item => (무작위)), Toarray();

 

2개 한쌍의 8쌍 16개 카드를 구분할 코드와, 한 쌍의 카드에는 동일한 그림이 부여돼야 한다.

위의 그림과 같이, 사용될 그림의 이름은 rtan0~7이다.

아래는 그 코드다.

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class gameManager : MonoBehaviour
{
    public GameObject card;

    void Start()
    {
        int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
        rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();

        for (int i = 0; i < 16; i++)
        {
            GameObject newCard = Instantiate(card);
            newCard.transform.parent = GameObject.Find("cards").transform;

            // float x = (i % 4 * 1.4f) - 2.1f;
            // float y = (i / 4 * 1.4f) - 3.0f;
            // newCard.transform.position = new Vector3(x, y, 0); 번거로워서 생략

            string rtanName = "rtan" + rtans[i].ToString();
            newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
        }
    } // 문제 해결 순서대로 활성화한다.

예를 들어 설명한다.

0~7 두개씩 구성원을 가진 rtans리스트를 작성한다.

0011223344처럼 가지런한 배열을 467753501426... 무작위로 섞고, 재배열한다.

"rtan+5(4번째 생성카드의 rtans번호)"데이터의 rtanName변수를 생성한다.

4번째 카드 front의 SpriteRender-sprite옵션에 Resources파일의 rtan5이미지 스프라이트를 할당한다.

같은 방식으로, 4번째 카드와 6번째 카드는 rtan5라는 동일한 이미지를 앞면으로 하는 카드가 됐다.


카드의 판단 로직

 

카드를 클릭하면 뒤집고, 뒤집은 카드는 1번째(2번째)인가 판단, 두 카드를 뒤집었을 때 서로 일치 여부 판단, 일치했을(서로 다를) 때 동작 지시, 게임오버 시점 판단 기능을 작성한다.

GM과 카드 스크립트의 동작교환이 필요한 중요한 부분이다.

당연히 GM에 싱글톤 디자인이 들어가고, 카드에서도 GM을 호출한다.

뒤집는다 어쩐다 글을 쓰는데, 사실은 앞뒷면의 (비)활성화를 교대하는 동작이다.

순서대로 작성한다.

 

1. 카드 뒤집기

카드에 button컴포넌트를 추가한다. 클릭 시 실행될 함수는 openCard()다.

아래는 button을 추가한 카드 프리팹이다.

 

2. 뒤집은 카드는 1번째(2번째)인가 판단

아래는 GM,카드의 코드다.

카드 (이걸로 코드는 완성이다.)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class card : MonoBehaviour
{
    public void openCard()
    {
        transform.Find("front").gameObject.SetActive(true);
        transform.Find("back").gameObject.SetActive(false);

        if (gameManager.I.firstCard == null)
        {
            gameManager.I.firstCard = gameObject;
        }
        else
        {
            gameManager.I.secondCard = gameObject;
            gameManager.I.isMatched();
        }
    }

    public void destroyCard()
    {
        Invoke("destroyCardInvoke", 1.0f);
    }
    public void destroyCardInvoke()
    {
        Destroy(gameObject);
    }
    public void closeCard()
    {
        Invoke("closeCardInvoke", 1.0f);
    }
    public void closeCardInvoke()
    {
        anim.SetBool("isOpen", false);
        transform.Find("front").gameObject.SetActive(false);
        transform.Find("back").gameObject.SetActive(true);

    }
}

GM

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class gameManager : MonoBehaviour
{
    public static gameManager I;

    public Text timeText;
    public GameObject card;
    public GameObject firstCard;
    public GameObject secondCard;
    public GameObject endText;

    private void Awake()
    {
        I = this;
    }
// start함수는 생략한다.
    public void isMatched()
    {
        string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;

        if (firstCardImage == secondCardImage)
        {
            firstCard.GetComponent<card>().destroyCard();
            secondCard.GetComponent<card>().destroyCard();

            int cardsLeft = GameObject.Find("cards").transform.childCount;
            if (cardsLeft == 2)
            {
                Invoke("gameEnd", 1.1f);
            }
        }
        else
        {
            firstCard.GetComponent<card>().closeCard();
            secondCard.GetComponent<card>().closeCard();
        }

        firstCard = null;
        secondCard = null;
    }

    public void gameEnd()
    {
        Time.timeScale = 0.0f;
        endText.SetActive(true);
    }
}

GM에서 선언한 게임오브젝트 firstCard, secondCard는 선언만 했으므로, 값은 null이다.

카드를 뒤집는 시점에서 이미 뒤집은 카드가 없을 때, 뒤집은 카드는 GM.I.firstCard에 할당된다. 그 외는 GM.I.secondCard에 할당되고, isMatched()가 작동한다.

 

isMatched()는 뒤집힌 카드 앞면 그림의 일치 여부의 판단, 이후 동작을 기능한다.

isMatched()가 작동하면, 우선 뒤집힌 두 카드의 그림을 firstCardImage, secondCardImage 각 카드에 할당한다.

그림이 같다면, 해당 두 카드의 card컴포넌트-destroyCard()를 호출한다. 이때 GM에서 카드스크립트를 호출한다.

후속 동작으로, cards의 하위 오브젝트(card)가 몇개 남았는지 세어 leftCard변수로 할당한다.

leftCard=2일때, 1.1초 뒤 gameEnd()를 호출한다.

남은 카드가 2일때? (앞면이 같은)마지막 남은 카드 2개가 뒤집힌 시점에 작동하는 기능이다. destroyCard()의 작동 시점과 같이 보면 맞는 코드라는 게 이해가 간다.

그림이 다르다면(그 외의 경우라면), 해당 두 카드의 card컴포넌트-closeCard()를 호출한다.

 

그림의 일치 여부의 판단이 끝난 후, firstCard, secondCard는 빈 상태로 돌아가야 하니 null을 다시 할당한다.

 

호출되는 함수 상세는 생략하겠다.


한 오브젝트의 애니메이션 교체

카드는 뒷면 상황일 때 틴트 애니메이션의 반복, 앞면 전환 시점에서 맥동 애니메이션 1회 작동을 가진다.

면 전환 과정과 동시에 애니메이션의 전환도 같이 작동한다.

다음은 카드의 컴포넌트, 코드다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class card : MonoBehaviour
{
    public Animator anim;

    public void openCard()
    {
        anim.SetBool("isOpen", true);
        transform.Find("front").gameObject.SetActive(true);
        transform.Find("back").gameObject.SetActive(false);

        /* if (gameManager.I.firstCard == null)
        {
            gameManager.I.firstCard = gameObject;
        }
        else
        {
            gameManager.I.secondCard = gameObject;
            gameManager.I.isMatched();
        } */
    }
    public void closeCard()
    {
        Invoke("closeCardInvoke", 1.0f);
    }
    public void closeCardInvoke()
    {
        anim.SetBool("isOpen", false);
        transform.Find("front").gameObject.SetActive(false);
        transform.Find("back").gameObject.SetActive(true);

    }
}

애니메이션 전환에 필요한 Animator변수의 설정, 전환 판단 논리값isOpen은 기본,
애니메이션 간 전환Transition 옵션 Has exit time, Transition Duration 설정이 필요하다.


이번 게임은 새로운 기능의 학습보다는 로직에 중심을 맞춘 게임이었다.
오브젝트 배치로직, GM과 타 오브젝트 간의 코드 교류 등 약간 고차원의 논리를 요하는 학습이었다.


진짜 작성 도중에 저장없이 다 날려서 마우스로 사슴벌레 만들 뻔했다.

컴퓨터 앞에 서는 노동자의 철칙은 무엇일까? 기록, 저장, 기록, 저장, 저장이다. 저장이다.

 

浮動小数点, 게임 프로그래밍 패턴, 튜터와의 고민상담소 기록


20230804부로 [스파르타코딩클럽 내일배움캠프 주관 UNITY개발자 양성과정]의 사전캠프가 종료됐다.
마지막 진도도 남아있고, 아직 기록 작성도 안 끝난 타이밍에 작성 기록 날리기같은 어처구니없는 짓을 하다니, 거품물고 고꾸라질 일이다.

그 와중에 매니저의 특별한 이벤트라는 말과 무섭게 하지도 않던 기록 제출, 참 기회주의적이며 교활하다고 할 수 있다.

 

튜터와의 고민 상담에서
"협업 과정에서 개발자가 가져야 하는 가장 중요한 자세는 무엇이라 생각하는가?"라는, 상당히 진부한 질문을 했다.

솔직히 "경청하는 자세, 솔선수범하며 능동적인 태도" 같은 뻔한 대답을 기다렸다.
하지만, "맡은 일을 우선 하는 것이죠. 내가 담당한 일이 무엇인지 파악하고, 언제 어떻게 얼마나 끝낼 수 있는지 일정과 자원을 파악하고 관리, 실행하는 것이 중요합니다."라는 답변이 돌아왔다. 
실로 현실적인 조언이다. 하필이면 내가 사회생활을 하면서 가장 지키기 힘들었던 자세를 콕 집어서 지적받은 느낌이었다.

작업 진도의 확인, 스케줄링은 사람이 함께 일할 때 지켜야 할 알파이자 오메가다.

'UNITY' 카테고리의 다른 글

UNITY_20230809[팀 프로젝트_2]  (0) 2023.08.09
UNITY_20230808[팀 프로젝트_1]  (0) 2023.08.08
UNITY_20230728[고양이밥줘서내쫓기]  (0) 2023.08.01
UNITY_TIL최적화를 위한 규칙  (0) 2023.07.31
UNITY_20230728[풍선 지키기]  (0) 2023.07.31