낮과 밤 구현

빈 오브젝트를 생성하고 해와 달 역할을 해줄 Directional Light 두 개를 하위에 넣는다.

 

낮과 밤, 시간이 바뀌게 할 스크립트를 만든다.

0부터 1까지 시간을 조절하도록 하고 게임 내의 하루 시간을 몇으로 할 것인지, 자정의 각도를 몇으로 할 것인지 정할 수 있게 해준다. AnimationCurve는 원하는 각도로 그래프를 그려서 타임 값을 가져올 수 있고, 반대로 타임 값을 주면 그 시간에 맞는 그래프를 가져온다. 시간에 따라 빛의 각도를 조절하여 실제 해가 움직이는 것처럼 만든다.

[Range(0.0f, 1.0f)]
public float time;
public float fullDayLength;
public float startTime = 0.4f;
private float timeRate;
public Vector3 noon;

[Header("Sun")]
public Light sun;
public Gradient sunColor;
public AnimationCurve sunIntensity;

[Header("Moon")]
public Light moon;
public Gradient moonColor;
public AnimationCurve moonIntensity;

[Header("Other Lighting")]
public AnimationCurve lightingIntensityMultiplier;
public AnimationCurve reflectionIntensityMultiplier;

private void Start()
{
    timeRate = 1.0f / fullDayLength;
    time = startTime;
}

private void Update()
{
    time = (time + timeRate * Time.deltaTime) % 1.0f;

    UpdateLighting(sun, sunColor,sunIntensity);
    UpdateLighting(moon, moonColor,moonIntensity);

    RenderSettings.ambientIntensity = lightingIntensityMultiplier.Evaluate(time);
    RenderSettings.reflectionIntensity = reflectionIntensityMultiplier.Evaluate(time);

}

void UpdateLighting(Light lightSource, Gradient colorGradiant, AnimationCurve intensityCurve)
{
    float intensity = intensityCurve.Evaluate(time);

    lightSource.transform.eulerAngles = (time - (lightSource == sun ? 0.25f : 0.75f)) * noon * 4.0f;
    lightSource.color = colorGradiant.Evaluate(time);
    lightSource.intensity = intensity;

    GameObject go = lightSource.gameObject;
    if(lightSource.intensity ==0&&go.activeInHierarchy)
        go.SetActive(false);
    else if( lightSource.intensity>0 &&!go.activeInHierarchy)
        go.SetActive(true);
}

 

유니티로 돌아와서 그래프로 시간에 따른 조명 색을 정해준다.

 

 

아이템 상호작용

Interactable 레이어를 추가하여 상호작용 할 수 있는 것들에 대한 처리를 준비한다. 아이템 데이터로 아이템을 생성하기 위해 스크립트를 만든다.

public enum ItemType
{
    Resource,
    Equipable,
    Consumable
}

public enum ConsumableType
{
    Hunger,
    Health
}

[System.Serializable]
public class ItemDataConsumable
{
    public ConsumableType type;
    public float value;
}

[CreateAssetMenu(fileName ="Item", menuName = "New Item")]
public class ItemData : ScriptableObject
{
    [Header("Info")]
    public string displayName;
    public string description;
    public ItemType type;
    public Sprite icon;
    public GameObject dropPrefab;

    [Header("Stacking")]
    public bool canStack;
    public int maxStackAmount;

    [Header("Consumable")]
    public ItemDataConsumable[] consumables;

}

 

아이템들을 생성한다.

 

Rigidbody와 Box Collider, Prefab를 붙이고 Prefab에 붙어있던 Capsule Collider는 지운다. 아까 생성한 Interactable 레이어로 바꿔준다.

 

Item에 ItemData를 전달하기 위해 스크립트를 붙인다.

public class ItemObject : MonoBehaviour
{
    public ItemData item;
}

 

InteractionManager 스크립트를 만들고 인터페이스를 ItemData 스크립트에 상속 시킨다.

public interface IInteractable
{
    string GetInteractPrompt();
    void OnInteract();
}

public class InteractionManager : MonoBehaviour
{
    public float checkRate = 0.05f;
    private float lastCheckTime;
    public float maxCheckDistance;
    public LayerMask layerMask;

    private GameObject curInteractGameobject;
    private IInteractable curInteractable;

    public TextMeshProUGUI promptText;
    private Camera camera;


    // Start is called before the first frame update
    void Start()
    {
        camera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {
        if (Time.time - lastCheckTime > checkRate)
        {
            lastCheckTime = Time.time;

            Ray ray = camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));
            RaycastHit hit;

            if (Physics.Raycast(ray, out hit, maxCheckDistance, layerMask))
            {
                if (hit.collider.gameObject != curInteractGameobject)
                {
                    curInteractGameobject = hit.collider.gameObject;
                    curInteractable = hit.collider.GetComponent<IInteractable>();
                    SetPromptText();
                }
            }
            else
            {
                curInteractGameobject = null;
                curInteractable = null;
                promptText.gameObject.SetActive(false);
            }
        }
    }

    private void SetPromptText()
    {
        promptText.gameObject.SetActive(true);
        promptText.text = string.Format("<b>[E]</b> {0}", curInteractable.GetInteractPrompt());
    }

    public void OnInteractInput(InputAction.CallbackContext callbackContext)
    {
        if (callbackContext.phase == InputActionPhase.Started && curInteractable != null)
        {
            curInteractable.OnInteract();
            curInteractGameobject = null;
            curInteractable = null;
            promptText.gameObject.SetActive(false);
        }
    }
}

 

 

public class ItemObject : MonoBehaviour, IInteractable
{
    public ItemData item;

    public string GetInteractPrompt()
    {
        return string.Format("Pickup {0}", item.displayName);
    }

    public void OnInteract()
    {
        Destroy(gameObject);
    }
}​

 

PromptText를 상태창 UI에 추가한다.

 

Player에 InteractionManager 스크립트를 연결하고 상호작용 문구를 연결한다.

 

Player에 Interact를 추가하여 상호작용 하도록 한다.

 

 

인벤토리 & 아이템 사용

인벤토리를 만들기 위해 Canvas를 만들고 클릭해서 사용할 것이기 때문에 Graphic Raycaster을 추가한다.

스크린 해상도를 설정하고 상태창과 겹쳐졌을 때, 가장 위에 와야하기 때문에 Sort Order을 1로 해준다.

 

하단에 인벤토리의 배경이 될 Image를 추가한다. 인벤토리 아이템을 선택할 수 있는 창을 만들고 아이템 슬롯을 만들어서 Prefab화 하여 Grid Layout Group으로 묶어준다.

 

인터페이스 창을 만들어준다.

 

Inventory 스크립트를 만들고 유니티에서 UI를 연결한다.Slot Prefab에 Outline을 추가하고 선택했을 때 Outline이 켜지고 아이템 정보가 뜨도록 한다. 기존에 같은 아이템을 가지고 있다면 쌓을 수 있는지 여부를 확인하고 쌓는다. 쌓지 못하거나 최대 수량으로 아이템이 쌓여있으면 다른 슬롯에 아이템을 추가한다. 인벤토리가 다 찼을 때, 아이템을 획득하면 랜덤으로 회전하며 떨어뜨린다.

public class ItemSlot
{
    public ItemData item;
    public int quantity;
}

public class Inventory : MonoBehaviour
{
    public ItemSlotUI[] uiSlots;
    public ItemSlot[] slots;

    public GameObject inventoryWindow;
    public Transform dropPosition;

    [Header("Selected Item")]
    private ItemSlot selectedItem;
    private int selectedItemIndex;
    public TextMeshProUGUI selectedItemName;
    public TextMeshProUGUI selectedItemDescription;
    public TextMeshProUGUI selectedItemStatNames;
    public TextMeshProUGUI selectedItemStatValues;
    public GameObject useButton;
    public GameObject equipButton;
    public GameObject unEquipButton;
    public GameObject dropButton;

    private int curEquipIndex;

    private PlayerController controller;
    private PlayerConditions condition;

    [Header("Events")]
    public UnityEvent onOpenInventory;
    public UnityEvent onCloseInventory;

    public static Inventory instance;
    void Awake()
    {
        instance = this;
        controller = GetComponent<PlayerController>();
        condition = GetComponent<PlayerConditions>();
    }
    private void Start()
    {
        inventoryWindow.SetActive(false);
        slots = new ItemSlot[uiSlots.Length];

        for (int i = 0; i < slots.Length; i++)
        {
            slots[i] = new ItemSlot();
            uiSlots[i].index = i;
            uiSlots[i].Clear();
        }

        ClearSeletecItemWindow();
    }

    public void OnInventoryButton(InputAction.CallbackContext  callbackContext)
    {
        if(callbackContext.phase == InputActionPhase.Started)
        {
            Toggle();
        }
    }


    public void Toggle()
    {
        if (inventoryWindow.activeInHierarchy)
        {
            inventoryWindow.SetActive(false);
            onCloseInventory?.Invoke();
            controller.ToggleCursor(false);
        }
        else
        {
            inventoryWindow.SetActive(true);
            onOpenInventory?.Invoke();
            controller.ToggleCursor(true);
        }
    }

    public bool IsOpen()
    {
        return inventoryWindow.activeInHierarchy;
    }

    public void AddItem(ItemData item)
    {
        if(item.canStack)
        {
            ItemSlot slotToStackTo = GetItemStack(item);
            if(slotToStackTo != null)
            {
                slotToStackTo.quantity++;
                UpdateUI();
                return;
            }
        }

        ItemSlot emptySlot = GetEmptySlot();

        if(emptySlot != null)
        {
            emptySlot.item = item;
            emptySlot.quantity = 1;
            UpdateUI();
            return;
        }

        ThrowItem(item);
    }

    void ThrowItem(ItemData item)
    {
        Instantiate(item.dropPrefab, dropPosition.position, Quaternion.Euler(Vector3.one * Random.value * 360f));
    }

    void UpdateUI()
    {
        for(int i = 0; i< slots.Length; i++)
        {
            if (slots[i].item != null)
                uiSlots[i].Set(slots[i]);
            else
                uiSlots[i].Clear();
        }
    }

    ItemSlot GetItemStack(ItemData item)
    {
        for (int i = 0; i < slots.Length; i++)
        {
            if (slots[i].item == item && slots[i].quantity < item.maxStackAmount)
                return slots[i];
        }

        return null;
    }

    ItemSlot GetEmptySlot()
    {
        for (int i = 0; i < slots.Length; i++)
        {
            if (slots[i].item == null)
                return slots[i];
        }

        return null;
    }

    public void SelectItem(int index)
    {
        if (slots[index].item == null)
            return;

        selectedItem = slots[index];
        selectedItemIndex = index;

        selectedItemName.text = selectedItem.item.displayName;
        selectedItemDescription.text = selectedItem.item.description;

        selectedItemStatNames.text = string.Empty;
        selectedItemStatValues.text = string.Empty;

        for(int i = 0; i< selectedItem.item.consumables.Length; i++)
        {
            selectedItemStatNames.text += selectedItem.item.consumables[i].type.ToString() + "\n"; 
            selectedItemStatValues.text += selectedItem.item.consumables[i].value.ToString() + "\n";
        }

        useButton.SetActive(selectedItem.item.type == ItemType.Consumable);
        equipButton.SetActive(selectedItem.item.type == ItemType.Equipable && !uiSlots[index].equipped);
        unEquipButton.SetActive(selectedItem.item.type == ItemType.Equipable && uiSlots[index].equipped);
        dropButton.SetActive(true);
    }

    private void ClearSeletecItemWindow()
    {
        selectedItem = null;
        selectedItemName.text = string.Empty;
        selectedItemDescription.text = string.Empty;

        selectedItemStatNames.text = string.Empty;
        selectedItemStatValues.text = string.Empty;

        useButton.SetActive(false);
        equipButton.SetActive(false);
        unEquipButton.SetActive(false);
        dropButton.SetActive(false);
    }

    public void OnUseButton()
    {
        if(selectedItem.item.type == ItemType.Consumable)
        {
            for (int i = 0; i < selectedItem.item.consumables.Length; i++)
            {
                switch (selectedItem.item.consumables[i].type)
                {
                    case ConsumableType.Health:
                        condition.Heal(selectedItem.item.consumables[i].value); break;
                    case ConsumableType.Hunger:
                        condition.Eat(selectedItem.item.consumables[i].value); break;
                }
            }
        }
        RemoveSelectedItem();
    }

    public void OnEquipButton()
    {

    }

    void UnEquip(int index)
    {

    }

    public void OnUnEquipButton()
    {

    }

    public void OnDropButton()
    {
        ThrowItem(selectedItem.item);
        RemoveSelectedItem();
    }

    private void RemoveSelectedItem()
    {
        selectedItem.quantity--;

        if(selectedItem.quantity <= 0 ) 
        {
            if (uiSlots[selectedItemIndex].equipped )
            {
                UnEquip(selectedItemIndex);
            }

            selectedItem.item = null;
            ClearSeletecItemWindow();
        }

        UpdateUI();
    }

    public void RemoveItem(ItemData item)
    {

    }

    public bool HasItems(ItemData item, int quantity)
    {
        return false;
    }
}

 

아이템의 떨어지는 위치를 지정하기 위해 Player 오브젝트에 오브젝트를 추가하고 연결한다.

 

마찬가지로 ItemUISlot 스크립트를 만들어서 Slot Prefab에 적용한다.

public Button button;
public Image icon;
public TextMeshProUGUI quatityText;
private ItemSlot curSlot;
private Outline outline;

public int index;
public bool equipped;

private void Awake()
{
    outline = GetComponent<Outline>();
}

private void OnEnable()
{
    outline.enabled = equipped;
}

public void Set(ItemSlot slot)
{
    curSlot = slot;
    icon.gameObject.SetActive(true);
    icon.sprite = slot.item.icon;
    quatityText.text = slot.quantity > 1 ? slot.quantity.ToString() : string.Empty;

    if (outline != null)
    {
        outline.enabled = equipped;
    }
}

public void Clear()
{
    curSlot = null;
    icon.gameObject.SetActive(false);
    quatityText.text = string.Empty;
}

public void OnButtonClick()
{
    Inventory.instance.SelectItem(index);
}

 

Button을 달고 Button에 자기 자신을 연결시켜준다.

 

아이템들을 Prefab화 시키고 아이템 데이터에 연결한다.

 

Player에 인벤토리를 연결하여 탭을 눌렀을 때, 인벤토리 창이 켜지도록 한다.

 

ItemObject 스크립트에 아이템을 주웠을 때, 인벤토리 창에 아이템이 추가되도록 코드를 추가한다.

public void OnInteract()
{
    Inventory.instance.AddItem(item);
    Destroy(gameObject);
}

 

인벤토리 버튼들을 연결한다.

 

아이템을 사용했을 때, 플레이어의 상태가 변하도록 한다.

 

 

+ Recent posts