카드 뒤집기 게임을 진행하면서 3가지 특수기능들을 각 스테이지마다 한개씩 부여해봤다.
1. 시간지나면 카드가 하나씩 원상복귀
2. 카드 맞추는게 틀리면 - 시간, 맞추면 +시간
3. 카드 셔플
여기서 1에 대한건 지난시간 구현을 해보았고. 2는 구현에 있어 큰 문제가 없었다.
3번에 해당하는 카드 셔플의 경우 구현난이도가 있었다.
의도는 5초가 지날때마다, 카드를 까서 보여주고 카드를 섞은뒤에 다시 감추고 다시 고르게하는 기능을 구현하는것인데,
관건은 짝을 맞춰서 distroy card 로 날려버린 카드 패어 비어있는 구간들은 냅두고 집어넣어야 하는것이다.
절차대로
1. 카드를 까서 보여주고
2. 카드를 섞고
3. 다시 감추고
를 실시하기 위해 코루틴 함수를 써보기로 결정했다.
먼저 Card.cs에 board에서 썻던 인덱스와는 별개로
리셔플때 남은 카드를 인덱싱하는 변수를 하나 생성해줬다.
public int slotIndex;
이것을 GameManager.cs 에서 리셔플할때 배열에 집어넣는 인자로 쓸 예정이다.
Board.cs에서는 Card 프리팹을 Go라는 객체에 인스턴스로 생성하는것을 할당후, 배열에 맞춰 랜덤하게 섞은 후 x,y의 좌표에 포지션 값을 할당해주는 수식을 만들었었다. 아래의 배치를 상상하면 이해하기쉽다.

그리고 Go의 Card 컴포넌트를 찾아서 setting 메서드를 실시할때 매개변수를 arr 이라는 랜덤하게 섞어서 배열화 한 값으로 받았었다.
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();
for (int i = 0; i < 16; i++)
{
GameObject Go = Instantiate(Card, this.transform);
float x = (i % 4) * 1.4f - 2.1f;
float y = (i / 4) * 1.4f - 3.6f;
Go.transform.position = new Vector2(x, y);
Go.GetComponent<Card>().Setting(arr[i]);
Go.GetComponent<Card>().slotIndex = i;
}
참고로 setting(int number); 함수는 아래와 같다
cardIndex = number;
frontimage.sprite = Resources.Load<Sprite>($"Jin{cardIndex}");
Card의 SpriteRenderer frontimage 컴포넌트에 sprite 프로퍼티에 접근후 Resources의 <Sprite> 타입의 () 안의 내용을 꺼내와 할당하는 함수식이며, 따라서 저렇게 함수를 실행하면 카드들이 좌표에 배열되고, 이미지들이 front 객체(frontimage가 들어있는 객체) 에 랜덤하게 resources에 있는 Jin0 ~Jin7까지의 이미지들을 넣어주는것이다.
여기서 추가로 Go.GetComponent<Card>().slotIndex = i; 에 대해서는 Go의 Card 컴포넌트를 찾아서 slotIndex들도 순차적으로 넣어준다는 것이다. i가 16번 시행되면서 0~ 15까지 순차적으로 Card의 slotIndex 필드에 할당되게된다.
그다음 리셔플을 위해선 시간을 확인해야한다.
필자는 숫자가 계속 늘어나는 방식으로 시간이 흘러가게 작성했다.
게임이 시작하면
time += Time.deltaTime; 을 기반으로 시간이 흘러가므로 이걸 기준으로 5초를 체크할 변수를 만들고 0초를 할당해준다.
private float lastReshuffleTime = 0f;
그다음은 리셔플 코루틴 메서드를 실행할 타이밍을 조건문으로 만들어본다.
void Update() 스코프안에 작성했다.
if (Time - lastReshuffleTime > 5f && cardCount > 0 )
// 예를들어 5.1초 즈음에 체크해보고 카드카운트도 0이어서 게임이 종료된게아니라면
{
StartCoroutine( 만들 코루틴 메서드 () ); // 리셔플 메서드를 실시한다.
lastREshuffleTime = Time; // 그리고 그시간으로 lastReshuffleTime을 할당해주고 빠져나온다.
}
// 그다음 다시 체크해봤을때, 추가된 Time - 새로이 할당된 lastReshuffleTime (이번턴에선 약 5.1초라 가정) > 5f 일때 다시 카드카운트가 0(종료?) 가 아닌지 체크하고 또다시 리셔플 메서드를 실시하는 식의 반복이다.
즉 5초마다 실시되게하는 조건문이다.
코루틴메서드는 IEnumerator 타입으로 만들 수 있다.
코루틴 관련되서는 이전에 작성한 해당글을 참고해보자 -> https://muchmercy.tistory.com/32
[내배캠] 9일차 코루틴에 대한 기초 공부. (25.04.02)
Unity 엔진 내에서 C# 코드로 구현하는 코루틴(Coroutine) 방식을 공부해보자.예제를 한번 던져보고 예제를 통해 공부를 한다.아래는회피와 저스트 회피에 대해서 Unity 에서 제공되는 클래스, 메서드
muchmercy.tistory.com
private IEnumerator ReshuffleRoutine()
{
Card[] allCards = Object.FindObjectsByType<Card>(FindObjectsSortMode.None);
// 먼저 씬내의 모든 Card타입의 컴포넌트를 가지고있는 활성화된 객체를 ( FindObjectsSortMode.None 즉, 아무런 정렬 기준없이) 찾아서 Card 컴포넌트를 allCards라는 Card 배열에 반환하도록 한다. 참고로 ()를 비워놓으면 그냥 해당 하는 내용과 똑같다, 매개변수가 비어져있으면 그냥 아무런 정렬기준없이 쓴다는 뜻이다.
여기서 우리는 Card를 프리팹화 시켜서 Board에서 Card들을 Instantiate 으로 clone을 씬 내에 16장 펼치고 있다.
즉, 위의 문장의 Card 타입 컴포넌트가 붙은 것들은 Card 프리팹의 클론들을 일컫는거고 여기서 카드가 짝이 맞춰진 경우라면 남은 카드들, 아무것도 건드려도 남은 카드들에대해서 찾는것이다.
Card[] remainingCards = allCards.Where(card => card != null).ToArray();
여기서는 좀더 재밌는 사전지식이 필요하다.
해석만 먼저해보자면, remainingCards라는 배열 변수에 allCards에 들어간 카드들을 card의 상태가 null이 아닌것만 찾아서 할당하는 것이다.
그런데 card = null 은 무슨 상황이냐, 처음에 board에 의해 instantiate 되었을때 card들은 null이 아니다.
다만 Distroy() 로 오브젝트를 파괴했을때 해당 참조는 '가짜 null' 상태가 되며 실제로 null 처럼 동작하는것이다.
소괄호안의 람다식은 이해했다면
where의 경우 소괄호안의 조건으로 찾는다는 뜻이야. 즉 Card가 null이 아닌 상태의 allCards 내부의 card 들을 찾아서, remainingCards에 넣는데 이 Where 의 경우 IEnumerable<T>를 반환하니까... ToArray(); 해서 배열에 넣어주기전에 바꿔주는것이다. 실제로 DestroyCard()로 파괴되었을때
FindObjectsByType<Card>로 호출하면 파괴된 카드들도 가짜 null 상태여서 함께 호출될 수 있다.
여기서 remainingCards에 할당한 함수식을 통해 card !=null인 경우에 제외해버리는것이다.
이를 통해 MissingReferenceException 같은 에러를 방지한다. 실제로 해당식에서 where로 예외처리를 하지않았을때 destroyCard 로 인한 null 발생 에러가 떴었다.
그다음
foreach (Card card in remainingCards)
{
if (card != null)
{
card.anim.SetBool("IsOpen", true);
card.front.SetActive(true);
card.back.SetActive(false);
}
}
로 배열안의 모든 카드들에 대해서 반복하여 card가 null이 아니라면으로 한번더 체크하고
카드를 다시 까주는 코드를 작성하였다.
yield return new WaitForSeconds(0.01f);
으로 동기화되어서 약간의 텀을 주고
List<Vector2> availablePositions = new List<Vector2>(); 라고 리스트를 하나 만들어주었다. Vector2라는 x,y 좌표값을 가져오는 구조체를 타입으로 하는 리스트인데 리셔플할 카드들을 가져와 좌표로 뿌리기 위한 리스트다.
foreach (Card card in remainingCards)
{
Vector2 pos = new Vector2((card.slotIndex % 4) * 1.4f - 2.1f,
(card.slotIndex / 4) * 1.4f - 3.6f);
availablePositions.Add(pos);
}
remainingCards 라고 앞서만든 Card[] 배열 안에 들어있는 카드들에 대해서 설정된 slotIndex 값이 있을것이다. 앞서서
Board.cs에서 Go.GetComponent().slotIndex = i; 를 이용해 16개의 넘버에 할당되게 Card 인스턴스들에 값이 매겨져있다. 여기서 remainingCards라 하니, destroyCard 된 Card들은 제외하고 생각하자.
그것들에 대해서 Vecot2 타입의 pos 라는 변수에 할당한것은
Vector2 pos = new Vector2((card.slotIndex % 4) * 1.4f - 2.1f,
(card.slotIndex / 4) * 1.4f - 3.6f);
즉, 좌표값에 맞춰서 다시 배치하는것을 pos에 넣었다. 배열안에 모든 Card들에 대해 좌표를 설정해주는것을 실시해주며 자신의 위치를 그대로 사수하게 하였다.
그다음
availablePositions.Add(pos) 을 함으로서
List<Vector2>availablePositions에 해당 카드들의 위치값들을 넣어주었다.
availablePositions = availablePositions.OrderBy(p => Random.value).ToList();
여기에다가 앞서 Board.cs에서 섞듯이
OrderBy 오름차순 기능을 이용해서 소괄호안의 람다식 p를 랜덤한 값으로 섞어준다. 그후 그 값에 대응되게 오름차순으로 Order 정리해준뒤에 IEnumerable<T>이므로 ToList(); 함수를 통해 변환시켜주는 작업까지 마친다
이러면 List<Vector2>availablePositions 안의 카드들은 랜덤하게 재배열된다. 좌표값이 랜덤하게 재배열되었다고 보면 좋다.
해당 리셔플 메서드 스코프 안 상단에,
if (firstcard != null)
{
firstcard.CloseCardInvoke();
firstcard = null;
}
if (secondcard != null)
{
secondcard.CloseCardInvoke();
secondcard = null;
}
를 써준다. 이문제는 코루틴으로 동기화되어서 실시되는 리셔플 메서드와 카드 오픈이 충돌하는 경우의 문제를 해결하기위해 작성됐다. 좀더 풀어보자면,
셔플전에 firstcard, secondcard는 카드를 오픈했을때 할당되는값인데, 할당이 되었다고 치면, 전부 앞면에서 뒷면으로 닫아버러며 Card스크립트내의 함수를 불러와서 실행시키고 firstcard,secondcard 할당을 비워버리는 함수를 실시하여 하나를 open 했을때 초가 지나서 셔플이 코루틴으로 실시되었을경우 firstcard는 여전히 할당된 상태여서 대응하는 카드를 찾아버렸을때 하나만 지워지는 불참사를 방지했다. 실제로 버그가 발생하여 해결과정에 추가하였다..
그다음 다시 아래로 내려와서,
for (int i = 0; i < remainingCards.Length; i++)
{
if (remainingCards[i] != null && remainingCards[i].gameObject != null)
{
remainingCards[i].transform.position = availablePositions[i];
}
}
라는 코드를 작성한다.
현재 남아있는 카드의 갯수까지 실시하는 for문을 작성하였다.
그리고 스코프안에
remainingCards[] 값이 null이거나 그 객체 자체도 비활성화되어있는건 아닌지 체크후 아니라면,
remainingCards[]의 position 값들에 랜덤하게 재배열된 availablePosition[] 값들을 할당해주는것이다.
이 과정을 통해 remainingCrads 들의 위치는 실제로 scene안에서 재배열된다.
yield return new WaitForSeconds(0.05f); // 그다음 0.05f 뒤에
remainingCards 배열의 모든 카드에 대해서
foreach (Card card in remainingCards)
{
if (card != null)
card.CloseCardInvoke();
}
카드가 null 상태가 아니라면 체크하고
다시 back면이 보이도록 카드를 뒤집어버린다.
yield break;
하여 강제로 코루틴을 종료하며 빠져나온다.
정리하자면 이 모든 과정을 통해 5초뒤에 리셔플 코루틴을 실시하게되면 카드를 모두 까고
몇초뒤에 카드를 뒤섞고
몇초뒤에 카드를 다시 뒤집는다. 과정이 실시된다.
shuffle 되는동안에 카드를 오픈하는 것을 막음으로서 셔플중에 카드를 골라서 firstcard등에 할당되어 오류가 생기게하는것을 방지하는 또다른 코드들을 추가해보자.
먼저
shuffle중임을 체크하는
public bool isSuffling = false; 로 선언해준다.
그다음 Card.cs의 OpenCard() 메서드에서
if(GameManager.instance.isSuffling)
{
return;
}
를 작성하여 shuffle중에는 OpenCard 가 아예 실시되지않도록 막아버린다.
돌아가서..
리셔플코루틴 메서드인
private IEnumerator ReshuffleRoutine() 내부에
씬에 있는 모든 카드타입을 체크하기전에
isSuffling = true; 로 하여 OpenCard() 메서드의 개입을 막아버리고
카드들이 뒤집혔을때, 그리고 yield Break; 하기전에
다시
isSulffling =false; 로 하여서
섞이는 과정에서는 OpenCard()를 못누르게 막는 방식을 추가로 구현하였다.
이것은 firstcard를 깠을때, 셔플되면서 할당되게 남아있던 문제와는 조금 다른 문제이며 정신없이 카드를 맞추다 셔플이 실시되었을때 문제가 생기는 부분을 보완하는 추가적인 조치다.
'Unity 개발 공부' 카테고리의 다른 글
| [내배캠] 본캠 5일차. 기능구현 전체 정리. 셔플, 코루틴 등, (0) | 2025.04.11 |
|---|---|
| [내배캠] 본캠 4일차. 팀 카드 뒤집기 게임 Git 병합 충돌 #4 (0) | 2025.04.10 |
| [내배캠] 본캠 2일차. 팀 카드 뒤집기 게임 #2 (0) | 2025.04.08 |
| [내배캠] 본캠 1일차. 팀 카드 뒤집기 게임 #1 wireframe. (0) | 2025.04.07 |
| [내일배움캠프] 10일차 공통과제 오목 - 메인메뉴 (25.04.03) (0) | 2025.04.03 |