2026-04-06 스킬시스템

This commit is contained in:
2026-04-06 18:05:11 +09:00
parent c0713abdaa
commit 42f92020c7
65 changed files with 457 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
using UnityEngine;
public class SkillManager : MonoBehaviour
{
[SerializeField] private int _maxSlots = 4;
private PlayerStateMachine _stateMachine;
private Animator _anim;
private Transform _transform;
private SkillInstance[] _equippedSkills;
private ISkillEffect[] _skillEffects;
private int _chargingSlot = -1;
private float _chargeTimer = 0f;
private void Awake()
{
_stateMachine = GetComponent<PlayerStateMachine>();
_anim = GetComponent<Animator>();
_transform = transform;
_equippedSkills = new SkillInstance[_maxSlots];
_skillEffects = new ISkillEffect[_maxSlots];
}
private void Update()
{
TickCooldowns();
TickCharge();
}
#region
public void LoadWeaponSkills(WeaponSkillSet skillSet)
{
for (int i = 0; i < _maxSlots; i++)
{
if (i < skillSet.Skills.Count && skillSet.Skills[i] != null)
{
_equippedSkills[i] = new SkillInstance(skillSet.Skills[i]);
_skillEffects[i] = ResolveEffect(skillSet.Skills[i].TargetType);
}
else
{
_equippedSkills[i] = null;
_skillEffects[i] = null;
}
}
ApplyPassives();
}
private ISkillEffect ResolveEffect(TargetType targetType)
{
return targetType switch
{
TargetType.Self => GetComponent<BuffEffect>(),
TargetType.Single => GetComponent<DamageEffect>(),
TargetType.Area => GetComponent<AreaEffect>(),
TargetType.Projectile => GetComponent<ProjectileEffect>(),
_ => null
};
}
#endregion
#region
public void SkillInput(int slotIndex, InputState inputState)
{
if (slotIndex < 0 || slotIndex >= _maxSlots) return;
SkillInstance skill = _equippedSkills[slotIndex];
if (skill == null) return;
if (skill.Data.SkillType == SkillType.Passive) return;
if (inputState == InputState.Started)
{
if (!CanUseSkill(skill)) return;
if (skill.Data.ActivationType == ActivationType.Instant)
{
ExecuteSkill(slotIndex);
}
else if (skill.Data.ActivationType == ActivationType.Charge)
{
StartCharge(slotIndex);
}
}
else if (inputState == InputState.Canceled)
{
if (_chargingSlot == slotIndex)
{
ReleaseCharge();
}
}
}
#endregion
#region
private bool CanUseSkill(SkillInstance skill)
{
if (skill.IsOnCooldown) return false;
PlayerState state = _stateMachine.CurrentState;
if (state == PlayerState.Dead || state == PlayerState.Hit
|| state == PlayerState.Dodge || state == PlayerState.Trans
|| state == PlayerState.Action)
return false;
return true;
}
private void ExecuteSkill(int slotIndex)
{
SkillInstance skill = _equippedSkills[slotIndex];
ISkillEffect effect = _skillEffects[slotIndex];
_stateMachine.ChangeState(PlayerState.Attack);
if (!string.IsNullOrEmpty(skill.Data.AnimTrigger))
_anim.SetTrigger(skill.Data.AnimTrigger);
float chargeRatio = 1f;
if (skill.Data.ActivationType == ActivationType.Charge)
{
float maxCharge = skill.CurrentLevelData.ChargeTimeMax;
chargeRatio = maxCharge > 0 ? Mathf.Clamp01(_chargeTimer / maxCharge) : 1f;
}
effect?.Execute(skill, _transform, chargeRatio);
skill.StartCooldown();
_chargeTimer = 0f;
_chargingSlot = -1;
}
#endregion
#region
private void StartCharge(int slotIndex)
{
_chargingSlot = slotIndex;
_chargeTimer = 0f;
_stateMachine.ChangeState(PlayerState.Charge);
SkillInstance skill = _equippedSkills[slotIndex];
if (!string.IsNullOrEmpty(skill.Data.AnimTrigger))
_anim.SetTrigger(skill.Data.AnimTrigger);
}
private void ReleaseCharge()
{
if (_chargingSlot < 0) return;
ExecuteSkill(_chargingSlot);
}
private void TickCharge()
{
if (_chargingSlot < 0) return;
SkillInstance skill = _equippedSkills[_chargingSlot];
_chargeTimer += Time.deltaTime;
if (_chargeTimer >= skill.CurrentLevelData.ChargeTimeMax)
{
ReleaseCharge();
}
}
#endregion
#region
private void TickCooldowns()
{
foreach (var skill in _equippedSkills)
{
skill?.TickCooldown(Time.deltaTime);
}
}
#endregion
#region
private void ApplyPassives()
{
foreach (var skill in _equippedSkills)
{
if (skill != null && skill.Data.SkillType == SkillType.Passive)
{
// PlayerStat에 스탯 보정 적용
}
}
}
#endregion
#region
public SkillInstance GetSkill(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= _maxSlots) return null;
return _equippedSkills[slotIndex];
}
public int MaxSlots => _maxSlots;
#endregion
}

View File

@@ -0,0 +1,54 @@
using UnityEngine;
/*
:
Project에서 Create Skill/SkillData로 ,
Create Skill/WeaponSkillSet로
SkillManager + Effect
LoadWeaponSkills(skillSet)
SkillInput(slotIndex, inputState)
*/
[CreateAssetMenu(menuName = "Skill/SkillData")]
public class SkillData : ScriptableObject
{
[Header("기본 정보")]
public string SkillName;
[TextArea] public string Description;
public Sprite Icon;
[Header("스킬 분류")]
public SkillType SkillType;
public ActivationType ActivationType;
public TargetType TargetType;
[Header("애니메이션")]
public string AnimTrigger;
[Header("이펙트")]
public GameObject EffectPrefab;
[Header("레벨별 수치")]
public SkillLevelData[] Levels;
public SkillLevelData GetLevelData(int level)
{
int idx = Mathf.Clamp(level - 1, 0, Levels.Length - 1);
return Levels[idx];
}
}
[System.Serializable]
public class SkillLevelData
{
public float Damage;
public float Range;
public float Cooldown;
public float ManaCost;
public float Duration;
public float ChargeTimeMax;
}
public enum SkillType { Active, Passive }
public enum ActivationType { Instant, Charge }
public enum TargetType { Self, Single, Area, Projectile }

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Skill/WeaponSkillSet")]
public class WeaponSkillSet : ScriptableObject
{
public WeaponType WeaponType;
public List<SkillData> Skills;
}

View File

@@ -0,0 +1,25 @@
using UnityEngine;
public class AreaEffect : MonoBehaviour, ISkillEffect
{
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
{
SkillLevelData levelData = skill.CurrentLevelData;
float finalDamage = levelData.Damage * chargeRatio;
Vector3 center = caster.position + caster.forward * levelData.Range;
if (skill.Data.EffectPrefab != null)
{
Instantiate(skill.Data.EffectPrefab, center, Quaternion.identity);
}
Collider[] hits = Physics.OverlapSphere(center, levelData.Range);
foreach (Collider hit in hits)
{
if (hit.transform == caster) continue;
Debug.Log($"[범위] {hit.name}에게 {finalDamage} 데미지");
}
}
}

View File

@@ -0,0 +1,14 @@
using UnityEngine;
public class BuffEffect : MonoBehaviour, ISkillEffect
{
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
{
SkillLevelData levelData = skill.CurrentLevelData;
// PlayerStat에 버프 적용
// caster.GetComponent<PlayerStat>()?.ApplyBuff(levelData);
Debug.Log($"버프 적용: {skill.Data.SkillName}, 지속시간 {levelData.Duration}초");
}
}

View File

@@ -0,0 +1,20 @@
using UnityEngine;
public class DamageEffect : MonoBehaviour, ISkillEffect
{
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
{
SkillLevelData levelData = skill.CurrentLevelData;
float finalDamage = levelData.Damage * chargeRatio;
float range = levelData.Range;
Collider[] hits = Physics.OverlapSphere(caster.position + caster.forward * range * 0.5f, range);
foreach (Collider hit in hits)
{
if (hit.transform == caster) continue;
// IDamageable 등 인터페이스가 있으면 여기서 적용
Debug.Log($"{hit.name}에게 {finalDamage} 데미지");
}
}
}

View File

@@ -0,0 +1,6 @@
using UnityEngine;
public interface ISkillEffect
{
void Execute(SkillInstance skill, Transform caster, float chargeRatio);
}

View File

@@ -0,0 +1,15 @@
using UnityEngine;
public class ProjectileEffect : MonoBehaviour, ISkillEffect
{
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
{
if (skill.Data.EffectPrefab == null) return;
Vector3 spawnPos = caster.position + caster.forward + Vector3.up;
GameObject proj = Instantiate(skill.Data.EffectPrefab, spawnPos, caster.rotation);
// 투사체에 데미지 정보 전달
// proj.GetComponent<Projectile>()?.Init(skill.CurrentLevelData.Damage * chargeRatio);
}
}

View File

@@ -0,0 +1,26 @@
public class SkillInstance
{
public SkillData Data { get; private set; }
public int Level { get; set; } = 1;
public float CooldownTimer { get; set; }
public bool IsOnCooldown => CooldownTimer > 0f;
public SkillLevelData CurrentLevelData => Data.GetLevelData(Level);
public SkillInstance(SkillData data, int level = 1)
{
Data = data;
Level = level;
}
public void StartCooldown()
{
CooldownTimer = CurrentLevelData.Cooldown;
}
public void TickCooldown(float deltaTime)
{
if (CooldownTimer > 0f)
CooldownTimer -= deltaTime;
}
}