XR플밍 - 11. 사전 합반 프로젝트 8일차(6/30)

이형원·2025년 6월 30일
0

XR플밍

목록 보기
120/215

0.들어가기에 앞서
맡은 시스템의 프로토타입이 슬슬 완성되어가고 있다. 완성된 결과물을 합치는 작업 단계에 들어가야 한다.

1. 금일 한 업무 정리, 구현 내용

분해 시스템의 기능을 구현했다.

구체적은 내용을 다루자면 다음과 같다.

  • 분해용 데이터 저장소는 인벤토리와 분리 - Static 선언되지 않은 별도의 Slot으로 생성.
  • 해당 분해용 Slot과 인벤토리 Slot간의 이동과정 구현
  • 아이템의 일괄 이동 - 분해슬롯으로 일괄이동 / 인벤토리로 일괄이동 구현
  • 아이템 분해 후 에너지 출력(연결 과정은 진행x)

2. 문제의 발생과 해결 과정

2.1 분해 슬롯의 구현 방법에 대한 고민

크래프팅 시스템을 구현했을 당시에는 추가로 슬롯을 만들지 않고 일종의 꼼수를 부리는 방식으로, InventoryManager의 내용을 추가하지는 않는 방식으로 진행했었다. 하지만 분해시스템부터는 이와 같은 추가 슬롯 및 상호작용의 구현을 피할 수 없게 되었다.

이렇게 UI 디자인을 요구했으니 별도의 분해대 슬롯을 구현해서 해당 배열에 아이템 정보를 저장해야 한다.
처음에는 이걸 구현하기 위해서는 어쩔 수 없이 InventoryManager에 새로운 슬롯을 추가할 수밖에 없다고 생각했다.

하지만 처음에 그런 방향으로 만들다가, 문득 든 생각이 InventoryManager가 그런 많은 책임을 들고 있을 필요가 있는지에 대한 의문이 들었다. 그리고 이와 같이 Static으로 선언했을 때 과도하게 소모되는 메모리 양도 많아질 것이다.

인벤토리의 경우에는 그 중요성 때문에 Static으로 선언한다 쳐도, 분해 슬롯의 경우에는 상시로 사용하는 옵션이 아닐 수도 있다. 그렇다면 이런 식의 설계는 적절하지 못할 수도 있다.

결국엔 조금 귀찮은 방향이 될 수도 있겠지만 분해슬롯용 데이터관리자를 따로 만들고, 분해용 슬롯을 만들어서 연결과정을 만드는 것에 초점을 두었다.

2.2 분해 시스템 설계 과정

분해 시스템은 크게 세 가지 코드로 구성했다.

  • 데이터를 담고 관리하는 DecompositionSystem
  • 하나의 슬롯의 출력을 담당하는 DecompositionSlotUnit
  • 슬롯 그룹의 출력을 컨트롤하는 DecompositionController

이와 같이 작업하고 세부적으로 내용이 추가된 건 ItemSlotUnit과 InventoryManager에 추가했다.

  • DecompositionSystem
using System;
using UnityEngine;

public class DecompositionSystem : MonoBehaviour
{
    [SerializeField] private ItemSO[] _decompositionItem;
    [SerializeField] private int[] _decompositionStack;

    public event Action OnDecompositionSlotUpdated;

    /// <summary>
    /// Read Data of decomposition slot.
    /// </summary>
    /// <param name="index"></param>
    /// <param name="stack"></param>
    /// <returns></returns>
    public ItemSO ReadFromDecompositionSlot(int index, out int stack)
    {
        stack = _decompositionStack[index];
        return _decompositionItem[index];
    }

    /// <summary>
    /// Used as OnClick event.
    /// Add all decomposible item into slots.
    /// </summary>
    public void AddAllDecomposibleItemIntoSlot()
    {
        int inventoryLength = InventoryManager.Instance.InventoryCount;
        for(int i = 0; i < inventoryLength; i++)
        {
            ItemSO item = InventoryManager.Instance.ReadFromInventory(i, out int stack);

            if(item!= null && item.IsDecomposable)
            {
                InventoryManager.Instance.SendItemToDecomposition(i);
            }
        }
    }

    /// <summary>
    /// Used as OnClick event.
    /// Return all items from decomposition slot to inventory.
    /// </summary>
    public void ReturnAllDecomposibleItemIntoSlot()
    {
        for(int i = 0; i < _decompositionItem.Length; i++)
        {
            InventoryManager.Instance.ReturnItemFromDecomposition(i);
        }
    }

    /// <summary>
    /// Used as OnClick event.
    /// Decompose all items in the slots and return energy gain.
    /// </summary>
    public void DecomposeAllItems()
    {
        int energy = 0;
        for(int i = 0; i < _decompositionItem.Length;i++)
        {
            if(_decompositionItem[i] != null) energy += _decompositionItem[i].Energy * _decompositionStack[i];
            _decompositionItem[i] = null;
            _decompositionStack[i] = 0;
        }
        OnDecompositionSlotUpdated?.Invoke();

        Debug.Log(energy);
        // TODO : energy gain.
    }

    /// <summary>
    /// Add item to Decomposition Slot.(From Inventory)
    /// </summary>
    /// <param name="item"></param>
    /// <param name="index"></param>
    /// <param name="stack"></param>
    /// <returns></returns>
    public bool AddItemToDecompositionSlot(ItemSO item, int stack)
    {
        if (!item.IsDecomposable) return false;

        int remain = stack;
        while (remain > 0)
        {
            for (int i = 0; i < _decompositionItem.Length; i++)
            {
                if (_decompositionItem[i] == item)
                {
                    remain = DecompositionSlotTryAdd(item, i, remain);
                    if (remain <= 0) break;
                }
            }

            if (remain <= 0) break;

            for (int i = 0; i < _decompositionItem.Length; i++)
            {
                if (_decompositionItem[i] == null)
                {
                    remain = DecompositionSlotTryAdd(item, i, remain);
                    if (remain <= 0) break;
                }
            }

            if (remain <= 0) break;

            else
            {
                Debug.Log("slot is full"); return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Return Item to the Inventory.
    /// </summary>
    /// <param name="index"></param>
    public void ReturnItemToInventory(int index)
    {
        if(index == -1) return; 

        if (_decompositionItem[index] != null)
        {
            InventoryManager.Instance.AddItemToInventory(_decompositionItem[index], _decompositionStack[index]);

            _decompositionItem[index] = null;
            _decompositionStack[index] = 0;
            OnDecompositionSlotUpdated?.Invoke();
        }
    }

    /// <summary>
    /// Move item in the decomposition slot.
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="endIndex"></param>
    public void MoveItemInDecompositionSlot(int startIndex, int endIndex)
    {
        // if there is no item in start cell.
        if (startIndex == -1 || endIndex == -1)
        {
            return;
        }

        // if there is item in start cell and drag into empty cell.
        else if (_decompositionItem[startIndex] != null && endIndex != -1 && _decompositionItem[endIndex] == null)
        {
            DecompositionSlotTryAdd(_decompositionItem[startIndex], endIndex, _decompositionStack[startIndex]);
            _decompositionItem[startIndex] = null;
            _decompositionStack[startIndex] = 0;
            OnDecompositionSlotUpdated?.Invoke();
        }

        // if there is item both in start and end
        else if (_decompositionItem[startIndex] != null && _decompositionItem[endIndex] != null)
        {
            // if item is same and stack is same, return.
            if (_decompositionItem[startIndex] == _decompositionItem[endIndex] && _decompositionStack[startIndex] == _decompositionStack[endIndex]) return;

            ItemSO tempItem = _decompositionItem[endIndex];
            int tempStack = _decompositionStack[endIndex];

            _decompositionItem[endIndex] = _decompositionItem[startIndex];
            _decompositionStack[endIndex] = _decompositionStack[startIndex];

            _decompositionItem[startIndex] = tempItem;
            _decompositionStack[startIndex] = tempStack;
            OnDecompositionSlotUpdated?.Invoke();
        }
    }

    /// <summary>
    /// Try add item in the decomposition slot.
    /// </summary>
    /// <param name="item"></param>
    /// <param name="index"></param>
    /// <param name="amount"></param>
    /// <returns></returns>
    private int DecompositionSlotTryAdd(ItemSO item, int index, int amount)
    {
        _decompositionItem[index] = item;
        // If the stack is enough.
        if (amount <= (item.MaxStackSize - _decompositionStack[index]))
        {
            _decompositionStack[index] += amount;
            OnDecompositionSlotUpdated?.Invoke();
            return 0;
        }
        // If the stack is not enough and has space.
        else if (item.MaxStackSize > _decompositionStack[index])
        {
            amount -= (item.MaxStackSize - _decompositionStack[index]);
            _decompositionStack[index] = item.MaxStackSize;            
            OnDecompositionSlotUpdated?.Invoke();
            return amount;
        }
        // If the stack is full.
        else
        {
            return amount;
        }
    }
}

아이템의 일괄 추가 및 제거 기능은 여기서 구현한 다음 버튼의 OnClick 이벤트로 연결시켰다. 그 다음 아이템의 상호 이동 같은 경우에는 InventoryManager와 연결하여 설계하고, 그걸 실제로 실행하는 과정을 ItemSlotUnit에 추가했다.

  • InventoryManager 추가 내용
...
    #region Decomposition

    /// <summary>
    /// Send item to decomposition slot.
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="endindex"></param>
    public void SendItemToDecomposition(int startIndex)
    {
       if(_decompositionSlotData.AddItemToDecompositionSlot(_inventoryItem[startIndex], _inventoryStack[startIndex]))
       {
            _inventoryItem[startIndex] = null;
            _inventoryStack[startIndex] = 0;
            OnInventorySlotChanged?.Invoke();
       }
    }

    /// <summary>
    /// Get Item to InventorySlot from decomposition slot.
    /// </summary>
    /// <param name="startIndex"></param>
    public void ReturnItemFromDecomposition(int startIndex)
    {
        _decompositionSlotData.ReturnItemToInventory(startIndex);
        OnInventorySlotChanged?.Invoke();
    }

    /// <summary>
    /// Move Item in the decomposition slot.
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="endIndex"></param>
    public void MoveItemInDecompositionSlot(int startIndex, int endIndex)
    {
        _decompositionSlotData.MoveItemInDecompositionSlot(startIndex, endIndex);
    }

    #endregion
}

그리고 이 과정에서의 아이템의 이동 방법을 구현하는 과정이 꽤 어려웠다. 아무래도 하드코딩을 할 수밖에 없었는데, 이와 같은 과정이 되었다.

  • ItemSlotUnit
...
    /// <summary>
    /// End Dragging.(interface)
    /// </summary>
    /// <param name="eventData"></param>
    public void OnEndDrag(PointerEventData eventData)
    {
        DragSlot.Instance.ClearDragSlot();

        if ((_startIsInventorySlot == true && _endIsInventorySlot == true) || (_startIsInventorySlot == true && _startDragPoint != -1 && _endDragPoint == -1))
        {
            InventoryManager.Instance.MoveItemInInventory(_startDragPoint, _endDragPoint);
        }
        else if (_startIsInventorySlot == true && _endIsInventorySlot == false)
        {
            if (_endDragPoint == -1) InventoryManager.Instance.MoveItemInInventory(_startDragPoint, _endDragPoint);
            else InventoryManager.Instance.SendItemToDecomposition(_startDragPoint);
        }
        else if (_startIsInventorySlot == false && _endIsInventorySlot == true)
        {
            InventoryManager.Instance.ReturnItemFromDecomposition(_startDragPoint);
        }
        else
        {
            InventoryManager.Instance.MoveItemInDecompositionSlot(_startDragPoint, _endDragPoint);
        }
    }
}

이와 같은 과정을 거치고 슬롯 간의 상호작용이 진행되고, 결과적으로 출력을 담당하는 DecompositionSlot과 DecompositionController는 간단하게 구성된다.

  • DecompositionSlot
using UnityEngine;

public class DecompositionSlotUnit : ItemSlotUnit
{
    [SerializeField] DecompositionSystem _data;

    public override void Awake()
    {
        _image.color = Color.clear;
        _text.text = "";
    }

    /// <summary>
    /// Update UI.
    /// </summary>
    /// <param name="index"></param>
    public override void UpdateUI(int index)
    {
        _item = _data.ReadFromDecompositionSlot(index, out int stack);
        if (_item == null)
        {
            _image.color = Color.clear;
            _itemStack = 0;
            _text.text = "";
        }
        else
        {
            _image.color = Color.white;
            _image.sprite = _item.Icon;
            _itemStack = stack;
            _text.text = stack > 1 ? stack.ToString() : "";
        }        
    }

    public void OnClick()
    {
        _data.ReturnItemToInventory(_index);
    }
}
  • DecompositionController
using UnityEngine;

public class DecompositionController : MonoBehaviour
{
    [SerializeField] private DecompositionSlotUnit[] _slots;
    [SerializeField] private DecompositionSystem _data;

    private void Start()
    {
        _data.OnDecompositionSlotUpdated += UpdateUISlot;
        UpdateUISlot();
    }

    private void OnEnable()
    {
        _data.OnDecompositionSlotUpdated += UpdateUISlot;
        UpdateUISlot();
    }

    private void OnDisable()
    {
        _data.OnDecompositionSlotUpdated -= UpdateUISlot;
    }

    private void UpdateUISlot()
    {
        for(int i = 0; i < _slots.Length; i++)
        {
            _slots[i].UpdateUI(i);
        }
    }
}

2.3 아이템 복사 버그의 발생과 해결 과정

이제 아이템 간 상호작용은 구성했는데 막판에 버그를 발견했다. 상황은 이와 같다.

예를 들어 10개짜리 스택의 포션과 2개짜리 스택의 포션이 있을 때, 2개 짜리 포션을 먼저 분해 슬롯에 드래그로 넣은 다음 10개짜리 스택의 포션을 넣으면 첫 번째 슬롯의 포션이 10개가 되고 그 다음 들어간 포션이 두 개가 되어야 한다. 하지만 의도하지 않게 첫 번째 슬롯의 포션도 10개, 두 번째 슬롯의 포션도 10개가 되었다.
(양방향 모두 문제가 발생)

이 문제를 해결하기 위해 코드를 살펴보면서 뭔가 문제가 있는지 살펴봤지만, 도통 보이지 않았다. 결국 유니티를 통해 디버깅을 해 보면서 어디에서 수치가 잘못 반영되고 있는지 확인했는데, 문제가 되는 위치를 발견해냈다.

  • 처음의 코드
private int InventoryTryAdd(ItemSO item, int index, int amount)
{
    _inventoryItem[index] = item;
    // If the stack is enough.
    if (amount <= (item.MaxStackSize - _inventoryStack[index]))
    {
        _inventoryStack[index] += amount;
        OnInventorySlotChanged?.Invoke();
        return 0;
    }
    // If the stack is not enough and has space.
    else if (item.MaxStackSize > _inventoryStack[index])
    {        
        _inventoryStack[index] = item.MaxStackSize;
        amount -= (item.MaxStackSize - _inventoryStack[index]);
        OnInventorySlotChanged?.Invoke();
        return amount;
    }
    // If the stack is full.
    else
    {
        return amount;
    }
} 

처음의 코드는 이러했다. 실제로 이 코드를 그대로 분해 시스템에 갖다 쓰기도 했고, 문제가 발생했다면 여기서 발생했을 거라는 건 예측이 가능했다. 그러면 구체적으로 문제인 게 무엇인가?

// If the stack is not enough and has space.
    else if (item.MaxStackSize > _inventoryStack[index])
    {        
        * _inventoryStack[index] = item.MaxStackSize;
        * amount -= (item.MaxStackSize - _inventoryStack[index]);
        OnInventorySlotChanged?.Invoke();
        return amount;
    }

여기서 별표를 친 두 줄의 코드를 잠시 생각해 보자. 아이템의 공간이 없을 경우, 필요한 과정이 이 두 가지다.

  • 아이템의 슬롯은 맥스치까지 채운다
  • amount는 채운 수치만큼 차감한다.

하지만 여기서 아이템의 슬롯을 맥스치로 먼저 채우는 작업을 먼저했기 때문에, 다음의 작업에서 amount에서 차감되는 값은 무조건 0이 된다. 해야되는 작업의 순서가 잘못된 것이다.

이 문제를 지금에아 발견한 이유는 아무래도 지금까지 스택이 작은 물체가 앞으로 오는 경우가 없었기 때문이었다.
그래서 눈치채지 못하고 있다가 이와 같이 슬롯이 늘어남에 따라 버그를 찾아낸 것임을 알게 되었다.

3. 개선점 및 과제

작업은 순조롭게 진행되고 있었지만, 오늘 좀 예상하지 못한 일이 있었다. 그건 기획 쪽에서 나온 기획의 방향에 관한 것이었다.

지금까지 기획의 내용을 보면서 대략적으로 작업의 방향을 정해왔고, 인벤토리의 시스템의 의도가 마인크래프트와 비슷하다고 생각했다. 같은 프로그래머 팀들 또한 비슷한 방향으로 생각하고 아이템을 주울 때 하이라이트와 UI를 구성하는 기능, 도감 시스템 등 많은 논의점이 있었다.

하지만 예상과 달리 기획 측에서는 필드에서 인벤토리를 사용하지 않는 기획을 준비한다고 했다. 이게 갑자기 무슨 소리인가 싶었는데, 기획 쪽에서는 필드의 파밍 및 루팅의 요소를 마치 스타크래프트의 미네랄 캐는 방식으로 기획하고 있었다나... 그래서 필드의 파밍 시스템 자체를 인벤토리에 아이템이 누적되는 방식이 아닌 자원 누적 시스템으로 만들고자 했단 것이다.

놀랍게도 플밍팀에서는 아무도 그런 내용으로 진행되고 있단 사실을 몰랐고, 기획서 내용에도 그런 내용이 안 적혀 있었다. 갑자기 왜 그런 방향으로 기획이 틀어졌는지는 몰라도 이 정도 기획 변동이면, 지금까지의 작업물을 싹 다 엎어야 할 상황이었다. 결국엔 그 정도의 변동은 불가능하다고 기획 팀과 협의를 진행해서 해당 기획 내용은 없는 일로 하고서 플밍팀이 구현한 방향대로 나가는 것으로 정했다.

기획 쪽에서도 시스템 기획으로 매우 바쁜 상황이고 회의도 계속 진행하고 있는 상황을 보았다. 하지만 어쩌다가 기획이 그렇게까지 변했는지는 이해하기가 조금 어려웠다. 소통의 부재 이슈를 느낀 아쉬운 상황이었다.

우선 추가된 내용과 함께 개선점 및 과제를 적고자 한다.

3.1 상자시스템 구현

상자시스템을 구현해야 한다는 기획 내용이 추가로 들어왔다. 내용을 보아하니 이전에 기획서를 확인해봤을 때 없었던 내용인 걸 보니 최근에 추가된 내용으로 보인다.

일전의 분해시스템을 구현할 때 아무래도 하드코딩을 한 부분이 있는데, 상자까지 구현을 하자고 하면 꽤나 복잡해질 것이 보였다. 어떻게든 그 구조를 리팩토링 할 방법이 없을까? 고민이 필요해 보인다.

3.2 아이템 사용 구현

아직 실행에 옮기진 않았지만, 아이템의 사용 구현을 뭔가 자동화시킬 수 없을까 고민을 했었다. 하지만 결과적으로 리서치를 해 본 결과, 아이템의 사용은 노가다로 아이템마다의 기능을 찍어내는 수밖에 없을 것 같다는 판단이 들었다. 지금 당장에 있는 아이템도 두 가지밖에 없다 보니, 이 부분은 금방 구현이 끝날 것이다.

3.3 퀵슬롯 관련 개편

퀵슬롯을 연결하는 방식에 대해서 아무래도 고민이 필요하다. 지금은 아무래도 인벤토리의 특정 칸이 연결된 방식이다 보니 유동성이 자유롭지 못하다. 그래서 아예 퀵슬롯을 인벤토리의 다른 칸을 빼는 방식도 고려하고 있다.

profile
게임 만들러 코딩 공부중

0개의 댓글