오늘은 프로젝트의 기본구조를 설정하고 1차 구현 목표로 설정한 카드 생성 ~ 게임 승리/패배 판정 흐름에서 카드 생성, 셔플, 드로우, 선택, 버리기 기능 구현에 대한 내용을 정리해보겠습니다.
먼저, 카드 생성 - 셔플 - 드로우의 경우 다음의 구현 흐름으로 기획하였습니다.
Deck.cs
1. 처음 52장의 카드를 생성할때, for문을 이용하여 각 카드의 Rank와 Suit를 i(=index)로 설정한다.
2. 설정이 완료되면 PlayCard의 변수를 기반으로 카드를 생성시킨다.(Initalize())
2-1. 추가로 카드 생성시 Rank에 맞게 BaseChip설정을 처리하되, A의 경우 10으로 설정한다.
3. 생성된 카드는 fullDeck(List)에 추가한다.
(List 추가 이유는 카드 추가 및 삭제로 인하여 크기가 변동이 발생할 가능성이 있기 때문.)
4. 카드 생성이 완료되었다면 Shuffle을 진행한다.
5. Shuffle은 Fisher-Yates 셔플을 사용한다.(중복 생성 방지)
6. fullDeck에 생성된 카드를 기반으로 Hand로 드로우한다.
7. 드로우 시, fullDeck[0]를 드로우 대상으로 지정하고 완료한 후, fullDeck에서 제거한다.
8. 이후, 카드 계산 or 버리기를 진행할때 제거될 카드를 disCardPack에 저장한다.
다음, Hand 드로우, 카드 선택, 버리기 기능은 다음과 같은 흐름으로 기획하였습니다.
Hand.cs
1. 시작시 카드를 드로우한다.
2. 카드 드로우 시에는 Hand의 for문을 돌려 hands[i]가 null인 경우 deck.DrawCardFromDeck();을 호출한다.
3. Hand의 드로우가 완료되면 정렬을 진행한다.
4. 정렬의 기준은 Rank를 1순위, Suit를 2순위로 지정하며 내림차순으로 진행한다.
5. 카드 선택의 경우 selectCardList.Contains을 사용하여 포함, 미포함에 따른 카드 추가, 제거를 진행한다.
6. 이후에 카드 선택시점마다 즉각적으로 카드의 족보를 계산하도록 한다.
7. 선택된 카드는 추후 버리기 or 계산하기 사용의 전제조건이된다.
8. 버리기는 선택한 카드리스트를 discardPack에 추가한 후, hands[i]와 일치하는 index의 정보를 null로 변경한다.
Hand와 Deck에 대해서 구현흐름을 기반으로 다음과 같이 구현해보았습니다.
<style>
Deck.cs
using System.Collections.Generic;
using UnityEngine;
public class Deck : MonoBehaviour
{
[Header("Play Deck Information")]
public List<PlayCard> fullDeck = new List<PlayCard>();
public List<PlayCard> discardPack = new List<PlayCard>();
public void Awake()
{
CreateDeck();
ShuffleDeck();
}
public void CreateDeck()
{
for (int i = 0; i < 52; i++)
{
Suit assignedSuit = (Suit)(i / 13);
int assignedRank = (i % 13) + 2;
if(assignedRank == 14) assignedRank = 1;
int chip = CalculateBaseChip(assignedRank);
PlayCard newCard = GetComponent<PlayCard>;
newCard.Initalize(i.ToString(), "Playing Card", " Chip: " + chip, assignedSuit, assignedRank, chip);
fullDeck.Add(newCard);
newCard.name = newCard.GetCardName();
newCard.gameObject.SetActive(false);
}
}
public void ShuffleDeck()
{
for (int i = fullDeck.Count - 1; i >= 1; i--)
{
int randomCard = Random.Range(0, i + 1);
PlayCard temp = fullDeck[i];
fullDeck[i] = fullDeck[randomCard];
fullDeck[randomCard] = temp;
}
}
public PlayCard DrawCardFromDeck()
{
if (fullDeck.Count > 0)
{
PlayCard card = fullDeck[0];
fullDeck.RemoveAt(0);
UpdateDeckUI();
return card;
}
return null;
}
public int CalculateBaseChip(int rank)
{
if (rank == 1) return 10;
return rank;
}
}
</style>
Hand.cs
<style>
using System.Collections.Generic;
using UnityEngine;
public class Hand : MonoBehaviour
{
public Deck deck;
[Header("Hand Information")]
public PlayCard[] hands = new PlayCard[8];
public List<PlayCard> selectCardList = new List<PlayCard>();
private void Awake()
{
selectCardList.Clear();
}
private void Start()
{
DrawHands();
}
public void DrawHands()
{
// TODO :: 카드 드로우하는 애니메이션 필요
for (int i = 0; i < hands.Length; i++)
{
if (hands[i] == null)
{
PlayCard drawnCard = deck.DrawCardFromDeck();
if (drawnCard != null)
{
hands[i] = drawnCard;
drawnCard.gameObject.SetActive(true);
}
}
}
SortCardToRank();
}
public void SelectCard(PlayCard card)
{
//Debug.Log($"Selected Card - Rank: {card.rank}, Suit: {card.suit}");
//TODO :: 카드 선택 및 취소시마다 UI에서 카드가 올라갔다 내려갔다 하는 애니메이션 필요.
if (selectCardList.Contains(card))
{
selectCardList.Remove(card);
}
else
{
if(selectCardList.Count < 5)
{
selectCardList.Add(card);
}
}
//TODO :: 카드 족보 계산로직 호출.
}
public void ThrowAwayCard()
{
ExecuteCardRemoveAndDraw();
}
private void ExecuteCardRemoveAndDraw()
{
if (selectCardList == null)
{
return;
}
foreach (PlayCard card in selectCardList)
{
deck.discardPack.Add(card);
for (int i = 0; i < hands.Length; i++)
{
if (hands[i] == card)
{
hands[i] = null;
break;
}
}
card.gameObject.SetActive(false);
}
selectCardList.Clear();
DrawHands();
}
private void SortCardToRank()
{
hands = hands.OrderBy(card => card == null)
.ThenByDescending(card => card != null ? card.rank : -1)
.ThenBy(card => card != null ? card.suit :Suit.None)
.ToArray();
}
}
</style>
오늘은 Hand, Deck에 대하여 기본적인 기능을 구현해보았습니다.
구현 흐름을 정리하다가 Balatro에서 카드 족보를 실시간으로 판정하는것을 보고 처음에는 FixedUpdate나 Update와 같은 곳에서 연산처리를 해야된다고 생각하여 둘 중 어느 하나가 적합한지를 고민하고 있었습니다.
후에 알게된 것은 실시간이라는 것은 선택한 카드가 변경된 시점때마다 계산이 이루어지면 되는 것이었고 그 얘기는 카드 선택 로직의 마지막에 족보 계산 로직을 호출하면 된다는 소리였습니다.
단순히 실시간으로 변경되어야한다는 점 때문에 라이프사이클의 접근만 고려했었는데 직접 기획을 진행하면서 아직은 일차원적인 생각으로 연결되는 부분이 많이 부족하다는걸 느꼈습니다.