83. Unity 최종 프로젝트 9주차

이규성·2024년 3월 2일
0

TIL

목록 보기
91/106

03/03

기술 스택

게임 소개

  • 각각의 섬들을 탐험하면서 자원을 채집하세요.




  • 다양한 장비를 제작해서 최대한 오래 생존하세요.



  • 밤 마다 몰려오는 몬스터의 공격에 대비하세요.


  • 다양한 설치 아이템들을 배치해서 나만의 공간을 만들어보세요.


  • 보스 몬스터를 사냥해서, 강력함을 뽐내세요.





  • 각 섬의 영향력 속에서 생존하세요.













트러블 슈팅 - 버그라고 보기는 힘든 무언가를 해결한 경험

오브젝트 투명화

public class ChangeMaterials : MonoBehaviour
{
    public Material[] fadeMaterials = new Material[2];
    public Material[] originMaterials = new Material[2];
    private Material[] _materials;
    private MeshRenderer _meshRenderer;

    private void Awake()
    {
        _meshRenderer = GetComponentInChildren<MeshRenderer>();
        _materials = _meshRenderer.materials;
    }
    public void ChangeFadeMaterials()
    {
        for (int i = 0; i < _materials.Length; i++)
        {
            _materials[i] = fadeMaterials[i];
        }
        _meshRenderer.materials = _materials;
    }

    public void ReturnMaterials()
    {
        for (int i = 0; i < _materials.Length; i++)
        {
            _materials[i] = originMaterials[i];
        }
        _meshRenderer.materials = _materials;
    }
}

  • 객체의 기존 재질을 담는 배열과 투명화된 재질을 담는 배열을 만듭니다.
  • 해당 배열에 담긴 재질을 적용하는 메서드를 통해 동작합니다.
public class RaycastToChangeMaterials : MonoBehaviour
{
    [SerializeField]
    private LayerMask _layerMask;
    [SerializeField]
    private Camera _camera;
    [SerializeField]
    private Transform _player;

    private RaycastHit[] _hits = new RaycastHit[10];

    private HashSet<ChangeMaterials> _prevChangeMaterials = new();
    private HashSet<ChangeMaterials> _curruentChangeMaterials = new();

    private void Start()
    {
        StartCoroutine(CheckPlayer());
    }

    private IEnumerator CheckPlayer()
    {
        while (Managers.Game.Player == null)
        {
            yield return null;
        }
        _player = Managers.Game.Player.ViewPoint;
    }

    private void FixedUpdate()
    {
        if (_player != null)
        {
            CheckForObjects();
        }
    }

    private void CheckForObjects()
    {        
        _prevChangeMaterials = _curruentChangeMaterials;
        _curruentChangeMaterials = new();

        int hits = Physics.RaycastNonAlloc(_camera.transform.position, (_player.transform.position - _camera.transform.position).normalized, _hits,
            Vector3.Distance(_camera.transform.position, _player.transform.position), _layerMask);
        if (hits >= 0)
        {
            // hashset
            for (int i = 0; i < hits; i++)
            {
                ChangeMaterials changeMaterials = _hits[i].transform.gameObject.GetComponentInParent<ChangeMaterials>();
                _curruentChangeMaterials.Add(changeMaterials);

                changeMaterials.ChangeFadeMaterials();                
            }

            _prevChangeMaterials.ExceptWith(_curruentChangeMaterials);

            foreach (var hit in _prevChangeMaterials)
                hit.transform.gameObject.GetComponentInParent<ChangeMaterials>().ReturnMaterials();
        }
    }
}
  • Player가 생성되면 CheckForObjects 메서드가 실행 되도록 Coroutine을 사용합니다.
  • RaycastAll이 아닌 RaycastNonAlloc을 사용한 이유는, RaycastAll은 호출되면서 내부적으로 배열을 생성 후 반환합니다. 반면에 RaycastNonAlloc은 미리 생성된 배열을 out 키워드로 재사용할 수 있어서 가비지 생성을 막을 수 있기 때문에 사용하였습니다.
  • 카메라와 플레이어 사이에 Ray를 쏴서 설정해둔 layer와 일치하는 객체를 hits에 할당합니다.
  • hits에 할당된 객체에 컴포넌트로 추가해둔 ChangeMaterials class를 GetComponent로 가져옵니다. 그 후에 currentMaterials HashSet에 추가한 후 투명화 재질로 바꿔주는 메서드를 호출하여 투명화합니다.
  • 투명화 후 prevChangeMaterials에서 currentChangeMaterals와 일치하는 객체를 삭제합니다. Ray에 현재 충돌하는 객체가 없다면 prevChangeMaterials 자료 구조에 존재하는 객체에 기존 재질로 바꿔주는 메서드를 호출하여 투명화를 해제합니다.
  • 처음에는 List 자료 구조를 사용하여서 구현하였으나, 나무 기둥과 나뭇잎 쪽에 각각 Box collider를 추가해 둔 상태여서 재질이 투명화 된 상태에서 나무 기둥을 왔다갔다하면 투명화가 풀렸다가 걸렸다가 깜빡거리는 현상이 발생하였습니다.
  • 그러한 이유로 할당된 데이터 중 지정한 값을 모두 없애주는 메서드가 있는 HashSet 자료 구조를 이용하여 해결하였습니다.

리팩토링

방어구 시스템
이전 코드

public class UIArmorSlot : UIItemSlot
{
    enum Images
    {
        Icon,
    }

    public ItemParts part;

    public override void Initialize()
    {
        Bind<Image>(typeof(Images));
        Get<Image>((int)Images.Icon).gameObject.SetActive(false);
        Get<Image>((int)Images.Icon).raycastTarget = false;
    }

    private void Awake()
    {
        Initialize();
        Managers.Game.Player.ToolSystem.OnEquip += EquipArmor;
        Managers.Game.Player.ToolSystem.OnUnEquip += UnEquipArmor;
        Managers.Game.Player.ArmorSystem.UnEquipArmor += UnEquipArmor;
    }

    public void EquipArmor(QuickSlot quickSlot)
    {
        int parts = GetPart(quickSlot);

        if (parts == 0)
        {
            switch (part)
            {
                case ItemParts.Head:
                    Set(quickSlot.itemSlot);
                    break;
            }
        }
        else if (parts == 1)
        {
            switch (part)
            {
                case ItemParts.Body:
                    Set(quickSlot.itemSlot);
                    break;
            }
        }
    }

    public void UnEquipArmor(QuickSlot quickSlot)
    {
        int parts = GetPart(quickSlot);

        if (parts == 0)
        {
            switch (part)
            {
                case ItemParts.Head:
                    Clear();
                    break;
            }
        }
        else if (parts == 1)
        {
            switch (part)
            {
                case ItemParts.Body:
                    Clear();
                    break;
            }
        }
    }

    public override void Set(ItemSlot itemSlot)
    {
        Get<Image>((int)Images.Icon).sprite = itemSlot.itemData.iconSprite;
        Get<Image>((int)Images.Icon).gameObject.SetActive(true);
    }

    public override void Clear()
    {
        Get<Image>((int)Images.Icon).gameObject.SetActive(false);
    }

    private int GetPart(QuickSlot slot)
    {
        var itemData = slot.itemSlot.itemData as EquipItemData;
        if (itemData == null) return -1;
        return (int)itemData.part;
    }
}
public class ArmorSystem : MonoBehaviour
{
    public int _defense;

    public QuickSlot[] _linkedSlots;

    public event Action<QuickSlot> UnEquipArmor;

    private void Awake()
    {
        _linkedSlots = new QuickSlot[2];

        Managers.Game.Player.ToolSystem.OnEquip += DefenseOfTheEquippedArmor;
        Managers.Game.Player.ToolSystem.OnUnEquip += DefenseOfTheUnEquippedArmor;
        Managers.Game.Player.OnHit += Duration;
    }
    private void Start()
    {
        Managers.Game.Player.Inventory.OnUpdated += OnInventoryUpdated;
    }

    public void DefenseOfTheEquippedArmor(QuickSlot quickSlot)
    {
        EquipItemData toolItemData = ArmorDefense(quickSlot);
        _defense += toolItemData.defense;
        Managers.Game.Player.playerDefense = _defense;

        if ((int)toolItemData.part == 0 || (int)toolItemData.part == 1)
        {
            _linkedSlots[(int)toolItemData.part] = quickSlot;
        }
    }

    public void DefenseOfTheUnEquippedArmor(QuickSlot quickSlot)
    {
        EquipItemData toolItemData = ArmorDefense(quickSlot);
        _defense -= toolItemData.defense;
        Managers.Game.Player.playerDefense = _defense;

        if ((int)toolItemData.part == 0 || (int)toolItemData.part == 1)
        {
            _linkedSlots[(int)toolItemData.part] = null;
        }
    }

    public void Duration() // Player Hit에서 호출
    {
        for (int i = 0; i < _linkedSlots.Length; i++)
        {
            if (_linkedSlots[i] != null && _linkedSlots[i].targetIndex != -1)
            {
                Managers.Game.Player.Inventory.UseToolItemByIndex(_linkedSlots[i].targetIndex, 1);
            }
        }
    }

    public void OnInventoryUpdated(int inventoryIndex, ItemSlot itemSlot)
    {
        for (int i = 0; i < 2; i++)
        {
            if (_linkedSlots[i] != null)
            {
                if (_linkedSlots[i].targetIndex == inventoryIndex)
                {
                    if (itemSlot.itemData == null)
                    {
                        EquipItemData toolItemData = ArmorDefense(_linkedSlots[i]);
                        _defense -= toolItemData.defense;
                        Managers.Game.Player.playerDefense = _defense;

                        UnEquipArmor?.Invoke(_linkedSlots[i]);
                        _linkedSlots[i].Clear();
                        Managers.Game.Player.ToolSystem.UnEquipArmor(_linkedSlots[i]);
                    }
                }
            }            
        }
    }

    private EquipItemData ArmorDefense(QuickSlot quickSlot)
    {
        ItemData armor = quickSlot.itemSlot.itemData;
        EquipItemData toolItemDate = (EquipItemData)armor;
        return toolItemDate;
    }
}

문제점

  • 슬롯 별로 enum 값을 통해 투구, 방어구의 구분을 지으려 했으나 사용 방법이 모호합니다.
  • ToolSystem에 의존합니다.
  • UIArmorSlot이 UI를 그리는 것이 아닌 다른 데이터 처리를 함으로써, MVC 패턴을 위반합니다.

수정 코드

public class ArmorSystem : MonoBehaviour
{
    private Player _player;
    private int _defense;

    private QuickSlot[] _equippedArmors;

    public event Action<QuickSlot> OnEquipArmor;
    public event Action<QuickSlot> OnUnEquipArmor;

    private void Awake()
    {
        _player = GetComponentInParent<Player>();

        _equippedArmors = new QuickSlot[2];
        for (int i = 0; i < _equippedArmors.Length; i++)
        {
            _equippedArmors[i] = new QuickSlot();
        }

        GameManager.Instance.Player.OnHit += OnUpdateDurabilityOfArmor;

        Load();

        GameManager.Instance.OnSaveCallback += Save;
    }

    private void Start()
    {
        _player.Inventory.OnUpdated += OnInventoryUpdated;
    }

    public void Equip(int index, ItemSlot itemSlot)
    {
        var part = GetPart(itemSlot);

        UnEquip(part);

        itemSlot.SetEquip(true);
        _equippedArmors[(int)part].Set(index, itemSlot);

        AddDefenseOfArmor(_equippedArmors[(int)part]);

        OnEquipArmor?.Invoke(_equippedArmors[(int)part]);
    }

    public void UnEquip(ItemParts part)
    {
        if (_equippedArmors[(int)part].itemSlot.itemData == null) return;

        SubtractDefenseOfArmor(_equippedArmors[(int)part]);

        _equippedArmors[(int)part].itemSlot.SetEquip(false);
        OnUnEquipArmor?.Invoke(_equippedArmors[(int)part]);

        _equippedArmors[(int)part].Clear();
    }

    private void AddDefenseOfArmor(QuickSlot quickSlot)
    {
        EquipItemData toolItemData = GetArmorItemData(quickSlot);
        _defense += toolItemData.defense;
    }

    private void SubtractDefenseOfArmor(QuickSlot quickSlot)
    {
        EquipItemData toolItemData = GetArmorItemData(quickSlot);
        _defense -= toolItemData.defense;
    }

    public void OnUpdateDurabilityOfArmor()
    {
        for (int i = 0; i < _equippedArmors.Length; i++)
        {
            if (_equippedArmors[i] != null && _equippedArmors[i].targetIndex != -1)
            {
                _player.Inventory.TrySubtractDurability(_equippedArmors[i].targetIndex, 1);
            }
        }
    }

    private EquipItemData GetArmorItemData(QuickSlot quickSlot)
    {
        EquipItemData toolItemDate = quickSlot.itemSlot.itemData as EquipItemData;
        return toolItemDate;
    }

    private ItemParts GetPart(ItemSlot slot)
    {
        var itemData = slot.itemData as EquipItemData;
        return itemData.part;
    }

    public int GetDefense()
    {
        return _defense;
    }

    public QuickSlot[] GetEquippedArmorsArray()
    {
        return _equippedArmors;
    }

    public void OnInventoryUpdated(int inventoryIndex, ItemSlot itemSlot)
    {
        if (itemSlot.itemData != null) return;
        for (int i = 0; i < _equippedArmors.Length; i++)
        {
            if (_equippedArmors[i] != null)
            {
                if (_equippedArmors[i].targetIndex == inventoryIndex && itemSlot.itemData == null)
                {
                    SubtractDefenseOfArmor(_equippedArmors[i]);
                    OnUnEquipArmor?.Invoke(_equippedArmors[i]);
                    _equippedArmors[i].Clear();
                }
            }
        }
    }

    private void Load()
    {
        if (SaveGame.TryLoadJsonToObject(this, SaveGame.SaveType.Runtime, "ArmorSystem"))
        {
            for (int i = 0; i < _equippedArmors.Length; ++i)
            {
                _equippedArmors[i].itemSlot.LoadData();
            }
        }
    }

    private void Save()
    {
        var json = JsonUtility.ToJson(this);
        SaveGame.CreateJsonFile("ArmorSystem", json, SaveGame.SaveType.Runtime);
    }
}

개선 사항

  • 기존에 ToolSystem에 의존하던 것을 분리하여 방어구 관련된 데이터 처리를 ArmorSystem에서 모두 처리합니다.
  • ItemData의 enum값을 두 번 비교하지 않고 바로 참조하여 사용합니다.
  • Player가 가지고 있던 Defense 값을 제거하고 ArmorSystem의 Defense 값을 private 한정자로 변경 후, GetDefense 메서드를 통해 데미지 처리 시 참조하게 만들어서 보안성을 높였습니다.
public class UIArmorSlotContainer : MonoBehaviour
{
    private List<UIArmorSlot> _armorSlots = new List<UIArmorSlot>();

    private void Awake()
    {
        GameManager.Instance.Player.ArmorSystem.OnEquipArmor += EquipArmor;
        GameManager.Instance.Player.ArmorSystem.OnUnEquipArmor += UnEquipArmor;
    }

    public void CreatArmorSlots<T>(GameObject slotPrefab, int count) where T : UIArmorSlot
    {
        for (int i = 0; i < count; i++)
        {
            var armorSlotPrefab = Instantiate(slotPrefab, this.transform);
            var armorSlotUI = armorSlotPrefab.GetComponent<T>();
            SetParts(armorSlotUI, i);
            _armorSlots.Add(armorSlotUI);
        }
    }

    public void Init<T>(QuickSlot[] quickSlot) where T : UIArmorSlot
    {
        foreach (var armor in quickSlot)
        {
            EquipArmor(armor);
        }
    }

    private void SetParts(UIArmorSlot armorSlotUI, int index)
    {
        armorSlotUI.SetPart((ItemParts)index);
    }

    public void EquipArmor(QuickSlot quickSlot)
    {
        if (quickSlot.itemSlot.itemData == null) return;

        int parts = GetPart(quickSlot);
        _armorSlots[parts].Set(quickSlot.itemSlot);
    }

    public void UnEquipArmor(QuickSlot quickSlot)
    {
        int parts = GetPart(quickSlot);
        _armorSlots[parts].Clear();
    }

    private int GetPart(QuickSlot slot)
    {
        var itemData = slot.itemSlot.itemData as EquipItemData;
        if (itemData == null) return -1;
        return (int)itemData.part;
    }
}

개선 사항

  • UIArmorSlot 객체를 관리하는 UIArmorSlotContainer class와 객체를 추가하였습니다.
  • 해당 class는 UIArmorSlot을 생성하고, enum 값을 지정하는 등의 메서드를 지니고 있습니다.
  • 슬롯의 생성은 해당 class를 참조하여 UIInventory에서 합니다.
public class UIArmorSlot : UIItemSlot
{
    public Sprite[] sprites;
    public Color emptyState;

    enum Images
    {
        Icon,
    }

    public ItemParts part;

    public override void Initialize()
    {
        Bind<Image>(typeof(Images));
        Clear();
        Get<Image>((int)Images.Icon).raycastTarget = false;
    }

    private void Awake()
    {
        Initialize();
    }

    public void SetPart(ItemParts part)
    {
        this.part = part;
        Clear();
    }

    public override void Set(ItemSlot itemSlot)
    {
        if (itemSlot.itemData == null)
        {
            Clear();
            return;
        }
        Get<Image>((int)Images.Icon).color = Color.white;
        Get<Image>((int)Images.Icon).sprite = itemSlot.itemData.iconSprite;
        Get<Image>((int)Images.Icon).gameObject.SetActive(true);
    }

    public override void Clear()
    {
        Get<Image>((int)Images.Icon).sprite = sprites[(int)part];
        Get<Image>((int)Images.Icon).color = emptyState;
    }
}

개선 사항

  • 오직 UI를 그리는 함수만 지니고 있도록 수정하였습니다.

0개의 댓글