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

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




이번 시간은 RPG 게임 플레이 시스템 구현하기 입니다.




+ 플레이어 속성 - 아이템 착용 상태에 따른 속성 변경
+ 플레이어 UI
+ 다이얼로그 시스템 구현 - 대화 시스템
+ 퀘스트 시스템 구현
+ 환경 장치(함정/문) 구현 - 레벨에 따른..





중요한 부분은 Attribute와 ModifiableInt 클래스 입니다.

+ Attribute - ModifiableInt를 가지고 Value를 설정
+ ModifiableInt - Base값을 가지고 변경되어진 값을 사용하는 Value(?)
+ AttributeType - 속성의 Type 설정
+ StatsObject - Attribute를 설정

이제 스크립트를 구현합니다. 바로 로직구현으로 고고고~~ ㅎㅎ


[Serializable]
public class ModifiableInt_New
{
    [NonSerialized]
    private int baseValue;
    [SerializeField]
    private int modifiedValue;
    
    public int BaseValue {
        get => baseValue;
        set {
            baseValue = value;
            UpdateModifiedValue();
        }
    }
    
    public int ModifiedValue {
        get => modifiedValue;
        set => modifiedValue = value;
    }
    
    // 변경되었다는 이벤트를 받기 위한 처리
    private event Action<ModifiableInt_New> OnModifiedValue;
    
    private List<IModifier> modifiers = new List<IModifier>();
    
    public ModifiableInt_New(Action<ModifiableInt_New> method = null) {
        ModifiedValue = baseValue;
        RegisterModEvent(method);
    }
    
    public void RegisterModEvent(Action<ModifiableInt_New> method) {
        if (method != null) {
            OnModifiedValue += method;
        }
    }
    
    public void UnregisterModEvent(Action<ModifiableInt_New> method) {
        if (method != null) {
            OnModifiedValue -= method;
        }
    }
    
    private void UpdateModifiedValue() {
        int valueToAdd = 0;
        foreach (IModifier modifier in modifiers) {
            modifier.AddValue(ref valueToAdd);
        }
        
        ModifiedValue = baseValue + valueToAdd;
        
        OnModifiedValue?.Invoke(this);
    }
    
    public void AddModifier(IModifier modifier) {
        modifiers.Add(modifier);
        UpdateModifiedValue();
    }
    
    public void RemoveModifier(IModifier modifier) {
        modifiers.Remove(modifier);
        UpdateModifiedValue();
    }
}




[Serializable]
public class Attribute_New
{
    public AttributeType type;
    public ModifiableInt_New value;
}



[CreateAssetMenu(fileName = "New Stats", menuName = "Stats System/New Character Stats New")[
public class StatsObject_New : ScriptableObject
{
    public Attribute_New[] attributes;
    public int level;
    public int exp;
    public int Health { get; set; } // Property로 만들면 Serializable 되지 않는다.
    public int Mana { get; set; }
    
    public float HealthPercentage {
        get {
            int health = Health;
            int maxHealth = Health;
            foreach (Attribute_New attribute in attributes) {
                if (attribute.type == AttributeType.Health) {
                    maxHealth = attribute.value.ModifiedValue;
                }
            }
            
            return (maxHealth > 0 ? ((float)health / (float)maxHealth) : 0f);
        }
    }
   
    public float ManaPercentage {
        get {
            int mana = Mana;
            int maxMana = Mana;
            foreach (Attribute_New attribute in attributes) {
                if (attribute.type == AttributeType.Mana) {
                    maxMana = attribute.value.ModifiedValue;
                }
            }
            
            return (maxMana > 0 ? ((float)mana / (float)maxMana) : 0f);
        }
    }
   
    public event Action<StatsObject> OnChangedStats;
    
    [NonSerialized]
    private bool isInitialize = false;
    public void OnEnable() {
        InitializeAttribute();
    }
    
    public void InitializeAttribute() {
        if (isInitialize) return;
        isInitialize = true;
        foreach (Attribute_New attribute in attributes) {
            attribute.value = new ModifiableInt_New(OnModifiedValue);
        }
        level = 1;
        exp = 0;
        SetBaseValue(AttributeType.Agility, 100);
        SetBaseValue(AttributeType.Intellect, 100);
        SetBaseValue(AttributeType.Stamina, 100);
        SetBaseValue(AttributeType.Strength, 100);
        SetBaseValue(AttributeType.Health, 100);
        SetBaseValue(AttributeType.Mana, 100);
        
        Health = GetModifiedValue(AttributeType.Health);
        Mana = GetModifiedValue(AttributeType.Mana);
    }
    
    private void OnModifiedValue(ModifiableInt_New value) {
        OnChangedStats?.Invoke(this);
    }
    
    public int GetBaseValue(AttributeType type) {
        foreach (Attribute attribute in attributes) {
            if (attribute.type == type) {
                return attribute.value.BaseValue;
            }
        }
        return -1;
    }
    
    public void SetBaseValue(AttributeType type, int value) {
        foreach (Attribute attribute in attributes) {
            if (attribute.type == type) {
                attribute.value.BaseValue = value;
            }
        }
    }
    
    public int GetModifiedValue(AttributeType type) {
        foreach (Attribute attribute in attributes) {
            if (attribute.type == type) {
                return attribute.value.ModifiedValue;
            }
        }
        return -1;
    }
    
    public int AddHealth(int value) {
        Health += value;
        OnChangedStats?.Invoke(this);
        return Health;
    }
    
    public int AddMana(int value) {
        Mana += value;
        OnChangedStats?.Invoke(this);
        return Mana;
    }
}






이제 Unity Editor에서 PlayerStats를 추가하여 위와 같이 설정해 줍니다.

위와 관련 기존 코드에서 수정되는 부분들만 작성합니다.

public class PlayerCharacter
{
    [SerializeField]
    public StatsObject playerStats;
    
    public void OnEnterAttackState() {
        UnityEngine.Debug.Log("OnEnterAttackState()");
        playerStats.AddMana(-3); // 임시 테스트값
    }
    
    public void OnExitAttackState() {
        UnityEngine.Debug.Log("OnExitAttackState()");
    }
    
    public bool IsAlive => playerStats.Health > 0;
    
    public void TakeDamage(int damage, GameObject damageEffectPrefab) {
        //..
        playerStats.AddHealth(-damage);
        //..
    }
    
    private void OnUseItem(ItemObject itemObject) {
        foreach (ItemBuff buff in itemObject.data.buffs) {
            if (buff.stat == AttributeType.Health) {
                playerStats.AddHealth(buff.value);
            }
        }
    }
}


 



Unity에서 플레이어의 속성 UI를 표시하기 위해 무료 Asset을 받아서 Canvas에 위와 같이 설정합니다.


public class PlayerInGameUI_New : MonoBehaviour
{
    public StatsObject_New playerStats;
    public Text levelText;
    public Image healthSlider;
    public Image manaSlider;
    
    void Start() {
        levelText.text = playerStats.level.ToString("n0");
        healthSlider.fillAmount = playerStats.HealthPercentage;
        manaSlider.fillAmount = playerStats.ManaPercentage;
    }
    
    private void OnEnable() {
        playerStats.OnChangedStats += OnChangedStats;
    }
    
    private void OnDisable() {
        playerStats.OnChangedStats -= OnChangedStats;
    }
    
    private void OnChangedStats(StatsObject_New statsObject) {
        levelText.text = playerStats.level.ToString("n0");
        healthSlider.fillAmount = playerStats.HealthPercentage;
        manaSlider.fillAmount = playerStats.ManaPercentage;
    }
}





플레이 화면입니다.
공격을 당하면 HP가 줄어들고, 공격을 하면 Mana가 줄어들고, 물약을 먹으면 HP가 다시 채워지는 것을 확인할 수 있습니다.






이제 위와 같은 속성을 표시하기 위한 구현입니다.

public class PlayerStatsUI_New : MonoBehaviour
{
    public InventoryObject equipment;
    public StatsObject playerStats;
    public Text[] attributeTexts;
    
    private void OnEnable() {
        playerStats.OnChangedStats += OnChangedStats;
        if (equipment != null && playerStats != null) {
            foreach (InventorySlot slot in equipment.Slots) {
                slot.OnPreUpdate += OnRemoveItem;
                slot.OnPostUpdate += OnEquipItem;
            }
            UpdateAttributeTexts();
        }
    }
    
    private void OnDisable() {
        playerStats.OnChangedStats -= OnChangedStats;
        if (equipment != null && playerStats != null) {
            foreach (InventorySlot slot in equipment.Slots) {
                slot.OnPreUpdate -= OnRemoveItem;
                slot.OnPostUpdate -= OnEquipItem;
            }
            UpdateAttributeTexts();
        }
    }
    
    private void UpdateAttributeTexts() {
        attributeTexts[0].text = playerStats.GetModifiedValue(AttributeType.Agility).ToString("n0");
        attributeTexts[1].text = playerStats.GetModifiedValue(AttributeType.Intellect).ToString("n0");
        attributeTexts[2].text = playerStats.GetModifiedValue(AttributeType.Stamina).ToString("n0");
        attributeTexts[3].text = playerStats.GetModifiedValue(AttributeType.Strength).ToString("n0");
    }
    
    private void OnRemoveItem(InventorySlot slot) {
        if (slot.ItemObject == null) return;
        foreach (ItemBuff buff in slot.item.buffs) {
            foreach (Attribute attribute in playerStats.attributes) {
                if (attribute.type == buff.stat) {
                    attribute.value.RemoveModifier(buff);
                }
            }
        }
    }
    
    private void OnEquipItem(InventorySlot slot) {
        if (slot.ItemObject == null) return;
        foreach (ItemBuff buff in slot.item.buffs) {
            foreach (Attribute attribute in playerStats.attributes) {
                if (attribute.type == buff.stat) {
                    attribute.value.AddModifier(buff);
                }
            }
        }
    }
    
    private void OnChangedStats(StatsObject statsObject) {
        UpdateAttributeTexts();
    }
}





여기까지 작성하고 플레이를 시작하여 장착 테스트를 해봅니다.
모든 값들이 잘 표시됨을 확인할 수 있습니다. @~@;;;;





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

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


 

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

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

www.fastcampus.co.kr

 

[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 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

 

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

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




PlayerInventory를 만들어서 등록 가능한 아이템들의 데이터 정보를 확인할 수 있습니다. 아직 등록된 아이템이 없기 때문에 내용은 없고 id도 -1인 상태로 등록된 아이템이 없음을 나타냅니다.




인벤토리에 사용하는 이미지들도 위와 같이 등록하여 설정해 줍니다.





현재까지 기본적인 Interface UI는 구현이 된 상태이고, DynamicInventoryUI를 구현해 보도록 하겠습니다.

public class DynamicInventoryUI_New : InventoryUI_New
{
    [SerializeField]
    protected GameObject slotPrefab; // slot에 대한 UI 오브젝트 Prefab
    
    // 격자 형태의 slot 구성.
    [SerializeField]
    protected Vector2 start;
    [SerializeField]
    protected Vector2 size;
    [SerializeField]
    protected Vector2 space;
    
    [Min(1), SerializeField]
    protected int numberOfColumn = 4;
    
    public override void CreateSlotUIs() {
        slotUIs = new Dictionary<GameObject, InventorySlot_New>();
        
        for (int i = 0; i < inventoryObject.Slots.Length; i++) {
            GameObject go = Instantiate(slotPrefab, Vector3.zero, Quaternion.identity, transform);
            go.GetComponent<RectTransform>().anchoredPosition = CalculatePosition(i);
            
            // 마우스 이벤트 트리거들 추가.
            AddEvent(go, EventTriggerType.PointerEnter, delegate { OnEnterSlot(go); });
            AddEvent(go, EventTriggerType.PointerExit, delegate { OnExitSlot(go); });
            AddEvent(go, EventTriggerType.BeginDrag, delegate { OnStartDrag(go); });
            AddEvent(go, EventTriggerType.EndDrag, delegate { OnEndDrag(go); });
            AddEvent(go, EventTriggerType.Drag, delegate { OnDrag(go); });
            
            inventoryObject.Slots[i].slotUI = go;
            slotUIs.Add(go, inventoryObject.Slots[i]);
            
            go.name += ": " + i;
        }
    }
    
    public void CalculatePosition(int i) {
        float x = start.x + ((space.x + size.x) * (i % numberOfColumn));
        float y = start.y + (-(start.y) + size.y) * (i / numberOfColumn));
        return new Vector3(x, y, 0f);
    }
}


CreateSlotUIs()가 가장 중요한 함수라고 볼 수 있겠습니다.

이와 관련하여 SlotPrefab의 구성은 다음과 같아야 합니다.





Slot의 배경 이미지와 Icon이 들어갈 위치, 그리고 하단에 Text가 배치되어 있어야 합니다. 왜냐면 소스 코드 구성시 Slot의 구성이 Icon과 Text가 무조건 존재해야만 구동이 되도록 프로그래밍되어 있기 때문입니다.


그리고 위에 구성한 DynamicInventoryUI_New 스크립트를 Unity UI 상에서 설정해줍니다.



DynamicInventory에다가 DynamicInventoryUI_New 스크립트를 추가하여주고,
+ Inventory Object = "PlayerInventory"
+ Slot Prefab = "SlotPrefab"
+ Start = -70, 120
+ Size = 50, 50
+ Space = 1, 1
+ Number Of Column = 4
으로 설정하여 줍니다.




플레이를 해보면 위와 같이 인벤토리 구역 내에 Slot 24개가 잘 배치되어 나타나는 것을 확인할 수 있습니다.

이것이 잘 동작하는지를 확인하는 TestItems 스크립트를 작성하면 쉽게 동작여부를 확인하기 수월합니다.




AddNewItem() 함수는 databaseObject에서 랜덤하게 아이템을 하나 얻어와서 AddItem()으로 추가해주고,
CreateInventory() 함수는 등록된 inventoryObject를 모두 제거해주는 함수입니다.




플레이를 진행해 봅니다.
Add Item을 클릭하면 랜덤하게 하나씩 추가되고, Slot간 아이템 이동과, 아이템 교체, 인벤토리가 아닌 영역에 드래그하여 아이템 버리기, Cleate Items를 클릭하여 모두 비우기 등의 모든 테스트를 해볼 수 있습니다.




물약과 음식과 같은 겹쳐질 수 있는 아이템 테스트입니다.
그림과 같이 동일한 아이템이 추가되는 경우 Stackable[v]이 켜져 있는 아이템의 경우에는 비어 있는 Slot에 또 추가되는 것이 아니라 기존에 있던 아이템에 동일하게 추가를 하고 Text로 개수를 표시하는 방식으로 구현되었습니다.



이번에는 장비 창 인벤토리를 구현해 보도록 하겠습니다.


public class StaticInventoryUI_New : MonoBehaviour
{
    public GameObject[] staticSlots = null; // 고정된 Slot.
    
    public override void CrateSlotUIs() {
        slotUIs = new Dictionary<GameObject, FastCampus.InventorySystem.Inventory.InventorySlot_New>();
        for (int i = 0; i < inventoryObject.Slots.Length; i++) {
            GameObject go = staticSlots[i]; // Scene에 등록된 고정된 UI를 사용함이 다른점.
            go.GetComponent<RectTransform>().anchoredPosition = CalculatePosition(i);
            
            // 마우스 이벤트 트리거들 추가.
            AddEvent(go, EventTriggerType.PointerEnter, delegate { OnEnterSlot(go); });
            AddEvent(go, EventTriggerType.PointerExit, delegate { OnExitSlot(go); });
            AddEvent(go, EventTriggerType.BeginDrag, delegate { OnStartDrag(go); });
            AddEvent(go, EventTriggerType.EndDrag, delegate { OnEndDrag(go); });
            AddEvent(go, EventTriggerType.Drag, delegate { OnDrag(go); });
            
            inventoryObject.Slots[i].slotUI = go;
            slotUIs.Add(go, inventoryObject.Slots[i]);
            
            go.name += ": " + i;
        }
    }
}





여기까지 작업하고 플레이를 해봅니다.
보시는 것과 같이 장비 착용 인벤토리에 아이템을 드래그하여 설정이 가능하고, 또한 동일한 타입이 아닌 아이템은 착용이 되지 않는 것도 확인할 수 있습니다.





이제는 캐릭터 장비 교체를 구현해보는 시간입니다 ^^~
캐릭터는 보통 다수의 Skinned mesh와 Static mesh로 구현되어 있습니다.

Skinned mesh는 그림의 사람처럼 Bone이라는 뼈대로 구성된 것 위에 영향을 받는 Vertex들이 구성되어 있는 mesh 구조입니다. 투구나 갑옷 등 몸에 착용되는 아이템은 1개의 뼈대를 가지는 아이템으로 구성되는 경우가 많습니다.
Static mesh는 우측 하단 그림의 칼처럼 Bone이 없이 Vertex들로만 구성된 Mesh Object 구조입니다.

Skinned mesh, 즉 사람 GameObject에 아이템을 입히는 효과를 처리하려면, Human Bone 구조를 가지는 것 하나에 아이템들을 얻혀서 하나처럼 조합시키는 작업을 해주어야 합니다.




EquipmentCombiner가 뼈대 정보를 가지고 있다고 보면 됩니다.


public class EquipmentCombiner_New
{
    private readonly Dictionary<int, Transform> rootBoneDictionary = new Dictionary<int, Transform>(); // 모든 뼈대 정보
    private readonly Transform transform;
    
    public EquipmentCombiner_New(GameObject rootGO) {
        transform = rootGO.transform;
        TraverseHierachy(transform);
    }
    
    public Transform AddLimb(GameObject itemGO, List<string> boneNames) {
        Transform limb = ProcessBoneObject(itemGO.GetComponentInChildren<SkinnedMeshRenderer>(), boneNames);
        limb.SetParent(transform);
        return limb;
    }
    
    // 새로운 게임 오브젝트에 기존의 Skinned Mesh를 복사.
    private Transform ProcessBoneObject(SkinnedMeshRenderer renderer, List<string> boneNames) {
        Transform itemTransform = new GameObject().transform;
        SkinnedMeshRenderer meshRenderer = itemTransform.gameObject.AddComponent<SkinnedMeshRenderer>();
        Transform[] boneTransforms = new Transform[boneNames.Count];
        for (int i = 0; i < boneNames.Count; i++) {
            boneTransforms[i] = rootBoneDictionary[boneNames[i].GetHashCode()];
        }
        
        meshRenderer.bones = boneTransforms;
        meshRenderer.sharedMesh = renderer.sharedMesh;
        meshRenderer.materials = renderer.sharedMaterials;
        
        return itemTransform;
    }
   
    // Bone을 포함하지 않는 Static Mesh 처리
    public Transform[] AddMesh(GameObject itemGO) {
        Transform[] itemTransforms = ProcessMeshObject(itemGO.GetComponentsInChildren<MeshRenderer>());
        return itemTransforms;
    }
    
    public Transform[] ProcessMeshObject(MeshRenderer[] meshRenderers) {
        List<Transform> itemTransforms = new List<Transform>();
        foreach (MeshRenderer renderer in meshRenderers) {
            if (renderer.transform.parent != null) {
                Transform parent = rootBoneDictionary[renderer.transform.parent.name.GetHashCode()];
                GameObject itemGO = GameObject.Instantiate(renderer.gameObject, parent);
                itemTransforms.Add(itemGO.transform);
            }
        }
        return itemTransforms.ToArray();
    }
    
    private void TraverseHierachy(Transform root) {
        foreach (Transform child in root) {
            rootBoneDictionary.Add(child.name.GetHashCode(), child);
            TraverseHierachy(child);
        }
    }
}




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];
}

여기까지가 구현의 일부입니다. 다음 시간에 이어서 구현을 합니다.



오늘은 굉장히 중요한 내용들이 많았습니다. 작성하기도 힘들었네요 헉헉..
인벤토리 구성을 위한 스크립트와 Unity 설정까지 모두 알아보았으며,
인벤토리에서 장비장착 인벤토리로 아이템을 설정할 때 실제 캐릭터에 장착되는 구현을 하고 있습니다.
그러다보니 중요한 내용도 많고 코드도 많고, Unity 설정도 엄청 많습니다. ㅎㅎ
실제로 처음부터 백지에서 시작해서 구현하려면 엄청난 시간이 필요할 것 같네요 ㅠ.,ㅜ;





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

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

 

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

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

www.fastcampus.co.kr

 

+ Recent posts