데이터 기반 스킬 설계

목차

1. 시스템 요구 사항

스킬 시스템의 실행 흐름과 책임 분리가 정리된 이후, 마지막으로 남은 문제는 스킬의 수치와 규칙을 어디에서 정의하고, 어떻게 관리할 것인가였다.

스킬은 단순히 폭발을 일으킨다라는 동작만으로 정의되지 않는다.

쿨타임, MP 소모량, 사거리, 데미지, 레벨에 따른 성장 수치 등 게임 밸런스에 직접적으로 영향을 주는 수많은 값들이 함께 존재한다.

이 값들을 코드 내부에 하드코딩할 경우 다음과 같은 문제가 발생한다.

스킬 수치를 수정할 때마다 코드를 수정해야 하고, 기획 단계에서 정의한 수치와 실제 게임 내 동작이 쉽게 어긋나며, 여러 스킬이 늘어날수록 실행 로직과 데이터가 강하게 결합된다.

따라서 이 프로젝트에서는 스킬의 규칙과 수치는 데이터로 정의하고, 코드는 그 데이터를 해석해 실행만 담당하는 구조가 필요했다.

2. 설계  목표

- 스킬의 쿨타임, MP 소모, 사거리, 데미지 등 핵심 수치를 데이터로 관리할 것

- 기획 단계에서 정의한 스킬 표와 실제 게임 동작이 1:1로 대응되도록 할 것

- 코드 수정 없이 데이터 변경만으로 밸런스 조정이 가능하도록 할 것

- 스킬 실행 로직이 개별 수치에 의존하지 않도록 구조를 분리할 것

3. 흐름도

이 흐름에서 중요한 점은 스킬의 수치와 규칙은 전부 데이터 계층에 존재하며, 실행 코드는 해당 데이터를 읽기만 한다는 점이다.

4. 구현

4.1. 공통 스킬 데이터 구조

이 프로젝트에서 스킬의 수치와 규칙은 코드가 아니라 데이터 중심으로 설계되었다.

스킬의 쿨타임, 사거리, MP 소모량, 데미지, 이펙트 프리팹과 같은 정보는 모두 SkillData, PlayerSkillData라는 ScriptableObject 기반 데이터 에셋에 정의되어 있으며, 이는 팀 단위로 합의된 공통 스킬 데이터 구조다.

public class SkillData : ScriptableObject
{
    public enum SkillUserType { Player = 0, Monster = 1 }

    [SerializeField] protected SkillUserType userType;

    [SerializeField] int skillId;
    [SerializeField] string skillName;

    [SerializeField] float duration;
    [SerializeField] float afterDelay;
    [SerializeField] float cooldownTime;
    bool isCooldown;

    [SerializeField] float maxRange;
    [SerializeField] GameObject skillEffectPrefab;

    public SkillUserType UserType => userType;
    public int SkillId => skillId;
    public string SkillName => skillName;
    public float Duration => duration;
    public float AfterDelay => afterDelay;
    public float CooldownTime => cooldownTime;

    public bool IsCooldown
    {
        get => isCooldown;
        set => isCooldown = value;
    }

    public float MaxRange => maxRange;
    public GameObject SkillEffectPrefab => skillEffectPrefab;
}

SkillData는 모든 스킬이 공통으로 가지는 속성만을 정의한 데이터 클래스다.

이 구조는 팀 내에서 공통 합의로 설계된 베이스 계층이며, 플레이어 스킬과 몬스터 스킬 모두를 포괄할 수 있도록 구성되어 있다.

ScriptableObject를 사용한 이유는 스킬 데이터를 씬이나 오브젝트에 종속시키지 않고, 에셋 단위로 관리하기 위함이다.

이를 통해 스킬 수치는 코드와 분리되어 에디터에서 직접 수정 가능하며, 런타임 중에도 여러 객체가 동일한 데이터를 참조할 수 있다.

쿨타임 플래그(IsCooldown) 역시 데이터 계층에 위치시켜, 스킬 실행 여부 판단이 애니메이션이나 입력 로직에 의존하지 않도록 했다.

4.2. 플레이어 전용 스킬 데이터 확장
[CreateAssetMenu(fileName = "PlayerSkill", menuName = "GameData/Skill/PlayerSkill")]
public class PlayerSkillData : SkillData
{
    public const int SKILL_MAX_LEVEL = 5;

    [SerializeField] Sprite icon;
    [SerializeField] string tooltip;
    [SerializeField] bool isAcquisition;

    [SerializeField] int currentLevel = 1;

    [SerializeField] float levelUpAddMpConsumption;
    [SerializeField] float levelUpAddDamage;

    [SerializeField] float initMpConsumption;
    [SerializeField] float initDamage;

    public Sprite Icon => icon;
    public string Tooltip => tooltip;

    public bool IsAcquisition
    {
        get => isAcquisition;
        set => isAcquisition = value;
    }

    public int CurrentLevel
    {
        get => currentLevel;
        set => currentLevel = Mathf.Clamp(value, 1, SKILL_MAX_LEVEL);
    }

    public float MpConsumption
    {
        get => initMpConsumption + (currentLevel - 1) * levelUpAddMpConsumption;
    }

    public float Damage
    {
        get => initDamage + (currentLevel - 1) * levelUpAddDamage;
    }
}

PlayerSkillData는 SkillData를 상속받아 플레이어 스킬에만 필요한 정보와 성장 규칙을 추가한 데이터 클래스다.

레벨에 따른 MP 소모량과 데미지는 필드에 값을 직접 저장하지 않고, 프로퍼티 접근 시 계산되도록 구성했다.

이 방식의 장점은 스킬 레벨만 변경하면 관련 수치가 자동으로 일관되게 갱신된다는 점이다.

이로 인해 데미지는 수정했는데 MP 소모는 수정하지 않는 불일치 상황을 구조적으로 방지할 수 있다.

5. 개발 의도

이 프로젝트에서는 스킬의 구조와 수치를 기획 단계에서 표 형태로 먼저 정의했다.

스킬 ID, 이름, 쿨타임, MP 소모, 사거리, 데미지, 습득 여부 등은 팀 내 합의를 통해 결정되었고, 그 결과를 그대로 PlayerSkillData 에셋에 옮기는 방식으로 작업했다.

인스펙터에서 보이는 PlayerSkillData 설정 값은 기획표의 각 컬럼과 1:1로 대응되며, 실제 게임 동작 역시 이 데이터를 기준으로 수행된다.

즉, 이 구조에서는 코드를 어떻게 고쳤는가보다 기획 데이터가 어떻게 실행으로 이어지는가가 중심이 된다.