스킬 시스템

전체 플레이 영상

전체 코드

using UnityEngine;

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

    [SerializeField] protected SkillUserType userType; // 사용자 (시전자)

    [SerializeField] int skillId;                      // 스킬 ID
    [SerializeField] string skillName;                 // 스킬명

    [SerializeField] float duration;                   // 지속 시간
    [SerializeField] float afterDelay;                 // 사용 후 딜레이 (스킬 사용 후 움직이지 못하는 시간)

    [SerializeField]
    float cooldownTime;                                // 재사용 대기시간 (쿨타임)
    bool isCooldown;                                   // 재사용 대기시간 (쿨타임) 플래그

    [SerializeField] float maxRange;                   // 최대 공격 범위 (최대 사정 거리)

    [SerializeField] GameObject skillEffectPrefab;     // 스킬 이펙트 프리팹
    //[SerializeField] GameObject attackEffectPrefab;    // 피격 이펙트 프리팹

    // 스킬 데미지의 경우 플레이어 스킬과 몬스터 스킬의 데미지 산정 방식(레벨에 따른 데미지)이 다르므로 각 자식 클래스에서 다룸

    public SkillUserType UserType
    {
        get { return userType; }
    }

    public int SkillId
    {
        get { return skillId; }
    }

    public string SkillName
    {
        get { return skillName; }
    }

    public float Duration
    { 
        get { return duration; }
    }

    public float AfterDelay
    {
        get { return afterDelay; }
    }

    public float CooldownTime
    {
        get { return cooldownTime; }
    }

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

    public float MaxRange
    {
        get { return maxRange; }
    }

    public GameObject SkillEffectPrefab
    {
        get { return skillEffectPrefab; }
    }
}
using UnityEngine;

public abstract class Skill : MonoBehaviour                                                                               
{
    /// <summary>
    /// 스킬을 사용할 때의 동작 처리
    /// </summary>
    public abstract void UseSkill();
}
using UnityEngine;

[CreateAssetMenu(fileName = "PlayerSkill", menuName = "GameData/Skill/PlayerSkill")]
public class PlayerSkillData : SkillData
{
    public const int SKILL_MAX_LEVEL = 5;            // 스킬 최대 레벨

    public enum NameId
    {
        Fireball = 10101,
        Explosion = 10102,
        Blaze = 10103,
        Meteor = 10104
    }

    public PlayerSkillData()
    {
        base.userType = SkillUserType.Player;
    }

    [SerializeField] Sprite icon;                    // 스킬 아이콘

    [SerializeField] string tooltip;                 // 스킬 툴팁

    [SerializeField] bool isAcquisition;             // 스킬 습득 유무

    [SerializeField] int currentLevel = 1;           // 현재 스킬 레벨

    [SerializeField] float levelUpAddMpConsumption;  // 레벨업 시 증가되는 Mp 소모량
    [SerializeField] float levelUpAddDamage;         // 레벨업 시 증가되는 데미지

    [SerializeField]
    float initMpConsumption;                         // 초기 Mp 소모량 (스킬 레벨 1일 때)
    float mpConsumption;                             // Mp 소모량

    [SerializeField]
    float initDamage;                                // 초기 스킬 데미지 (스킬 레벨 1일 때)                                 
    float damage;                                    // 스킬 데미지

    public Sprite Icon
    {
        get { return icon; }
    }

    public string Tooltip
    {
        get { return tooltip; }
    }

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

    public int CurrentLevel
    {
        get { return currentLevel; }
        set
        {
            currentLevel = value;

            // 레벨이 1과 최대 레벨 사이값이 되도록 변경
            currentLevel = Mathf.Clamp(currentLevel, 1, SKILL_MAX_LEVEL);
        }
    }

    public float MpConsumption
    {
        get
        {
            // MP 소모량 = 초기 Mp 소모량 + (현재 레벨 - 1) * 레벨업 시 증가되는 Mp 소모량
            mpConsumption = initMpConsumption + (currentLevel - 1) * levelUpAddMpConsumption;

            return mpConsumption;
        }
    }

    public float Damage
    {
        get
        {
            // 스킬 데미지 = 초기 데미지 + (현재 레벨 - 1) * 레벨업 시 증가되는 데미지
            damage = initDamage + (currentLevel - 1) * levelUpAddDamage;

            return damage;
        }
    }
}
public abstract class PlayerSkill : Skill                                                                               
{
    /// <summary>현재 조건에서 시전 가능?</summary>
    public abstract bool CanCast(PlayerManager pm);

    /// <summary>
    /// 시전을 시작(쿨타임·MP 잠금 포함). 성공하면 true.
    /// 실패면 아무것도 하지 말고 false 반환.
    /// </summary>
    public abstract bool TryStartCast(PlayerManager pm);

    /// <summary>실제 효과 실행(투사체 생성 등)</summary>
    public override abstract void UseSkill();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerSkillController : MonoBehaviour
{
    GameManager gameManager;
    PlayerManager playerManager;
    Animator anim;

    // 스킬 컴포넌트
    public PlayerSkill fireball;
    public PlayerSkill explosion;
    public PlayerSkill blaze;
    public PlayerSkill meteor;

    private enum SkillID { FireBall, Explosion, Blaze, Meteor }

    // 쿨타임 테이블
    private readonly Dictionary<SkillID, float> skillCooldowns = new()
    {
        { SkillID.Explosion, 5f },
        { SkillID.Blaze, 10f },
        { SkillID.Meteor, 60f }
    };

    // 마지막 사용 시간
    private readonly Dictionary<SkillID, float> lastUsed = new();

    // 스킬용 애니 해시만 따로 분리 (이동/점프/Die는 PlayerController에 둬도 됨)
    static class SkillAnimHash
    {
        public static readonly int FireBall = Animator.StringToHash("FireBall");
        public static readonly int Explosion = Animator.StringToHash("Explosion");
        public static readonly int Blaze = Animator.StringToHash("Blaze");
        public static readonly int Meteor = Animator.StringToHash("Meteor");
    }

    private void Awake()
    {
        anim = GetComponent<Animator>();

        // 쿨타임 초기화 : 시작하자마자 전부 사용 가능
        foreach (var kvp in skillCooldowns)
            lastUsed[kvp.Key] = Time.time - kvp.Value;
    }

    private void Start()
    {
        gameManager = GameManager.instance;
        playerManager = PlayerManager.instance;

        fireball = GetComponent<PlayerSkill_FireBall>();
        explosion = GetComponent<PlayerSkill_Explosion>();
        blaze = GetComponent<PlayerSkill_Blaze>();
        meteor = GetComponent<PlayerSkill_Meteor>();
    }

    public void HandleSkill()
    {
        // 무기 없으면 사용 불가
        if (EquipWeaponAtHand.instance == null || !EquipWeaponAtHand.instance.HasWeapon())
            return;

        // 마을 / 마법학교에서는 사용 불가
        if (gameManager.CurrentMap == GameManager.Map.Village || gameManager.CurrentMap == GameManager.Map.MagicSchool) 
            return;

        // F: Fireball (쿨타임 없음 예시)
        if (Input.GetKey(KeyCode.F) && 
            !anim.GetBool(SkillAnimHash.Explosion) &&
            !anim.GetBool(SkillAnimHash.Blaze) &&
            !anim.GetBool(SkillAnimHash.Meteor) &&
            playerManager.PlayerSkills[0].IsAcquisition)
        {
            if (fireball != null && fireball.TryStartCast(playerManager))
            {
                StartSkill(SkillID.FireBall);
                fireball.UseSkill();
            }
        }

        // E: Explosion
        if (Input.GetKey(KeyCode.E) &&
            !anim.GetBool(SkillAnimHash.FireBall) &&
            !anim.GetBool(SkillAnimHash.Blaze) &&
            !anim.GetBool(SkillAnimHash.Meteor) &&
            playerManager.PlayerSkills[1].IsAcquisition)
        {
            if (explosion != null && explosion.TryStartCast(playerManager))
            {
                StartSkill(SkillID.Explosion);
                explosion.UseSkill();
            }
        }

        // R: Blaze
        if (Input.GetKey(KeyCode.R) &&
            !anim.GetBool(SkillAnimHash.FireBall) &&
            !anim.GetBool(SkillAnimHash.Explosion) &&
            !anim.GetBool(SkillAnimHash.Meteor) &&
            playerManager.PlayerSkills[2].IsAcquisition)
        {
            if (blaze != null && blaze.TryStartCast(playerManager))
            {
                StartSkill(SkillID.Blaze);
                blaze.UseSkill();
            }
        }

        // T: Meteor
        if (Input.GetKey(KeyCode.T) &&
            !anim.GetBool(SkillAnimHash.FireBall) &&
            !anim.GetBool(SkillAnimHash.Explosion) &&
            !anim.GetBool(SkillAnimHash.Blaze) &&
            playerManager.PlayerSkills[3].IsAcquisition)
        {
            if (meteor != null && meteor.TryStartCast(playerManager))
            {
                StartSkill(SkillID.Meteor);
                meteor.UseSkill();
            }
        }
    }

    void StartSkill(SkillID id)
    {
        int hash = GetAnimHash(id);
        if (hash == 0) return;

        if (anim.GetBool(hash)) return; // 이미 같은 스킬 중이면 무시

        anim.SetLayerWeight(1, 0);
        StartCoroutine(CastSkill(hash));
    }

    IEnumerator CastSkill(int animHash)
    {
        // 1. 공격 레이어 활성화
        anim.SetLayerWeight(1, 1f);
        anim.SetBool(animHash, true);

        // 2. 전이 시작까지 대기
        yield return null;

        // 3. 전이 중이면 끝날 때까지 대기
        while (anim.IsInTransition(1))
            yield return null;

        // 4. 현재 스테이트 길이만큼 대기
        AnimatorStateInfo state = anim.GetCurrentAnimatorStateInfo(1);
        float length = state.length;
        yield return new WaitForSeconds(length);

        // 5. 종료
        anim.SetBool(animHash, false);
        anim.SetLayerWeight(1, 0f);
    }

    private static int GetAnimHash(SkillID id) => id switch
    {
        SkillID.FireBall => SkillAnimHash.FireBall,
        SkillID.Explosion => SkillAnimHash.Explosion,
        SkillID.Blaze => SkillAnimHash.Blaze,
        SkillID.Meteor => SkillAnimHash.Meteor,
        _ => 0
    };
}
using System.Collections;
using UnityEngine;

public class PlayerSkill_Explosion : PlayerSkill
{
    PlayerManager playerManager;
    PlayerSkillData skillDataOrigin;
    PlayerSkillData skillData;

    [SerializeField] Transform attackPosition;

    void Start()
    {
        playerManager = PlayerManager.instance;
        skillDataOrigin = Instantiate(Resources.Load<PlayerSkillData>("GameData/Skill/Player/ExplosionData"));
        skillData = playerManager.FindSkillData(skillDataOrigin);
    }

    public override bool CanCast(PlayerManager _)
            => !skillData.IsCooldown && playerManager.CurrentMp >= skillData.MpConsumption;

    public override bool TryStartCast(PlayerManager _)
    {
        if (!CanCast(playerManager)) return false;

        // 즉시 잠금(레이스 방지)
        skillData.IsCooldown = true;
        playerManager.CurrentMp -= skillData.MpConsumption;
        StartCoroutine(Co_Cooldown(skillData.CooldownTime));
        return true;
    }

    IEnumerator Co_Cooldown(float cd)
    {
        float t = cd;
        while (t > 0f) { t -= Time.deltaTime; yield return null; }
        skillData.IsCooldown = false;
    }

    public override void UseSkill()
    {
        if (!attackPosition || !skillData.SkillEffectPrefab) return;

        var go = Instantiate(skillData.SkillEffectPrefab, attackPosition.position, Quaternion.identity);
        go.transform.forward = transform.forward;

        var proj = go.GetComponent<ExplosionFireBall>();
        if (proj)
        {
            float finalDamage = skillData.Damage + playerManager.PlayerStatus.MagicAttack;
            proj.SetParams(finalDamage, skillData.MaxRange, 10f); 
        }
    }
}
using UnityEngine;

public class Explosion : MonoBehaviour
{
    ParticleSystem particle; // ParticleSystem 컴포넌트

    float particleDuration;  // 파티클 지속 시간

    private void Awake()
    {
        particle = GetComponentInChildren<ParticleSystem>();
    }

    void Start()
    {
        particleDuration = particle.main.duration;

        // 파티클 지속시간이 끝나면 해당 오브젝트 제거
        Destroy(gameObject, particleDuration);
    }
}
using UnityEngine;

public class ExplosionFireBall : MonoBehaviour
{
    private Rigidbody rb;
    private GameObject explosionEffect;

    private float damage;
    private float maxRange;
    private float speed;
    private Vector3 startPos;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        explosionEffect = Resources.Load<GameObject>("Prefabs/Skill/Player/Explosion/Explosion");
    }

    private void Start()
    {
        startPos = transform.position;
    }

    private void Update()
    {
        // 전방으로 이동
        rb.velocity = transform.forward * speed;

        // 거리 제한 체크
        if (Vector3.Distance(transform.position, startPos) > maxRange)
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// 스킬로부터 초기값 세팅
    /// </summary>
    public void SetParams(float dmg, float range, float moveSpeed)
    {
        damage = dmg;
        maxRange = range;
        speed = moveSpeed;
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.layer == LayerMask.NameToLayer("Monster"))
        {
            var monster = other.GetComponent<Monster>();
            if (monster)
                monster.DamagedPlayerSkill(damage);

            if (explosionEffect)
                Instantiate(explosionEffect, transform.position, transform.rotation);

            Destroy(gameObject);
        }
    }
}

전체 흐름도