https://www.youtube.com/watch?v=32UgKQzo7n8&list=RDMM32UgKQzo7n8&start_radio=1
힘든 나에게 힘을 주는~~~

이번에는 플레이어의 턴에 카드가 드로우 덱에서 들어오는 것과, 그 카드를 사용할 시 해당 카드가 무덤으로 들어가는 것을 구현하려 한다
근데 당장 드로우, 무덤에 사용될 덱의 자료구조가 뭐가 적합한지 모르겠다
그런 당신을 위한 GPT-5~
구조 그대로 가셔도 됩니다. 다만 3가지만 보완하면 안정적입니다.
- 식별자 관리
- 드로우/무덤/손패는 **List<int>**로 카드ID만 보유.
- CSV는 런타임 초기에 1회 로딩해 **Dictionary<int, CardSpec>**로 캐시.
- 카드 효과 라우팅은 **effectId**로 하고, 사람 읽기용 문구(Instruction)는 UI 전용으로 분리.
- 데이터 스키마 정리
CSV 필드는 다음 최소 집합을 권장합니다.
- id:int, cost:int, type:enum, effectId:int, params:string(json|kv), name:string, desc:string
이렇게 두면 효과 매니저는 effectId와 params만 보고 동작하고, 텍스트 변경이 로직에 영향 주지 않습니다.
- 실행 흐름(턴·드로우·사용·무덤)
- 셔플: 드로우덱 List<int>에 피셔–예이츠. RNG는 시드 고정 가능. top=리스트 끝.
- 드로우: draw(n)은 끝에서 n개 pop → 손패에 push. 비면 무덤을 섞어 통째 이동(또는 참조 스왑) 후 잔여 수만큼 추가 드로우.
- 사용: 손패에서 해당 id 제거 → EffectRouter에 PlayContext{caster, targets, cardId, rng, turn } 전달 → 처리 완료 후
- 일반: 무덤 push
- 소거: 소거존(List<int>)로 이동
- 지속/현장: 보드영역으로 이동 후 만료 시 무덤
- 정리 단계: 핸드 버리기 규칙 있으면 손패 → 무덤.
설계 분리(권장 컴포넌트)
- CardDatabase: CSV 로딩, GetSpec(id) 제공.
- DeckState: List<int> draw, discard, hand, exhaust 보유와 조작.
- EffectRouter: effectId→핸들러 매핑. 신규 카드 추가 시 CSV 행과 핸들러만 추가. 기존 코드 수정 최소화.
- RNGProvider: 전투 단위 시드 관리. 리플레이·동기화에 필요.
주의점
- 중간 제거가 잦은 손패는 List<int>로도 충분합니다. 마지막 항목 스왑-제거를 쓰면 비용 최소화.
- Queue/Stack은 셔플·임의 접근이 불편해 비권장.
- CSV 파싱 비용은 초기화 때만. 프레임 중에는 항상 사전(Dictionary) 조회.
- 멀티 타겟 기술은 targets를 리스트로 컨텍스트에 전달하고, UI에서만 선택 로직을 처리합니다.
결론: 상태는 List<int> 4개, 정의는 Dictionary<int, CardSpec>, 효과는 EffectRouter.
지금 구상에 effect(타겟, cardId:int) 형태를 유지하되, CSV에 effectId/params를 추가해 데이터 주도형으로 고도화하시면 됩니다.
라고 한다
뭔가 굉장히 복잡한 이야기를 하고 있는 것 같다
일단 int 핸들링을 의도해보자
using System.Collections.Generic;
using UnityEngine;
public class Deck : MonoBehaviour
{
[Header("덱 리스트")]
[SerializeField]
private List<int> _cards = new List<int>();
// 덱에서 덱으로 이동
// ex - 드로우 덱 전부 사용시 무덤덱 -> 드로우 덱으로 리스트 이동
private void MoveDeckToDeck(Deck deck)
{
deck.SetDeck(_cards);
ResetDeck();
}
// 자신의 덱을 반환
public List<int> GetDeck()
{
return _cards;
}
// 덱 세팅
public void SetDeck(List<int> cardIdData)
{
_cards = cardIdData;
}
// 자신의 덱을 비움
private void ResetDeck()
{
_cards.Clear();
}
}
기본적으로 부모가 될 카드를 담는 덱 구조를 만들어 준다
여기에는 덱이 기본적으로 행할 것 같은 기능을 모두 미리 구현해 둔다
private void MoveDeckToDeck(Deck deck)
{
deck.SetDeck(_cards);
ResetDeck();
}
근데 당장 이렇게 하면 두 덱이 같은 리스트를 참조하게 되기 때문에 이를 수정해주어야 한다
근본적인 구조가 글러먹었다
인큐나 디큐를 for로 반복시켜서 옮기는게 좀 더 자료구조 친화적인 무언가인 것 같다
new list를 남발하면 되지만, 메모리 낭비가 아닐까? 아무리 GC가 있다고는 하지만;;
// 랜덤 카드 한장을 드로우 후 반환
public int DrawCard()
{
if (_cards.Count == 0)
{
Debug.Log("deck is null");
}
int randCardIndex = Random.Range(0, _cards.Count);
int cardId = _cards[randCardIndex];
_cards.Remove(randCardIndex);
return cardId;
}
일단 카드 한장을 드로우 하는 dequene같은 무언가를 구현했다
그와중에 remove가 아니라 removeat을 써야한다는 것을 알게 되었다
질문. 네 구조에서 “랜덤 드로우”가 진짜 필요한 거야?
보통 덱 시스템은 미리 셔플 → 순서대로 드로우가 더 일반적이거든. 매번 랜덤 인덱스로 뽑으면 셔플과 순서 관리가 무의미해져.
정말로 “무작위 카드 뽑기”가 목적이면 지금 방식이 맞지만, “덱” 개념을 살리고 싶으면 셔플 후 맨 위에서 뽑기가 더 자연스러워.
그 와중에 gpt의 날카로운 지적이 들어왔다
// 카드 한장을 드로우 후 반환
public int DrawCard()
{
if (_cards.Count == 0)
{
Debug.Log("deck is null");
return -1; // 핸들링으로 if (return == -1) 로 빈 것 확인
}
else
{
int cardId = _cards[0];
_cards.RemoveAt(0);
return cardId;
}
}
맨 앞 카드를 가져오도록 변환하고 덱이 빈 경우도 처리한다
// 덱에서 덱으로 이동
// ex - 드로우 덱 전부 사용시 무덤덱 -> 드로우 덱으로 리스트 이동
private void MoveCardsToDeck(Deck deck)
{
List<int> targetDeck = deck.GetCards();
targetDeck.AddRange(_cards);
ResetDeck();
}
덱에서 덱으로 옮기는 것도 new list없이 처리하도록 수정한다
자료만 그대로 가져온 뒤, 시작점 덱은 비우는 것으로 해결한다
이게 조금 씩 수정을 더 해준다
현재
을 사용하고 있는데 이렇게 하면 항상 카드를 뽑을때 끝까지 와야해서 o(n)이 되기 때문에
가장 윗 부분을 뽑도록 유도한다
// 카드 한장을 드로우 후 반환
public int DrawCard()
{
if (_cards.Count == 0)
{
Debug.Log("deck is empty");
return -1; // 핸들링으로 if (return == -1) 로 빈 것 확인
}
else
{
int cardId = _cards[_cards.Count - 1];
_cards.RemoveAt(_cards.Count - 1);
return cardId;
}
}
// 덱 섞기 = 피셔 예이츠 제자리 셔플
private void SuffleDeck()
{
for (int i = _cards.Count - 1; i > 0; i++)
{
int j = Random.Range(0, i + 1);
(_cards[i], _cards[j]) = (_cards[j], _cards[i]);
}
}
덱에서 덱으로 이동시 사용될, 혹은 독자적으로 사용될 셔플 기능을 피셔 예이츠 셔플로 구현해 준다
이때 신기한게 있는데 (a, b) = (b, a)라는 튜플 스왑 문법이 c#에 존재한다는 것을 처음 알았다
매일 매일이 새로워 으아악
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Deck : MonoBehaviour
{
[Header("덱 리스트")]
[SerializeField]
private List<int> _cards = new List<int>();
// 덱에서 덱으로 이동
// ex - 드로우 덱 전부 사용시 무덤덱 -> 드로우 덱으로 리스트 이동
private void MoveCardsToDeck(Deck deck)
{
List<int> targetDeck = deck.GetCards();
targetDeck.AddRange(_cards);
ResetDeck();
}
// 카드 한장을 드로우 후 반환
public int DrawCard()
{
if (_cards.Count == 0)
{
Debug.Log("deck is empty");
return -1; // 핸들링으로 if (return == -1) 로 빈 것 확인
}
else
{
int cardId = _cards[_cards.Count - 1];
_cards.RemoveAt(_cards.Count - 1);
return cardId;
}
}
// 덱 섞기 = 피셔 예이츠 제자리 셔플
private void SuffleDeck()
{
for (int i = _cards.Count - 1; i > 0; i++)
{
int j = Random.Range(0, i + 1);
(_cards[i], _cards[j]) = (_cards[j], _cards[i]);
}
}
// 난수 드로우 기능
// public int DrawCard()
// {
// if (_cards.Count == 0)
// {
// Debug.Log("deck is null");
// }
// else
// {
// int randCardIndex = Random.Range(0, _cards.Count);
// int cardId = _cards[randCardIndex];
// _cards.RemoveAt(randCardIndex);
// return cardId;
// }
// }
// 자신의 덱을 반환
public List<int> GetCards()
{
return _cards;
}
// 덱 세팅
public void SetCards(List<int> cardIdData)
{
_cards = cardIdData;
}
// 자신의 덱을 비움
private void ResetDeck()
{
_cards.Clear();
}
}
일단 대충 필요한 최소한의 덱 설계가 되었다
'개발일지 > 게임개발' 카테고리의 다른 글
| Project_DT - 카드 사용 구현 (2) | 2025.09.23 |
|---|---|
| Project_DT - 플레이어 드로우 기능 (0) | 2025.09.23 |
| Project_DT - 구글 시트 parsing 기능 만들기 (0) | 2025.09.19 |
| Project_DT - 턴제 전투, 적 상태 구현 (0) | 2025.09.18 |
| Project_DT - 적 공격 상태 수정(유한 상태 기계) / 실패 (0) | 2025.09.18 |