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


02 디아블로 게임 - 31. 인벤토리와 아이템 구현하기 - 02 인벤토리 시스템 구현하기
02 디아블로 게임 - 32. 인벤토리와 아이템 구현하기 - 03 플레이어와 상점을 위한 인벤토리 UI 구현하기




ItemDatabaseobject는 모든 아이템을 가지는 형태로 고려되어 있기때문에 InventoryObject에 할당될 때 문제가 없지만, 칼/투구/장갑 등 종류별로 ItemDatabaseObject를 구성하고자 하는 경우에는 id의 충돌이 발생하게 됩니다.
이를 간단히 피하기 위해서 baseId를 추가하여 처리하는 방식을 고려할 수 있습니다. 칼은 100번대, 투구는 200번대, 장갑은 300번대 baseId를 고정시켜두고 내부 id가 baseId에서 증가되는 형태로 제작할 수 있는 것입니다.





InventoryObject_New 스크립트를 생성합니다. UI와 관련이 높은 클래스입니다.

public enum InterfaceType_New { // 인벤토리 종류
    Inventory,
    Equipment,
    QuickSlot,
    ChestBox,
}

[CreateAssetMenu(fileName = "New Inventory", menuName = "Inventory System/Inventory_New")]
public class InventoryObject_New : ScriptableObject
{
    public ItemObjectDatabase_New database;
    public InterfaceType_New type;
    
    [SerializeField]
    private Inventory_New container = new Inventory_New();
    
    public InventorySlot[] Slots => container.slots;
    
    public int EmptySlotCount {
        get {
            int counter = 0;
            foreach (InventorySlot_New slot in Slots)
                if (slot.item.id < 0)
                    counter++;
            return counter;
        }
    }
    
    public bool AddItem(Item_New item, int amount) {
        if (EmptySlotCount <= 0) return false;
        InventorySlot_New slot = FindItemInInventory(item);
        if (!database.itemObjects[item.id].stackable || slot)
            GetEmptySlot().AddItem(item, amount);
        else
            slot.AddAmount(amount);
        return true;
    }
    
    public InventorySlot_New FindItemInInventory(Item_New item) {
        return Slots.FindOrDefault(i => i.item.id == item.id);
    }
    
    public InventorySlot_New GetEmptySlot() {
        return Slots.FirstOrDefault(i => i.item.id < 0);
    }
    
    public bool IsContainItem(ItemObject_New itemObject) {
        return Slots.FirstOrDefault(i => i.item.id == itemObject.data.id) != null;
    }
    
    public void SwapItems(InventorySlot_New itemSlotA, InventorySlot_New itemSlotB) {
        if (itemSlotA == itemSlotB) return;
        if (itemSlotB.CanPlaceInSlot(itemSlotA.ItemObject) && itemSlotA.CanPlaceInSlot(itemSlotB.ItemObject)) {
            InventorySlot tempSlot = new InventorySlot_New(itemSlotB.item, itemSlotB.amount);
            itemSlotB.UpdateSlot(itemSlotA.item, itemSlotA.amount);
            itemSlotA.UpdateSlot(tempSlot.item, tempSlot.amount);
        }
    }
}




이제 InventoryUI를 구현해볼 차례입니다.



UI 시스템은 다른 입력시스템이나 이벤트 트리거 등과 연관이 많이 되는 부분이라 복잡해지는 경향이 있습니다.

여기에서 InventoryUI는 실제 InventorySlot UI와 EventManager를 중재해주는 컨트롤러의 역할입니다.

즉, InventoryObject라는 Model, InventorySlot & EventManager라는 View, InventoryUI라는 Controller로 구분하여 MVC 모델을 구현한 것입니다.

StaticInventoryUI는 형태가 고정된 인벤토리 UI이며, DynamicInventoryUI는 형태가 고정되지 않은 인벤토리 UI입니다.
StaticInventoryUI는 장비창, DynamicInventoryUI는 인벤토리창으로 구현합니다.


 


public static class MouseData_New {
    public static InventoryUI interfaceMouseIsOver; // 어느 UI에 있는가?
    public static GameObject slotHoveredOver; // 어느 Slot 위에 있는가?
    public static GameObject tempItemBeingDragged;
}

[RequireComponent(typeof(EventTrigger))]
public abstract class InventoryUI_New : MonoBehaviour
{
    public InventoryObject_New inventoryObject;
    private InventoryObject_New previousInventoryObject;
    
    public Dictionary<GameObject, InventorySlot_New> slotUIs = new Dictionary<GameObject, InventoryObject_New>();
    
    private void Awake() {
        CreateSlotUIs();
        for (int i = 0; i < inventoryObject.Slots.Length; i++) {
            inventoryObject.Slots[i].parent = inventoryObject; // 초기화
            inventoryObject.Slots[i].OnPostUpdate += OnPostUpdate;
        }
        
        AddEvent(gameObject, EventTriggerType.PointerEnter, delegate { OnEnterInterface(gameObject); });
        AddEvent(gameObject, EventTriggerType.PointerExit, delegate { OnExitInterface(gameObject); });
    }
   
    protected virtual void Start() {
        for (int i = 0; i < inventoryObject.Slots.Length; i++) {
            inventoryObject.Slots[i].UpdateSlot(inventoryObject.Slots[i].item, inventoryObject.Slots[i].amount);
        }
    }
    
    public abstract CreateSlotUIs();
    
    protected void AddEvent(GameObject go, EventTriggerType type, UnityAction<BaseEventData> action) {
        EventTrigger trigger = go.GetComponent<EventTrigger>();
        if (!trigger) {
            Debug.LogWarning("No EventTrigger component found!");
            return;
        }
        
        EventTrigger.Entry eventTrigger = new EventTrigger.Entry { eventID = type };
        EventTrigger.callback.AddListener(action);
        trigger.triggers.Add(eventTrigger);
    }
    
    public void OnPostUpdate(InventorySlot_New slot) {
        slot.slotUI.transform.GetChild(0).GetComponent<Image>().sprite = slot.item.id < 0 ? null : slot.ItemObject.icon;
        slot.slotUI.transform.GetChild(0).GetComponent<Image>().color = slot.item.id < 0 ? new Color(1, 1, 1, 0) : new Color(1, 1, 1, 1);
        slot.slotUI.GetComponentInChildren<TextMeshProUGUI>().text = slot.item.id < 0 ? string.Empty : (slot.amount == 1);
    }
    
    public void OnEnterInterface(GameObject go) {
        MouseData_New.interfaceMouseIsOver = go.GetComponent<InventoryUI>();
    }
    
    public void OnExitInterface(GameObject go) {
        MouseData_New.interfaceMouseIsOver = null;
    }
    
    public void OnEnterSlot(GameObject go) {
        MouseData_New.slotHoveredOver = go;
    }
    
    public void OnExitSlot(GameObject go) {
        MouseData_New.slotHoveredOver = null;
    }
    
    public void OnStartDrag(GameObject go) {
        MouseData_New.tempItemBeingDragged = CreateDragImage(go);
    }
   
    public void CreateDragImage(GameObject go) {
        if (slotUIs[go].item.id < 0) return null;
        GameObject dragImageGo = new GameObject();
        RectTransform rectTransform = dragImageGo.AddComponent<RectTransform>();
        rectTransform.sizeDelta = new Vector2(50, 50);
        dragImageGo.transform.SetParent(transform.parent);
        Image image = dragImageGo.AddComponent<Image>();
        image.sprite = slotUIs[go].ItemObject.icon;
        image.raycastTarget = false;
        dragImageGo.name = "Drag Image";
        return dragImageGo;
    }
   
    public void OnDrag(GameObject go) {
        if (MouseData_New.tempItemBeingDragged == null) return;
        MouseData_New.tempItemBeingDragged.GetComponent<RectTransform>().position = Input.mousePosition;
    }
    
    public void OnEndDrag(GameObject go) {
        Destroy(MouseData_New.tempItemBeingDragged);
        if (MouseData_New.interfaceMouseIsOver == null)
            slotUIs[go].RemoveItem();
        else if (MouseData_New.slotHoveredOver) {
            InventorySlot_New mouseHoverSlotData = MouseData_New.interfaceMouseIsOver.slotUIs[MouseData_New.slotHoveredOver];
            inventoryObject.SwapItems(slotUIs[go], mouseHoverSlotData);
        }
    }
}


마우스가 아이콘을 클릭하여 드래그를 시작하면 임시 이미지를 만들어서 드래그중에는 마우스포인터 위치에 임시 이미지가 따라다니도록 하는 루틴입니다.
마우스를 특정 위치에서 놓았을 때에는 특정 UI에 있지 않은 경우라면 아이템을 버리는 것으로 보고 삭제합니다.
인터페이스위 슬롯 위에 있다면 아이템을 서로 교체하는 로직이 구동되게 됩니다.

 

 

오늘도 역시 눈은 즐겁지만 머리는 뱅글뱅글 도는 시간이 되었네요..



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

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

 

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

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

www.fastcampus.co.kr

 


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




이제부터는 게임내에 인벤토리 시스템을 구현하고, 장비 시스템과 아이템 시스템을 구현합니다.
오늘은 아이템 시스템 구현하기와 플레이어와 상점을 위한 인벤토리 시스템 구현하기의 시작입니다.

인벤토리 시스템은 Scriptable Object를 사용합니다. 이를 사용하면 Game내 데이터 관리와 전역적 접근을 쉽게 해줍니다.




+ 아이템 시스템 구현하기
+ 인벤토리 시스템 구현하기 - 구현된 아이템 시스템 이용.
+ 인벤토리 UI 구현하기 - 인벤토리 시스템에 UI 구현
+ 캐릭터 장비 교체 구현하기
+ 아이템 획득/사용 구현하기





가장 기본이되는 것은 Item 클래스입니다. 캐릭터의 스텟을 변화시키는 기능을 해주는 것이고, Item의 buffs에 ItemBuff 오브젝트를 생성하여 Attach하는 방식으로 구현할 수 있습니다.
ItemObject는 Item을 가지는 데이터 오브젝트입니다.
Item은 캐릭터에 대한 행동을 명시하고, ItemObject는 캐릭터의 행동과 표현방법, 설명 등을 포함하는 것입니다.
그리고 이를 List화하여 가지고 있는 ItemDatabaseObject를 구현하게 됩니다.

먼저 Item 행동에 해당하는 ItemBuff와 Item, ItemObject 컴포넌트 스크립트를 작성합니다.



ItemBuff Script

public enum CharacterAttribute
{
    Agility,
    Intellect,
    Stamina,
    Strength
}

[Serializable]
public class ItemBuff
{
    public CharacterAttribute stat;
    public int value;
    
    [SerializeField]
    private int min;
    [SerializeField]
    private int max;
    
    public int Min => min;
    public int Max => max;
    
    public ItemBuff(int min, int max) {
        this.min = min;
        this.max = max;
        
        GenerateValue();
    }
    
    public void GenerateValue() {
        value = UnityEngine.Random.Range(min, max); // 획득 아이템 랜덤 값
    }
    
    public void AddValue(ref int v) {
        v += value;
    }
}



Item Script

[Serializable]
public class Item
{
    public int id = -1;
    public string name;
    public ItemBuff[] buffs;
    
    public Item() {
        id = -1; // 비어있는 아이템
        name = "";
    }
    
    public Item(ItemObject itemobject) {
        name = itemObject.name;
        id = itemobject.data.id;
        buffs = new ItemBuff[itemObject.data.buffs.Length];
        for (int i = 0; i < buffs.Length; i++) {
            buffs[i] = new ItemBuff(itemobject.data.buffs[i].Min, itemObject.data.buffs[i].Max) {
                stat = itemObject.data.buffs[i].stat;
            };
        }
    }
}




ItemObject Script

public enum ItemType: int
{
    Helmet = 0,
    Chest = 1,
    Pants = 2,
    Boots = 3,
    Pauldrons = 4,
    Gloves = 5,
    LeftWeapon = 6,
    RightWeapon = 7,
    Food = 8,
    Default,
}

[CreateAssetMenu(fileName = "New Item", menuName = "Inventory System/Items/New Item")]
public class ItemObject : ScriptableObject
{
    public ItemType type;
    public bool stackable;
    public Sprite icon;
    public GameObject modelPrefab;
    public Item data = new Item();
    public List<string> boneNames = new List<string>();
    
    [TextArea(15, 20)]
    public string description;
   
    private void OnValidate() {
        boneNames.Clear();
        if (modelPrefab == null || modelPrefab.GetComponentInChildren<SkinnedMeshRenderer>() == null) {
            return;
        }
        
        SkinnedMeshRenderer renderer = modelPrefab.GetComponentInChildren<SkinnedMeshRenderer>();
        Transform[] bons = renderer.bones;
        
        for (Transform t in bones) {
            boneNames.Add(t.name);
        }
    }
    
    public Item CrateItem() {
        Item newItem = new Item(this);
        return newItem;
    }
}



여기까지하여 컴파일이 정상적으로 되면 아래와 같이 Create - Inventory System - Items - New Item 과 같이 추가된 것을 확인할 수 있습니다. 허거거걱... 머리가 핑핑... 눈도 핑핑 @v@~



 

 




이렇게 추가한 아이템들을 관리하는 ItemObjectDatabase 스크립트를 작성합니다. ㅠ


[CreateAssetMenu(fileName = "New Item Database", menuName = "Inventory System/Items/Database_New")]
public class ItemObjectDatabase : ScriptableObject
{
    public ItemObject[] itemObjects;
    
    public void OnValidate() {
        for (int i = 0; i < itemObjects.Length; i++) {
            itemObjects[i].data.id = i;
        }
    }
}


어마어마하네요.. CreateAssetMenu() 함수를 통해서 Unity에 원하는 형태로 Menu를 등록하고, Item 등의 컴포넌트도 생성할 수 있도록 구성한데다가, ItemObjectData를 구현해서 Unity UI 상에서 그냥 드래그해서 자동 추가가 되어버리다니... 헐...
무언가 할당하는 개념이 없고 UI에서 생성, 할당하는 부분들이 많아서 적응은 잘 되지 않고, 언제 왜 써야하는지 애매하긴 하지만 일단 어마한 기능이라고 밖에 말할 수 없겠네요.






이제 Inventory 시스템 구현입니다.
여기서의 메인은 InventorySlot 입니다. 이것은 아이템이 놓이는 공간이지요. 여기서 아이템이 추가, 삭제될 때마다 갱신 메시지를 발생시켜서 Action으로 관리하고, type 지정으로 해당하는 위치에만 놓이도록 하게 만들 예정입니다.

Inventory 부분은 Inventory UI와 연결되는 부분입니다

 



Item을 ItemObject에서 분리하고, Inventory를 InventoryObject와 구분하여 만든 이유는,
Item과 ItemBuff 내용을 그대로 Json으로 저장하기 편하며, Inventory도 클래스 그대로 Json으로 저장하기 편하기 때문입니다.
ItemObject나 InventoryObject는 데이터를 뽑아내는 과정을 거쳐야 처리할 수 있는데 말이지요.
Json으로 저장하지 않는다면 한 클래스로 구현하는 것이 관리상의 편의점은 있겠습니다.


[Serializable]
public class InventorySlot
{
    public ItemType[] allowedItems = new ItemType[0];
    
    [NonSerialized] // Json 저장시 제외
    public InventoryObject parent;
    [NonSerialized]
    public GameObject slotUI;
    [NonSerialized]
    public Action<InventorySlot> OnPreUpdate;
    [NonSerialized]
    public Action<InventorySlot> OnPostUpdate;
    
    public Item item;
    public int amount;
    public ItemObject ItemObject {
        get {
            return item.id >= 0 ? parent.database.itemObjects[item.id] : null;
        }
    }
    
    public InventorySlot() => UpdateSlot(new Item(), 0);
    public InventorySlot(Item item, int amount) => UpdateSlot(item, amount);
   
    public void RemoveItem() => UpdateSlot(new Item(), 0);
    public void AddAmount(int value) => UpdateSlot(item, amount += value);
    
    public void UpdateSlot(Item item, int amount) {
        OnPreUpdate?.Invoke(this);
        this.item = item;
        this.amount = amount;
        OnPostUpdate?.Invoke(this);
    }
    
    public bool CanPlaceInSlot(ItemObject itemObject) {
        if (allowedItems.Length <= 0 || itemObject == null || itemObject.data.id < 0) {
            return true;
        }
        
        foreach (ItemType type in allowedItems) {
            if (itemObject.type == type) {
                return true;
            }
        }
        
        return false;
    }
}



[Serializable]
public class Inventory
{
    public InventorySlot[] slots = new InventorySlot[24];
    
    public void Clear() {
        foreach (InventorySlot slot in slots) {
            slot.RemoveItem(); // or slot.UpdateSlot(new Item(), 0);
        }
    }
   
    public bool IsContain(ItemObject itemObject) {
        return IsContain(itemObject.data.id);
    }
    
    public bool IsContain(int id) {
        return slots.FirstOrDefault(i => i.item.id == id) != null;
    }
}





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

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


 

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

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

www.fastcampus.co.kr

 

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


전투 시스템의 NPC 전투 UI 구현하기 입니다.




항상 카메라를 바라보는 Billboard 기법라는 것을 사용하여 만들어지게 됩니다.
캐릭터와 UI가 회전을 하더라도 항상 카메라를 바라보게 해서 유저가 항상 동일한 형태의 UI를 볼 수 있도록 하는 기법입니다.

이러한 Billboard를 구현하기 위해 Camera Facing이라는 컴포넌트 스크립트를 구현하도록 합니다.


public class CameraFacing_New : MonoBehaviour
{
    Camera referenceCamera;
    public bool reverseFace = false; // 고정이되는 축과 향하는 방향이 반대로 보여주는 flag
    
    public enum Axis // 특정 축을 기준으로 회전.
    {
        up, down, left, right, forward, back
    }
    public Axis axis = Axis.up;
    
    public Vector3 GetAxis(Axis refAxis)
    {
        switch (refAxis) {
            case Axis.down: return Vector3.down;
            case Axis.forward: return Vector3.forward;
            case Axis.back: return Vector3.back;
            case Axis.left: return Vector3.left;
            case Axis.right: return Vector3.right;
        }
        return Vector3.up;
    }
    
    private void Awake()
    {
        if (!referenceCamera) {
            referenceCamera = Camera.main;
        }
    }
    
    private void LateUpdate()
    {
        Vector3 targetPos = transform.position + referenceCamera.transform.rotation * (reverseFace ? Vector3.forward : Vector3.back);
        Vector3 targetOrientation = referenceCamera.rotation * GetAxis(axis);
        
        transform.LookAt(targetPos, targetOrientation);
    }
}





UI에서 "Slider"를 추가하면 "Canvas"의 자식으로 "Slider"가 추가됩니다.
+ Render Mode를 "Screen Space - Overlay"에서 "World Space"로 변경합니다.
+ Pos X, Y, Z도 모두 "0"으로 지정합니다.
+ Scale X, Y, Z를 모두 "0.05"로 설정합니다.
+ Width = 100, Height = 20




일단 실행해 보면 캐릭터이 회전에 따라 체력바가 따라서 회전하는 것을 볼 수 있는데, 어색하지요? 아직 CameraFacing을 연동하지 않았기 때문입니다.

Canvas에 Camera Facing 컴포넌트를 추가하고 실행하면 아래와 같이 캐릭터가 회전하더라도 Y축으로 고정된 체력바가 고정되어 표시되는 것을 확인할 수 있습니다.





이제 여기 NPC에 HP를 변경하는 스크립트를 추가하도록 하겠습니다.


public class NPCBattleUI_New : MonoBehaviour
{
    private Slider hpSlider;
    
    public float MaximunValue
    {
        get => hpSlider.maxValue;
        set {
            hpSlider.maxValue = value;
        }
    }
    
    public float MinimunValue
    {
        get => hpSlider.minValue;
        set {
            hpSlider.minValue = value;
        }
    }
    
    public float Value
    {
        get => hpSlider.value;
        set {
            hpSlider.value = value;
        }
    }
    
    public void Awake() {
    {
        hpSlider = gameObject.GetComponentInChildren<Slider>();
    }
    
    private void OnEnable()
    {
        GetComponent<Canvas>().enabled = true;
    }
    
    private void OnDisble()
    {
        GetComponent<Canvas>().enabled = false;
    }
}


위와 같이 작업하고 EnemyController_Range_Original.cs에서 battleUI 인스턴스를 사용하여 최대, 최소, 현재 체력값을 설정하도록 수정해 줍니다. 그리고 데미지가 입었을 때 HP가 변화하는 것을 처리하기 위해 아래와 같이 추가해줍니다.

public void TakeDamage(int damage, GameObject hitEffectPrefab)
{
    // 기존 코드 유지
    
    health -= damage;
    
    if (battleUI) {
        battleUI.Value = health;
    }
    
    // 기존 코드 유지
}





현재까지 상태로 실행을 해보면 적캐릭터가 데미지를 입을 때마다 HP가 주는 것을 확인할 수 있습니다. 단지 HP나 숫자가 반대로 표시되는 것을 볼 수 있습니다.

카메라를 UI가 바라보는 상황이 되어 유저가 바라보는 방향이 Flip 상태로 되기 때문에 "Reverse Face [v]"를 체크하여 유저가 바라보는 방향으로 맞추어진 UI로 만들어줍니다.




그 상태로 플레이를 진행하면 정상적으로 표시되는 것을 확인할 수 있습니다.



데미지 관련하여서도.. (캐릭터 위에 표시되는 숫자값) 동일하게 NPC UI를 활용하여 구현하면 됩니다.



TextMeshPro를 사용하여 보도록 하겠습니다. Damage값을 애니메이션으로하여 위로 튀는 것처럼 보여주면 좀더 재미있는 효과가 되므로 해당 애니메이션을 추가하도록 합니다.

 

 



Text 애니메이션 작업은 시간이 걸리는 관계로 미리 제작된 내용으로 진행합니다.
여튼 동작은 그렇습니다. Text가 위로 통통 튀는 애니메이션이고, 위로 올라갈때 Text Alpha값이나, Scale값등이 적용된 애니메이션이라고 보면 됩니다.

동일하게 NPCBattleUI 로 구현한 것이므로 해당 클래스에서 처리하면 됩니다. 추가된 코드만 작성합니다.


public class NPCBattleUI_New : MonoBehaviour
{
    [SerializeField]
    private GameObject damageTextPrefab;
    
    public void CrateDamageText(int damage)
    {
        if (damageTextPrefab != null) {
            GameObject damageTextGO = Instantiate(damageTextPrefab, transform);
            DamageText damageText = damageTextGO.GetComponent<DamageText>();
            if (damageText == null) {
                Destroy(damageTextGO);
            }
            
            damageText.Damage = damage;
        }
    }
}

// 특정 시간후에 DamageText가 사라지도록 구현
public class DamageText_New : MonoBehaviour
{
    public float delayTimeToDestroy = 1.0f;
    
    void Start()
    {
        Destroy(gameObject, delayTimeToDestroy);
    }
}




실행을 해보면 TakeDamage()가 발생할 때마다 DamageText가 애니메이션으로 잘 동작하는 것을 확인할 수 있습니다.
여기까지가 NPC UI를 사용하여 적의 HP바와 DamageText를 표시하도록 구현이 완료되었습니다.

 




이제 캐릭터의 이동지점을 클릭하였을 때 클릭 지점을 표시하는 기능을 만들어 보겠습니다.
Pointer를 하나 추가하고 Position Y 값을 -100으로 설정합니다. Pointer의 Enable/Disable을 사용하여 보였다 안 보였다 하는 형태를 취할 수도 있고, 여기에서와 같이 -100 위치에 있다가 클릭 위치로 순간 이동시켜서 표시하는 방법도 있습니다.

해당 Pointer에 Image를 추가하여 아래와 같이 설정한 상태로 진행합니다.




이제 클릭 지점에 해당 Pointer Image가 표시되도록 하는 컴포넌트 스크립트를 작성하면 되겠지요~


public class PlaceTargetWithMouse : MonoBehaviour
{
    public float surfaceOffset = 1.5f; // gameObject의 위치에서 조금 높여주기 위해.. 지면과 겹치는 문제 해결위해
    public Transform target = null;
    
    private void Update()
    {
        if (target) {
            transform.position = target.position + Vector3.up * surfaceOffset;
        }
    }
    
    // 마우스 클릭 hit 정보
    public void SetPosition(Raycast hit)
    {
        target = null;
        transform.position = hit.point + hit.normal * surfaceOffset;
    }
}


그리고 PlayerCharactor에서 마우스 클릭을 할 때 적이 아니라면,

if (picker) picker.SetPosition(hit);

코드를 추가해 주면 되고,

만약 적이라면,

if (picker) picker.target = hit.collider.transform;

코드를 추가해 주면 됩니다.




실행해보면 잘 동작하는 것을 확인할 수 있는데요..
여기서의 문제점은 굴곡이 있는 지형이나 벽에서 클릭을 할 경우 파묻히는 현상이 나타날 수 있습니다.

파묻힘을 해결하기 위해서는 데칼 기법이나 프로젝터 기법을 사용하면 됩니다.
이런 구현은 유니티 기본 샘플에 포함되어 있으니 필요한 경우 가져다 사용하면 됩니다.



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

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


 

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

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

www.fastcampus.co.kr

 

+ Recent posts