[2025/07/01]턴제RPG 스킬구현

오수호·2025년 7월 1일

TIL

목록 보기
31/60

SO를 사용하여 스킬데이터를 저장

턴제 RPG에서 사용할 스킬데이터를 SO에 저장하고, 이를 실제 게임 내에서 스킬로 만들어 사용하여야 한다. 스킬에 필요한 데이터들을 SO로 정의하고 스킬을 사용했을 때 실제로 적용되는 로직을 만들어야 한다.

스킬에 필요한 데이터 구성

스킬을 사용하는데 필요한 데이터들을 생각해보자.

  1. 효과
    효과에는 크게 2가지가 존재한다. 데미지와 버프.
    데미지는 실질적으로 체력을 깎게 만든다. 크게보면 데미지 또한 버프로 볼 수 있다. 데미지를 주었을때, 체력 스텟을 깎으면 되기 때문이다.

    문제는 우리의 프로젝트에서는 데미지를 줄 때 TakeDamage라는 메서드를 호출하여 주는데 만약 이 메서드를 통해서 데미지를 주지않으면, 체력이 0이하가 되었을때 유닛을 Dead시키는 로직을 통과하지 않기때문에 데미지를 주는 효과와 버프(디버프 포함)를 주는 효과를 나누었다.


  2. 사용되는 스텟
    현재 플레이어의 공격력을 기반한 공격스킬이 있을 수 있고, 플레이어의 최대체력에 비례하여 파티원들에게 실드를 분배하는 실드스킬이 존재할 수 있다. 따라서 어떤 스텟을 기준으로 삼을 것이냐에 따라서 위 스텟기준이 달라질 것이다.
    2-1. 가중치
    스텟에 대한 가중치를 얼마나 두느냐이다. 스텟의 퍼센트에 비례하여 효과를 발휘할때 필요하다.
    2-2. 절대값
    스텟과 상관없이 발동되면 고정값으로 발휘되는 값이다.



  3. 스킬이 근거리 스킬인지 원거리 스킬인지
    이 부분은 전혀 생각치 못한 부분이다. 스킬을 사용할 때 원거리이고 투사체가 있는 스킬이라면 캐릭터가 제자리에서 투사체를 날리면 되지만, 스킬을 사용할 때 근거리 스킬이라면 캐릭터가 목표대상에게 이동하여 스킬을 사용하고 다시 제자리로 돌아와야한다. 즉, 스킬의 종류에 따라서 동작이 바뀔 수 있다. 따라서, 스킬을 만들 때 이 스킬이 원거리 스킬인 지 근거리 스킬인 지 나눌 필요가 있다.


  4. 선택가능한 대상종류
    스킬이 힐 스킬인 경우 아군만을 선택할 수 있게 만들고, 공격 스킬인 경우에는 적군만을 선택 가능하도록 만들어야한다. 이를 스킬데이터에 담아 주어야 UI에서 이를 구분하여 해당 스킬을 사용할 때 선택가능한 유닛을 구분 할 수 있다.


  5. 타겟 선택 종류
    메인 타겟을 선택했을 때, 메인 타겟만 효과를 받을 뿐 아니라 다른 타겟들도 효과를 받는 스킬이 존재할 수 있다. 메인타겟 한 명과 주위 적 2명에게 데미지를 준다던가, 메인타겟에게 데미지와 부정적인 효과를 주고 우리팀을 분노상태로 만든다거나 하는 스킬은 분명히 존재할 수 있다. 따라서, 해당 효과들에 대한 타겟 선택 종류를 만들어 주어야한다.


  6. JobType
    스킬을 고를 때, 해당 직업군 고유의 스킬만이 나타날 수 있도록 JobType을 스킬데이터에서 넣어주어야한다.


  7. CoolTime
    우리 게임에서 턴에 기반한 쿨타임이 스킬에 존재하기 때문에 쿨타임 값이 스킬데이터에 있어야 한다.


  8. ReuseCount
    우리 게임엔 각 스킬마다 전투 내에서 최대 재사용 가능횟수가 정해져 있기 때문에 스킬데이터값에 ReuseCount가 필요하다.


  9. SkillICon
    스킬 아이콘이 스킬데이터에 필요하다. 이는 UI에 표시되어야 하기 때문이다.


  10. 애니메이션 클립
    스킬에 맞는 애니메이션 클립이 필요하다. 런타임에 다른 스킬을 장착할 수 있는 시스템이기 때문에 애니메이션 클립을 스킬데이터로 종속시켜주어 장착한 스킬데이터를 기반으로 애니메이션 클립을 바꿔주어야한다.


    스크립트

    위의 정보를 기반으로 실제 SO를 다음과 같이 만들었다.

    public class ActiveSkillSO : ScriptableObject
    {
      public int ID;
      public string skillName;
      public string skillDescription;
      public CombatActionSo skillType;
      public SelectCampType selectCamp;
      public StatBaseSkillEffect skillEffect;
      public JobType jobType;
      public int reuseMaxCount;
      public int coolTime;
      public Sprite skillIcon;
      public AnimationClip skillAnimation;
    }

    위에서 언급 하지 않은 ID값이 존재하는데 이는 스킬들을 분별할 고유의 값이 필요한 경우가 있을 때 사용하기 위한 값이고 현재로썬 사용하지 않는다.

스킬 효과 구현

SO를 만들었으니, 이 데이터들로 스킬효과를 구현해야한다.

sing System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class SkillEffectData
{
  [HideInInspector] public Unit owner;
  public SelectCampType selectCamp;
  public SelectTargetType selectTarget;
  public string projectileID;
  public ParticleSystem.Particle skillVFX;
  public List<StatBaseDamageEffect> damageEffects;
  public List<StatBaseBuffSkillEffect> buffEffects;

  public void AffectTargetWithSkill(Unit target) // 실질적으로 영향을 끼치는 부분
  {
      foreach (var result in buffEffects)
      {
          var statusEffect = result.StatusEffect;
          statusEffect.Stat.Value = owner.StatManager.GetValue(result.ownerStatType) * result.weight + result.value;
          target.StatusEffectManager.ApplyEffect(BuffFactory.CreateBuff(statusEffect));
          target.ChangeEmotion(result.emotionType);
      }


      foreach (var result in damageEffects)
      {
          target.ExecuteCoroutine(result.DamageEffectCoroutine(target, owner));
      }
  }
}

[System.Serializable]
public class StatBaseSkillEffect
{
  [HideInInspector] public Unit owner;
  public List<SkillEffectData> skillEffectDatas;


  public void Init()
  {
      foreach (SkillEffectData effect in this.skillEffectDatas)
      {
          effect.owner = this.owner;
          foreach (var buffSkillEffect in effect.buffEffects)
          {
              buffSkillEffect.StatusEffect = new StatusEffectData();
              {
                  //불변 데이터
                  buffSkillEffect.StatusEffect.ID = buffSkillEffect.ID;
                  buffSkillEffect.StatusEffect.EffectType = buffSkillEffect.statusEffectType;
                  buffSkillEffect.StatusEffect.Duration = buffSkillEffect.lastTurn;
                  buffSkillEffect.StatusEffect.Stat = new StatData();
                  buffSkillEffect.StatusEffect.Stat.StatType = buffSkillEffect.opponentStatType;
                  buffSkillEffect.StatusEffect.Stat.ModifierType = buffSkillEffect.modifierType;
              }
          }
      }
  }
}

//대미지 주는 class 하나
[System.Serializable]
public class StatBaseDamageEffect
{
  [Header("공격 횟수")]
  public int attackCount = 1; // 공격 횟수 => weight * attackCount = 실제 weight

  private float attackDelay = 0.2f;

  [Header("어떤 스텟을 기준으로 데미지를 줄까")]
  public StatType ownerStatType; // 사용자에 의해 강해지거나 약해지는 사용자의 스텟타입 => ex) 남기사 실드스킬에서 남기사의 최대체력

  [Header("기준스텟에 대한 가중치와 고정 값")]
  public float weight; // 계수 => ownerStatType의 value값에서 몇 배율을 적용할 것인가, weight = 0.1이면 value * 0.1 

  public float value; // 기본 고정 값 : weight와 관계없이 고정으로 부여할 수 있는 값

  // 데미지 계산 식 => value + (weight * attackCount * statValue) 
  public void DamageEffect(Unit target, Unit owner)
  {
      float finalValue = value + (weight * owner.StatManager.GetValue(ownerStatType));
      target.TakeDamage(finalValue);
  }

  public IEnumerator DamageEffectCoroutine(Unit target, Unit owner)
  {
      int currentCount = 0;

      while (currentCount < attackCount)
      {
          DamageEffect(target, owner);
          currentCount++;
          // yield return null;

          // yield return new WaitForSeconds(attackDelay);
      }

      currentCount = 0;
      yield return null;
  }
}

//디버프 주는 class 하나
[System.Serializable]
public class StatBaseBuffSkillEffect
{
  public int ID;

  [Header("영향을 주는 스텟")]
  public StatType ownerStatType; // 사용자에 의해 강해지거나 약해지는 사용자의 스텟타입 => ex) 남기사 실드스킬에서 남기사의 최대체력

  [Header("가중치와 고정값")]
  public float weight; // 계수 => ownerStatType의 value값에서 몇 배율을 적용할 것인가, weight = 0.1이면 value * 0.1 

  public float value; // 기본 고정 값 : weight와 관계없이 고정으로 부여할 수 있는 값

  [Header("영향을 받는 스텟")]
  public StatType opponentStatType; // 영향을 받는 스텟의 타입 => ex) 공격 스킬을 사용하면 상대의 curHP가 영향을 받음

  [Header("지속 턴")]
  public int lastTurn = 1; // 영향이 지속되는 턴 ( 턴 마다 지속되는 도트데미지, 디버프 등)

  [Header("받는 디버프의 타입")]
  public StatusEffectType statusEffectType;

  public StatModifierType modifierType;

  [Header("대상의 감정을 바꾸는 공격")]
  public EmotionType emotionType;

  [HideInInspector] public StatusEffectData StatusEffect; //디버프
}

위의 스크립트는 ActiveSkillSO에서 스킬의 효과를 담당하는 클래스인 StatBaseSkillEffect의 내부가 어떻게 이루어져 있는 지를 보여준다.
데미지를 주는 클래스와 버프/디버프를 주는 클래스가 나누어져 있는데 이는 위에서 말했던 문제때문에 이러한 구성이 되었다.
또한, 우리 프로젝트의 경우 유닛의 스텟을 관리하는 스텟매니저와 스텟을 올리고 내리는 StatusEffectManager과 같은 Base코드가 있었기 때문에 이를 재사용하기 위해서 스킬에 대한 데이터를 입력받은 ActiveSkillSO를 내부에서 StatusEffectData로 변환하여 이를 버프/디버프를 적용하는데 사용한다.

다음 번에는 실제 스킬이 실제로 적용되는 부분을 구현해보도록 하겠다.

profile
게임개발자 취준생입니다

0개의 댓글