본문 바로가기
개발일지/게임개발

Project_DT - 카드 사용 구현

by 라이티아 2025. 9. 23.

기본적인 설계를 그림으로 한다

 

핵심은

카드를 실제로 게임에 그려주는 비주얼 드로어

카드를 사용하는 효과를 관리하는 전용 매니저

 

그 사이에 플레이어와 적 객체의 관리이며

이때 서로 책임 분리를 해야 할 것 같다

 

생각해보니 책임 분리가 과하게 들어간 것 같다

 

"카드"에 대한 책임만 집중해서 처리하면 될 것 같다

 

그럼 책임이 핵심 기능 2개로 나뉘게 되는데

  1. 카드를 화면에 그리는 기능
  2. 카드를 사용했을때 처리에 대한 기능

 

단순 무식하게 처리할려면 update로 연결해서 처리하면 되는데

너무 단순하고 무식하게 처리하는 방식이라 꺼려진다

 

일단 horizontal layout을 이용하여 카드가 모일 장소를 만들어 준 뒤

안쪽에서 사용될 카드에 대해서 정의한다

 

using UnityEngine;

public class Card : MonoBehaviour
{
    [Header("Card ID")]
    [SerializeField]
    private int _cardID = -1; // 초기값, -1인 상태로 사용되면 예외처리 핸들링

    /// <summary>
    /// 카드의 id를 세팅 = card manager에서 call해서 사용
    /// </summary>
    /// <param name="id"></param>
    public void SetCardID(int id)
    {
        _cardID = id;
    }
}

 

 

이 컴포넌트를 가진 오브젝트를 어떻게 핸드에 넣을지를 생각해야 한다

    public event System.Action<int> OnDrawCard;

player쪽에 델리게이트를 만들고

 

using UnityEngine;

public class CardManager : MonoBehaviour
{
    [Header("card area")]
    [SerializeField]
    private Transform _handArea;

    [Header("card prefab")]
    [SerializeField]
    private GameObject _card;

    [Header("draw Deck")]
    [SerializeField]
    private Deck _drawDeck;

    [Header("player object")]
    [SerializeField]
    private Player _player;

    void Start()
    {
        _player.OnDrawCard += InstanceCard;
    }

    public void InstanceCard(int cardID)
    {
        GameObject card = Instantiate(_card, _handArea);
    }
}

이를 카드 매니저에서 카드 생성과 연결해 구독한다

 

    public void InstanceCard(int cardID)
    {
        GameObject card = Instantiate(_card, _handArea);
        card.GetComponent<Card>().SetCardID(cardID);
    }

생성시 id세팅을 동시에 해준다

 

더보기

결론: 패턴은 적절합니다. 다만 현재 구현은 핵심 이벤트가 발행되지 않아 UI가 갱신되지 않습니다. 아래만 고치면 안정적입니다.

  • 이벤트 발행 지점: DrawCard()에서 카드 한 장을 핸드에 넣은 직후 OnDrawCard?.Invoke(cardId)를 호출하십시오. 지금은 구독만 있고 발행이 없습니다.
  • 구독/해제 수명:
    • CardManager.Start()에서 구독했다면 OnDestroy()에서 반드시 -=로 해제하십시오.
    • Player도 OnDestroy()에서 TurnManager.Instance.OnTurnChanged -= TurnChanged 해제하십시오.
  • 이벤트 의미 명확화: 이벤트명을 OnCardAddedToHand로 바꾸고 “핸드에 실제 반영 완료” 시점에만 쏘십시오. UI와 모델의 순서를 일치시켜 경쟁 상태를 막습니다.
  • 페이로드 개선: int 하나보다 cardId와 소스/목적 덱, 드로우 인덱스 등을 담은 전용 args(예: CardDrawnArgs)로 확장 여지를 두십시오. 여러 장 드로우 시 OnCardsDrawn(IReadOnlyList<int>)도 고려하십시오.
  • 핸드 한도 처리: 핸드가 가득 찬 상태에서는 드로우를 막거나 버림 처리 규칙을 적용하고, 그 결과를 이벤트로 알리십시오.
  • 빈 드로우 덱 처리: 비었을 때 무덤 셔플→드로우 보충 로직을 먼저 실행한 뒤 드로우를 재시도하십시오. 실패 시에는 “실패 이벤트”를 별도로 발행하십시오.
  • 중복 인스턴스 방지: 씬 리로드나 프리팹 재활성화 시 중복 구독이 일어나지 않도록 Awake/OnEnable에서 구독하고 OnDisable에서 해제하는 패턴을 지키십시오.
  • 책임 분리: 카드 생성은 CardManager가 전담하고, Player는 상태 변경과 이벤트 발행만 담당하십시오. Deck은 순수 데이터 조작만 맡기십시오.
  • 디버그 로그: 단순 로그 대신 이벤트 기반으로 상태를 추적하고, 필요 시 마지막 드로우 결과를 기록해 재현성을 높이십시오.

요약: 이벤트 기반 구조는 맞습니다. “드로우 → 핸드 반영 → 이벤트 발행”의 순서 보장, 구독 해제, 빈 덱·핸드 한도 예외 처리만 추가하면 됩니다.

    private void DrawCard(int n)
    {
        for (int i = 0; i < n; i++)
        {
            if (!_drawDeck.IsDeckEmpty())
            {
                int drawCardId = _drawDeck.DrawCard();
                _handDeck.SetCardToTop(drawCardId);
                OnDrawCard.Invoke(drawCardId);
            }
            else
            {
                Debug.Log("draw deck is empty");
            }
        }
    }

invoke가 빠져 있어서 추가해 준다

 

    public void SetCardID(int id)
    {
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();
    }

확인용으로 card cs에 Tmpro text를 붙여준다

 

카드 오브젝트를 프리팹화 해준 뒤, 사용한다