부모 클래스 : DragAndDropSlot
자식 클래스 : AnctionBarSlot, InventorySlot, EquipmentSlot
DragAndDropSlot
public class DragAndDropSlot : MonoBehaviour, IDropHandler
{
protected GameObject currentItem;
public GameObject GetCurrentItem() { return currentItem; }
public virtual void OnDrop(PointerEventData eventData)
{
GameObject droppedItem = eventData.pointerDrag;
if (droppedItem != null && droppedItem.GetComponent<ItemContoller>() != null)
{
droppedItem.transform.SetParent(transform);
droppedItem.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
}
}
//아이템 유효성 검사
public virtual bool CheckVaildItem<T>(GameObject item, T? validType = null) where T : struct
{
ItemData itemData = item.GetComponent<ItemDataSC>().GetItem;
if (validType.HasValue)
{
return (item != null && itemData != null && itemData.itemType.Equals(validType));
}
else
{
return (item != null && itemData != null);
}
}
public void AssignCurrentItem(GameObject item)
{
currentItem = item;
public void CleanCurrentItem()
{
currentItem = null;
}
}
ActionBarSlot
public class ActionBarSlot : DragAndDropSlot
{
private KeyCode assignedKey;
public void SetAssigneKey(KeyCode assignedKey) { this.assignedKey = assignedKey; }
public void Update()
{
if (Input.GetKeyDown(assignedKey))
{
if (currentItem != null)
{
UseItem();
}
}
}
void UseItem()
{
currentItem.GetComponent<ConsumableItemSC>().GetItem.Use();
}
public override void OnDrop(PointerEventData eventData)
{
base.OnDrop(eventData);
}
public bool CheckConsumableItem(GameObject item)
{
return base.CheckVaildItem<ItemType>(item, ItemType.Consumable);
}
}
EquipmentSlot
public class EquipmentSlot : DragAndDropSlot
{
//중략...
public bool CheckEquipmentItem(GameObject item)
{
return base.CheckVaildItem<ItemType>(item, ItemType.Equipment);
}
}
먼저 부모 클래스의 OnDrop event처리 메서드를 override하여 각각의 slot의 기능에 맞도록 재구성합니다.
AnctionBarSlot 와 EquipmentSlot을 봅시다. 부모의 CheckValidItem을 통해 해당 슬롯에 들어오는 아이템이 해당 Slot에 알맞은 아이템인지 판별하고 해당 슬롯에 할당할지 결정합니다. 해당 메서드는 주로 itemController에서 호출하여 사용하도록 했습니다.
⇒ Slot에 대한 할당 여부를 왜 ItemController를 통해 하는 가는 아래에서 설명하겠습니다.
이번 파트에서는 다양한 조건을 분리하여 아이템Icon과 UI요소간의 상호작용을 구현하고자 합니다.
저는 크게 [이동, 교환, 복제, 삭제]를 기반으로 조건마다 기능을 설정했습니다.
이동private void TransformItemIcon(Transform slot)
{
transform.SetParent(slot.transform);
}
교환//item1 : 드래그 상태인 아이템 아이콘 Transform
//item2 : 위치를 바꿀 아이템 Transform
private void SwapItemIcon(Transform item1, Transform item2)
{
Transform newParent = item2.parent;
item1.SetParent(newParent);
item2.SetParent(originalParent);
item2.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
}
복제private void DuplicateItemIcon(Transform newTransform)
{
GameObject iconInstance = Instantiate(transform.gameObject);
iconInstance.transform.SetParent(newTransform);
iconInstance.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
transform.transform.SetParent(originalParent);
ItemData itemData = GetComponent<ItemDataSC>().GetItem;
if (itemData != null && itemData is Consumable consumable)
{
consumable.isPresetting = true;
}
}
삭제Destroy(gameObject);
먼저 ActionBar와 Inventory간의 상호작용입니다.
ItemIcon 요소의 컴포넌트를 아래와 같이 설정해줍니다.
Image Sprite[Source Image]는 이전 챕터의 ItemIcon생성기 코드로부터 받습니다.
먼저 ActionBar와 Inventory안에 있는 Slot들에 각각 Tag를 설정해줍니다.
ActionBarSlot, InventorySlot
조건[드래그한 아이템]
- Inventory
- Inventory의 빈 Slot에 Drop
이동- Inventory내 이미 다른 아이템이 할당된 곳에 Drop
교환- Action Bar
- Inventory → ActionBar
- ActionBar에 존재하지 않는 아이템을 Drop했을 때
복제- ActionBar에 존재하는 아이템을 Drop했을 때
무시- ActionBar
- ActionBar → 빈 ActionBarSlot
이동- ActionBar → 외부 요소
삭제- ActionBar 아이템을 우클릭
삭제
public class ItemIconContoller : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
//중략...
//드래그 시작 이벤트
public void OnBeginDrag(PointerEventData eventData)
{
originalParent = transform.parent;
rectTransform.SetParent(transform.root); // 아이템을 최상위로 이동 (canvas)
canvasGroup.blocksRaycasts = false; // 드래그 중 드롭이 가능한지 설정
currentItemData = GetComponent<ItemDataSC>().GetItem;
}
//드래그 진행 이벤트
public void OnDrag(PointerEventData eventData)
{
rectTransform.anchoredPosition += eventData.delta / transform.root.GetComponent<Canvas>().scaleFactor;
}
//드래그 종료 이벤트
public void OnEndDrag(PointerEventData eventData)
{
ItemData itemData = GetComponent<ItemDataSC>().GetItem;
bool isPresetting = false; //임시 플래그.
if (itemData != null && itemData is Consumable consumable)
{
isPresetting = consumable.isPresetting;
}
canvasGroup.blocksRaycasts = true;
originalParent.GetComponent<DragAndDropSlot>().CleanCurrentItem();
/*
조건부 중략...
*/
originalParent.GetComponent<DragAndDropSlot>().AssignCurrentItem(gameObject);
}
DragAndDropSlot [부모 클래스]
public void AssignCurrentItem(GameObject item)
{
currentItem = item;
public void CleanCurrentItem()
{
currentItem = null;
}
item
//originalParent는 Slot
originalParent.GetComponent<DragAndDropSlot>().CleanCurrentItem();
/*
조건부 중략...
*/
originalParent.GetComponent<DragAndDropSlot>().AssignCurrentItem(gameObject);
먼저 이 위 코드에서는 Drag 이벤트가 종료 되었을 때[Drop 이벤트] 실행 코드의 일부분입니다. 위에서 Slot에서 직접적으로 Drop이벤트를 통해 아이템을 할당할 수도 있습니다.
하지만, 슬롯에 아이템을 할당하는 것은 문제가 없지만, 슬롯의 할당 해제에 대한 코드가 복잡하게 됩니다. 왜냐하면, 아이템을 Drag할 때, slot은 특정 이벤트를 받지 못합니다. Drag를 위해 아이템 아이콘을 클릭하면 아이콘이 ray를 삼키기 때문에 slot에는 마우스 관련 이벤트가 발생하기 않습니다.
[물론 ray의 범위를 늘려 다양한 Object들을 인식하는 방법도 있지만, 이는 조건부가 너무 많아지기 때문에 사용하지 않겠습니다.]
그래서 생각한 방법이 Slot은 들어온 아이콘 위치 조정 기능만 수행하고, 나머지 할당-해제 기능은 item에 몰아서 주기로 했습니다.
Hierarchy 주의
rectTransform.SetParent(transform.root);
public void OnBeginDrag(PointerEventData eventData) { originalParent = transform.parent; rectTransform.SetParent(transform.root); // 아이템을 최상위로 이동 (canvas) canvasGroup.blocksRaycasts = false; // 드래그 중 드롭이 가능한지 설정 currentItemData = GetComponent<ItemDataSC>().GetItem; }이 코드를 통해 UI상의 최상위 Object(Canvas)로 이동해야 아이템 아이콘이 맨 앞 Layer로 이동한 상태에서 드래그가 나타나게 됩니다.
해당 코드를 사용하지 않는다면…
아래와 같이 Slot의 뒤로 이동하여 불안전한 모양을 보이게 됩니다.
[gif]
canvasGroup.blocksRaycasts = false;
해당 프로퍼티는 아이템 드래그를 시작할 때 호출됩니다. 드래그 상태에서 해당 아이콘이 이동만 수행하고 다른 Raycast를 통한 로직 수행을 못하도록 방지합니다.
ActionBar는 일반적으로 소비아이템(Consumable)이랑만 상호작용을 합니다.
그래서 소비아이템이 ActionBar에는 오직 하나만 존재하게 됩니다.
만약 플래그를 두지 않는다면..? 아래처럼 동일한 소비아이템이 ActionBar 상에 다수가 존재하게됩니다.
[gif]
해결
Consumable 스크립트에 isPresetting이라는 플래그를 두고 DropEvent가 발생할 때마다 ActionBar에 세팅이 되어 있는 지 확인하는 로직으로 위와 같은 문제를 해결합니다.
ActionBar에서 해당 아이템의 할당을 해제할 때, 해당 플래그도 false로 설정하는 것도 잊지맙시다!
[gif]
물론 ActionBar의 슬롯에 해당 item에 할당되어있는지 확인하는 방법도 있습니다. 하지만 해당 코드는 ActionBarSlot들을 모두 탐색을 해야하고, 호출할 메서드도 많기 때문에 훨씬 비효율적입니다.
해당 ItemIcon은 ScriptableObject로 구현되었기 때문에, 해당 소비아이템에 대해서 일괄적으로 처리하기 때문에 간단하게 Consumable 스크립트에 플래그를 두면 클라이언트 상 Inventory와 ActionBar에 존재하는 해당 아이템에 대해 플래그가 적용되어 사용 및 코드도 간결해집니다.
기본적으로 ActionBar를 통해 할당할 수 있는 Key는 8개라고 해봅시다. 그럼 아래의 코드들을 설정하고 Inspector를 통해 각각의 Slot에 Key들을 할당해줍시다.
public int maxSlotSize = 8;
public GameObject slotPrefab;
public List<ActionBarSlot> slots;
public Transform slotParent;
public KeyCode[] keyCodes = new KeyCode[8];

위처럼 Slot마다 KeyCode가 할당 됩니다.
.png)
동적으로 Slot을 생성하는 로직은 Inventory와 동일하므로 자세한 설명은 앞 포스팅을 참고해주세요!
public void Update()
{
if (Input.GetKeyDown(assignedKey))
{
if (currentItem != null)
{
UseItem();
}
}
}
void UseItem()
{
currentItem.GetComponent<ConsumableItemSC>().GetItem.Use();
}
그럼 할당된 Key를 누르면 해당 Slot의 아이템을 소모하게 됩니다. 앞에서 말한 것처럼, Icon 역시 Scriptable Object를 통해 만들어 졌으므로 Inventory 내 동일 아이템 수도 줄어듭니다.
[gif]
public class ActionBar: MonoBehaviour
{
public int maxSlotSize = 8;
public GameObject slotPrefab;
public List<KeyboardSlot> slots;
public Transform slotParent;
public KeyCode[] keyCodes = new KeyCode[8];
private void Start()
{
CreateKeyboardSlot();
}
void CreateKeyboardSlot()
{
float spacingX = 0f;
Vector2 startPosition = new Vector2(-300f, 45f);
Vector2 componentSize = new Vector2(85f, 85f);
for (int i = 0; i < maxSlotSize; i++)
{
GameObject slotInstance = Instantiate(slotPrefab);
slotInstance.transform.SetParent(slotParent);
slotInstance.GetComponent<KeyboardSlot>().SetAssigneKey(keyCodes[i]);
slots.Add(slotInstance.GetComponent<KeyboardSlot>());
RectTransform rectTransform = slotInstance.GetComponent<RectTransform>();
rectTransform.sizeDelta = componentSize;
rectTransform.anchoredPosition = new Vector2(startPosition.x + i * (componentSize.x + spacingX), startPosition.y);
rectTransform.localScale = Vector2.one;
}
}
public void UpdateActionBar()
{
foreach(KeyboardSlot slot in slots)
{
if(slot.gameObject.transform.childCount == 0) continue;
else
{
slot.SetCurrentItem(slot.gameObject.transform.GetChild(0).gameObject);
}
}
}
}
public class ActionBarSlot : DragAndDropSlot
{
private KeyCode assignedKey;
public void SetAssigneKey(KeyCode assignedKey) { this.assignedKey = assignedKey; }
public void Update()
{
if (Input.GetKeyDown(assignedKey))
{
if (currentItem != null)
{
UseItem();
}
}
}
void UseItem()
{
currentItem.GetComponent<ConsumableItemSC>().GetItem.Use();
}
public override void OnDrop(PointerEventData eventData)
{
base.OnDrop(eventData);
}
public bool CheckConsumableItem(GameObject item)
{
return base.CheckVaildItem<ItemType>(item, ItemType.Consumable);
}
}
이렇게 아이템 DragAndDrop 기능의 이벤트들을 분리하여 실제 RPG게임에서의 아이템 처리 이벤트 로직을 구현해보았습니다.
다음에는 장비 아이템을 장비창에 넣고 캐릭터의 스탯을 Update하는 로직을 구현해보겠습니다.