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

이 기능은 언제 실행돼야 하는가? 이 텍스트는 언제 활성화하고, 플레이어에게 비쳐야 하는가?
첫번째 카드를 클릭했을 때다. 다음은 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단게 스테이지에 접근하지 못하도록 구현한다.

난이도에 따른 카드 수 변화
진입 스테이지에 따라 배치되는 카드 배열의 데이터 수를 변화시킨다. 아래는 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 |
제한 시간이 일정량 경과한 후에도 남은 카드가 있다면, 서로 일치하는 한쌍의 카드에 애니메이션을 추가해 힌트를 부여한다.

기가 막힌 기능이다. 다음은 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 |