커서 매니저를 인벤토리 시스템에 포함시킬까 말까 고민을 많이 했다.
사실 커서 매니저의 역할은 인벤토리 시스템에 국한되는 것이 아니라 다양한 문맥에서 다양한 이벤트 호출에 의해서 커서를 변경하는데 이 이벤트가 인벤토리 시스템도 존재한다.
그래서 완벽한 인벤토리를 위해서는 필요하다고 생각해서 인벤토리 시스템에 엮었다.
그럼 시작해보자.
커서는 앞서 말했듯이 다양한 문맥과 다양한 이벤트에서 변경된다. 즉, 전역적으로 접근을 할 수 있있으면 편하다는 말이다.
하지만 전역적으로 접근을 하면 의존성 관리 측면에서 피해를 보는 면이 많기 때문에 SO를 사용하여 여러 씬에서 사용할 수 있게끔 하되, SO를 의존성 주입하는 방식을 채택해볼까 했다.
우선 커서가 변경될 경우를 생각하여 어떤 이벤트에서 어떤 커서가 될 지에 대한 설계가 우선적으로 필요하다.
내가 생각한 경우는 다음과 같다.
1. 기본 커서
2. 아이템을 잡을 수 있는 경우, 아이템을 사용할 수 있는 경우
3. 아이템을 잡은 경우
4. 대기가 걸린 경우
5. NPC에 커서가 닿았을 경우
6. 적에서 커서가 닿았을 경우
따라서 커서 모드를 표현하기 위해 다음과 같이 CursorMode를 정의한다.
public enum CursorMode
{
NONE = -1,
DEFAULT = 0,
CAN_GRAB = 1,
GRAB = 2,
WAITING = 3,
CAN_TALK = 4,
CAN_ATTACK = 5,
}
앞서 정의한 CursorMode에 해당하는 동작들을 연결시키기 위해서는 커서의 텍스처와 커서의 핫스팟에 대한 정보가 필요하다.
커서 모드, 커서 텍스처, 커서 핫스팟을 하나로 묶어 커서 데이터로 구성하고 커서 모드를 통해 해당하는 데이터를 불러올 수 있게끔하기 위함이다.
using UnityEngine;
[System.Serializable] // 인스펙터에 커서 데이터를 노출시키기 위함이다.
public class CursorData
{
[Header("커서 모드")]
public CursorMode Mode;
[Header("커서 텍스처")] // 커서 모드에 따라 변경될 텍스처
public Texture2D Cursor;
[Header("커서 핫스팟")] // 커서 모드에 따라 변경될 핫스팟
public Vector2 Hotspot;
}
핫스팟에 대해서 간단하게 설명하자면 클릭될 위치를 말한다.

좌상단은 (0, 0)이며 우하단은 (1, 1)의 좌표를 가진다. 이 핫스팟의 위치를 잘 설정해야 위화감이 없이 자연스럽게 클릭된다.
하지만 너무 딱딱 맞추려고 하면 마우스 커서 자체의 피벗이 커서마다 달라지게 되어 그것도 위화감이 생긴다. 따라서 적당한 선에서 타협하는 것이 좋다.
커서 매니저라는 네이밍도 좋지만 앞선 인벤토리 시스템을 구성하면서 아이템 매니저를 아이템 데이터베이스라는 이름으로 네이밍했기 때문에 통일감을 위해서 커서 데이터베이스라고 칭하겠다.
커서 데이터베이스는 SO로도 구현할 수 있지만 여러 방식으로 구현될 수 있다. 변경의 여지가 다분하다는 점이다. 따라서 나는 커서 데이터베이스를 위한 인터페이스를 정의했다.
public interface ICursorDataBase
{
// CursorMode에 해당하는 커서 데이터를 이용하여 커서를 변경한다.
void SetCursor(CursorMode mode);
}
그리고 이를 의도한 대로 SO를 이용하여 구체화한다.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Cursor DataBase", menuName = "SO/DB/Cursor DataBase")]
public class CursorDataBase : ScriptableObject, ICursorDataBase
{
[Header("커서 데이터의 목록")]
[SerializeField] private List<CursorData> m_cursor_datas;
private Dictionary<CursorMode, CursorData> m_cursor_dict;
private CursorMode m_current_mode;
#if UNITY_EDITOR
private void OnEnable()
{
Initialize();
}
#endif
private void Initialize()
{
if (m_cursor_dict != null)
{
return;
}
m_current_mode = CursorMode.NONE;
m_cursor_dict = new();
if (m_cursor_datas == null || m_cursor_datas.Count == 0)
{
return;
}
foreach (var data in m_cursor_datas)
{
m_cursor_dict.TryAdd(data.Mode, data);
}
}
public void SetCursor(CursorMode mode)
{
if (m_cursor_dict == null)
{
Initialize();
}
if (m_current_mode.Equals(mode))
{
return;
}
if (m_cursor_dict.TryGetValue(mode, out var data))
{
var pivot = new Vector2(data.Cursor.width * data.Hotspot.x,
data.Cursor.height * data.Hotspot.y);
Cursor.SetCursor(data.Cursor, pivot, UnityEngine.CursorMode.Auto);
m_current_mode = mode;
}
}
}
설명없이도 충분히 이해할만한 코드라고 생각해서 설명은 생략하겠다.
앞서 ItemSlotView에서 커서 데이터베이스를 SerializeField 어트리뷰트를 이용하여 인스펙터에서 주입받도록 구현했었다.
그리고 이에 대한 확장을 위해서 SetCursor()를 남겨뒀었는데 이를 확장한다. 왜 CursorDataBase를 구현해놓고 굳이 SetCursor()를 또 구현하는지 의문이 들 수도 있다.
SetCursor()를 구현하지 않으면 ItemSlotPresenter에서 이벤트 처리를 할 때 직접적으로 CursorDataBase에 의존해야만 한다.
이 상황은 둘 사이의 결합도를 올려 모듈화를 해치는 원인이기 때문에 Presenter로부터 결합도를 낮출 필요가 있다.
// ItemSlotView의 일부에서
public void SetCursor(CursorMode mode)
{
m_cursor_db.SetCursor(mode);
}
using InventoryService;
using SkillService;
public class ItemSlotFactory
{
private readonly IInventoryService m_inventory_service;
private readonly IItemDataBase m_item_db;
private readonly ICursorDataBase m_cursor_db; // 확장된 부분
public ItemSlotFactory(IInventoryService inventory_service,
IItemDataBase item_db,
ICursorDataBase cursor_db) // 확장된 부분
{
m_inventory_service = inventory_service;
m_item_db = item_db;
m_cursor_db = cursor_db; // 확장된 부분
}
// 이전과 동일
}
// ItemSlotFactoryInstaller의 일부에서
public void Install()
{
var item_slot_factory = new ItemSlotFactory(ServiceLocator.Get<IInventoryService>(),
m_item_db,
m_cursor_db);
DIContainer.Register<ItemSlotFactory>(item_slot_factory);
}
확장이 모두 끝났다면 커서 데이터베이스를 생성하고 데이터를 채워넣어야 한다.

위의 사진과 같이 커서 데이터베이스를 생성한다. 그리고 다음과 같이 데이터베이스에 데이터를 채워넣는다.

설명하지 않은 부분이 있는데 커서 데이터에 사용되는 커서 텍스처는 Texture Type을 Cursor로 설정해야만 사용할 수 있다.

이번 글에서는 커서 데이터베이스를 생성하여 의존성 주입하는 것을 구현해봤다.
다음 글에서는 아이템 슬롯 컨텍스트를 구현하여 여러 문맥에서 범용적으로 사용할 수 있도록 아이템 슬롯을 디자인할 것이다.