Print Friendly and PDF

유니티/게임 제작

[디펜스 게임 제작] 스킬 시스템 제작

나는야 개발자 2025. 6. 15. 22:29
반응형

스킬 시스템이란?

- 하나의 스테이지를 클리어할 때마다 "영웅 능력치 버프" 또는 "영웅 스킬 강화"를 선택지가 존재하는데 그때 사용되는 시스템이다.

 

스킬 시스템 구조

- 템플릿 메서드 패턴을 활용하기 위해 BaseSkill에 기본적인 것들의 정의

- 전략 패턴 활용을 위해 성격이 다른 Buff와 Attack을 분리하고 ScriptableObject로 제작

- 버프, 공격에 필요한 것들을 개별로 상속받아 각 프리팹에서 불러오는 방식으로 사용

*두 패턴과 ScriptableObject를 이용해 중복 코드 및 확장성을 살리고 메모리 절약을 목표로 함

 

BaseSkill 클래스 추가

using System;
using UnityEngine;

public abstract class BaseSkill : ScriptableObject
{

    [Header("스킬 정보")]
    public St_SkillInfo _skillInfo;
    [Header("실행할 애니메이션")]
    public EANIMATION _eanimation = EANIMATION.NONE;
    //발동할 애니메이션
    [Header("내 위치에 나타나는 이펙트 오브젝트")]
    public GameObject[] _active_skillEffect;

    /// <summary>
    /// <summary>
    /// 내 위치에 이펙트 생성
    /// </summary>
    /// <param name="myposition"></param>

    //스킬 발동 시 발동자 나오는 이펙트
    public void ActiveSkillEffectToTarget(Vector3 myposition)
    {
        var maxcount = _active_skillEffect.Length;
        for (int i = 0; i < maxcount; i++)
        {
            var mypositioneffect = Instantiate<GameObject>(_active_skillEffect[i], myposition, default);
        }
    }
}

[Serializable]
public struct St_SkillInfo
{
    public float _cooltime;
    public float _duration;
    public int _mid;

    //이 스킬 사용 후 다음 스킬 사용까지 딜레이 시간
    public float _next_skilldelaytime;
}

- 기본적으로 사용될 스킬 고유 아이디인 _mid부터 쿨타임, 지속시간 등 추가

- 스킬 발동 시 나타날 이펙트 함수 구현

- 추후 이펙트 풀링 제작하여 연동 예정

 

공격용 클래스 추가

using UnityEngine;

public class SO_Skill_Attack : BaseSkill
{
    //타겟에게 나타나는 이펙트 오브젝트
    [Header("타겟 위치에 나타나는 이펙트 오브젝트")]
    [SerializeField] GameObject[] _target_attackeffect;

    /// <summary>
    /// 스킬 실행
    /// </summary>
    /// <param name="me"></param>
    /// <param name="target"></param>
    public virtual void ActiveSkill(BaseNPC me, BaseNPC target)
    {
        //이펙트 생성
        ActiveSkillEffectToTarget(me.transform.position);
        TargetToEffect(target.transform.position);

        //데미지 주기
        me.Target_To_Attack(target);
    }

    /// <summary>
    /// 타겟 위치에 생성되는 이펙트
    /// </summary>
    public virtual void TargetToEffect(Vector3 targetposition)
    {
        var maxcount = _target_attackeffect.Length;
        for (int i = 0; i < maxcount; i++)
        {
            var targettoeffect = Instantiate<GameObject>(_target_attackeffect[i], targetposition, default);
        }
    }
}

 

- _target_attackeffect 상태 위치에 나타날 이펙트 오브젝트로 피격 이펙트 등 추가

- ActiveSkill, TargetToEffect는 어떤 스킬엔 딜레이가 있을 수 있고 또는 방식 자체가 다를 수 있기 때문에 virtual로 정의

 

기본공격 클래스 추가

using NUnit.Framework.Internal;
using UnityEngine;

[CreateAssetMenu(fileName = "SO_Skill_BasicAttack", menuName = "SO_Skill_BasicAttack", order = 0)]
public class SO_Skill_BasicAttack : SO_Skill_Attack
{

}

 

- 기본 공격 ScriptableObject 생성

 

버프 스킬 제작

using UnityEngine;

public class SO_Skill_Buff : BaseSkill
{
    [Header("발동시킬 트리거")]
    public EBUFFSKILLTRIGGER _ebuffskilltrigger;
    [Header("버프 줄 스테이터스")]
    public St_Status _add_status; //상승 시킬 스테이터스

    /// <summary>
    /// 스킬 실행
    /// </summary>
    /// <param name="me"></param>
    /// <param name="target"></param>
    public virtual void ActiveSkill(BaseNPC me, EBUFFSKILLTRIGGER buffskillactivetrigger)
    {
        if (buffskillactivetrigger != _ebuffskilltrigger)
        {
            return;
        }

        //나에게 스테이터스 값 적용
        me.AddStatus(_add_status);

        //내 위치에 이펙트 생성
        ActiveSkillEffectToTarget(me.transform.position);
    }

    /// <summary>
    /// SkillController에서 UniTask를 이용해 종료 되었을때 해당 함수를 부르도록 해뒀음
    /// </summary>
    /// <param name="me"></param>
    public virtual void DisableSkill(BaseNPC me)
    {
        //나에게 적용된 스테이터스 값 제거 
        me.RemoveStatus(_add_status);
    }
}

public enum EBUFFSKILLTRIGGER
{
    SPAWN,
}
using UnityEngine;

[CreateAssetMenu(fileName = "SO_Skill_DamageUpBuff", menuName = "SO_Skill_DamageUpBuff", order = 0)]
public class SO_Skill_DamageUpBuff : SO_Skill_Buff
{

}

- SO_Skill_Buff클래스에 _ebuffskilltrigger를 이용해서 특정 Trigger때 버프 스킬이 발동되도록 추가

- _add_status로 원하는 Status 증가 하도록 제작 

 

BaseNPC에 St_Status 구조체 적용 및 스킬 배열 추가

using System;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(fileName = "SO_NPC", menuName = "SO_NPC", order = 0)]
public class SO_NPC : ScriptableObject
{
    public St_Status _status;
    public SO_Skill_Attack[] _skill_Attack;
    public SO_Skill_Buff[] _skill_buff;
}


[Serializable]
public struct St_Status
{
    public int _hp;
    public int _damge;
    public int _armor;
    public float _critical;// 0~1
    public float _critical_damage; // 0~1
}

- St_Status 구조체를 추가하여 능력치가 추가되도 일일이 변경하지 않아도 되도록 추가

- _skill_Attack와 _skill_buff로 원하는 스킬 할당할 수 있도록 확장성 있게 추가

 

SkillController클래스 추가

using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class SkillController : MonoBehaviour
{
    [SerializeField] BaseNPC _me;
    [SerializeField] AttackAreaController _attackAreaController;

    //일반 변수
    float _attackskillnextdelaytime;
    float _buffskillnextdelaytime;
    int _current_attack_skill_index = 0;
    int _current_buff_skill_index = 0;

    //쿨타임 및 지속시간 관련 변수
    Dictionary<int, bool> _skillcoolTime = new Dictionary<int, bool>();
    Dictionary<int, CancellationTokenSource> _skillcooltimetoken = new Dictionary<int, CancellationTokenSource>();
    Dictionary<int, CancellationTokenSource> _skilldurationtoken = new Dictionary<int, CancellationTokenSource>();

    //개별 이벤트
    public event Action _skill_attack_event;
    public event Action _skill_buff_event;

    private void Start()
    {
        _attackAreaController._enter_active_skill_event += ActiveAttackSkill;
    }

    private void Update()
    {
        _attackskillnextdelaytime -= Time.deltaTime;
        _buffskillnextdelaytime -= Time.deltaTime;
    }

    void ActiveAttackSkill(BaseNPC target)
    {
        //다음 스킬 발동 딜레이 중일 경우 return, 이때는 index를 올릴 필요 없음 
        if (_attackskillnextdelaytime > 0)
        {
            return;
        }

        var myattackskilllist = _me._so_npc._skill_Attack;
        if (myattackskilllist.Length <= 0)
        {
            return;
        }

        var skillinfo = myattackskilllist[_current_attack_skill_index]._skillInfo;

        //현재 스킬이 쿨타임 진행중인지 체크
        if (CheckCoolTime(skillinfo._mid))
        {
            AddAttackSkillIndex();
            return;
        }

        //스킬 발동 
        _me._so_npc._skill_Attack[_current_attack_skill_index].ActiveSkill(_me, target);
        SkillCoolTime(skillinfo).Forget();
        _skill_attack_event?.Invoke();
        AddAttackSkillIndex();
    }

    public void ActiveBuffSkill(EBUFFSKILLTRIGGER ebuffskillactivetirrger)
    {
        //다음 스킬 발동 딜레이 중일 경우 return, 이때는 index를 올릴 필요 없음 
        if (_buffskillnextdelaytime > 0)
        {
            return;
        }

        var mybufflist = _me._so_npc._skill_buff;
        if (mybufflist.Length <= 0)
        {
            return;
        }

        var skillinfo = mybufflist[_current_attack_skill_index]._skillInfo;

        //다음 스킬 발동 쿨타임인지 체크
        if (CheckCoolTime(skillinfo._mid))
        {
            //쿨타임 중인 스킬이라면 다음 index스킬 체크
            AddBuffSkillIndex();
            return;
        }

        //스킬 발동
        _me._so_npc._skill_buff[_current_attack_skill_index].ActiveSkill(_me, ebuffskillactivetirrger);

        //스킬 발동 후 지속시간 후 종료되도록 처리
        BuffSkillDisable(skillinfo, _current_attack_skill_index).Forget();
        SkillCoolTime(skillinfo).Forget();
        _skill_buff_event?.Invoke();
        AddBuffSkillIndex();
    }

    void AddAttackSkillIndex()
    {
        _current_attack_skill_index++;
        if (_me._so_npc._skill_Attack.Length >= _current_attack_skill_index)
        {
            _current_attack_skill_index = 0;
        }
    }

    void AddBuffSkillIndex()
    {
        _current_buff_skill_index++;
        if (_me._so_npc._skill_buff.Length >= _current_buff_skill_index)
        {
            _current_buff_skill_index = 0;
        }
    }

    async UniTaskVoid BuffSkillDisable(St_SkillInfo skillinfo, int idx)
    {
        if (skillinfo._duration <= 0)
        {
            _me._so_npc._skill_buff[idx].DisableSkill(_me);
            return;
        }

        //강제 종료용 토큰 생성
        if (!_skilldurationtoken.ContainsKey(skillinfo._mid))
        {
            _skilldurationtoken.Add(skillinfo._mid, null);
        }

        if (_skilldurationtoken[skillinfo._mid] != null)
        {
            _skilldurationtoken[skillinfo._mid].Cancel();
            _skilldurationtoken[skillinfo._mid].Dispose();
        }
        _skilldurationtoken[skillinfo._mid] = new CancellationTokenSource();

        await UniTask.WaitForSeconds(skillinfo._duration, cancellationToken: _skilldurationtoken[skillinfo._mid].Token);
        _me._so_npc._skill_buff[idx].DisableSkill(_me);
    }

    async UniTaskVoid SkillCoolTime(St_SkillInfo skillinfo)
    {
        if (skillinfo._cooltime <= 0)
        {
            return;
        }

        if (!_skillcooltimetoken.ContainsKey(skillinfo._mid))
        {
            _skillcooltimetoken.Add(skillinfo._mid, null);
        }

        if (_skillcooltimetoken[skillinfo._mid] != null)
        {
            _skillcooltimetoken[skillinfo._mid].Cancel();
            _skillcooltimetoken[skillinfo._mid].Dispose();
        }
        _skillcooltimetoken[skillinfo._mid] = new CancellationTokenSource();

        _skillcoolTime[skillinfo._mid] = true;
        await UniTask.WaitForSeconds(skillinfo._cooltime, cancellationToken: _skillcooltimetoken[skillinfo._mid].Token);
        _skillcoolTime[skillinfo._mid] = false;
    }

    bool CheckCoolTime(int skillid)
    {
        if (_skillcoolTime.ContainsKey(skillid))
        {
            return _skillcoolTime[skillid];
        }

        _skillcoolTime.Add(skillid, false);
        return false;
    }


    void OnDisable()
    {
        foreach (var item in _skillcooltimetoken)
        {
            if (item.Value == null)
            {
                continue;
            }

            item.Value.Cancel();
            item.Value.Dispose();
        }

        foreach (var item in _skilldurationtoken)
        {
            if (item.Value == null)
            {
                continue;
            }

            item.Value.Cancel();
            item.Value.Dispose();
        }
    }
}

- 가진 스킬을 순차적으로 사용하도록 _current_attack_skill_index, _current_buff_skill_index 추가

- UniTask를 이용해 지속시간과 쿨타임 처리, 중간에 사망하거나 씬이 이동될 것을 고려하여 Token으로 Cancel작업 추가

 

 

BaseNPC 클래스 수정

using System;
using UnityEngine;

public abstract class BaseNPC : MonoBehaviour
{
    //NPC 별 데이터 
    public SO_NPC _so_npc;
    [SerializeField] protected AnimationController _animationController;
    [SerializeField] protected HpbarController _hpbarController;
    [SerializeField] protected SkillController _skillController;

    //기본 맴버변수 
    protected int _current_hp;
    protected St_Status _status;


    //이벤트 변수들
    public event Action _die_event;
    public event Action _hit_event;

    //함수
    protected virtual void Start()
    {
        //애니메이션 관련
        if (_animationController)
        {
            _die_event += () => PlayAnimation(EANIMATION.DEATH);
            _hit_event += () => PlayAnimation(EANIMATION.HIT);
        }

        //hp바 업데이트
        if (_hpbarController)
        {
            _hit_event += () => _hpbarController.Hpbar_Update(_status._hp, _current_hp);
        }
    }

    public virtual void OnSpawn()
    {
        //생성 시 스테이터스를 기본 스테이터스로 복사하기
        _status = _so_npc._status;

        //생성 버프 발동 내부에 스테이터스 변화하는 게 존재할 수 있음
        _skillController?.ActiveBuffSkill(EBUFFSKILLTRIGGER.SPAWN);

        //생성 버프 발동 후 hp셋팅하기
        _current_hp = _status._hp;
    }

    public virtual void Target_To_Attack(BaseNPC target_npc)
    {
        var my_damage = TotalDamage();
        target_npc.Hp_Update(my_damage);
    }

    public void AddStatus(St_Status addstatus)
    {
        _status._armor += addstatus._armor;
        _status._damge += addstatus._damge;
        _status._critical += addstatus._critical;
        _status._critical_damage += addstatus._critical_damage;
        _status._hp += addstatus._hp;
    }

    public void RemoveStatus(St_Status removestatus)
    {
        _status._armor -= removestatus._armor;
        _status._damge -= removestatus._damge;
        _status._critical -= removestatus._critical;
        _status._critical_damage -= removestatus._critical_damage;
        _status._hp -= removestatus._hp;
    }

    int TotalDamage()
    {
        float damage = _status._damge;

        var critical_random_value = UnityEngine.Random.Range(0f, 1f);
        if (critical_random_value <= _status._critical)
        {
            damage = damage * (_status._critical_damage + 1);
        }

        return Mathf.FloorToInt(damage);
    }

    protected virtual void Hp_Update(int target_damage)
    {
        _current_hp -= target_damage;
        _hit_event?.Invoke();

        if (_current_hp <= 0)
        {
            NPC_Die();
        }
    }

    protected virtual void NPC_Die()
    {
        _die_event?.Invoke();
    }

    public bool CheckDie()
    {
        return _current_hp <= 0;
    }

    protected virtual void PlayAnimation(EANIMATION eanimation)
    {
        _animationController.PlayAnimation(eanimation);
    }

    protected virtual void PlayAnimation(EANIMATION eanimation, bool isaction)
    {
        _animationController.PlayAnimation(eanimation, isaction);
    }
}

- OnSpawn 했을때 기본 스테이터스 및 버프 스테이터스 적용되도록 추가

- status값이 개별적으로 가지고 있어야기 때문에 TotalDamage 함수 이동하여 개별 status값으로 적용

- AddStatus, RemoveStatus 추가

 

 

버프 ScriptableObject 추가 

- 데미지 상승 버프 ScriptableObject추가 

 

- 원하는 NPC ScriptableObject에 스킬 추가하면 완료

 

*공격과 관련된 건 다음 시간에 계속!


1차 12:00~14:00 - 구조 설계, BaseSkill/SO_Skill_Attack(추가), SO_Skill_BasicAttack(추가)

2차 21:10 ~ 22:30  - BaseSkill/SO_Skill_Attack/SO_Skill_BasicAttack(수정)

3차 21:10 ~ 23:10 - BaseNPC/SO_NPC(수정), BaseSkill/SO_Skill_Attack/SO_Skill_BasicAttack(수정), SkillController(추가)

4차 21:00 ~ 22:10 - BaseSkill/SO_Skill_Attack/SO_Skill_BasicAttack(헤더 추가), SkillController(수정)

반응형