게임 개발 공부 기록

60일차 - Abyss_Slayer 최종 팀 프로젝트 10 (플레이어 스킬 구조 변경)

00lwt 2025. 4. 17. 22:08

▶▷ 구조 변경

모든 직업에 스킬 데이터를 배치하다 보니 앞 글에서 설명했던 기존에 사용하던 스킬 구조에 약간 문제점을 느꼈다.

기존 방식은 모든 스킬에 SkillData라는 공통된 필드가 들어갔었는데 이렇게 할 경우 버프나 대쉬 스킬에 불필요한 정보들도 포함이 된다는 부분이다. 예를 들면 데미지 타입 같은 것은 버프나 대쉬에는 해당이 되지 않기 때문에 없어도 된다.

그렇기 때문에 각 스킬에 해당하는 필드만 할당하기 위해 스킬이 범위 공격인지 근거리 공격인지 버프인지 이동기인지로 기준을 나누고 그 안에서 범위 공격이라면 한번만 공격하는 스킬인지 지속적으로 공격하는 스킬인지를 기준으로
ScriptableObject를 나눠서 만들면 각 스킬마다 필요한 요소들만 필드에 할당할 수 있게 된다.

범위 공격 ScriptableObject 생성 경로

이렇게 스킬을 만들면 아래와 같이 필요한 필드만 가진 채로 생성이 된다.

공격 스킬
버프 스킬
대쉬

▶▷ 구조 로직

Skill.cs

public class Skill : ScriptableObject
{
    // 모든 스킬에 공통으로 적용되는 변수
    [HideInInspector] public Player player;
    [field: SerializeField] public string SkillName { get; private set; } = "스킬 이름";
    [field: SerializeField] public string SkillDesription { get; private set; } = "스킬 설명";
    [field: SerializeField] public Sprite SkillIcon {get; private set;}                         // 스킬 아이콘

    [field: SerializeField] public bool CanUse { get; set; } = true;                            // 스킬 사용 가능 여부
    [field: SerializeField] public bool CanMove { get; private set; } = true;                   // 스킬 사용 중 움직임 가능 여부

    [field: SerializeField] public ReactiveProperty<float> MaxCoolTime { get; private set; }    // 최대 쿨타임
        = new ReactiveProperty<float>(10f);
    [field: SerializeField] public ReactiveProperty<float> CurCoolTime { get; private set; }    // 현재 쿨타임
        = new ReactiveProperty<float>(0f);
    [field: SerializeField] public ApplyState ApplyState { get; set; }                          // 연결해서 작동시킬 State 설정
    
    // 플레이어 초기화
    public void Init(Player player)
    {
        this.player = player;
    }

    // 스킬 사용 추상 메서드
    public virtual void UseSkill()
    {

    }

    // 플레이어 방향 계산
    public float PlayerFrontXNomalized()
    {
        float x = player.SpriteRenderer.flipX ? -1f : 1f;
        return x;
    }

    // 플레이어 위치 반환
    public Vector3 PlayerPosition()
    {
        Vector3 playerPosition = player.transform.position;
        return playerPosition;
    }
}

모든 스킬에 공통적으로 사용되는 요소들을 작성한 스크립트로 플레이어 초기화 및 방향과 위치를 반환하는 메서드가 포함되어있다.

RangeAttackSkill.cs

public class RangeAttackSkill : Skill
{
    [field: SerializeField] public float Damage { get; private set; }     // 데미지
    [field: SerializeField] public float Range { get; private set; }      // 사거리
    [field: SerializeField] public float Speed { get; private set; }      // 투사체 속도
    [field: SerializeField] public int SpriteNum { get; private set; }    // Sprite 인덱스 번호


    /// <summary>
    /// 투사체 발사사
    /// </summary>
    /// <typeparam name="T">투사체 타입</typeparam>
    /// <param name="startPos">투사체 시작 위치</param>
    /// <param name="dir">투사체 방향</param>
    public void ThrowProjectile<T>(Vector3 startPos, Vector3 dir) where T : BasePoolable
    {
        PoolManager.Instance.Get<T>().Init(startPos, dir, Range, Speed, SpriteNum, Damage);
    }

    /// <summary>
    /// 버프 상태 투사체 발사
    /// </summary>
    /// <typeparam name="T">투사체 타입</typeparam>
    /// <param name="startPos">투사체 시작 위치</param>
    /// <param name="dir">투사체 방향</param>
    /// <param name="damageMultiple">투사체 데미지 배율</param>
    public void ThrowProjectile<T>(Vector3 startPos, Vector3 dir, float damageMultiple) where T : BasePoolable
    {
        PoolManager.Instance.Get<T>().Init(startPos, dir, Range, Speed, SpriteNum, Damage * damageMultiple);
    }
}

RangeAttackSkill.cs는 투사체 스킬에 대한 스크립트로, 투사체 정보를 초기화하는 역할을 한다.

BuffSkill.cs

using UniRx;
using UnityEngine;

public enum BuffType
{
    None = 0,
    ArcherDoubleShot = 1,  //아처 더블 샷 버프
}

public class BuffSkill : Skill
{
    [field: SerializeField]public ReactiveProperty<float> MaxBuffDuration { get; set; }     //최대 지속시간
        = new ReactiveProperty<float>(5f);    
    [field: SerializeField] public ReactiveProperty<float> CurBuffDuration { get; set; }    //현재 지속시간
        = new ReactiveProperty<float>(0f);     
    [field: SerializeField] public bool IsApply { get; set; } = false;                      //현재 버프 적용 여부
    [field: SerializeField] public BuffType Type { get; private set; } = BuffType.None;     //버프 타입
}

BuffSkill.cs는 버프 스킬에 대한 스크립트로 지속시간과 적용 여부 그리고 버프타입 enum을 통해 어떤 버프인지를 할당할 수있다.

위 두가지 스크립트들은 각각 Skill.cs를 상속받으며 각 스킬에만 있어야 하는 필드가 포함되어 있다.

OneShotRangeSkill.cs

[CreateAssetMenu(fileName = "RangeOneShotSkill", menuName = "SkillRefactory/Range/OneShot")]
public class OneShotRangeSkill : RangeAttackSkill
{
    private Vector3 distanceY = new Vector3(0, 0.25f, 0);
    
    public override void UseSkill()
    {
        base.UseSkill();
        Vector3 dirX = new Vector3(PlayerFrontXNomalized() * 1.5f, 0 ,0);  
        Vector3 spawnPos = PlayerPosition() + dirX;

        // 버프 상태일 경우 추가 화살 생성
        if (player.BuffDuration.ContainsKey(BuffType.ArcherDoubleShot) && player.BuffDuration[BuffType.ArcherDoubleShot].IsApply)
        {
            ThrowProjectile<ArcherProjectile>(spawnPos + distanceY, dirX, 0.8f);
            ThrowProjectile<ArcherProjectile>(spawnPos - distanceY, dirX, 0.8f);
        }
        else
        {
            ThrowProjectile<ArcherProjectile>(spawnPos, dirX);
        }
    }
}

실제 생성되는 스킬의 스크립트로, 범위공격 스킬이기 때문에 RangeAttackSkill.cs를 상속 받는다.
스킬 사용 메서드를 통해 화살의 생성 및 발사를 담당한다.

이렇게 상속 기능을 포함하니 스크립트의 양은 좀 많아졌지만 생성해야하는 ScriptableObject의 수가 줄어서 할당할 것이 많지 않고 인스펙터에서 관리하기 수월해졌다.

초반 기획 단계에서 스킬 구조를 관리할 때 상속을 사용할지 말지 고민을 했었다가 스킬의 수가 많지 않아 사용하지 않았었는데 아예 사용하지 않는 것보다는 적절히 섞어서 사용하니 조금 더 효율적인 방법이 될 수 있다.