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

Project_DT - 카드 효과 처리 매커니즘

by 라이티아 2025. 10. 9.

이전 글에서 카드의 asset파일을 만드는 것에 대한 처리를 했고, 이제는 해당 파일을 사용해서 여러 처리를 어떻게 할 것인가에 대해서 다룬다

 

using UnityEngine;

[CreateAssetMenu(menuName = "Cards/CardDefinition")]
public class CardSpec : ScriptableObject
{
    // 나중에 enum으로 정리하는 것을 고려해봐야 할 것 같다
    public int id;
    public string cardName;
    public int cost;
    public string type;
    public string instruction;
    public string[] targeting;
    public string rarity;
    public string discardPolicy;
}

현재 카드의 그래픽, GUI적 요소에 대해서 정리하고 있고, 여기에 +a로 카드 효과에 대해서 다루어야 할 것 같다

 

현재 생각하는 매커니즘은 따로 다른 asset을 만들고 그곳에서 전투에 관련된 처리 수치를 다루도록 해서 list로 관리하는것이다

 

using UnityEngine;

public abstract class CardEffect : ScriptableObject
{
    public abstract void Execute(Player source, Character target, Card card);
}

따로 작성될 scriptableobject를 작성하고

using UnityEngine;

[CreateAssetMenu(menuName = "Cards/Effects/DealDamage")]
public class DealDamageEffect : CardEffect
{
    [SerializeField] private int damageAmount;
    public int DamageAmount
    {
        get => damageAmount;
        set => damageAmount = value;
    }

    public override void Execute(Player source, Character target, Card card)
    {
        BattleManager.Instance.ApplyDamage(target, damageAmount);
    }
}

이를 상속해서 처리를 받도록 한다

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

    private Canvas _canvas;

    // if using card, than call this delegate
    public event System.Action<Card, Character> OnUsingCard;

현재 card cs는 이렇게 변수 리스트를 가지는데 이를 해당 asset을 가져오도록 처리한다

    private int _cardID = -1; // 초기값, -1인 상태로 사용되면 예외처리 핸들링
    [Header("Card ID")]
    public int CardID { get => _cardID; set => _cardID = value; }

    private Canvas _canvas;

    [SerializeField]
    private CardSpec _cardSpec;
    [SerializeField]
    private List<CardEffect> _cardEffects = new List<CardEffect>();

카드에 asset을 넣을 수 있는 구간이 생겼다

 

이제 여기에 어떻게 넣을지에 대해서 논의한다

 

 

using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class CardDatabase : MonoSingleton<CardDatabase>
{
    [SerializeField]
    private CardSpec[] cardSpecs;

    private Dictionary<int, CardSpec> _map;

    protected override void Awake()
    {
        base.Awake();

        _map = new Dictionary<int, CardSpec>();
        foreach (var spec in cardSpecs)
        {
            _map[spec.id] = spec;
        }
    }
    // void Start()
    // {
    //     _map = new Dictionary<int, CardSpec>();
    //     foreach (var spec in cardSpecs)
    //     {
    //         _map[spec.id] = spec;
    //     }
    // }

    public CardSpec Get(int id) => _map.TryGetValue(id, out var spec) ? spec : null;
}

현재 시도해볼 방법은 미리 database 객체를 만들고 이를 호출해서 asset파일을 가져오는 방식이다

 

해당 객체는 시작할때 미리 id - spec파일과 dictionary를 만들어 두어 쉽게 가져올 수 있게 디자인 되어 있다

 

    public void Init(Canvas canvas, int id)
    {
        _canvas = canvas;
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();

        _cardSpec = CardDatabase.Instance.Get(_cardID);
    }

이를 카드에서 호출해서 가져오도록 유도한다

스펙 시트가 딸려오지 않는다

뭐가 문제일까

 

그냥 핸드에 없는 id가 있는게 문제였다

현재 1-3밖에 없는데 4를 호출하니 당연히 연결이 되지 않는것이다

 

리스트를 수정하니 정상적으로 들어오는 모습을 볼 수 있다

 

이제 spec을 카드에서 가져올 수 있도록 대충 테스트용 prefab을 제작해 준다

 

tmp에서는 한글 지원 폰트가 기본으로 없으니, 하나 만들어 줘야 한다

https://noonnu.cc/font_page/1671

 

가비아 던체 | 눈누 - 상업용 무료 한글 폰트

가비아 던체 폰트. 가비아에서 제작한 상업용 무료 한글 폰트로 개인, 상업적 용도 모두 사용 가능합니다.

noonnu.cc

대충 무료 폰트중 괜찮아 보이는 것을 찾아준다

 

다만 이런 폰트는 재배포를 금지하고 있으니 철저하게 관리를 해주어야 git에서 관리가 가능하다

 

gitignore을 세팅해 준다

그후 ttf를 넣어준 뒤

국룰 세팅으로 구워준다

잘 들어가는 것을 확인할 수 있다

 

필요한 부분만 임시로 넣어 주었다

    [SerializeField]
    private TextMeshProUGUI _costText;
    [SerializeField]
    private TextMeshProUGUI _instructionText;
    [SerializeField]
    private TextMeshProUGUI _nameText;
    [SerializeField]
    private TextMeshProUGUI _idText;

이를 받을 값을 만들고

    public void Init(Canvas canvas, int id)
    {
        _canvas = canvas;
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();

        _costText = transform.GetChild(0).GetComponent<TextMeshProUGUI>();
        _instructionText = transform.GetChild(1).GetComponent<TextMeshProUGUI>();
        _nameText = transform.GetChild(2).GetComponent<TextMeshProUGUI>();
        _idText = transform.GetChild(3).GetComponent<TextMeshProUGUI>();

        _cardSpec = CardDatabase.Instance.Get(_cardID);
        if (_cardSpec == null)
        {
            Debug.LogError($"CardSpec을 찾을 수 없습니다: {_cardID}");
        }
        _costText.text = _cardSpec.cost.ToString();
        _instructionText.text = _cardSpec.instruction.ToString();
        _nameText.text = _cardSpec.cardName.ToString();
        _idText.text = _cardSpec.id.ToString();
    }

이를 받아 들여서 처리한다

 

정말 더럽지만 일단 이렇게 처리한다

 

잘 나오는 것을 확인할 수 있다

 

    void Awake()
    {
        _costText = transform.GetChild(0).GetComponent<TextMeshProUGUI>();
        _instructionText = transform.GetChild(1).GetComponent<TextMeshProUGUI>();
        _nameText = transform.GetChild(2).GetComponent<TextMeshProUGUI>();
        _idText = transform.GetChild(3).GetComponent<TextMeshProUGUI>();
    }

    /// <summary>
    /// 카드의 여러 값을 생성시 세팅
    /// </summary>
    /// <param name="id"></param>
    public void Init(Canvas canvas, int id)
    {
        _canvas = canvas;
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();

        _cardSpec = CardDatabase.Instance.Get(_cardID);
        if (_cardSpec == null)
        {
            Debug.LogError($"CardSpec을 찾을 수 없습니다: {_cardID}");
        }
        _costText.text = _cardSpec.cost.ToString();
        _instructionText.text = _cardSpec.instruction.ToString();
        _nameText.text = _cardSpec.cardName.ToString();
        _idText.text = _cardSpec.id.ToString();
    }

일단 찟어서 분리한다

 

이제 effect를 어떻게 처리할까에 대한 생각을 한다

 

우선 이전에 작성한 스크립터블 생성기로 데미지 처리를 생성한다

이를 양산한 뒤

public class CardDatabase : MonoSingleton<CardDatabase>
{
    [SerializeField]
    private CardSpec[] cardSpecs;

    [SerializeField]
    private CardEffect[] cardDamageEffects;
    private Dictionary<int, CardSpec> _map;

미리 이를 가져온다

 

이제 이를 어떻게 연결할지 생각해야 한다

일단 구글 시트에서 생각하기로 한다

여러번 효과가 생기는 것에 대비하여 /를 사용하여 구분한다

 

using UnityEngine;

[CreateAssetMenu(menuName = "Cards/CardDefinition")]
public class CardSpec : ScriptableObject
{
    // 나중에 enum으로 정리하는 것을 고려해봐야 할 것 같다
    public int id;
    public string cardName;
    public int cost;
    public string type;
    public string instruction;
    public string[] targeting;
    public string rarity;
    public string discardPolicy;
    public string[] effect;
    public int[] effectAmount;
    public int[] effectHoldingTime;
}

 

다만 sheet에서 가져온 것은 결국 str이기 때문에 이를 int list로 변환을 해주는 작업을 코드 레벨에서 거쳐야 한다

 

            // CardSpec.asset 생성
            var cardSpec = ScriptableObject.CreateInstance<CardSpec>();
            cardSpec.id = int.Parse(id);
            cardSpec.cardName = name;
            cardSpec.cost = int.Parse(cost);
            cardSpec.type = type;
            cardSpec.instruction = instruction;
            cardSpec.targeting = targeting;
            cardSpec.rarity = rarity;
            cardSpec.discardPolicy = discardPolicy;
            cardSpec.effect = effect;

                int[] effectAmount = new int[effectAmountStr.Length];
                for (int j = 0; j < effectAmount.Length; j++)
                    effectAmount[j] = int.Parse(effectAmountStr[j]);

                int[] effectHoldingTime = new int[effectHoldingTimeStr.Length];
                for (int j = 0; j < effectHoldingTime.Length; j++)
                    effectHoldingTime[j] = int.Parse(effectHoldingTimeStr[j]);

            cardSpec.effectAmount = effectAmount;
            cardSpec.effectHoldingTime = effectHoldingTime;

변환 과정을 for로 거치게 한다

 

깔끔한 에러~~

 

해결해보자

 

더보기

FormatException: Input string was not in a correct format.
System.Number.ThrowOverflowOrFormatException (System.Boolean overflow, System.String overflowResourceKey) (at <66b6ff42547b49058d130806d84e7dcf>:0)
System.Number.ParseInt32 (System.ReadOnlySpan`1[T] value, System.Globalization.NumberStyles styles, System.Globalization.NumberFormatInfo info) (at <66b6ff42547b49058d130806d84e7dcf>:0)
Systehttp://m.Int32.Parse (System.String s) (at <66b6ff42547b49058d130806d84e7dcf>:0)
AssetDataMaker.GenerateAttackCards () (at Assets/01.Scripts/GeneralManager/AssetDataMaker.cs:54)

effectAmount[j] = int.Parse(effectAmountStr[j]);

해당 줄에서 처리중 형이 맞지 않아 오류가 나왔다

 

            int[] effectAmount = new int[effectAmountStr.Length];
            for (int j = 0; j < effectAmount.Length; j++)
            {
                string word = effectAmountStr[j].Trim();
                if (int.TryParse(word, out int value))
                {
                    effectAmount[j] = value;
                }
                else
                {
                    Debug.LogWarning($"[ID {id}] EffectAmount 변환 실패: '{word}'");
                    effectAmount[j] = -1;
                }
            }

tryparse를 통해 좀더 빡빡하게 변환한다

 

원인은 같아서 별 차이가 나지 않는다

 

            string[] effectAmountStr = parts[9].Trim('"').Split('/');
            foreach (var test in effectAmountStr)
                Debug.Log(test + " Test EAS");

로그를 찍어보자

 

????? 그냥 공백이 나온다

확인해보니 9번에는 tag가 자리를 잡고 있다

즉, 위치가 잘못된 것이다

위치를 수정하고 다시 테스트 한다

뭔가 뭔가 되고 있다

 

순서가 잘못된게 맞는 것 같다

 

다시 순서를 보정해 준다

 

정상적으로 잘 들어온 것을 확인할 수 있다

 

그냥 단순한 순서 문제였다

 

이제 이 list를 순회하면서 처리하도록 하면 된다

 

    public CardEffect GetCardEffect(string effectName, int amount, int holdingTime)
    {
        if (effectName == "Damage")
        {
            return cardDamageEffects[amount];
        }
        return null; 
    }

카드 데이터베이스에서 해당하는 effect를 가져올 수 있는 함수를 작성한 뒤

    /// <summary>
    /// 카드의 여러 값을 생성시 세팅
    /// </summary>
    /// <param name="id"></param>
    public void Init(Canvas canvas, int id)
    {
        _canvas = canvas;
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();

        _cardSpec = CardDatabase.Instance.Get(_cardID);
        if (_cardSpec == null)
        {
            Debug.LogError($"CardSpec을 찾을 수 없습니다: {_cardID}");
        }
        _costText.text = _cardSpec.cost.ToString();
        _instructionText.text = _cardSpec.instruction.ToString();
        _nameText.text = _cardSpec.cardName.ToString();
        _idText.text = _cardSpec.id.ToString();

        for (int i = 0; i < _cardSpec.effect.Length; i++)
        {
            _cardEffects.Append<CardEffect>(CardDatabase.Instance.GetCardEffect(
                _cardSpec.effect[i], _cardSpec.effectAmount[i], _cardSpec.effectHoldingTime[i]));
        }
    }

이를 리스트에서 순회하며 가져오도록 처리한다

 

지금은 Damage에 대해서만 가지고 있기 때문에 해당 처리만 하도록 한다

이때 holdingTime값은 무시하게 된다

 

근데 안들어 간다

why??

더보기

🔍 질문 요지:

.asset 파일(ScriptableObject)을 플레이 중에 List<CardEffect>에 Append()로 넣었는데,
왜 인스펙터에 안 보이는가?


✅ 결론부터 말하면:

"Unity 인스펙터는 `플레이 중에 동적으로 변경된 리스트 요소는 기본적으로 표시하지 않는다."

즉, 런타임 중에 List에 값을 추가하더라도 Unity 에디터에서 보이지 않는 것이 정상이야.


🧠 이유 요약:

항목설명
🔒 SerializeField 에디터에서 보이게는 하지만 런타임 중 수정사항은 표시되지 않음
🔧 List<T> 필드 플레이 중에는 내부적으로 값이 있어도 에디터는 그걸 실시간 반영하지 않음
💾 .asset (ScriptableObject) 이것도 런타임 중 참조가 설정돼도 인스펙터에 "보이게" 반영되지 않음

🧪 확인 방법

✅ 확인하고 싶다면 로그로 찍어봐야 함:

 
foreach (var effect in _cardEffects) { Debug.Log($"Card에 등록된 Effect: {effect.name}"); }

혹은 Watch/Inspector 창으로 Debug 모드에서 리스트 내용 직접 확인


🧪 에디터에서도 확인하고 싶다면

에디터 확장으로 직접 Repaint() 호출하거나, OnValidate() 사용한 강제 갱신밖에 없어. 일반적인 워크플로우에서는 그렇게까지 안 해도 돼.

원래 에디터에서 안보인다고 한다

 

        for (int i = 0; i < _cardSpec.effect.Length; i++)
        {
            _cardEffects.Append<CardEffect>(CardDatabase.Instance.GetCardEffect(
                _cardSpec.effect[i], _cardSpec.effectAmount[i], _cardSpec.effectHoldingTime[i]));
        }
        foreach (var effect in _cardEffects)
        {
            Debug.Log($"Card에 등록된 Effect: {effect.name}");
        }

따로 체크용 코드를 넣어서 확인해야 한다

 

는 그냥 list에는 append라는게 없다고 한다

        for (int i = 0; i < _cardSpec.effect.Length; i++)
        {
            _cardEffects.Add(CardDatabase.Instance.GetCardEffect(
                _cardSpec.effect[i], _cardSpec.effectAmount[i], _cardSpec.effectHoldingTime[i]));
        }

Add함수로 처리해야 한다

잘 처리되어서 보이는 것을 확인할 수 있다

 

또GPT 환상!! 너야!!!

심지에 에디터에서 잘 보이잖아 으아악

이제 왜 2번 공격하는 것과 같은 다중 처리가 안되는지 찾아야 한다

 

range오류가 나는것을 보면 무언가 잘못 되었음을 짐작할 수 있다

에러 메시지와 카드 데이터(0003)를 보면, 원인은 CSV 파싱 시 배열 길이가 서로 달라졌기 때문이야.

💥 문제 핵심

카드 데이터:

Effect: Damage/Damage  
EffectAmount: 2/2  
EffectHoldingTime: 0


즉,

_cardSpec.effect.Length = 2

_cardSpec.effectAmount.Length = 2

_cardSpec.effectHoldingTime.Length = ❌ 1

그런데 코드에서 이렇게 반복하고 있지?

for (int i = 0; i < _cardSpec.effect.Length; i++)
{
    _cardEffects.Add(CardDatabase.Instance.GetCardEffect(
        _cardSpec.effect[i], _cardSpec.effectAmount[i], _cardSpec.effectHoldingTime[i]));
}


→ i = 1일 때 _cardSpec.effectHoldingTime[1]에 접근하려고 하니까,
그 배열에는 요소가 1개밖에 없어서 IndexOutOfRangeException이 발생한 거야.

amount만 처리해도 되는 부분에서 holdingtime도 가져와서 없는 배열을가져오려 해서 생기는 문제이다

그냥 추가로 해준 뒤

인덱스 에러가 없게 해주면 된다

 

갑자기 생각난건데, 일일이 spec을 db에 넣어주는게 귀찮으니 자동화 한다

            // CardSpec.asset 생성
            var cardSpec = ScriptableObject.CreateInstance<CardSpec>();
            cardSpec.id = int.Parse(id);
            cardSpec.cardName = name;
            cardSpec.cost = int.Parse(cost);
            cardSpec.type = type;
            cardSpec.instruction = instruction;
            cardSpec.targeting = targeting;
            cardSpec.rarity = rarity;
            cardSpec.discardPolicy = discardPolicy;
            cardSpec.effect = effect;

            cardSpec.effectAmount = effectAmount;
            cardSpec.effectHoldingTime = effectHoldingTime;

            // string assetPath = $"Assets/CardAssets/Card_{id}.asset";
            string assetPath = $"Assets/Resources/CardAssets/Card_{id}.asset";
            AssetDatabase.CreateAsset(cardSpec, assetPath);
            Debug.Log($"CardSpec 생성: {assetPath}");

resources폴더에 데이터를 저장하도록 유도한 뒤

 

public class CardDatabase : MonoSingleton<CardDatabase>
{
    [SerializeField]
    private CardSpec[] cardSpecs;

    [SerializeField]
    private CardEffect[] cardDamageEffects;
    private Dictionary<int, CardSpec> _map;

    protected override void Awake()
    {
        base.Awake();

        cardSpecs = Resources.LoadAll<CardSpec>("CardAssets");

폴더 통째로 가져오도록 유도한다

 

...

언제나 그렇듯 에러

수정하는데 시간이 걸릴 것 같으니 나중으로 미루도록 하자

잘 처리되는 것을 확인할 수 있다

 

이제 카드를 사용시 해당 effectlist를 순회해서 처리하도록 하면 된다

 

    /// <summary>
    /// 카드 효과 호출 함수
    /// </summary>
    /// <param name="target"></param>
    /// <param name="cardID"></param>
    public void UsingCard(Card card, Character target)
    {
        Debug.Log($"target:{target} / card : {card.CardID}");

        foreach (var cardeffect in card.cardEffects)
        {
            cardeffect.Execute(target, card);
        }

        card.ActiveCard(); // 카드 사용 처리
        _graveyardDeck.SetCardToTop(card.CardID);
    }

순회하면서 함수를 호출한다

 

잘 처리되는 것을 확인할 수 있다