[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 20회차 미션 시작합니다.

02 디아블로 게임 - 35. 36 챕터를 진행합니다.




ItemInstances는 기능에 대한 오브젝트는 아니고, 아이템들에 대한 GameObject List를 가진 클래스라고 보면 됩니다.

public class ItemInstances_New
{
    public List<Transform> itemTransforms = new List<Transform>();
    
    public void OnDestroy() {
        // 하위 아이템들도 모두 삭제
        foreach (Transform item in itemTransforms) {
            Destroy(item.gameObject);
        }
    }
}




public class PlayerEquipment_New : MonoBehaviour
{
    public InventoryObject equipment;
    private EquipmentCombiner_New combiner;
    private ItemInstances_New[] itemInstances = new ItemInstances_New[8];
    public ItemObject[] defaultItemObjects = new ItemObject[8];
    
    private void Awake() {
        combiner = new EquipmentCombiner_New(gameObject);
        
        for (int i = 0; i < equipment.Slots.Length; i++) {
            equipment.Slots[i].OnPreUpdate += OnRemoveItem;
            equipment.Slots[i].OnPostUpdate += OnEquipItem;
        }
    }
    
    void Start() {
        foreach (InventorySlot slot in equipment.Slots) { // 기본 장착
            OnEquipItem(slot);
        }
    }
    
    private void OnEquipItem(InventorySlot slot) {
        ItemObject itemObject = slot.ItemObject;
        if (itemObject == null) { // 장비 삭제
            EquipDefaultItemBy(slot.AllowedItems[0]);
            return;
        }
       
        int index = (int)slot.AllowedItems[0];
        
        switch (slot.AllowedItems[0]) {
            case ItemType.Helmet:
            case ItemType.Chest:
            case ItemType.Pants:
            case ItemType.Boots:
            case ItemType.Gloves:
                itemInstances[index] = EquipSkinnedItem(itemObject);
                break;
            case ItemType.Pauldrons:
            case ItemType.LeftWeapon:
            case ItemType.RightWeapon:
                itemInstances[index] = EquipMeshItem(itemObject);
                break;
        }
    }
   
    // Skinned Item 장착
    private ItemInstances_New EquipSkinnedItem(ItemObject itemObject) {
        if (itemObject == null) return null;
        Transform itemTransform = combiner.AddLimb(itemObject.modelPrefab, itemObject.bonNames);
        
        if (itemTransform != null) {
            ItemInstances_New instance = new ItemInstances_New();
            instance.itemTransform.Add(itemTransform);
            return instance;
        }
        return null;
    }
    
    // Static Item 장착
    private ItemInstances_New EquipMeshItem(ItemObject itemObject) {
        if (itemObject == null) return null;
        Transform[] itemTransforms = combiner.AddMesh(itemObject.modelPrefab);
        
        if (itemTransform.Length > 0) {
            ItemInstances_New instance = new ItemInstances_New();
            instance.itemTransform.AddRange(itemTransform.ToList<Transform>());
            return instance;
        }
        return null;
    }
    
    private void EquipDefaultItemBy(ItemType type) {
        int index = (int)type;
        
        ItemObject itemObject = defaultItemObjects[index];
        switch (type) {
            case ItemType.Helmet: 
            case ItemType.Chest: 
            case ItemType.Pants: 
            case ItemType.Boots: 
            case ItemType.Gloves: 
                itemInstances[index] = EquipSkinnedItem(itemObject); 
                break; 
            case ItemType.Pauldrons: 
            case ItemType.LeftWeapon: 
            case ItemType.RightWeapon: 
                itemInstances[index] = EquipMeshItem(itemObject); 
                break; 
        } 
    } 
    
    private void OnDestroy() {
        foreach (ItemInstances_New item in itemInstances) {
            item.Destroy();
        }
    }
    
    private void OnRemoveItem(InventorySlot slot) {
        ItemObject itemObject = slot.ItemObject;
        if (itemObject == null) {
            RemoveItemBy(slot.AllowedItems[0]);
            return;
        }
        
        if (slot.ItemObject.modelPrefab != null) {
            RemoveItemBy(slot.AllowedItems[0]);
        }
    }
    
    private void RemoveItemBy(ItemType type) { // 특정 위치 아이템 삭제
        int index = (int)type;
        if (itemInstances[index] != null) {
            itemInstances[index].Destroy();
            itemInstances[index] = null;
        }
    }
}





위와 같이 보이지는 않지만 Bone 정보가 있고, 여기에 아이템의 Skinned Mesh를 잘 입혀서 보여주는 것이 아이템 장착이라고 해석하면 될 것 같네요.




Default Equipment를 설정합니다.




유니티 플레이를 해보면, 기본 장착 아이템들을 가지고 있는 캐릭터를 확인할 수 있습니다.
인벤토리에서 아이템을 드래그하여 장착하면 UI 상의 캐릭터가 아이템을 장착하는 것을 확인할 수 있고,
장비 인벤토리에서 아이템을 바닥에 드래그하여 버리게 되면, 다시 Default Item들을 가진 캐릭터로 설정됨을 확인할 수 있습니다.

아이템들에 대한 Mesh 형태에 따라 처리 방법이 달라지기 때문에 기획자, 디자이너와 어떤 형태로 만들고 구동할 것인지를 협의하고 그에 맞게 코드를 수정하여 만들어주면 됩니다.





 

 



이제 해당 아이템들을 땅에서 획득하고, 물약 등을 사용하는 루틴을 구현해 보겠습니다.


public class GroundItem_New : MonoBehaviour
{
    public ItemObject itemObject;
    
    private void OnValidate() {
#if UNITY_EDITOR
        GetComponent<SpriteRenderer>().sprite = itemObject?.icon;
#endif
    }
}


public class PlayerCharacter
{
    private void OnTriggerEnter(Collider other) {
        var item = other.GetComponent<GroundItem_New>();
        if (item) {
            if (inventory.AddItem(new Item(item.itemObject), 1)) // 인벤토리에 아이템을 추가하고
                Destroy(other.gameObject); // 땅의 아이템은 삭제.
        }
    }
}


 



이렇게 구현된 상태로 플레이를 해보면 바닥의 투구 아이템을 지나갈 때 캐릭터와 부딪히게 되고, 캐릭터는 아이템을 획득하게 됩니다.




이제 근처에 있는 아이템을 클릭하여 획득하는 루틴을 구현해 보겠습니다.

public interface IInteractable_New // 상자나 다른 다른 오브젝트에도 사용 가능하기에 별도 interface로 구현
{
    float Distance { get; }
    bool Interact(GameObject other);
    void StopInteract(GameObject other);
}


public class PickupItem_New : MonoBehaviour, IInteractable_New
{
    public float distance = 3.0f; // 아이템과 특정 거리 내에서만 픽업 가능하도록
    public float Distance => distance;
    
    public ItemObject itemObject;
    
    public bool Interact(GameObject other) {
        float calcDistance = Vector3.Distance(transform.position, other.transform.position);
        if (calcDistance > distance) return false;
        
        return other.GetComponent<PlayerCharacter_New>()?.PickupItem(this) ?? false;
    }
    
    public void StopInteract(GameObject other) {
    }
    
    private void OnValidate() {
#if UNITY_EDITOR
        GetComponent<SpriteRenderer>().sprite = itemObject?.icon;
#endif
    }
    
    private void OnDrawGizmosSelected() {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, distance);
    }
}

public class PlayerCharacter_New에서 우클릭할 때의 처리도 추가해 줍니다.
아이템을 우클릭하면 캐릭터는 해당 위치로 이동을 하다가 설정된 거리 내에 아이템이 있다면 Interact가 처리되도록 하여 아이템을 획득하는 루틴을 처리하면 됩니다.




이렇게 처리를 하고 플레이를 해보면 아이템을 지나가도 자동으로 획득이 되지 않지만, 먼 거리에서 아이템을 우클릭하고 이동하면 근처에 도달하여 아이템을 획득하는 장면을 볼 수 있습니다.





물약과 같은 아이템을 사용하는 루틴입니다. 기존 소스 코드에서 추가된 주요 함수들만 작성하였습니다.

public class DynamicInventoryUI
{
    protected override void OnRightClick(InventorySlot slot) {
        inventoryObject.UseItem(slot);
    }
}


public class InventoryObject : ScriptableObject
{
    public void UseItem(InventorySlot slotToUse) {
        if (slotToUse.ItemObject == null || slotToUse.item.id < 0 || slotToUse.amount <= 0) return;
        
        ItemObject itemObject = slotToUse.ItemObject;
        slotToUse.UpdateSlot(slotToUse.item, slotToUse.amount - 1);
        
        OnUseItem.Invoke(itemObject);
    }
}


public class PlayerCharacter
{
    private void OnUseItem(ItemObject itemObject) {
        foreach (ItemBuff buff in itemObject.data.buffs) {
            if (buff.stat == CharacterAttribute.Health)
                this.health += buff.value;
        }
    }
}


중요한 시스템들에 대한 구현이 많았어서 어렵기도 하고 복잡하기도 하고.. 그러네요 ^^
하지만 그만큼 중요한 내용이고, 그래도 어려운 내용을 이렇게 하나씩 구현해 가며 차근히 설명해주시니 너무 감격스럽지 않을 수 있겠습니까.



<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>

패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0






 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

+ Recent posts