본문 바로가기

UNITY

UNITY_20230811[팀 프로젝트_4]

카드게임 프로젝트 진행 마지막 날이다.

 

프로젝트에서 구현한 기능에 대해 기록한다.


  • 첫번째 카드를 뒤집은 후 2초 동안 다른 카드를 뒤집지 않았을 경우, 첫번째 카드를 되돌린다. "n초 안에 뒤집으세요!"텍스트도 팝업됐다가 사라진다.
  • 스테이지 구현. 난이도에 따라 총 3개의 스테이지가 있으며, 스테이지에 따라 카드의 수도 달라진다. n단계의 스테이지를 통과하지 못하면 n+1단게 스테이지에 접근하지 못하도록 구현한다.
  • 제한 시간이 일정량 경과한 후에도 남은 카드가 있다면, 서로 일치하는 한쌍의 카드에 애니메이션을 추가해 힌트를 부여한다.

첫번째 카드를 뒤집은 후 2초 동안 다른 카드를 뒤집지 않았을 경우, 첫번째 카드를 되돌린다. "n초 안에 뒤집으세요!"텍스트도 팝업됐다가 사라진다.

 

우선, 텍스트 오브젝트를 먼저 작성한다.

countDownTxt, 조건이 맞아야 팝업하니까 비활성 상태가 기본값이다.

이 기능은 언제 실행돼야 하는가? 이 텍스트는 언제 활성화하고, 플레이어에게 비쳐야 하는가?
첫번째 카드를 클릭했을 때다. 다음은 card 스크립트에서 작성 해당 기능의 작동 시점이다.

StartCountDown으로 이 기능은 실행된다.

public void OpenCard()
    { // 관련 없는 코드 생략
        if (gameManager.I.firstCard == null) // 카드를 클릭했을 때 firstCard에 아무 값도 없다면
        {
            gameManager.I.firstCard = gameObject; // 클릭한 오브젝트를 first카드에 할당하고
            gameManager.I.StartCountDown(); // StartCountDown를 실행한다.
        }
        else
        {
            gameManager.I.secondCard = gameObject;
            gameManager.I.IsMatched();
        }
    }

이 기능은 언제 종료돼야 하는가? 이 텍스트는 언제 다시 비활성화해야 하는가?

두번째 카드를 클릭했을 때, 게임이 종료됐을 때다. 다음은 GM에서 작성한 해당 기능의 종료 시점이다.
StopCountDown으로 이 기능은 종료된다.

public void IsMatched()
    { // 코드 대량 생략, IsMatched에 의한 판정과 대응 기능 작동이 끝난 후
        StopCountDown(); // StopCountDown 작동
    } 
void GameEnd() // 카드를 다 못맞출 경우도 GameEnd다. 그래서 GameEnd에서도 반드시
    {
        StopCountDown(); // StopCountDown이 작동해야한다.
    } // 막상 써놓으니 굳이 써야하는 코드인가 생각이 든다.

다음은 위 StartCountDown, StopCountDown의 작동 내용을 구현한 GM스크립트다.

    public GameObject countDownGO; // countDownTxt를 오브젝트로 가지는 변수
    public Text countDownTxt; // "n초 안에 뒤집으세요"를 표시할 텍스트

    bool isCountingDown = false; // 카운트다운 중인지 여부를 나타내는 변수
        
    public void StopCountDown() 
    {
        StopAllCoroutines(); // 스크립트의 모든 코루틴을 정지시키는 내장 코루틴함수
        countDownGO.SetActive(false); // countDownTxt는 비활성화한다.
        isCountingDown = false; // 카운트다운 종료
    }
    public void StartCountDown()
    {
        isCountingDown = true; // 카운트다운 시작
        StartCoroutine(CountDownCoroutine()); // 텍스트 팝업, 클릭 후 2초 경과 계측 기능 함수 실행
    }

    IEnumerator CountDownCoroutine()
    {
        float initialCount = 2f; // 초기 카운트 값
        float count = initialCount;


        while (count > 0 && isCountingDown)
        {
            count -= Time.deltaTime; // 2,1.. 시간에 따라 count감소
            countDownGO.SetActive(true); // 팝업 오브젝트 활성화
            countDownTxt.text = count.ToString("N0") + "초 안에 뒤집으세요!"; // 카운트 시간의 정수부분만 띄워서 표시
            yield return null; 
        }

        // 카운트가 0이 되면 카드 다시 뒤집기
        if (count <= 0 && firstCard != null) // count는 0이 됐는데 아직 첫번째 카드가 뒤집혀 있는 상황
        {
            if (firstCard != null && secondCard == null) // 첫번째 카드만 뒤집혀 있으면
            {
                firstCard.GetComponent<card>().CloseCard(); // 카드를 원래대로 되돌린다.
                firstCard = null;
                secondCard = null;
            }
        }

        // 카운트 완료 후 초기화
        count = initialCount;
        countDownGO.SetActive(false);
        countDownTxt.text = count.ToString("N0") + "초 안에 뒤집으세요!";
        isCountingDown = false;
    }

주석으로 설명 내용을 다 기록했다.
왜 코루틴을 쓰는가? 

<시간지연프레임 타이머 애니메이션 최적화, 자원절약>

이전 기록에서 언급했듯이, 스크립트의 모든 로직은 하나의 메시지 루프를 가진다.  이 메시지 루프가 끝나기 전까지 다른 로직은 실행되지 않고, 모든 로직은 직렬로 연결돼 실행된다. 따라서 로직이 실행되는 시간 동안은 게임이 멈추게 된다.

만약 코루틴이 추가된 코드가 실행된다면, 코드 내 로직의 메시지 루프가 진행되는 동시에 코루틴도 조건이 맞으면 메시지 루프와 번갈아 가며 실행돼 일반 함수의 로직 실행 시간에도 같이 실행되고, 마치 코루틴과 타 일반 함수가 번갈아 가며 실행되는 것처럼 작동한다. 
이러한 작동 특성으로 코루틴은 애니메이션, 카운트다운, 시간 지연 기능 등을 구현하는 함수로 자주 사용되며, 메모리 절약과 코드 최적화라는 효과를 볼 수 있다.


스테이지 구현. 난이도에 따라 총 3개의 스테이지가 있으며, 스테이지에 따라 카드의 수도 달라진다. n단계의 스테이지를 통과하지 못하면 n+1단게 스테이지에 접근하지 못하도록 구현한다.

NORMAL을 클리어하지 못하면 바로 아래의 스테이지에 접근 불가

난이도에 따른 카드 수 변화

진입 스테이지에 따라 배치되는 카드 배열의 데이터 수를 변화시킨다. 아래는 GM의 해당 코드다.

 void Start()
    {
        int[] members = new int[] { 0, 0, 1, 1, 2, 2, 3, 3 }; // 카드 8개
        
        if (PlayerPrefs.GetInt("stageLevel") == 2)
        {
            members = new int[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5 }; // 12개
        }
        else if (PlayerPrefs.GetInt("stageLevel") == 3)
        {
            members = new int[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 }; // 16개
        } // 이하 카드 배치 코드 생략
    }

당연히 간단한 코드다. 그러면 왜 썼냐?
아래의 스테이지 접근과 연결된다.

 

스테이지 접근 설정

PlayerPrefs.클래스를 사용한다. 복습 먼저 한다.

PlayerPrefs.Set데이터형("level", 데이터형에 맞는 변수); => "level"이라는 Key는 데이터형에 맞는 변수를 갖는다.
PlayerPrefs.Get데이터형("level"); => "level"이라는 Key가 가지는 변수를 나타낸다.

PlayerPrefs.HasKey("level"); => PlayerPrefs에 "level"이라는 Key가 있는지 판단하는 논리값 변수다.

아래는 스테이지 선택 씬에서 각 스테이지 오브젝트가 가지는 stageSelect 코드다.

using System.Collections;
using System.Collections.Generic;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;

public class stageSelect : MonoBehaviour
{
    public GameObject alert; // 

    void Start()
    {
        Time.timeScale = 1.0f;
        if (PlayerPrefs.HasKey("level") == false && PlayerPrefs.HasKey("stageLevel") == false)
        {		// 스테이지 선택씬이 처음 열리고 level, stageLevel key가 아직 존재하지 않는 상황
            PlayerPrefs.SetInt("level", 0); // level key : 0
            PlayerPrefs.SetInt("stageLevel", 0); // stageLevel key : 0
        }

        if (PlayerPrefs.GetInt("level") == 0) // level : 0일 때, 2,3 스테이지 잠금
        {
            Stage2Lock();
            Stage3Lock();
        } 
        else if (PlayerPrefs.GetInt("level") == 1)
        {
            Stage3Lock();
        }

        if (PlayerPrefs.HasKey("bestScore")) // 게임 점수 최고기록
        {
            PlayerPrefs.SetFloat("bestScore", 0f);
        }
    }
    public void s1() // 1스테이지 버튼 클릭 이벤트, 즉 1스테이지 클릭 시
    {
        PlayerPrefs.SetInt("stageLevel", 1); // stageLevel : 1 이 되어 MainScene에서 카드 8장이 배치된다.
        SceneManager.LoadScene("MainScene");
    }
    public void s2() // 2스테이지 버튼 클릭 이벤트
    {
        if (1 <= PlayerPrefs.GetInt("level")) // 1스테이지를 클리어하면 level key는 GM에서 stageLevel key : 1값을 받는다.
        {
            PlayerPrefs.SetInt("stageLevel", 2);
            SceneManager.LoadScene("MainScene");
        } else // 2스테이지 진입할 조건을 달성하지 못한 채 클릭할 경우
        {
            alertActive(); // 경고 알림 패널이 팝업된다.
        }
    }
    public void s3() // 3스테이지 버튼 클릭 이벤트
    {
        if (2 <= PlayerPrefs.GetInt("level"))
        {
            PlayerPrefs.SetInt("stageLevel", 3);
            SceneManager.LoadScene("MainScene");
        } else
        {
            alertActive();
        }
    }
    void alertActive()
    {
        alert.SetActive(true);
        Invoke("closeAlert", 1f);
    }

    void closeAlert()
    {
        alert.SetActive(false);
    }

    void Stage2Lock()
    {
        GameObject.Find("Canvas").transform.Find("stage2Lock").gameObject.SetActive(true);
        GameObject.Find("Canvas").transform.Find("stage2").gameObject.SetActive(false);
    }

    void Stage3Lock()
    {
        GameObject.Find("Canvas").transform.Find("stage3Lock").gameObject.SetActive(true);
        GameObject.Find("Canvas").transform.Find("stage3").gameObject.SetActive(false);
    }
}

아직 확실하지 않은 로직이 있다. GM과 교환하는 코드가 많아서 그렇다.

아래는 stageSelect 스크립트와 교환하는 GM의 코드다.

public void IsMatched()
    {
        if (firstCardImage == secondCardImage)
            {
            if (leftCards == 2) // 스테이지 클리어 시
            {
                if (PlayerPrefs.GetInt("stageLevel") > PlayerPrefs.GetInt("level"))
                {
                    PlayerPrefs.SetInt("level", PlayerPrefs.GetInt("stageLevel"));
                }
                Invoke("GameEnd", 0.5f);
            }
        }
    }
void GameEnd() // 게임 종료
    {
        if (PlayerPrefs.HasKey("bestScore") == false)
        {
            PlayerPrefs.SetFloat("bestScore", time);
        }
        else
        {
            if (time > PlayerPrefs.GetFloat("bestScore"))
            {
                PlayerPrefs.SetFloat("bestScore", time);
            }
        }
    } // 관련 코드만 싹 골라서 작성

사실, 왔다갔다 헷갈린다. 처음 스테이지 선택부터 마지막 클리어까지 표로 정리한다. (bestScore는 생략한다. 표보고 연관해서 생각한다.)

KEY level  stageLevel
처음 스테이지선택 씬 0 0
NORMAL 스테이지 진입 0 1
NORMAL 스테이지 클리어 1 1
HARD 스테이지 진입 1 2
HARD 스테이지 클리어 2 2
EXPERT 스테이지 진입 2 3
EXPERT 스테이지 클리어 3 3

제한 시간이 일정량 경과한 후에도 남은 카드가 있다면, 서로 일치하는 한쌍의 카드에 애니메이션을 추가해 힌트를 부여한다.

10초 경과 후, 아직 카드가 남았으면 남은 카드 중 한 쌍에 애니메이션을 추가해 힌트를 보여준다.

기가 막힌 기능이다.  다음은 GM에서 실행되는 해당 코드다.

bool ShowHint = false;

void Update()
    {
        if (IsStartAniOff == true)
        {
            time -= Time.deltaTime;

            timeTxt.text = time.ToString("N2");
            if (time <= 10.0f) // 남은 제한시간 10초 이하일 시
            {
                anim.SetBool("under10seconds", true);
                ShowMeTheHint(); // 여기서 힌트함수 작동한다.
            }
            if (time <= 0.0f)
            {
                GameEnd();
            }
        }
    }

void ShowMeTheHint()
    {
        if (ShowHint == false)
        {
            ShowHint = true; // 업데이트에서 실행돼서 스위치 달아줌
            GameObject cards = GameObject.Find("cards"); // cards는 card의 부모이기때문에 불러옴
            int RandomCard = UnityEngine.Random.Range(0, cards.transform.childCount); // cards.transform.childCount -> cards의 자식 갯수(남은 카드 갯수)
                                                                          // 남은 카드 중에서 힌트를 줄 카드 랜덤 선택

            for (int num = 0; num < cards.transform.childCount; num++) // 카드들을 비교하기위해 사용
            {
                if (cards.transform.GetChild(RandomCard).Find("front").GetComponent<SpriteRenderer>().sprite.name // RandomCard의 스프라이트 이름과
                    == cards.transform.GetChild(num).Find("front").GetComponent<SpriteRenderer>().sprite.name // for문으로 차례대로 카드 스프라이트 이름을 비교
                    && RandomCard != num) // RandomCard와 for문의 카드 번호가 같으면 안됨
                {
                    cards.transform.GetChild(RandomCard).GetComponent<Animator>().SetTrigger("IsHint"); //애니메이션 트리거 작동
                    cards.transform.GetChild(num).GetComponent<Animator>().SetTrigger("IsHint"); // 트리거 = 1회 작동
                    break; // 짝을 찾으면 바로 중단해서 퍼포먼스 상향
                }
            }
        }
    }

친절하게도 주석이 다 있다. 생각나면 또 와서 보도록 한다.


상기의 내용은 다른 팀원의 과제 내역 일부에 불과하다.

음... 다른 팀원의 공적에 비해 내가 한 일이 너무 초라한데??

나는 열등감을 먹고 크는 존재다. 항상 열등감을 가지면 그것 나름대로 심적으로 악영향을 줄 수 있겠지만, 지금의 나는 타 팀원에 비해 내가 부족한 점을 명확히 알아야 하는 중요한 시기이기에,
막말로 염치없이 빨대꽂는 학습 패턴도 얼굴에 철판깔고 할 작정이다.

'UNITY' 카테고리의 다른 글

UNITY_20230905[UNITY - 옵저버 패턴-Event]  (0) 2023.09.05
UNITY_20230811[1주차 회고록]  (0) 2023.08.11
UNITY_20230810[팀 프로젝트_3]  (0) 2023.08.10
UNITY_20230809[팀 프로젝트_2]  (0) 2023.08.09
UNITY_20230808[팀 프로젝트_1]  (0) 2023.08.08