2026-04-13 스킬,퀵슬롯 시스템 수정중

This commit is contained in:
2026-04-13 18:04:55 +09:00
parent de0ba90953
commit 71a6fda0da
57 changed files with 9074 additions and 59 deletions

View File

@@ -43,6 +43,7 @@ public class InputManager : MonoBehaviour
public event Action OnKeyDown_DownArrowEvent;
public event Action OnKeyDown_EnterEvent;
public event Action OnKeyDown_IKeyEvent;
public event Action OnKeyDown_KKeyEvent;
@@ -94,6 +95,7 @@ public void SetUIInputMap(string mapName)
BindActionUI("OnKeyDown_DownArrow", OnKeyDown_DownArrow);
BindActionUI("OnKeyDown_Enter", OnKeyDown_Enter);
BindActionUI("OnkeyDown_IKey", OnKeyDown_IKey);
BindActionUI("OnKeyDown_KKey", OnKeyDown_KKey);
_uiInputActionMap.Disable();
@@ -128,6 +130,7 @@ public void SetCharacterInputMap(string mapName)
BindActionCharacter("Interaction", OnInteraction);
BindActionCharacter("OnkeyDown_IKey", OnKeyDown_IKey);
BindActionCharacter("OnKeyDown_KKey", OnKeyDown_KKey);
_characterInputActionMap.Disable();
@@ -307,5 +310,11 @@ private void OnKeyDown_IKey(InputAction.CallbackContext ctx)
if(ctx.started)
OnKeyDown_IKeyEvent?.Invoke();
}
private void OnKeyDown_KKey(InputAction.CallbackContext ctx)
{
if (ctx.started)
OnKeyDown_KKeyEvent?.Invoke();
}
#endregion
}

View File

@@ -385,4 +385,31 @@ public void ItemUse(int slotIndex)
}
UpdateUI();
}
// 퀵슬롯 동기화용: 해당 아이템 데이터의 총 보유량 합산 (0이면 퀵슬롯에서 자동 해제)
public int GetTotalStack(Item itemData)
{
if (itemData == null) return 0;
int total = 0;
for (int i = 0; i < Items.Length; i++)
{
if (Items[i] != null && Items[i].Data == itemData)
total += Items[i].CurrentStack;
}
return total;
}
// 퀵슬롯에서 아이템 사용 시 호출: 해당 데이터가 담긴 가장 앞 슬롯을 사용
public void UseItemByData(Item itemData)
{
if (itemData == null) return;
for (int i = 0; i < Items.Length; i++)
{
if (Items[i] != null && Items[i].Data == itemData)
{
ItemUse(i);
return;
}
}
}
}

View File

@@ -126,6 +126,7 @@ public void OnSceneLoaded(Scene scene, LoadSceneMode mode)
//UI매핑
InputManager.Instance.OnKeyDown_IKeyEvent += GameManager.Instance.InGameUI.InventoryToggle;
InputManager.Instance.OnKeyDown_KKeyEvent += GameManager.Instance.InGameUI.SkillWindowToggle;
//화면 켜기
}
@@ -204,6 +205,7 @@ private void OnDestroy()
if(GameManager.Instance != null)
{
InputManager.Instance.OnKeyDown_IKeyEvent -= GameManager.Instance.InGameUI.InventoryToggle;
InputManager.Instance.OnKeyDown_KKeyEvent -= GameManager.Instance.InGameUI.SkillWindowToggle;
}
}
}

View File

@@ -9,6 +9,8 @@ public class InGameUIManager : BaseUIManager
public TooltipUI Tooltip;
public Transform DragCanvas;
public GameObject InventoryRoot;
public QuickSlotController QuickSlot;
public SkillWindowUI SkillWindow;
[SerializeField] private GameObject _crosshairRoot;
@@ -32,6 +34,26 @@ public void InventoryToggle()
InventoryOnOff(!InventoryRoot.activeSelf);
}
public void SkillWindowToggle()
{
if (SkillWindow == null) return;
SkillWindow.Toggle();
// 인벤토리와 동일한 커서/입력 모드 규칙 적용
bool anyOpen = InventoryRoot.activeSelf || SkillWindow.gameObject.activeSelf;
if (anyOpen)
{
InputManager.Instance.ActiveOnlyOneActionMap("InGameUI");
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
else
{
InputManager.Instance.ActiveOnlyOneActionMap("Character");
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
public void InventoryOnOff(bool isOn)
{
InventoryRoot.SetActive(isOn);

View File

@@ -18,6 +18,7 @@ public class SkillModule : MonoBehaviour
private SkillInstance[] _equippedSkills;
private ISkillEffect[] _skillEffects;
private SkillData[] _equippedSkillDataCache; // 장착 순서 보존용
// 차지
private int _chargingSlot = -1;
@@ -49,13 +50,6 @@ public class SkillModule : MonoBehaviour
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>();
@@ -64,6 +58,7 @@ private void Awake()
_equippedSkills = new SkillInstance[_maxSlots];
_skillEffects = new ISkillEffect[_maxSlots];
_equippedSkillDataCache = new SkillData[_maxSlots];
}
private void Start()
@@ -74,46 +69,17 @@ private void Start()
_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]);
}
}
// 장착된 스킬 목록 반환 (스킬창 UI에서 사용)
public IReadOnlyList<SkillData> GetEquippedSkillDatas() => _equippedSkillDataCache;
private void Update()
{
@@ -133,11 +99,13 @@ public void LoadWeaponSkills(WeaponSkillSet skillSet)
{
_equippedSkills[i] = new SkillInstance(skillSet.Skills[i]);
_skillEffects[i] = ResolveEffect(skillSet.Skills[i].TargetType);
_equippedSkillDataCache[i] = skillSet.Skills[i];
}
else
{
_equippedSkills[i] = null;
_skillEffects[i] = null;
_equippedSkillDataCache[i] = null;
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 619b9f88d08881b4cb478bfd9f27c22a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,234 @@
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class QuickSlot : MonoBehaviour, IDropHandler, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("Slot Binding")]
public string SlotName; // "UseSlot_Q", "UseSlot_E", ...
[Header("References")]
[SerializeField] private Image _iconImage;
[SerializeField] private TextMeshProUGUI _stackText;
public UseableEntry CurrentEntry { get; private set; }
private Transform _dragTransform;
private Transform _iconOriginalParent;
private CanvasGroup _iconCanvasGroup;
private void Awake()
{
if (_iconImage != null)
{
_iconCanvasGroup = _iconImage.GetComponent<CanvasGroup>();
if (_iconCanvasGroup == null)
_iconCanvasGroup = _iconImage.gameObject.AddComponent<CanvasGroup>();
}
ClearSlot();
}
private void Start()
{
_dragTransform = GameManager.Instance.InGameUI.DragCanvas;
}
private void Update()
{
// 아이템 퀵슬롯은 인벤토리와 동기화: 스택 수량 갱신 및 소진 시 자동 해제
if (CurrentEntry is Item itemData)
{
int total = GameManager.Instance.Inventory.GetTotalStack(itemData);
if (total <= 0)
{
ClearSlot();
return;
}
if (_stackText != null)
{
_stackText.text = itemData.IsStackable ? total.ToString() : "";
_stackText.enabled = itemData.IsStackable;
}
}
else if (_stackText != null)
{
_stackText.enabled = false;
}
}
public void SetEntry(UseableEntry entry)
{
CurrentEntry = entry;
UpdateUI();
}
public void ClearSlot()
{
CurrentEntry = null;
if (_iconImage != null)
{
_iconImage.sprite = null;
_iconImage.enabled = false;
}
if (_stackText != null)
{
_stackText.text = "";
_stackText.enabled = false;
}
}
public void UpdateUI()
{
if (CurrentEntry == null || CurrentEntry.Icon == null)
{
if (_iconImage != null) _iconImage.enabled = false;
return;
}
_iconImage.sprite = CurrentEntry.Icon;
_iconImage.enabled = true;
}
public void Execute(InputState state)
{
if (CurrentEntry == null) return;
if (CurrentEntry is Item itemData)
{
// 아이템은 누를 때(Started)만 사용
if (state == InputState.Started)
GameManager.Instance.Inventory.UseItemByData(itemData);
}
else if (CurrentEntry is SkillData skillData)
{
// 스킬은 Started/Canceled 모두 전달 (Charge/Channel 지원)
var character = GameManager.Instance.Level.CurrentCharacter;
if (character == null) return;
if (character.TryGetComponent<SkillModule>(out var skillModule))
skillModule.SkillInputByData(skillData, state);
}
}
public void OnDrop(PointerEventData eventData)
{
GameObject dropped = eventData.pointerDrag;
if (dropped == null) return;
// 인벤토리에서 드래그
InventoryItemControl invItem = dropped.GetComponent<InventoryItemControl>();
if (invItem != null && invItem.ParentSlot != null && invItem.ParentSlot.currentItem != null)
{
SetEntry(invItem.ParentSlot.currentItem.Data);
return;
}
// 다른 퀵슬롯에서 드래그 → 스왑
QuickSlot otherSlot = dropped.GetComponent<QuickSlot>();
if (otherSlot != null && otherSlot != this)
{
UseableEntry temp = CurrentEntry;
SetEntry(otherSlot.CurrentEntry);
otherSlot.SetEntry(temp);
}
// 스킬창에서 드래그는 스킬창 제작 시 SkillEntryControl 같은 컴포넌트를 여기서 감지
}
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.button == PointerEventData.InputButton.Right)
{
ClearSlot();
}
}
public void OnBeginDrag(PointerEventData eventData)
{
if (CurrentEntry == null || _iconImage == null)
{
eventData.pointerDrag = null;
return;
}
_iconOriginalParent = _iconImage.transform.parent;
_iconImage.transform.SetParent(_dragTransform);
_iconCanvasGroup.blocksRaycasts = false;
}
public void OnDrag(PointerEventData eventData)
{
if (_iconImage == null) return;
_iconImage.transform.position = eventData.position;
}
public void OnEndDrag(PointerEventData eventData)
{
_iconCanvasGroup.blocksRaycasts = true;
_iconImage.transform.SetParent(_iconOriginalParent);
_iconImage.transform.localPosition = Vector3.zero;
UpdateUI();
}
}
/*
1. (InputManager, LevelManager)
: OnKeyDown_KKeyEvent + OnKeyDown_KKey K
: LevelManager에서 K키 InGameUIManager.SkillWindowToggle에
: Input Action Asset의 Character와 InGameUI OnKeyDown_KKey
2. SkillModule ( )
Before: Q/E/R/T가 InputManager에
After: _equippedSkills만 . .
SkillSlotNames, _skillBindData, DispatchSlot, RegisterSkillsToQuickslot, BindSlotAction
_equippedSkillDataCache[] + GetEquippedSkillDatas() ( )
3. QuickSlot ( )
: 1. UseableEntry
SlotName (UseSlot_Q~UseSlot_G)
SetEntry(entry) / ClearSlot() / Execute(state)
Execute(): Item이면 Inventory.UseItemByData(), SkillData면 SkillModule.SkillInputByData()
:
OnDrop: ,
OnBeginDrag/OnEndDrag: ( )
OnPointerClick :
Update(): , 0
4. QuickSlotController (QuickSlotRoot에 )
:
Awake(): QuickSlot[]
Start(): SlotName으로 LevelManager.BindSlotAction() slot.Execute(state)
RegisterEntry(slotName, entry) / GetSlot(slotName) API
5. InventoryManager ( )
GetTotalStack(Item): ( )
UseItemByData(Item):
6.
SkillEntry: . IBeginDragHandler/IDragHandler/IEndDragHandler로 SetEntry() .
SkillWindowUI: Toggle() / + Refresh() SkillModule.GetEquippedSkillDatas()
7. InGameUIManager
QuickSlot, SkillWindow
SkillWindowToggle(): /
Q
InputManager.OnKeyDown_SlotQEvent
LevelManager.BindSlotAction이
QuickSlot.Execute(state)
: Item Inventory.UseItemByData
SkillData SkillModule.SkillInputByData
K키 SkillWindow SkillModule에서
SkillEntry QuickSlot
QuickSlot.SetEntry(skillData)
InventoryItemControl
QuickSlot.OnDrop SetEntry(itemData)
QuickSlot.Update가 ClearSlot
*/

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1b986e78d40a4ec4ebe640f5dc4defdc

View File

@@ -0,0 +1,41 @@
using UnityEngine;
public class QuickSlotController : MonoBehaviour
{
private QuickSlot[] _slots;
private void Awake()
{
_slots = GetComponentsInChildren<QuickSlot>(true);
}
private void Start()
{
foreach (var slot in _slots)
{
if (string.IsNullOrEmpty(slot.SlotName)) continue;
QuickSlot captured = slot;
GameManager.Instance.Level.BindSlotAction(captured.SlotName, (state) => captured.Execute(state));
}
}
// 외부(스킬창/SkillModule 등)에서 슬롯에 등록할 때 호출
public void RegisterEntry(string slotName, UseableEntry entry)
{
foreach (var slot in _slots)
{
if (slot.SlotName == slotName)
{
slot.SetEntry(entry);
return;
}
}
}
public QuickSlot GetSlot(string slotName)
{
foreach (var slot in _slots)
if (slot.SlotName == slotName) return slot;
return null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0150a6644db52824c9b546e9f34c221d

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2f4e891fc20854a4f9c63c81ade744c6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class SkillEntry : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[SerializeField] private Image _iconImage;
[SerializeField] private TextMeshProUGUI _nameText;
public SkillData Data { get; private set; }
private Transform _dragTransform;
private Transform _originalParent;
private CanvasGroup _canvasGroup;
private Vector3 _originalLocalPos;
private void Awake()
{
_canvasGroup = GetComponent<CanvasGroup>();
if (_canvasGroup == null)
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
public void SetData(SkillData data)
{
Data = data;
if (_iconImage != null)
{
_iconImage.sprite = data != null ? data.Icon : null;
_iconImage.enabled = data != null && data.Icon != null;
}
if (_nameText != null)
_nameText.text = data != null ? data.EntryName : "";
}
public void OnBeginDrag(PointerEventData eventData)
{
if (Data == null)
{
eventData.pointerDrag = null;
return;
}
_dragTransform = GameManager.Instance.InGameUI.DragCanvas;
_originalParent = transform.parent;
_originalLocalPos = transform.localPosition;
transform.SetParent(_dragTransform);
_canvasGroup.blocksRaycasts = false;
}
public void OnDrag(PointerEventData eventData)
{
transform.position = eventData.position;
}
public void OnEndDrag(PointerEventData eventData)
{
_canvasGroup.blocksRaycasts = true;
// 드롭된 오브젝트 판정 — 퀵슬롯이면 등록
GameObject hovered = eventData.pointerEnter;
if (hovered != null)
{
QuickSlot slot = hovered.GetComponentInParent<QuickSlot>();
if (slot != null && Data != null)
{
slot.SetEntry(Data);
}
}
// 엔트리는 항상 제자리로 복귀 (원본은 목록에 남아있어야 함)
transform.SetParent(_originalParent);
transform.localPosition = _originalLocalPos;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 58eac31de8e96ed4c9a5fa7206bd63f3

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using UnityEngine;
public class SkillWindowUI : MonoBehaviour
{
[SerializeField] private Transform _listContentRoot;
[SerializeField] private GameObject _skillEntryPrefab;
private readonly List<SkillEntry> _entries = new List<SkillEntry>();
public void Toggle()
{
bool willOpen = !gameObject.activeSelf;
gameObject.SetActive(willOpen);
if (willOpen) Refresh();
}
public void Refresh()
{
// 기존 엔트리 정리
foreach (Transform child in _listContentRoot)
Destroy(child.gameObject);
_entries.Clear();
var character = GameManager.Instance.Level.CurrentCharacter;
if (character == null) return;
if (!character.TryGetComponent<SkillModule>(out var skillModule)) return;
var skills = skillModule.GetEquippedSkillDatas();
for (int i = 0; i < skills.Count; i++)
{
if (skills[i] == null) continue;
GameObject entryObj = Instantiate(_skillEntryPrefab, _listContentRoot);
SkillEntry entry = entryObj.GetComponent<SkillEntry>();
if (entry != null)
{
entry.SetData(skills[i]);
_entries.Add(entry);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0abbd5b063416be4c97a8429d13b4b53