스킬 시스템이란?
- 하나의 스테이지를 클리어할 때마다 "영웅 능력치 버프" 또는 "영웅 스킬 강화"를 선택지가 존재하는데 그때 사용되는 시스템이다.
스킬 시스템 구조
- 템플릿 메서드 패턴을 활용하기 위해 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(수정)
'유니티 > 게임 제작' 카테고리의 다른 글
[디펜스 게임 제작] 캐릭터 제작 (0) | 2025.06.19 |
---|---|
[디펜스 게임 제작] 공격 처리 (0) | 2025.06.18 |
[디펜스 게임 제작] 보호 오브젝트 (0) | 2025.06.15 |
[디펜스 게임 제작] 몬스터 제작 (1) | 2025.06.10 |
[디펜스 게임 제작] 유니티 프로젝트 등록 및 Git 등록 (0) | 2025.06.10 |