2026-04-13 스킬시스템 진행중

This commit is contained in:
2026-04-13 01:42:32 +09:00
parent ba93dc6d2f
commit b2cceb5b27
20 changed files with 1243 additions and 37 deletions

View File

@@ -44,8 +44,5 @@ public class Item : UseableEntry
public float IntervalDamage;
public float IntervalDamageTime;
public override void Use()
{
throw new System.NotImplementedException();
}
public override IUseableRuntime CreateRuntime() => new ItemInstance(this);
}

View File

@@ -1,7 +1,7 @@
using UnityEngine;
[System.Serializable]
public class ItemInstance
public class ItemInstance : IUseableRuntime
{
public Item Data; // 원본 ScriptableObject 참조 (이름, 아이콘 등 불변 데이터)
@@ -18,4 +18,9 @@ public ItemInstance(Item sourceData, int stack = 1)
this.EnhancementLevel = -1;// 기본 강화 수치 (-1은 강화수치가 없는 아이템)
this.Durability = -1; // 기본 내구도 (-1은 내구도가 없는 아이템)
}
public void Execute(UseContext ctx)
{
throw new System.NotImplementedException();
}
}

View File

@@ -26,12 +26,16 @@ public class InputManager : MonoBehaviour
public event Action OnNormalAttackEvent;
public event Action OnHeavyAttackEvent;
public event Action OnInteractionEvent;
public event Action OnKeyDown_SlotQEvent;
public event Action OnKeyDown_SlotEEvent;
public event Action OnKeyDown_SlotREvent;
public event Action OnKeyDown_SlotTEvent;
public event Action OnKeyDown_SlotFEvent;
public event Action OnKeyDown_SlotGEvent;
public event Action<InputState> OnKeyDown_SlotQEvent;
public event Action<InputState> OnKeyDown_SlotEEvent;
public event Action<InputState> OnKeyDown_SlotREvent;
public event Action<InputState> OnKeyDown_SlotTEvent;
public event Action<InputState> OnKeyDown_SlotFEvent;
public event Action<InputState> OnKeyDown_SlotGEvent;
public event Action<InputState> OnAreaConfirmEvent; // Remote 스킬 범위 확정
// 범위 지정 중 기본 공격 차단용 플래그 (SkillModule이 제어)
public bool SuppressNormalAttack { get; set; }
//키조작
@@ -119,6 +123,8 @@ public void SetCharacterInputMap(string mapName)
BindActionCharacter("UseSlot_F",OnKeyDown_Slot);
BindActionCharacter("UseSlot_G",OnKeyDown_Slot);
BindActionCharacter("AreaConfirm", OnAreaConfirm);
BindActionCharacter("Interaction", OnInteraction);
BindActionCharacter("OnkeyDown_IKey", OnKeyDown_IKey);
@@ -224,6 +230,7 @@ private void OnDodge(InputAction.CallbackContext ctx)
private void OnNormalAttack(InputAction.CallbackContext ctx)
{
if (SuppressNormalAttack) return;
OnNormalAttackEvent?.Invoke();
}
@@ -238,11 +245,40 @@ private void OnInteraction(InputAction.CallbackContext ctx)
OnInteractionEvent?.Invoke();
}
private void OnAreaConfirm(InputAction.CallbackContext ctx)
{
if (ctx.started)
OnAreaConfirmEvent?.Invoke(InputState.Started);
}
private void OnKeyDown_Slot(InputAction.CallbackContext ctx)
{
if(ctx.started)
if (ctx.started)
{
if (ctx.action.name == "UseSlot_Q")
{
OnKeyDown_SlotQEvent?.Invoke(InputState.Started);
}
else if (ctx.action.name == "UseSlot_E")
{
OnKeyDown_SlotEEvent?.Invoke(InputState.Started);
}
else if (ctx.action.name == "UseSlot_R")
{
OnKeyDown_SlotREvent?.Invoke(InputState.Started);
}
else if (ctx.action.name == "UseSlot_T")
{
OnKeyDown_SlotTEvent?.Invoke(InputState.Started);
}
else if (ctx.action.name == "UseSlot_F")
{
OnKeyDown_SlotFEvent?.Invoke(InputState.Started);
}
else if (ctx.action.name == "UseSlot_G")
{
OnKeyDown_SlotGEvent?.Invoke(InputState.Started);
}
}
}
#endregion

View File

@@ -109,6 +109,14 @@ public void OnSceneLoaded(Scene scene, LoadSceneMode mode)
InputManager.Instance.OnLookEvent += CurrentCharacterController.LookInput;
InputManager.Instance.OnDodgeEvent += CurrentCharacterController.DodgeInput;
//스킬 범위 확정 (Remote 스킬)
SkillModule sm = CurrentCharacter.GetComponent<SkillModule>();
if (sm != null)
{
InputManager.Instance.OnAreaConfirmEvent -= sm.AreaConfirmInput;
InputManager.Instance.OnAreaConfirmEvent += sm.AreaConfirmInput;
}
//공격매핑
//InputManager.Instance.OnNormalAttackEvent;
//InputManager.Instance.OnHeavyAttackEvent;
@@ -134,6 +142,40 @@ private void ApplyCharacterInfo(PlayerCharacterController pcc, UserCharacter uc)
pcc.PlayerCharacterIdentity.IsDefaultControl = uc.DefaultControl;
}
public void BindSlotAction(string slotName,Action<InputState> slotUseAction)
{
if (slotName == "UseSlot_Q")
{
InputManager.Instance.OnKeyDown_SlotQEvent -= slotUseAction;
InputManager.Instance.OnKeyDown_SlotQEvent += slotUseAction;
}
else if (slotName == "UseSlot_E")
{
InputManager.Instance.OnKeyDown_SlotEEvent -= slotUseAction;
InputManager.Instance.OnKeyDown_SlotEEvent += slotUseAction;
}
else if (slotName == "UseSlot_R")
{
InputManager.Instance.OnKeyDown_SlotREvent -= slotUseAction;
InputManager.Instance.OnKeyDown_SlotREvent += slotUseAction;
}
else if (slotName == "UseSlot_T")
{
InputManager.Instance.OnKeyDown_SlotTEvent -= slotUseAction;
InputManager.Instance.OnKeyDown_SlotTEvent += slotUseAction;
}
else if (slotName == "UseSlot_F")
{
InputManager.Instance.OnKeyDown_SlotFEvent -= slotUseAction;
InputManager.Instance.OnKeyDown_SlotFEvent += slotUseAction;
}
else if (slotName == "UseSlot_G")
{
InputManager.Instance.OnKeyDown_SlotGEvent -= slotUseAction;
InputManager.Instance.OnKeyDown_SlotGEvent += slotUseAction;
}
}
private void OnDestroy()
{
if(InputManager.Instance != null)

View File

@@ -116,10 +116,45 @@ private void Awake()
_renderers = GetComponentsInChildren<Renderer>();
}
// 범위 지정 중 회전모드 복구용
private PlayerRotationMode _rotationModeBeforeAreaSelect;
private SkillModule _skillModule;
private void Start()
{
_stateMachine.SetMaxJumpCount(_maxJumpCount);
SetCursorLockState(true);
// 범위 지정 이벤트 구독 — 회전모드만 토글 (이동은 영향 없음)
_skillModule = GetComponent<SkillModule>();
if (_skillModule != null)
{
_skillModule.OnAreaSelectStarted += HandleAreaSelectStarted;
_skillModule.OnAreaSelectEnded += HandleAreaSelectEnded;
}
}
private void OnDestroy()
{
if (_skillModule != null)
{
_skillModule.OnAreaSelectStarted -= HandleAreaSelectStarted;
_skillModule.OnAreaSelectEnded -= HandleAreaSelectEnded;
}
}
private void HandleAreaSelectStarted()
{
_rotationModeBeforeAreaSelect = RotationMode;
RotationMode = PlayerRotationMode.CameraDecoupled;
SetCursorLockState(false); // 마우스 자유 이동
}
private void HandleAreaSelectEnded()
{
RotationMode = _rotationModeBeforeAreaSelect;
SetCursorLockState(true);
}
public void PlayerStart()

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
private WeaponType weaponType;
public WeaponType WType;
}

View File

@@ -65,16 +65,16 @@ public class SkillData : UseableEntry
[Header("레벨별 수치")]
public SkillLevelData[] Levels;
[Header("키 액션 설정")]
public string InputActionName;
public SkillLevelData GetLevelData(int level)
{
int idx = Mathf.Clamp(level - 1, 0, Levels.Length - 1);
return Levels[idx];
}
public override void Use()
{
throw new System.NotImplementedException();
}
public override IUseableRuntime CreateRuntime() => new SkillInstance(this);
}
[System.Serializable]
@@ -90,6 +90,7 @@ public class SkillLevelData
public float ChannelDuration;
public float TickInterval;
public float TickDamage;
public float ActionDuration; // Attack 상태 지속 시간 (스킬 발동 후 Idle 복귀까지)
}
public enum SkillType { Active, Passive }

View File

@@ -1,4 +1,4 @@
public class SkillInstance
public class SkillInstance : IUseableRuntime
{
public SkillData Data { get; private set; }
public int Level { get; set; } = 1;
@@ -23,4 +23,12 @@ public void TickCooldown(float deltaTime)
if (CooldownTimer > 0f)
CooldownTimer -= deltaTime;
}
public void Execute(UseContext ctx)
{
SkillModule skillModule = ctx.Caster.GetComponent<SkillModule>(); //사용자(캐스터)의 스킬 모듈
if (skillModule == null) return;
skillModule.SkillInputByData(Data, ctx.UseInputState);
}
}

View File

@@ -1,10 +1,17 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class SkillModule : MonoBehaviour
{
[SerializeField] private int _maxSlots = 4;
[SerializeField] private LayerMask _groundLayer; // 바닥 레이캐스트용
// 범위 지정 시작/종료 이벤트 (외부에서 RotationMode/UI 등 반응)
public event Action OnAreaSelectStarted;
public event Action OnAreaSelectEnded;
private PlayerStateMachine _stateMachine;
private Animator _anim;
private Transform _transform;
@@ -37,6 +44,18 @@ public class SkillModule : MonoBehaviour
public bool IsCasting => _castingSlot >= 0;
public bool IsChanneling => _channelingSlot >= 0;
[SerializeField] private List<WeaponSkillSet> _weaponSkills;
[SerializeField] private Weapon _equippedWeapon;
private Dictionary<WeaponType, WeaponSkillSet> _dicSkills = new Dictionary<WeaponType, WeaponSkillSet>();
// 슬롯 이름 → 런타임 매핑 (Dispatcher 테이블)
private Dictionary<string, IUseableRuntime> _skillBindData = new Dictionary<string, IUseableRuntime>();
// 퀵슬롯에 바인딩할 슬롯 이름 목록 (스킬용)
private static readonly string[] SkillSlotNames =
{ "UseSlot_Q", "UseSlot_E", "UseSlot_R", "UseSlot_T" };
private void Awake()
{
_stateMachine = GetComponent<PlayerStateMachine>();
@@ -47,6 +66,55 @@ private void Awake()
_skillEffects = new ISkillEffect[_maxSlots];
}
private void Start()
{
// 무기별 스킬셋 테이블 구성
foreach (WeaponSkillSet wss in _weaponSkills)
{
_dicSkills[wss.WeaponType] = wss;
}
// 현재 장착 무기의 스킬셋 로드 (무기 미장착 시 스킬 없음)
if (_equippedWeapon != null
&& _dicSkills.TryGetValue(_equippedWeapon.WType, out WeaponSkillSet out_wss)
&& out_wss != null)
{
LoadWeaponSkills(out_wss);
RegisterSkillsToQuickslot(out_wss);
}
// 각 슬롯에 dispatcher를 한 번만 바인딩 (이후 내용물만 _skillBindData로 교체)
foreach (string slotName in SkillSlotNames)
{
string captured = slotName;
GameManager.Instance.Level.BindSlotAction(captured, (state) => DispatchSlot(captured, state));
}
}
private void DispatchSlot(string slotName, InputState state)
{
if (!_skillBindData.TryGetValue(slotName, out IUseableRuntime runtime) || runtime == null)
return;
runtime.Execute(new UseContext
{
Caster = gameObject,
Target = null,
UseInputState = state
});
}
private void RegisterSkillsToQuickslot(WeaponSkillSet wss)
{
for (int i = 0; i < SkillSlotNames.Length; i++)
{
if (i < wss.Skills.Count && wss.Skills[i] != null)
_skillBindData[SkillSlotNames[i]] = wss.Skills[i].CreateRuntime();
else
_skillBindData.Remove(SkillSlotNames[i]);
}
}
private void Update()
{
TickCooldowns();
@@ -149,9 +217,6 @@ public void AreaConfirmInput(InputState inputState)
#endregion
#region
/// <summary>
/// ActivationType에 따라 적절한 흐름을 시작
/// </summary>
private void BeginActivation(int slotIndex)
{
SkillInstance skill = _equippedSkills[slotIndex];
@@ -220,6 +285,22 @@ private void ExecuteSkill(int slotIndex)
skill.StartCooldown();
_chargeTimer = 0f;
_chargingSlot = -1;
// ActionDuration 경과 후 Idle 복귀
float duration = skill.CurrentLevelData.ActionDuration;
if (duration > 0f)
{
_ = Util.RunDelayed(duration, () =>
{
if (this != null && _stateMachine != null
&& _stateMachine.CurrentState == PlayerState.Attack)
_stateMachine.ChangeState(PlayerState.Idle);
}, default);
}
else
{
_stateMachine.ChangeState(PlayerState.Idle);
}
}
private void ClearRemoteTarget()
@@ -416,13 +497,21 @@ private void StartAreaSelect(int slotIndex)
float range = skill.CurrentLevelData.Range;
_areaIndicator.transform.localScale = new Vector3(range * 2f, range * 2f, range * 2f);
}
// 범위 지정 중에는 좌클릭이 확정 용도로 쓰이므로 기본 공격 차단
InputManager.Instance.SuppressNormalAttack = true;
// 이벤트 발행 — 구독자(PlayerCharacterController 등)가 회전모드 등 처리
OnAreaSelectStarted?.Invoke();
}
private void TickAreaSelect()
{
if (_areaSelectSlot < 0 || _areaIndicator == null) return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Mouse.current == null) return;
Vector2 mousePos = Mouse.current.position.ReadValue();
Ray ray = Camera.main.ScreenPointToRay(mousePos);
if (Physics.Raycast(ray, out RaycastHit hit, 200f, _groundLayer))
{
_areaIndicator.transform.position = hit.point;
@@ -444,6 +533,10 @@ private void ConfirmAreaSelect()
_areaIndicator = null;
_areaSelectSlot = -1;
// 범위 지정 종료 → 기본 공격 복구
InputManager.Instance.SuppressNormalAttack = false;
OnAreaSelectEnded?.Invoke();
// ActivationType에 따라 흐름 시작
BeginActivation(slotIndex);
}
@@ -455,6 +548,10 @@ public void CancelAreaSelect()
_areaIndicator = null;
_areaSelectSlot = -1;
// 범위 지정 종료 → 기본 공격 복구
InputManager.Instance.SuppressNormalAttack = false;
OnAreaSelectEnded?.Invoke();
}
#endregion
@@ -488,6 +585,23 @@ public SkillInstance GetSkill(int slotIndex)
return _equippedSkills[slotIndex];
}
public void SkillInputByData(SkillData data, InputState inputState)
{
int slotIndex = FindSlotByData(data);
if (slotIndex < 0) return;
SkillInput(slotIndex, inputState);
}
private int FindSlotByData(SkillData data)
{
for (int i = 0; i < _maxSlots; i++)
{
if (_equippedSkills[i] != null && _equippedSkills[i].Data == data)
return i;
}
return -1;
}
public int MaxSlots => _maxSlots;
#endregion
}

View File

@@ -1,12 +1,24 @@
using UnityEngine;
[System.Serializable]
public abstract class UseableEntry : ScriptableObject
public abstract class UseableEntry : ScriptableObject
{
[Header("기본 정보")]
public string EntryName;
[TextArea] public string EntryDesc;
public Sprite Icon;
public abstract void Use();
public abstract IUseableRuntime CreateRuntime();
}
public interface IUseableRuntime
{
public void Execute(UseContext ctx);
}
public struct UseContext
{
public GameObject Caster;
public GameObject Target;
public InputState UseInputState;
}