2026-04-06 스킬시스템
This commit is contained in:
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"visualstudiotoolsforunity.vstuc"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
.vscode/launch.json
vendored
Normal file
10
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Attach to Unity",
|
||||||
|
"type": "vstuc",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
71
.vscode/settings.json
vendored
Normal file
71
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.vs": true,
|
||||||
|
"**/.gitmodules": true,
|
||||||
|
"**/.vsconfig": true,
|
||||||
|
"**/*.booproj": true,
|
||||||
|
"**/*.pidb": true,
|
||||||
|
"**/*.suo": true,
|
||||||
|
"**/*.user": true,
|
||||||
|
"**/*.userprefs": true,
|
||||||
|
"**/*.unityproj": true,
|
||||||
|
"**/*.dll": true,
|
||||||
|
"**/*.exe": true,
|
||||||
|
"**/*.pdf": true,
|
||||||
|
"**/*.mid": true,
|
||||||
|
"**/*.midi": true,
|
||||||
|
"**/*.wav": true,
|
||||||
|
"**/*.gif": true,
|
||||||
|
"**/*.ico": true,
|
||||||
|
"**/*.jpg": true,
|
||||||
|
"**/*.jpeg": true,
|
||||||
|
"**/*.png": true,
|
||||||
|
"**/*.psd": true,
|
||||||
|
"**/*.tga": true,
|
||||||
|
"**/*.tif": true,
|
||||||
|
"**/*.tiff": true,
|
||||||
|
"**/*.3ds": true,
|
||||||
|
"**/*.3DS": true,
|
||||||
|
"**/*.fbx": true,
|
||||||
|
"**/*.FBX": true,
|
||||||
|
"**/*.lxo": true,
|
||||||
|
"**/*.LXO": true,
|
||||||
|
"**/*.ma": true,
|
||||||
|
"**/*.MA": true,
|
||||||
|
"**/*.obj": true,
|
||||||
|
"**/*.OBJ": true,
|
||||||
|
"**/*.asset": true,
|
||||||
|
"**/*.cubemap": true,
|
||||||
|
"**/*.flare": true,
|
||||||
|
"**/*.mat": true,
|
||||||
|
"**/*.meta": true,
|
||||||
|
"**/*.prefab": true,
|
||||||
|
"**/*.unity": true,
|
||||||
|
"build/": true,
|
||||||
|
"Build/": true,
|
||||||
|
"Library/": true,
|
||||||
|
"library/": true,
|
||||||
|
"obj/": true,
|
||||||
|
"Obj/": true,
|
||||||
|
"Logs/": true,
|
||||||
|
"logs/": true,
|
||||||
|
"ProjectSettings/": true,
|
||||||
|
"UserSettings/": true,
|
||||||
|
"temp/": true,
|
||||||
|
"Temp/": true
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"*.asset": "yaml",
|
||||||
|
"*.meta": "yaml",
|
||||||
|
"*.prefab": "yaml",
|
||||||
|
"*.unity": "yaml",
|
||||||
|
},
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"*.sln": "*.csproj",
|
||||||
|
"*.slnx": "*.csproj"
|
||||||
|
},
|
||||||
|
"dotnet.defaultSolution": "NJ_Project_20260312.slnx"
|
||||||
|
}
|
||||||
202
Assets/02_Scripts/Managers/Local/SkillManager.cs
Normal file
202
Assets/02_Scripts/Managers/Local/SkillManager.cs
Normal 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
|
||||||
|
}
|
||||||
54
Assets/02_Scripts/Skill/Data/SkillData.cs
Normal file
54
Assets/02_Scripts/Skill/Data/SkillData.cs
Normal 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 }
|
||||||
9
Assets/02_Scripts/Skill/Data/WeaponSkillSet.cs
Normal file
9
Assets/02_Scripts/Skill/Data/WeaponSkillSet.cs
Normal 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;
|
||||||
|
}
|
||||||
25
Assets/02_Scripts/Skill/Effects/AreaEffect.cs
Normal file
25
Assets/02_Scripts/Skill/Effects/AreaEffect.cs
Normal 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} 데미지");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Assets/02_Scripts/Skill/Effects/BuffEffect.cs
Normal file
14
Assets/02_Scripts/Skill/Effects/BuffEffect.cs
Normal 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}초");
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Assets/02_Scripts/Skill/Effects/DamageEffect.cs
Normal file
20
Assets/02_Scripts/Skill/Effects/DamageEffect.cs
Normal 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} 데미지");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Assets/02_Scripts/Skill/Effects/ISkillEffect.cs
Normal file
6
Assets/02_Scripts/Skill/Effects/ISkillEffect.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public interface ISkillEffect
|
||||||
|
{
|
||||||
|
void Execute(SkillInstance skill, Transform caster, float chargeRatio);
|
||||||
|
}
|
||||||
15
Assets/02_Scripts/Skill/Effects/ProjectileEffect.cs
Normal file
15
Assets/02_Scripts/Skill/Effects/ProjectileEffect.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Assets/02_Scripts/Skill/SkillInstance.cs
Normal file
26
Assets/02_Scripts/Skill/SkillInstance.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user