개발/유니티

[유니티] 무모한 난동 애니메이션추가, 동적 설명문

kimchangmin02 2025. 8. 13. 17:57

 

 

근데 현재 4개정도의 이미지가 준비됭어있는데
이거를 일단 유니티의 하이아키에 추가해야겟지?
또한 스킬 10초에 한번씩 사용되는데 그럴때마다
이 스킬 효과 4단계가 차례대로 호출되는 것도 호출해야겟지?

 

 

객체지향적인 구조 설계

 

 

1. 하이라키(Hierarchy) 창 작업 (딱 한 번만 필요)

하이라키 창에서는 스킬 이펙트의 '원본'이 되는 **프리팹(Prefab)**을 만드는 작업을 합니다.

  1. 빈 게임 오브젝트 생성: 하이라키 창의 빈 공간에 마우스 우클릭 -> Create Empty를 선택합니다.
  2. 이름 변경: 새로 생긴 GameObject의 이름을 FortressExplosionEffect 와 같이 알아보기 쉽게 바꿔줍니다.
  3. 컴포넌트 추가: FortressExplosionEffect 오브젝트를 선택한 상태에서, 오른쪽 인스펙터(Inspector) 창 맨 아래의 Add Component 버튼을 누릅니다.
    • Sprite Renderer 를 검색해서 추가합니다. (이미지를 화면에 보여주는 역할)
    • Effect Animator 를 검색해서 추가합니다. (직접 만드신 EffectAnimator.cs 스크립트)
  4. 프리팹으로 만들기: 하이라키 창에 있는 FortressExplosionEffect 오브젝트를 마우스로 끌어서, 아래쪽 프로젝트(Project) 창의 Assets 폴더 안으로 드래그 앤 드롭합니다.
    • 프로젝트 창에 파란색 아이콘의 FortressExplosionEffect 프리팹이 생성됩니다.
    • 이제 하이라키 창에 있던 원본 FortressExplosionEffect 삭제해도 괜찮습니다. 원본의 복사본인 프리팹을 코드에서 계속 찍어낼 것이기 때문입니다.

결론: 하이라키 창에서는 프리팹을 만들기 위한 1회성 작업만 진행하고, 최종적으로는 비워두시면 됩니다.


2. 프로젝트(Project) 창 연결 작업 (가장 중요)

이제 코드와 유니티 에셋들을 서로 연결하는 핵심 단계입니다.

  1. 스킬 데이터 에셋 선택: 프로젝트 창에서 FortressExplosion 이라는 이름의 SkillData 에셋을 찾아 클릭합니다. (만약 없다면, 마우스 우클릭 -> Create -> Rewards -> FortressExplosion을 선택하여 생성합니다.)
  2. 인스펙터 창에서 값 연결: FortressExplosion 에셋을 선택하면 인스펙터 창에 아래와 같은 항목들이 나타납니다. 여기에 값을 채워 넣어야 합니다.
    • Amount: (원하는 방어도 소모량 입력, 예: 10)
    • Damage Multiplier: (원하는 데미지 계수 입력, 예: 10)
    ▼ VFX 설정 (이 부분이 핵심입니다)
    • Effect Prefab: 1단계에서 만든 FortressExplosionEffect 프리팹을 프로젝트 창에서 이곳으로 드래그 앤 드롭하여 연결합니다.
      *
    • Animation Frames: 이 항목 옆의 자물쇠 아이콘을 눌러 인스펙터를 잠급니다. 그런 다음, 프로젝트 창에서 준비된 4개의 스킬 이미지(스프라이트)를 한 번에 모두 선택하여 Animation Frames 라는 이름 위로 드래그 앤 드롭합니다.
      • Size 4로 바뀌고 각 슬롯에 이미지가 등록됩니다.
    • Frame Delay: 애니메이션 속도를 조절합니다. 0.1 또는 0.15 정도의 값을 추천합니다.
    • Damage Frame: 데미지가 들어갈 타이밍을 정합니다. 4개의 이미지 중 3번째 이미지에서 터지는 효과를 원한다면 2를 입력합니다 (컴퓨터는 0부터 숫자를 세므로 0, 1, 2, 3 순서입니다).

 

 

 

 

 

 

 

 

 

 

무엇이 위험한가? - "붕어빵 틀" 이야기

사용자님이 프로젝트 창(Assets/Data/Rewards/)에 만들어두신 FortressExplosion 스킬 데이터 에셋은, 게임 속 실제 아이템이 아니라 **"붕어빵 틀"**과 같습니다.

  • 붕어빵 틀 (원본 에셋): amount는 10, damageMultiplier는 10f 라고 적혀있는 원본 설계도입니다.
  • 붕어빵 (게임 내 데이터): 게임을 시작하면, 이 "틀"을 보고 스킬의 정보를 읽어옵니다.

현재 코드의 문제점:

C#
public void upgradeSkill()
{
    amount += 5; // amount 값을 10에서 15로 바꾼다.
}

이 코드는 게임 중에 붕어빵 틀 자체를 망치로 두드려 모양을 바꾸는 것과 같습니다. amount가 10이던 원본 설계도 파일의 값을 15로 영구적으로 수정해버립니다.

어떤 버그가 생기는가?

  1. 게임을 플레이해서 '성채 폭발' 스킬을 두 번 먹습니다. amount는 15가 됩니다.
  2. 게임을 끕니다.
  3. 다음 날, 새 게임을 시작합니다.
  4. '성채 폭발' 스킬을 처음 먹었는데, amount가 10이 아니라 어제 망가뜨려놓은 15에서 시작합니다.

이것이 바로 "런타임에 원본 에셋을 수정하면 안 된다"는 원칙입니다.

"프리팹 복사본"에 대한 기억의 정체

"원본이 아니라 복사본을 만들어야 한다"는 기억은 바로 이 ScriptableObject에 대한 이야기입니다! (프리팹과 원리가 같습니다.) 즉, 게임이 시작되면 **원본 "붕어빵 틀"은 그대로 두고, 게임 안에서만 사용할 "복사본"**을 만들어야 합니다. 그리고 강화를 할 때는 원본이 아닌, 이 메모리 위의 복사본을 수정해야 합니다.


올바른 해결 방법: 인스턴스(복사본) 생성하기

스킬을 획득했을 때, 원본 에셋을 그대로 목록에 넣는 것이 아니라, Instantiate()를 통해 메모리에 복사본을 만들고, 그 복사본을 관리해야 합니다.

아래와 같이 코드를 수정하면 완벽하게 해결됩니다.

1. PlayerSkills.cs 수정

이 스크립트가 스킬의 '원본'과 '복사본'을 모두 관리하는 핵심적인 역할을 맡게 됩니다.

C#
// 파일 이름: PlayerSkills.cs (수정)
using UnityEngine;
using System.Collections.Generic;
using System.Linq; // Linq를 사용하기 위해 추가

public class PlayerSkills : MonoBehaviour
{
    // 현재 내가 가진 스킬의 '복사본(인스턴스)' 목록입니다.
    // 모든 강화, 쿨타임 계산은 이 목록에 있는 복사본을 대상으로 이루어집니다.
    public List<SkillData> ownedSkillInstances = new List<SkillData>();

    // 각 복사본의 마지막 시전 시간을 기록할 사전
    private Dictionary<SkillData, float> skillCooldowns = new Dictionary<SkillData, float>();

    void Update()
    {
        // 이제 쿨타임 확인 대상은 원본이 아닌, 내가 가진 복사본들입니다.
        foreach (SkillData skillInstance in ownedSkillInstances)
        {
            if (!skillCooldowns.ContainsKey(skillInstance) || Time.time - skillCooldowns[skillInstance] >= skillInstance.cooldown)
            {
                skillInstance.Activate(this);
                skillCooldowns[skillInstance] = Time.time;
            }
        }
    }

    // 외부(Reward)에서 스킬을 획득했을 때 호출하는 함수
    public void AddSkill(SkillData originalSkill) // 이제 넘어오는 것은 '원본 붕어빵 틀'
    {
        // 내가 가진 스킬 복사본 목록에서, 원본이 같은 스킬이 있는지 찾아봅니다.
        // skill.name은 ScriptableObject의 파일 이름이므로 고유합니다.
        SkillData existingSkill = ownedSkillInstances.FirstOrDefault(s => s.name == originalSkill.name);

        if (existingSkill != null)
        {
            // 이미 같은 종류의 스킬 복사본을 가지고 있다면, 강화 로직을 호출합니다.
            Debug.Log(existingSkill.skillName + " 스킬을 강화합니다!");
            existingSkill.Upgrade(); // 복사본의 Upgrade 함수를 호출
        }
        else
        {
            // 이 종류의 스킬을 처음 얻는다면,
            // 원본 붕어빵 틀(originalSkill)을 사용해 메모리에 복사본을 만듭니다.
            SkillData skillInstance = Instantiate(originalSkill);
            
            // 복사본의 이름을 원본과 같게 맞춰줍니다 (Instantiate하면 이름 뒤에 (Clone)이 붙기 때문).
            skillInstance.name = originalSkill.name;

            // 복사본을 내가 가진 스킬 목록에 추가합니다.
            ownedSkillInstances.Add(skillInstance);
            Debug.Log(skillInstance.skillName + " 스킬을 새로 획득했습니다!");
        }
    }
}

2. FortressExplosionSkill.cs 수정

이제 이 스크립트는 훨씬 단순해집니다. 자신이 원본인지 복사본인지 신경 쓸 필요 없이, 자신의 값만 바꾸면 됩니다.

C#
// 파일 이름: FortressExplosionSkill.cs (수정)
using UnityEngine;
using System;

[CreateAssetMenu(fileName = "FortressExplosion", menuName = "Rewards/FortressExplosion")]
public class FortressExplosionSkill : SkillData
{
    public int amount;
    public float damageMultiplier = 10f;

    [Header("VFX 설정")]
    public GameObject effectPrefab;
    public Sprite[] animationFrames;
    public float frameDelay = 0.1f;
    public int damageFrame = 2;

    // ApplyToPlayer는 이제 PlayerSkills의 AddSkill을 호출하는 역할만 합니다.
    // 'haveSelected' 같은 상태 변수는 더 이상 필요 없습니다.
    public override void ApplyToPlayer(PlayerSkills player)
    {
        // "사장님, 저 '성채 폭발'이라는 종류의 스킬을 획득했습니다. 처리해주세요."
        // 원본인 '나(this)'를 그대로 넘깁니다.
        // 그러면 PlayerSkills가 알아서 복사본을 만들거나, 기존 복사본을 강화해줍니다.
        player.AddSkill(this);
    }

    // SkillData에 Upgrade 함수를 가상(virtual)으로 만들어두면 좋습니다.
    // 여기서는 이 스킬만의 강화 로직을 정의합니다.
    public override void Upgrade()
    {
        // 이 함수는 PlayerSkills에 의해 '복사본'에서 호출되므로,
        // 원본 에셋에는 전혀 영향을 주지 않고 안전하게 값을 바꿀 수 있습니다.
        amount += 5;
        damageMultiplier += 2f; // 데미지 계수도 같이 강화해봅시다.
    }

    // Activate 함수는 수정할 필요가 전혀 없습니다.
    // 복사본에서 호출되면 강화된 amount 값을, 원본에서 호출되면 기본 amount 값을 사용합니다.
    public override void Activate(PlayerSkills player)
    {
        // ... (이하 모든 코드는 이전과 동일)
        PlayerStats playerStats = player.GetComponent<PlayerStats>();
        if (playerStats == null || playerStats.shield <= amount) return;
        // ...
    }
}

(참고: SkillData.cs 부모 스크립트에 public virtual void Upgrade() {} 라는 빈 함수를 하나 만들어두면, 모든 자식 스킬들이 이 함수를 편리하게 재정의하여 사용할 수 있습니다.)

 

 

 

 

 

 

 

Q. 왜 원본에서 복사본으로 바뀌었나? 왜 PlayerSkills가 복사본을 다루나?

이것이 이번 수정의 전부라고 해도 과언이 아닙니다.

A. 다른 모든 스킬들도 똑같이 강화되어야 하기 때문입니다. (정확히 추측하셨습니다!)

PlayerSkills는 모든 스킬들을 관리하는 **"스킬 매니저"**라고 생각해보세요. 상점에서 '성채 폭발' 스킬 카드를 구매하면, 이 매니저는 다음과 같이 행동합니다.

  1. 매니저: "어, '성채 폭발' 카드가 들어왔네. 잠깐, 내가 이미 가지고 있는 스킬인지 내 목록(ownedSkillInstances)을 확인해봐야겠다."
  2. 경우 A (처음 얻는 스킬):
    • 매니저: "목록을 보니 처음 보는 종류의 스킬이군. 좋아, 이 원본 카드(originalSkill)를 복사해서 **게임 안에서 쓸 진짜 스킬(복사본)**을 하나 만들어야겠다."
    • Instantiate(originalSkill): 바로 이 순간, 원본 설계도는 그대로 두고, 메모리 위에 실제 데이터를 가진 복사본이 생성됩니다.
    • 매니저: "이제 이 복사본을 내 스킬 목록에 추가해야지. 앞으로 스킬을 쓸 땐 이 복사본을 사용하면 돼."
  3. 경우 B (이미 가진 스킬):
    • 매니저: "아, '성채 폭발' 스킬은 이미 내 목록에 있잖아? 그럼 새 복사본을 만들 필요 없겠네."
    • 매니저: "대신, 내가 이미 가지고 있던 그 복사본을 꺼내서 강화(Upgrade())해야겠다."

이런 식으로 "스킬 매니저"인 PlayerSkills가 복사본 생성과 강화를 모두 책임지기 때문에, FortressExplosionSkill이나 나중에 만들 FireballSkill, IcewallSkill 등은 자신이 어떻게 관리되는지 전혀 신경 쓸 필요가 없어집니다. 그저 자신의 Upgrade 로직만 잘 정의해두면 됩니다. 이것이 바로 좋은 중앙 관리 시스템입니다.

 

 

 

 

 

 

 

 

 

 

 

 

Q. 복사본 처리하는 코드가 이해가 안 된다.

네, PlayerSkills.cs AddSkill 메소드를 한 줄 한 줄 해부해 보겠습니다.

C#
// 스킬 카드(원본)를 받았을 때 호출되는 함수
public void AddSkill(SkillData originalSkill)
{
    // 1. "내가 가진 스킬 복사본 목록(ownedSkillInstances) 중에서..."
    //    .FirstOrDefault()는 LINQ라는 C#의 강력한 도우미 기능입니다. 목록에서 특정 조건에 맞는 첫 번째 항목을 찾아줍니다.
    //    (s => s.name == originalSkill.name) 이 부분이 "찾을 조건" 입니다.
    //    "목록 안의 각 스킬(s)의 이름(s.name)이, 방금 받은 원본 카드의 이름(originalSkill.name)과 같은 것을 찾아라!"
    //    찾으면 해당 스킬 복사본을, 못찾으면 null(없음)을 existingSkill 변수에 저장합니다.
    SkillData existingSkill = ownedSkillInstances.FirstOrDefault(s => s.name == originalSkill.name);

    // 2. 위에서 스킬을 찾았는지 (null이 아닌지) 확인합니다.
    if (existingSkill != null)
    {
        // 2-1. 찾았다면 (이미 가지고 있는 스킬이라면)
        //      내가 이미 가지고 있던 그 '복사본(existingSkill)'의 Upgrade 함수를 호출합니다.
        existingSkill.Upgrade();
    }
    else
    {
        // 2-2. 못찾았다면 (처음 얻는 스킬이라면)
        //      Instantiate(originalSkill) 함수로 원본의 '복사본'을 메모리에 새로 만듭니다.
        //      이것이 "붕어빵을 굽는" 행위입니다.
        SkillData skillInstance = Instantiate(originalSkill);
        
        // Instantiate()로 복사본을 만들면 이름 뒤에 "(Clone)"이 붙기 때문에,
        // 나중에 또 비교할 수 있도록 이름을 원본과 똑같이 맞춰줍니다.
        skillInstance.name = originalSkill.name;

        // 방금 만든 따끈따끈한 복사본(skillInstance)을 내가 가진 스킬 목록에 추가합니다.
        ownedSkillInstances.Add(skillInstance);
    }
}

 

 

 

 

 

 

근데 이번에는, 스킬 설명도 바뀌게 하고 싶은데
가령 젤 처음에는 방어도 10을 소모합니다, 모든적에게 피해를 줍니다에서
다음 선택할떄는 방어도15를 소모합니다. 소모한 방어도의 2배만큼 화면의 모든적에게 피해줍니다
다음에는 20, 3..등으로
근데 이것도 프리팹 수정이잖아

Q. 이것도 프리팹 수정이 아닌가?

아닙니다! 그리고 이 차이를 이해하는 것이 이번 단계의 핵심입니다.

MaxHp_ver2.cs 코드의 DynamicDescription 부분을 다시 보겠습니다.

C#
public override string DynamicDescription
{
    get
    {
         // ... 계산 로직 ...
         return $"선택 시 최대 체력이 {amountToApply} 증가합니다.";
    }
}

이 코드는 description이라는 변수의 값을 직접 수정하지 않습니다. 대신, 누군가가 DynamicDescription의 값을 물어볼 때마다 get { ... } 안의 코드를 실시간으로 실행해서 매번 새로운 설명문(string)을 계산하여 돌려주는 방식입니다.

  • 변수: public string description = "설명"; -> 창고에 "설명"이라는 상자를 보관하는 것.
  • get 프로퍼티: public string DynamicDescription { get; } -> "설명문 줘"라는 요청을 받을 때마다 즉석에서 설명문을 만들어주는 주문 기계와 같습니다. 창고에 보관하지 않습니다.

따라서 우리는 원본 에셋(붕어빵 틀)을 전혀 건드리지 않고, **"현재 스킬의 강화 상태(값)에 따라 동적으로 설명문을 만들어내는 기능"**을 추가하는 것이므로 완벽하게 안전합니다.

Q. MaxHp_ver2.cs는 너무 복잡하다. 이렇게 해야 하나?

그 코드가 복잡해 보이는 이유는, 중앙 관리자인 RewardManager에게 "저 지금 몇 레벨이에요?" 라고 물어봐야 하기 때문입니다. 즉, 강화 상태를 자기 자신이 아닌 외부에서 관리했기 때문입니다.

하지만 지금 우리가 사용하는 '복사본(인스턴스) 방식'은 이 문제를 훨씬 더 간단하고 우아하게 해결합니다.

우리는 강화된 값을 amount, damageMultiplier에 직접 저장하고 있습니다. 따라서 외부 관리자에게 물어볼 필요 없이, 그냥 자기 자신의 amount 값을 읽어서 설명문을 만들면 됩니다. 훨씬 간단해지죠.

Q. 이 코드는 성체 폭발에서만 있어야 할 코드 아닌가?

네, 100% 맞는 말씀입니다. 스킬의 설명문을 만드는 규칙은 그 스킬만이 알고 있어야 합니다. 따라서 이 로직은 FortressExplosionSkill.cs 안에 있는 것이 가장 이상적인 설계입니다.

이전 방식 (MaxHp_ver2): 자기 월급을 모르는 신입사원

이전 방식의 스킬은 마치 자기 월급이 얼마인지 모르는 신입사원과 같습니다.

  • 신입사원 (MaxHp_ver2): "제 기본급은 100만원이고, 진급할 때마다 10만원씩 오르는 건 알아요. 근데... 제가 지금 몇 년차(레벨)죠?"
  • 인사팀 (RewardManager): "어디 보자... 장부를 보니 김사원은 지금 3년차(레벨 2)네요."
  • 신입사원 (MaxHp_ver2): "아, 그럼 제 이번 달 월급은 100만원 + (2 * 10만원) 이니까 120만원이군요! 감사합니다!"

이 방식이 복잡했던 이유:

  1. 외부 의존성: 스킬이 자기 자신의 효과를 계산하기 위해, 반드시 RewardManager라는 외부 관리자에게 "저 지금 몇 레벨이에요?" 라고 물어봐야만 합니다.
  2. 계산의 필요: 매번 효과를 적용하거나 설명문을 만들 때마다 기본값 + (레벨 * 증가량) 이라는 계산을 실시간으로 해야 합니다.
  3. 관리 포인트 증가: RewardManager라는 별도의 관리 시스템을 계속 유지하고 신경 써야 합니다.

현재 방식 (FortressExplosionSkill): 자기 월급을 아는 경력직

현재 방식의 스킬은 자신의 통장 잔고(현재 상태)를 정확히 아는 경력직과 같습니다.

  • 경력직 (FortressExplosionSkill 복사본): "제 현재 방어도 소모량(amount)은 20이고요, 데미지 배율(damageMultiplier)은 11.0f 입니다."

이 방식이 훨씬 간단한 이유:

  1. 자기 완결성: 스킬 복사본은 강화될 때마다 자신의 amount 값이 10 -> 15 -> 20 으로 직접 바뀝니다. 외부 관리자에게 물어볼 필요가 전혀 없습니다.
  2. 계산 불필요: 효과를 적용하거나 설명문을 만들 때, 복잡한 계산 없이 그냥 자신의 amount 값을 바로 가져다 쓰면 됩니다.
  3. 단순한 구조: PlayerSkills는 그저 복사본을 만들거나 Upgrade()를 호출해줄 뿐, 각 스킬의 세세한 레벨을 관리할 필요가 없습니다. 책임이 명확하게 분산됩니다.

두 방식의 비교: 한눈에 보기

구분 이전 방식 (MaxHp_ver2) 현재 방식 (FortressExplosionSkill)
상태 저장 RewardManager가 "레벨"을 저장 스킬 복사본이 "강화된 값"을 직접 저장
강화 방식 RewardManager의 레벨 카운트를 1 올림 스킬 복사본 amount 같은 변수 값을 직접 수정
설명문 생성 RewardManager에게 레벨을 물어본 후, 계산해서 생성 자신의 amount 값을 읽어서 즉시 생성
핵심 특징 외부 의존적 (Coupled) 자기 완결적 (Self-Contained / Encapsulated)
복잡도 높음 (외부 통신과 실시간 계산 필요) 낮음 (자신의 값만 사용하면 됨)

결론적으로, 사용자님이 이전에 사용하셨던 RewardManager 방식도 틀린 것은 아니며, 많은 게임에서 사용하는 "중앙 관리자 패턴"입니다. 하지만 우리가 지금 구현한 **"상태를 가진 복사본(인스턴스) 패턴"**이, 지금과 같은 스킬 강화 시스템에는 훨씬 더 직관적이고, 간단하며, 객체 지향적인 아름다운 해결책이라고 할 수 있습니다.

 

 

 

 

 

 

RewardCardUI RewardData를 받도록 설계되었는데, 우리가 만든 FortressExplosionSkill SkillData입니다. 이 둘은 서로 다른 부모를 가진, 완전히 별개의 클래스입니다.

이것은 "택배 기사(RewardCardUI)가 '편지(RewardData)'만 배송할 수 있는데, '소포(SkillData)'를 배송해달라고 요청하는" 상황과 같습니다. 당연히 택배 기사는 소포를 어떻게 다뤄야 할지 모릅니다.

해결책: "우편물"이라는 공통 조상 만들기

이 문제를 해결하는 가장 세련되고 확장성 있는 방법은, '편지(RewardData)'와 '소포(SkillData)'의 공통 조상인 "우편물" 이라는 개념을 만드는 것입니다.

즉, RewardData SkillData 같은 부모를 상속하도록 구조를 리팩토링하는 것입니다. 이렇게 하면 택배 기사(RewardCardUI)는 더 이상 편지나 소포를 구분할 필요 없이, 그저 "우편물"을 받아서 처리하면 됩니다.

 

 

 

 

 

 

 

 

두 시스템의 공존: "레벨 방식" vs "복사본 방식"

현재 RewardManager의 상태를 비유하자면, **"구형 화폐와 신형 화폐를 모두 취급하는 은행"**과 같습니다.

  1. rewardLevels 관련 코드 (구형 화폐 시스템):
    • MaxHp_ver2 같은 옛날 보상들이 찾아오면, 이 시스템을 사용해 "지금 몇 레벨이세요?"를 확인하고 처리합니다.
    • GetRewardLevel, IncreaseRewardLevel 함수는 오직 이 구형 시스템을 위해서만 존재합니다.
  2. allChoices 리스트와 SelectReward 함수 (신형 화폐 시스템):
    • FortressExplosionSkill 같은 새로운 스킬들이 오면, 이 시스템을 사용해 "복사본을 만들거나, 기존 복사본을 강화"합니다.
    • 이 새로운 스킬들은 rewardLevels 사전을 전혀 쳐다보지도 않습니다.

이 두 시스템은 서로에게 전혀 영향을 주지 않고 독립적으로 동작하기 때문에, 우리는 과거의 코드를 버리지 않고도 새로운 시스템을 성공적으로 도입할 수 있습니다.


최종 완성본: RewardManager.cs (전체 코드)

내용이 생략되지 않은, 온전한 최종 RewardManager 코드입니다. 이 코드를 사용하시면, MaxHp_ver2 같은 옛날 방식의 보상과 FortressExplosionSkill 같은 새로운 방식의 스킬이 모두 완벽하게 공존하며 작동할 것입니다.

C#
// 파일 이름: RewardManager.cs
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class RewardManager : MonoBehaviour
{
    public static RewardManager Instance;

    // ------------------ [구 시스템] 레벨 기반 보상(e.g. MaxHp_ver2)을 위한 부분 ------------------
    // 이 시스템은 새로운 '인스턴스 방식' 스킬(e.g. FortressExplosion)과는 독립적으로 동작합니다.
    // 두 시스템이 공존하므로, 기존 보상도 문제없이 작동합니다.
    public static Dictionary<string, int> rewardLevels = new Dictionary<string, int>();

    public static int GetRewardLevel(string rewardName)
    {
        // rewardLevels 사전에 해당 이름이 있는지 확인하고, 있으면 그 값을, 없으면 0을 반환합니다.
        rewardLevels.TryGetValue(rewardName, out int currentLevel);
        return currentLevel;
    }

    public static void IncreaseRewardLevel(string rewardName)
    {
        // 현재 레벨을 가져와서 1을 더한 값으로 사전에 다시 저장합니다.
        int currentLevel = GetRewardLevel(rewardName);
        rewardLevels[rewardName] = currentLevel + 1;
    }
    
    public static void ResetAllRewardStates()
    {
        // 게임이 새로 시작될 때, 레벨 기록을 모두 삭제합니다.
        rewardLevels.Clear();
        Debug.Log("모든 (구버전) 보상 레벨이 초기화되었습니다.");
    }
    // -----------------------------------------------------------------------------------------

 

 

 

 

 

 

 

[PlayerSkills] GetOwnedSkillInstance 호출됨: 'FortressExplosion'의 복사본을 찾습니다.
[PlayerSkills] 경고: 일치하는 복사본을 찾지 못했습니다.
[RewardCardUI] 정보: 강화된 복사본이 없습니다. 원본 데이터로 UI를 설정합니다.

이것이 의미하는 바는 명확합니다.

  • RewardCardUI PlayerSkills에게 제대로 물어봤습니다. (성공)
  • 하지만 PlayerSkills 자기 스킬 목록을 아무리 뒤져봐도, FortressExplosion이라는 이름의 스킬을 찾지 못했다고 대답했습니다. (실패!)
  • 결국 RewardCardUI는 강화된 정보를 못 받았으니, 원본 정보를 표시한 것입니다.

진짜 범인: 이름 뒤에 붙는 (Clone)

문제는 GetOwnedSkillInstance 함수가 스킬을 못 찾는 데 있습니다. 왜 못 찾을까요?

범인은 바로, Instantiate 함수가 복사본을 만들 때 이름 뒤에 자동으로 붙이는 (Clone) 이라는 글자입니다.

이해하기 쉬운 명찰에 비유해 보겠습니다.

  1. 스킬 획득 (AddSkill 실행):
    • PlayerSkills는 "FortressExplosion"이라고 적힌 원본 명찰을 받습니다.
    • Instantiate() 함수로 이 명찰을 복사합니다.
    • 그런데 복사된 명찰에는 "FortressExplosion(Clone)" 이라고 적혀있습니다.
    • PlayerSkills는 이 "FortressExplosion(Clone)" 명찰을 자기 주머니(ownedSkillInstances)에 넣습니다.
  2. 레벨업 (보상 화면 표시):
    • RewardCardUI가 "FortressExplosion"이라고 적힌 원본 명찰을 들고 옵니다.
    • PlayerSkills에게 "이 명찰이랑 똑같은 거 주머니에 있어요?" 라고 물어봅니다.
    • PlayerSkills는 주머니를 뒤져봅니다. 주머니에는 "FortressExplosion(Clone)" 명찰이 있습니다.
    • "FortressExplosion"  "FortressExplosion(Clone)" 은 글자가 다르므로, PlayerSkills는 "아니요, 똑같은 거 없는데요?" 라고 대답합니다.
    • 결국, 검색은 실패합니다.

해결책: 복사본의 이름표를 바꿔주자!

해결책은 아주 간단합니다. 복사본을 만든 직후, 그 복사본의 이름에서 (Clone)을 떼어버리고 원본과 똑같이 만들어주면 됩니다.

 

 

 

 

 

 

 

문제의 원인은 바로 "순서"입니다.

현재 우리가 겪는 현상을 시간 순서대로 따라가 보겠습니다.

  1. 첫 번째 레벨업:
    • 무모한 난동 카드가 화면에 보입니다. (설명: 방어도 10 소모)
    • 플레이어가 카드를 클릭합니다.
    • OnCardSelected() -> SelectReward() -> ApplyToPlayer() -> AddSkill() 순서로 함수가 호출됩니다.
    • AddSkill 함수 안에서, GetOwnedSkillInstance 실패하고, else 블록이 실행되어 방어도 10짜리 복사본 ownedSkillInstances 리스트에 추가됩니다.
  2. 두 번째 레벨업:
    • 무모한 난동 카드가 화면에 보이기 위해 RewardCardUI Setup() 함수가 호출됩니다.
    • Setup 함수가 PlayerSkills.Instance.GetOwnedSkillInstance()를 호출합니다.
    • PlayerSkills는 목록을 확인하고, 방금 막 추가된 방어도 10짜리 복사본을 찾아 돌려줍니다.
    • RewardCardUI는 전달받은 방어도 10짜리 복사본의 설명문을 화면에 표시합니다. (-> 그래서 설명이 안 바뀐 것처럼 보입니다!)
    • 플레이어가 이 카드를 클릭합니다.
    • OnCardSelected() -> SelectReward() -> ApplyToPlayer() -> AddSkill() 순서로 함수가 호출됩니다.
    • AddSkill 함수 안에서, GetOwnedSkillInstance 성공하고, if 블록이 실행됩니다.
    • existingSkill.Upgrade()가 호출되어, 이 시점에 비로소 복사본의 방어도가 10에서 15로 바뀝니다.
  3. 세 번째 레벨업:
    • 무모한 난동 카드가 화면에 보이기 위해 Setup() 함수가 호출됩니다.
    • GetOwnedSkillInstance()가 호출되고, 이제는 방어도 15로 강화된 복사본을 찾아 돌려줍니다.
    • RewardCardUI는 드디어 바뀐 설명문을 표시합니다.

보이시나요? 강화는 "클릭한 후"에 이루어지기 때문에, 다음번 보상 화면이 나타날 때는 "강화되기 전"의 정보를 보여주는 것입니다.


해결책: "선택하기 전"에 미리 강화 상태를 반영하자!

이 문제를 해결하는 가장 직관적인 방법은, RewardCardUI Setup 함수 로직을 살짝 바꾸는 것입니다.

"혹시 이미 가진 스킬이라면, 만약 이번에 또 선택한다면 어떻게 될지 미리 계산해서 보여주자!" 라는 개념입니다.

이를 위해 SkillData "미리보기"용 강화 함수를 하나 더 만들면 매우 깔끔하게 해결됩니다.