ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 내일배움캠프 사전 4주차 - 르탄이 카드 뒤집기 게임 제작
    TIL/Unity 2024. 4. 5. 07:53

     

    [학습목표]

    4 x 4로 배치된 카드 두 장을 뒤집어 같은 그림일 경우 그 카드들을 지운다. 최종적으로 배치되어있는 모든 카드를 지우는 것을 목표로 하는 게임을 만든다.

     

    [학습 내용]

    이전 사전 1 ~ 3주차에서 유니티의 기본기에 대해 학습하였다. 그렇기 때문에 이번 강의는 카드 뒤집기의 핵심 로직을 C#으로 구현하는 것에 중점을 두었다.

     

    우선 기본 화면을 구성하였다. 이전 주차에서 계속 했던 것들이기 때문에 빠르게 진행하였다. 화면은 모바일 환경을 가정하여 760 x 1280으로 설정하였고, 메인 카메라의 백그라운드 색상을 푸른빛(90, 90, 255)로 변경해주었다. 또한 기본 씬의 이름을 MainScene으로 변경해주었다. 그리고 화면 중앙 상단에 경과 시간을 표시해주는 텍스트를 배치하였다. 결과적으로 화면은 아래의 모습이 되었다.

    설정해준 기본 화면

     

    동시에 캠프 측에서 제공해준 리소스 파일들(폰트와 스프라이트)도 임포트 시켜줬다.

    제공받은 리소스

     

    다음으로 게임에 필요한 카드를 만들었다. Card라는 이름의 오브젝트를 만들어주고, 하위 오브젝트로 Front와 Back을 만들어 카드의 앞뒷면을 따로 구분해주었다. Front에는 임시적으로 rtan0 스프라이트를, Back에는 물음표가 그려진 사각형을 배치해주었다. 그리고 만든 카드 오브젝트를 활용하기 위해 프리펩으로 등록해주었다.

     

    기본적인 것들이 끝난 뒤, 제일 먼저 (지난 주차에서 학습했던) 시간 측정을 구현했다. GameManager를 생성해주고, GameManger.cs 스크립트를 붙여주어 아래의 코드를 작성했다. 그리고 시간이 정상적으로 측정되는 것을 확인하였다.

    	public Text timeTxt;
    	float time = 0.0f;
    
    	void Update()
    	{
    		time += Time.deltaTime;
    		timeTxt.text = time.ToString("N2"); // N2는 소수점 둘째자리까지 표기한다는 뜻
    	}

     

    다음으로는 카드를 화면에 배치하였다. 먼저 Board 오브젝트를 만들었다. 이 Board 아래에 Card 오브젝트들을 붙여주면 된다. 이전에 만들어놨던 Card 프리펩을 활용한다. 배치에 있어서 4 x 4의 형태를 갖추기 위해서 나눗셈을 이용했다. x좌표는 % 연산을 이용하여 나머지 값을 줬고, y좌표는 / 연산을 이용하여 몫 값을 줬다. 아래는 Board.cs 일부이다. 

        void Start()
        {
            for(int i = 0; i < 16; i++)
            {
                GameObject go = Instantiate(card, this.transform);
    
                float x = (i % 4) * 1.4f - 2.1f; // 1.3f은 카드 크기, 0.1f은 사이 간격
                float y = (i / 4) * 1.4f - 3.0f; // -2.1f과 -3.0f은 화면 중앙에 오게 하기 위해
    
                go.transform.position = new Vector2(x, y);
            }
        }

     

    카드가 잘 배치된 모습

     

    다음으로는 카드를 무작위로 섞는 기능을 구현하였다. 이 부분이 이번 주차에서 가장 생각할 부분이 많았다. 먼저 0 ~ 7의 인덱스를 배열로 만들어주고(16장 중, 2장씩 서로 같은 카드이므로) 순서를 섞어주었다. 그 방법이 신기했는데, Linq의 OrderBy()를 활용하였다. Linq가 무엇인가 하여 찾아보니 C#에서 데이터를 다룰 때 작업을 간소하게 만들어주는 것이라고 한다.

            int[] arr = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
            arr = arr.OrderBy(x => Random.Range(0f, 7f)).ToArray();
            ...
            go.GetComponent<Card>().Setting(arr[i]);

     

    그런데 위의 코드에서 Setting()을 아직 구현하지 않았다. 그래서 Card.cs에 구현해주었다. 해당 메서드는 위의 코드에서 부여받은 인덱스대로 스프라이트를 적용시켜준다. String에서 변수명을 사용하기 위해 $를 사용한다는 것을 배웠다. 또한 Resources 폴더에 리소스를 넣어놓는다면 이런 식으로 불러올 수 있다는 사실도 새로웠다.

    public SpriteRenderer frontImage;
        
    public void Setting(int number)
    {
    	idx = number;
    	frontImage.sprite = Resources.Load<Sprite>($"rtan{idx}");
    }

     

    다음으로는 카드에 애니메이션을 할당해주었다. 카드가 계속 조금씩 진동하여 생동감을 준다거나, 카드를 뒤집을 때 카드가 잠깐 작아지는 애니메이션이다. 카드를 뒤집을 때를 판별하기 위해 불리언 isOpen 변수를 만들어 활용했다. 

    회전에 변화를 주어 떨림을 구현한 모습

     

    그 다음으로는 어쩌면 게임에서 가장 중요한 카드 뒤집기 로직을 구현하였다. 카드를 깔 때 앞면(Front)를 활성화해주고 뒷면(Back)을 비활성화 해주면 된다. 두 장의 카드를 뒤집어야 하므로 첫 카드와 두번째 카드는 구별한다. 이전에 뒤집은 카드가 없다면 첫 카드로 생각한다. 두 번째 카드도 뒤집고 나서 같은 카드인지 판별하는 Matched() 메서드를 실행한다.

        public GameObject front;
        public GameObject back;
        public Animator anim;
    	
        public void OpenCard()
        {
            anim.SetBool("isOpen", true);
            front.SetActive(true);
            back.SetActive(false);
    
            if(GameManager.Instance.firstCard == null)
            {
                GameManager.Instance.firstCard = this;
            }
            else
            {
                GameManager.Instance.secondCard = this;
                GameManager.Instance.Matched();
            }
        }
        
        public void CloseCard()
        {
            anim.SetBool("isOpen", false);
            front.SetActive(false);
            back.SetActive(true);
        }

     

    GameManger에 있는 Matched() 메서드다. 첫 카드와 두번째 카드의 인덱스가 같으면 카드를 지운다. 인덱스가 다르면 다시 뒤집어 놓는다.

        public void Matched()
        {
            if(firstCard.idx == secondCard.idx)
            {
                firstCard.DestroyCard();
                secondCard.DestroyCard();
                }
            }
            else
            {
                firstCard.CloseCard();
                secondCard.CloseCard();
            }
    
            firstCard = null;
            secondCard = null;
        }

     

    모든 카드가 지워지면 게임이 끝나야한다. 카드 숫자만큼 cardCount를 설정해주고, 카드가 지워질 때마다 2씩 뺀다. 다 지워졌을 경우 엔딩 텍스트를 보여준다.

        void Start()
        {
        	...
            GameManager.Instance.cardCount = arr.Length;
        }
        public GameObject endTxt;
        public int cardCount = 0;
        
        public void Matched()
        {
            if(firstCard.idx == secondCard.idx)
            {
                ...
                cardCount -= 2;
                if(cardCount == 0)
                {
                    Time.timeScale = 0.0f;
                    endTxt.SetActive(true);
                }
            }
            ...
        }

     

    엔딩 텍스트를 클릭했을 때 게임이 재시작하는 기능도 구현해줬다. 텍스트에 버튼을 붙여서 구현했다.

    public class RetryButton : MonoBehaviour
    {
        public void Retry()
        {
            SceneManager.LoadScene("MainScene");
        }
    }

     

    강의는 여기까지였고, 추가적으로 나온 숙제가 있었다. 시간이 30초가 경과하면 게임이 종료되도록 구현하는 것이었다. 어렵지 않아서 금방 해결했다.

        void Update()
        {
        	...
            if(time >= 30.0f)
            {
                Time.timeScale = 0.0f;
                endTxt.SetActive(true);
            }
        }

     

     

    [결과물]

    플레이 화면

     

    카드 몇 개를 지운 모습
    엔딩 화면