60일차 - Abyss_Slayer 최종 팀 프로젝트 10 (플레이어 스킬 구조 변경)
▶▷ 구조 변경
모든 직업에 스킬 데이터를 배치하다 보니 앞 글에서 설명했던 기존에 사용하던 스킬 구조에 약간 문제점을 느꼈다.
기존 방식은 모든 스킬에 SkillData라는 공통된 필드가 들어갔었는데 이렇게 할 경우 버프나 대쉬 스킬에 불필요한 정보들도 포함이 된다는 부분이다. 예를 들면 데미지 타입 같은 것은 버프나 대쉬에는 해당이 되지 않기 때문에 없어도 된다.
그렇기 때문에 각 스킬에 해당하는 필드만 할당하기 위해 스킬이 범위 공격인지 근거리 공격인지 버프인지 이동기인지로 기준을 나누고 그 안에서 범위 공격이라면 한번만 공격하는 스킬인지 지속적으로 공격하는 스킬인지를 기준으로
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의 수가 줄어서 할당할 것이 많지 않고 인스펙터에서 관리하기 수월해졌다.
초반 기획 단계에서 스킬 구조를 관리할 때 상속을 사용할지 말지 고민을 했었다가 스킬의 수가 많지 않아 사용하지 않았었는데 아예 사용하지 않는 것보다는 적절히 섞어서 사용하니 조금 더 효율적인 방법이 될 수 있다.