게임에서 캐릭터가 움직이거나 점프를 하는 모션이 있는 것을 본 적이 있을 것이다.

유니티 엔진에서는 Animator라는 기능이 존재하는데, 조건에 따라서 에니메이션의 전환을 설정할 수 있다.

프로젝트 창에 마우스 우클릭을 하여 Create - Animator Controller하여 새 파일을 만들어준다. 

만든 파일을 더블 클릭하면 이런 기본 창이 등장하는데, 휠 버튼을 눌러서 이동할 수 있다.

 

Animator Controller

 

이렇게 생성한 Animator Controller에서 새 상태를 추가할 수 있다.

예시를 보이기 위해 아래 사진에 생성을 해보았다.

Base Layer 화면에 마우스 우클릭을 하여 Create State - Empty로 새 상태를 만들고 Inspector에서 이름을 변경한다.

또한 Inspector에서 Motion에 원하는 애니메이션을 넣으면 해당 상태에 그 애니메이션이 작동된다.

 

Animator Controller

 

만든 상태에 마우스 우클릭을 하여 Make Transition을 클릭하면 선이 하나 따라온다.

다른 상태에 클릭을 해주면 연결이 된다. 이런식으로 상태 연결을 한다.

이런식으로 만든 Animator Controller를 애니메이션을 적용할 오브젝트에 컴포넌트로 붙여주면 된다.

 

코드에서 설정한 로직을 구현하기 전 몇 가지 설정을 해야한다.

 

AnimatorController / 화살표 클릭

 

왼쪽 위에 Parameters에 있는 speed는 임의로 생성한 변수이다. 코드에서는 이 speed 변수를 이용하여 애니메이션의 순서를 제어할 계획이다.

 

AnimatorController에서 화살표 클릭시 Inspector 창

 

AnimatorController에서 화살표를 클릭한 후 Inspector 창을 살펴보면 맨 아래 Conditions에 임의로 생성했던 speed라는 변수가 있다. speed라는 변수의 값이 1보다 크면 WAIT->RUN 으로 변경한다는 조건을 달아놓았다.

또한 Settings 아래 시간이 표시되어있는 부분은 Animation Blending을 할 수 있다. 상태가 바뀌며 Animation이 변경될 때, 자연스럽게 변경될 수 있도록 Animation을 섞어주는 기능이다.

 

이제 코드로 넘어가보자.

 

PlayerController.cs

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    float _speed = 10.0f;

    Vector3 _destPos;
    void Start()
    {
        Managers.Input.MouseAction -= onMouseClicked;
        Managers.Input.MouseAction += onMouseClicked;
    }

    float wait_run_ratio = 0;

    public enum PlayerState
    {
        Moving,
        Idle,
    }
    PlayerState _state = PlayerState.Idle;

    void UpdateMoving()
    { // 이동 상태일 때 실행되는 함수
        Vector3 dir = _destPos - transform.position;
        if (dir.magnitude < 0.0001f)
        {
            _state = PlayerState.Idle;
        }
        else
        {
            float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);

            transform.position += dir.normalized * moveDist;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime);
        }


	// 이동하려는 플레이어에 Animator 컴포넌트를 붙여주고, 그 안에 AnimatorController 넣어줌
        Animator anim = GetComponent<Animator>(); // AnimatorController 요소 가져옴
        anim.SetFloat("speed", _speed); // AnimatorController의 speed 변수에 기존에 설정한 속도값을 넣어줌

    }
    void UpdateIdle()
    {
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("speed", 0); // AnimatorController의 speed 변수에 0을 넣음

    }
    void Update()
    {
	// 1프레임 당 한번 업데이트가 실행되는데, 실행마다 상태를 체크해서 함수를 실행한다.
        switch (_state)
        {
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
        }
    }

    void onMouseClicked(Define.MouseEvent evt)
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);
        // int mask = (1 << 8) | (1 << 9);

        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Wall")))
        {
            //hit.collider.gameObject.tag;
            //Debug.Log($"RayCase Camera @ {hit.collider.gameObject.name}");
            _destPos = hit.point;
            _state = PlayerState.Moving;
        }
    }
}

 

코드에서 AnimatorController의 speed 변수에 접근함으로써 애니메이션 상태를 변경할 수 있다.

speed 변수에서 1을 기준으로 낮으면 WAIT, 높으면 RUN 에 해당하는 애니메이션이 실행되는 것을 볼 수 있다.

'UNITY' 카테고리의 다른 글

UNITY - UI 자동화 - 게임 UI 구현  (0) 2024.01.22
UNITY - Scene  (1) 2024.01.12
UNITY - Camera - 마우스 클릭으로 이동하기  (0) 2024.01.12
UNITY - ResourceManager  (0) 2024.01.12
UNITY - UI 자동화(2) - 인벤토리 구현  (0) 2024.01.11

게임을 하다 보면 WASD를 통해 이동하는 것 말고도 마우스 클릭을 통해 이동하는 게임도 종종 있다.

이번에는 Raycast를 이용해서 구현해 보았다.

 

간단한 원리부터 보자면, 유니티의 Camera에는 ScreenPointToRay라는 함수가 존재한다. 인자로 Input.mousePosition을 넣어준다면 게임 화면에서의 2D 좌표를 3D 공간에서 발사한 Ray의 원점(origin), 방향(direction)을 Vector3의 형태로 반환해준다. Ray 구조체를 Raycast 함수의 인자로 넣어서 RaycastHit 구조체에 정보를 저장해준다. RaycastHit 구조체에 있는 point는 발사한 Ray가 부딪힌 오브젝트의 지점을 좌표로 가지고 있다. 마지막으로 플레이어는 point 지점으로 이동을 하기만 하면 마우스 클릭으로 이동을 하는 것이다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{

    enum PlayerState
    {
        Idle,
        Moving,
    }

    [SerializeField]
    float _speed = 10;

    PlayerState _state = PlayerState.Idle;
    Vector3 _destPos;
    void Start()
    {

        Managers.Input.MouseAction -= OnMouseClicked;
        Managers.Input.MouseAction += OnMouseClicked;
    }

    private void Update()
    {
        switch (_state)
        {
            case PlayerState.Idle:
                UpdateIdle();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
        }
    }

    void UpdateIdle()
    {

    }

    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position;

        if (dir.magnitude < 0.01f)
        {
            _state = PlayerState.Idle;
        }
        else
        {
            float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 0.2f);
        }
    }

    void OnMouseClicked()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.yellow, 1.0f);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f))
        {
            _destPos = hit.point;
            _state = PlayerState.Moving;
        }
    }
}

 

이번 코드에서는 State 패턴을 이용하였다. 플레이어의 상태에 따라 함수를 실행함으로써 관리가 편리해진다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ResourceManager
{
    public T Load<T>(string path) where T : Object
    {
        return Resources.Load<T>(path);
    }

    public GameObject Instantiate(string path, Transform parent = null)
    {
        
        GameObject original = Load<GameObject>($"Prefabs/{path}");
        if (original == null)
        {
            Debug.Log($"Failed to load prefab : {path}");
            return null;
        }

        GameObject go = Object.Instantiate(original, parent);
        go.name = original.name;

        return go;
    }

    public void Destroy(GameObject go)
    {
        if(go == null)
        {
            return;
        }

        Object.Destroy(go);
    }
}

 

기존에 있는 기능들이지만 어떤 로직으로 작동되는지 구현한 코드이다.

Managers에서 접근하여 사용하기 때문에 관리가 관리가 편하다.

 

[UNITY] UI 자동화(1) - 인벤토리 구현

일단 결론부터 말하자면 Scene을 관리하는 Scene 객체에 붙여서, Scene이 로드되면 SceneUI를 불러오는 코드이다. 자동화를 해놓았기 때문에, 코드 한 줄만 작성하면 된다. GameScene.cs using System.Collections;

mainsdev.tistory.com

 

1편에 이어서 글을 써보고자 한다.

 

UI_Scene.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_Scene : UI_Base
{
    public override void Init()
    {
        Managers.UI.SetCanvas(gameObject, false);
    }
}

 

모든 SceneUI의 부모로 두어 관리를 편하게 하려고 만든 코드이기 때문에 상당히 간단한 것을 볼 수 있다.

UIManager의 SetCanvas 함수를 이용하여 호출한 자식 SceneUI에 Canvas 컴포넌트를 추가해준다.

 

UI_Inven.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_Inven : UI_Scene
{ // 인벤토리를 생성하는 코드
    enum GameObjects
    {
        GridPanel // 인벤토리 프리팹 산하에 있는 GridPanel을 불러오기 위해 Enum으로 저장
    }
    void Start()
    {
        Init();
        
    }
    public override void Init()
    {
        base.Init(); // UI_Scene의 Init()을 실행

        Bind<GameObject>(typeof(GameObjects)); // 불러올 오브젝트들이 저장된 Enum을 바인드
        GameObject gridPanel = Get<GameObject>((int)GameObjects.GridPanel); // 바인드 딕셔너리에 enum의 값을 넣어서 오브젝트를 가져온다.
        foreach (Transform child in gridPanel.transform) // 위에서 선언된 enum 안의 객체 이름에 해당하는 객체의 자식을 모두 제거한다.
            Managers.Resource.Destroy(child.gameObject);

        for (int i = 0; i < 8; i++)
        {
            GameObject item = Managers.UI.MakeSubItem<UI_Inven_Item>(gridPanel.transform).gameObject;
            // 위에서 불러온 GridPanel 객체에 인벤토리에 들어갈 아이템을 생성해준다.
            // GridPanel을 parent로 설정해주고, 생성한 객체를 item에 저장한다.
            UI_Inven_Item invenItem = item.GetOrAddComponent<UI_Inven_Item>();
            // 객체에 UI_Inven_Item 컴포넌트를 붙여주고, 불러온다.
            invenItem.SetInfo($"Item {i} 번");
            // UI_Inven_Item 에 선언되어있는 SetInfo 함수를 통해 생성한 아이템에 이름을 붙여준다
        }
    }

    void Update()
    {
        
    }
}

 

인벤토리를 생성하는 주요 코드이다. 

Scene을 관리하는 GameScene.cs에서 ShowSceneUI<UI_Inven>()을 호출하여 UI_Inven 이름을 가진 prefab을 불러온다.

prefab 산하에 GridPanel에 UI_Scene의 SetCanvas를 통해 Canvas 컴포넌트를 추가해준다.

그 다음, 기존 prefab에 존재하던 GridPanel 산하의 자식들을 지운 뒤, 반복문에 GetOrAddComponent<UI_Inven_Item>()을 통해 UI_Inven_Item 이라는 프리팹으로 아이템을 만들고, 인자로 넣어준 gridPanel.transform을 부모로 지정해주었다.

굳이 지우고 다시 만들 필요는 없지만 만들어둔 코드가 잘 작동하는지 테스트하기 위해 남겨두었다.

 

UI_Inven_Item.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_Inven_Item : UI_Base
{ // 인벤토리 안에 있는 아이템 클래스
    enum GameObjects
    {
        ItemIcon,
        ItemNameText,
    }

    string _name;

    void Start()
    {
        Init();
    }

    public override void Init()
    {
        Bind<GameObject>(typeof(GameObjects)); // 위에서 선언한 Enum 타입을 바인드
        Get<GameObject>((int)GameObjects.ItemNameText).GetComponent<Text>().text = _name; 
        // 프리팹에 존재하는 ItemNameText 객체안의 Text 컴포넌트의 text 값을 SetInfo로 받아온 이름으로 설정한다.

        Get<GameObject>((int)GameObjects.ItemIcon).BindEvent(PointerEventData => { Debug.Log($"item click! {_name}"); }); 
        // 아이템 아이콘을 클릭 했을 경우 Debug.Log로 출력하는 이벤트 생성
    }
        
    public void SetInfo(string name)
    {
        _name = name; // 아이템 이름 설정
    }
}

 

UI_Inven_Item 프리팹은 2가지 요소로 나뉘어 있는데, 이미지를 담은 ItemIcon, 아이템의 이름을 저장할 ItemNameText로 구성되어 있다. 초기화 함수에서 프리팹의 요소들을 바인드한다.

Get을 통해 각각 요소들을 설정한다. 

 

UI_EventHandler.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class UI_EventHandler : MonoBehaviour, IDragHandler, IPointerClickHandler
{
    public Action<PointerEventData> OnClickHandler = null;
    public Action<PointerEventData> OnDragHandler = null;

    public void OnPointerClick(PointerEventData eventData) // 클릭 했을 경우
    {
        if (OnClickHandler != null)
            OnClickHandler.Invoke(eventData); // 이벤트 함수 실행
    }
    public void OnDrag(PointerEventData eventData) // 드래그 했을 경우
    {
        if (OnDragHandler != null)
            OnDragHandler.Invoke(eventData); // 이벤트 함수 실행
    }


}

 

드래그와 클릭 이벤트를 처리하기 위해 인터페이스를 상속 받는다.

각각의 이벤트들은 델리게이트를 통해 저장되고, 각 이벤트가 감지될 시, 저장된 이벤트 함수를 실행시킨다.

 

마치며

이로써 모든 코드들을 살펴 보았는데, 기능들을 나누어 관리를 하여 확실히 유지, 보수에서는 편할 것 같다. 

그러나 어떤 역할을 하는지 모르면 상당히 복잡한 코드가 될 것 같다. 다음에는 SceneUI, PopupUI를 이용하여 실제 게임의 UI를 구현해 볼 계획이다.

'UNITY' 카테고리의 다른 글

UNITY - Camera - 마우스 클릭으로 이동하기  (0) 2024.01.12
UNITY - ResourceManager  (0) 2024.01.12
UNITY - UI 자동화(1) - 인벤토리 구현  (0) 2024.01.11
UNITY - UIManager  (2) 2024.01.11
UNITY - Camera - 3인칭 시점(2)  (4) 2024.01.08

일단 결론부터 말하자면 Scene을 관리하는 Scene 객체에 붙여서, Scene이 로드되면 SceneUI를 불러오는 코드이다.

자동화를 해놓았기 때문에, 코드 한 줄만 작성하면 된다.

 

GameScene.cs

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class GameScene : BaseScene
{
    protected override void Init()
    {
        base.Init();

        SceneType = Define.Scene.Game;

        Managers.UI.ShowSceneUI<UI_Inven>(); // 이 부분에서 UI가 자동으로 실행 !!

        Dictionary<int, Stat> dict = Managers.Data.StatDict;
    }

    public override void Clear()
    {

    }
}

 

UI가 실행되는 부분을 살펴 보았으니, 본격적으로 자동화 구조를 작성해보고자 한다.

보통 인벤토리를 생각하면 키보드의 키를 눌러서 Popup 형식으로 등장하곤 한다.

그러나 이번에 구현할 코드에서는 Scene에 할당되어 SceneUI로 관리한다.

즉, Scene을 실행하면 계속 띄워져 있다.

 

먼저, 작성된 클래스의 역할과 구조를 알아야 한다.

  • UI_Base
    • UI의 최상위 클래스이다. 모든 UI 클래스들의 부모가 된다.
  • UI_Scene
    • SceneUI의 최상위 클래스이다. 모든 SceneUI의 부모가 된다.
  • UI_Popup
    • PopupUI의 최상위 클래스이다. 모든 PopupUI의 부모가 된다.
    • 코드에 작성은 했으나 사용은 하지 않는다.
  • UI_Inven
    • UI_Inven이라는 이름의 prefab을 관리하기 위한 코드이다.
  • UI_Inven_Item
    • UI_Inven의 prefab 안에 자식으로 딸려 있는 아이템들을 위한 코드이다.
  • UI_EventHandler
    • UI의 이벤트를 관리하기 위한 코드이다.

 

오늘 구현해 볼 인벤토리의 구조이다.

  • @Root_UI
    • UI_Inven
      • GridPanel
        • UI_Inven_Item
        • UI_Inven_Item
        • ... 

 

UI를 생성할 때, 결국 UIManager를 실행하게 되는데, 이때 @Root_UI가 없다면 자동으로 생성하게 되고, 모든 UI를 자식으로 붙여서 관리를 해준다. 코드가 궁금하다면 아래 글을 참고해보자.

 

[UNITY] UIManager

UI를 관리하는 코드이다. SceneUI는 Scene이 열렸을 때, 한번만 열리면 되지만, PopupUI는 Scene 중간에 올라올 수 있기 때문에, 스택으로 관리한다. 평소 게임에서 팝업이 열리고, 끄는 과정을 생각해보

mainsdev.tistory.com

 

UI_Base.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public abstract class UI_Base : MonoBehaviour
{
    Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>(); // UI를 바인딩 할 딕셔너리 생성

    public abstract void Init(); // 자식 클래스에서 추가적으로 구현할 추상 클래스 함수

    protected void Bind<T>(Type type) where T : UnityEngine.Object // Object - Component - Behaviour - Monobehaviour 순서로 상속
    {

        string[] names = Enum.GetNames(type); // reflection을 사용해 선언한 Enum 타입의 이름을 가져온다.

        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length]; // 딕셔너리에 넣을 배열을 선언
        _objects.Add(typeof(T), objects);

        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true); // Bind 함수를 사용한 객체의 게임 오브젝트에서 인자로 받은 type의 자식 객체를 찾는다.
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true); // 게임 오브젝트 타입이 아니라면 제너릭을 이용한 함수 호출

            if (objects[i] == null) // 호출 실패
                Debug.Log($"Failed to bind : {names[i]}");

        }
    }

    protected T Get<T>(int idx) where T : UnityEngine.Object
    {
        UnityEngine.Object[] objects = null;
        if (_objects.TryGetValue(typeof(T), out objects) == false) // 바인딩 딕셔너리에서 입력받은 타입을 Key 값으로 검색하여 objects에 Value를 저장한다
            return null;
        return objects[idx] as T; // 존재할 시 T 타입으로 저장한 값을 반환한다.
    }

    protected GameObject GetObject(int idx) { return Get<GameObject>(idx); } // 타입에 따라 GET 함수를 사용할 수 있게 만들어 놓음
    protected Text GetText(int idx) { return Get<Text>(idx); }
    protected Button GetButton(int idx) { return Get<Button>(idx); }
    protected Image GetImage(int idx) { return Get<Image>(idx); }

    public static void BindEvent(GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
    { // UI에 이벤트를 바인드 하는 코드

        UI_EventHandler evt = Util.GetOrAddComponent<UI_EventHandler>(go); // 함수를 호출한 오브젝트에서 UI_EventHandler 컴포넌트가 있는지 확인하고, 없으면 붙여준다.

        switch (type) // Enum 타입으로 선언된 이벤트 타입에 따라 이벤트 설정을 다르게 한다.
        {
            case Define.UIEvent.Click:
                evt.OnClickHandler -= action; // 델리게이트로 받은 함수를 클릭 핸들러에 지정
                evt.OnClickHandler += action;
                break;
            case Define.UIEvent.Drag:
                evt.OnDragHandler -= action; // 델리게이트로 받은 함수를 드래그 핸틀러에 지정
                evt.OnDragHandler += action;
                break;
        }

        
        evt.OnDragHandler += ((PointerEventData data) => { evt.gameObject.transform.position = data.position; });
        // 예제코드 : 드래그 핸들러에 게임 오브젝트를 잡고 드래그 하면 그 위치로 이동하는 함수
    }
}

 

UI_Base에서는 Bind와 Get, BindEvent 함수가 선언되어 있다. 

 

Bind는 UI의 이름을 enum 타입으로 선언된 값을 인자로 받는다. 인자로 받아온 값을 변환하여 모든 자식들을 찾아서 _objects 딕셔너리에 저장한다. 

Get은 _objects 딕셔너리에서 받은 Type 을 Key로 하여 해당하는 게임 오브젝트를 Value로 받는다.

BindEvent는 인자로 받은 게임 오브젝트에 UI_EventHandler를 컴포넌트로 붙여주고, 인자로 받은 이벤트 함수를 지정해준다. switch 문을 통해 이벤트 타입에 따라 이벤트를 설정할 수 있다.

 

나머지 부분은 다음 글에 작성.

 

 

[UNITY] UI 자동화(2) - 인벤토리 구현

[UNITY] UI 자동화(1) - 인벤토리 구현 일단 결론부터 말하자면 Scene을 관리하는 Scene 객체에 붙여서, Scene이 로드되면 SceneUI를 불러오는 코드이다. 자동화를 해놓았기 때문에, 코드 한 줄만 작성하면

mainsdev.tistory.com

 

'UNITY' 카테고리의 다른 글

UNITY - ResourceManager  (0) 2024.01.12
UNITY - UI 자동화(2) - 인벤토리 구현  (0) 2024.01.11
UNITY - UIManager  (2) 2024.01.11
UNITY - Camera - 3인칭 시점(2)  (4) 2024.01.08
UNITY - Camera - 3인칭 시점(1)  (3) 2024.01.08

UI를 관리하는 코드이다. 

SceneUI는 Scene이 열렸을 때, 한번만 열리면 되지만, PopupUI는 Scene 중간에 올라올 수 있기 때문에, 스택으로 관리한다. 평소 게임에서 팝업이 열리고, 끄는 과정을 생각해보면 스택으로 관리하는 것이 편해보인다.

UIManager에는 UI 자동화를 위한 코드들이 구현 되어있다.

 

추가적으로, Util 클래스에 자주 사용하는 함수를 구현하여 놓았다.


UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIManager
{
    int _order = 10;

    Stack<UI_Popup> _popupStack = new Stack<UI_Popup>();
    UI_Scene _sceneUI = null;

    public GameObject Root
    {
        get
        {
            GameObject root = GameObject.Find("@UI_Root"); // UI의 최상위 부모를 찾는다
            if (root == null)
                root = new GameObject { name = "@UI_Root " }; // 없을 시 생성
            return root;
        }
    }

    public void SetCanvas(GameObject go, bool sort = true)
    {
        Canvas canvas = Util.GetOrAddComponent<Canvas>(go); // 인자로 받은 게임 오브젝트의 Canvas 컴포넌트를 받아온다
        canvas.renderMode = RenderMode.ScreenSpaceOverlay; //UI를 카메라와 일정 거리에 두어 render한다
        canvas.overrideSorting = true; // UI의 정렬 순서를 활성화 한다.

        if (sort)
        {
            canvas.sortingOrder = _order; // 초기에 설정한 (10+@)의 값으로 정렬 순서를 설정한다
            _order++; // 다음에 올라올 UI를 위해 정렬 값을 1 추가한다
        }
        else
        {
            canvas.sortingOrder = 0; // 인자로 받은 정렬 여부가 false일 경우 정렬 순서를 0으로 설정한다.
        }
        
    }

    public T MakeSubItem<T>(Transform parent = null, string name = null) where T : UI_Base // UI_Base 또는 상속받는 타입만 사용
    { // UI 안의 자식 아이템을 생성하는 코드
        if (string.IsNullOrEmpty(name)) // 만들 아이템의 이름을 설정하지 않았을 시
            name = typeof(T).Name; // 타입, 클래스의 이름으로 설정한다.

        GameObject go = Managers.Resource.Instantiate($"UI/SubItem/{name}"); // 해당하는 프리팹을 불러옴
        if (parent != null) // 부모를 인자로 받았을 경우
            go.transform.SetParent(parent); // 인자를 부모로 설정한다.

        return Util.GetOrAddComponent<T>(go); // 불러온 프리팹에서 T 에 해당하는 컴포넌트를 가져오고, 없으면 더해서 가져온다.
    }

    public T ShowSceneUI<T>(string name = null) where T : UI_Scene // UI_Scene 또는 상속받는 타입만 사용가능
    { // SceneUI 를 보여주는 코드
        if (string.IsNullOrEmpty(name)) // 이름을 설정하지 않았을 경우
            name = typeof(T).Name; // 인자로 받은 제네릭 타입으로 이름을 설정한다.

        GameObject go = Managers.Resource.Instantiate($"UI/Scene/{name}"); // 이름에 해당하는 프리팹을 생성한다.
        T sceneUI = Util.GetOrAddComponent<T>(go); // 불러온 프리팹에 T 에서 해당하는 컴포넌트를 불러온다. 없으면 더해주고 불러온다.
        _sceneUI = sceneUI;

        go.transform.SetParent(Root.transform); // Root 클래스에 접근하여 @UI_Root를 생성하고, 그 아래에 붙여준다

        return sceneUI; // 생성한 프리팹의 T에 해당하는 컴포넌트를 반환해 준다.

    }

    public T ShowPopupUI<T>(string name = null) where T : UI_Popup // UI_Popup 이거나 상속 받는 타입만 받는다.
    {
        if (string.IsNullOrEmpty(name)) // 인자로 이름을 설정하지 않았을 경우
            name = typeof(T).Name; // 클래스 이릅으로 설정한다.

        GameObject go = Managers.Resource.Instantiate($"UI/Popup/{name}"); // 프리팹을 불러온다
        T popup = Util.GetOrAddComponent<T>(go); // T에 해당하는 컴포넌트를 프리팹에 붙여주고 가져온다.
        _popupStack.Push(popup); // 팝업 처리 스택에 넣는다.

        go.transform.SetParent(Root.transform); // @UI_Root 의 자식으로 설정한다.

        return popup; // 컴포넌트를 반환한다.

    }

    public void ClosePopupUI(UI_Popup popup) // 팝업을 닫는다. (지정)
    {
        if (_popupStack.Count == 0) // 팝업 스택이 비어있으면
            return; // 반환한다.

        if (_popupStack.Peek() != popup) // 팝업 스택의 맨 위가 인자로 받은 팝업과 다르면
        {
            Debug.Log("Close Popup Failed"); // 팝업 닫기에 실패한다
            return; // 팝업 스택 순서에 맞게 닫아야함.
        }
        ClosePopupUI();
    }

    public void ClosePopupUI() // 팝업을 닫는다. (자동, 맨위에 있는 거부터)
    {
        if (_popupStack.Count == 0)
            return;

        UI_Popup popup = _popupStack.Pop();
        Managers.Resource.Destroy(popup.gameObject);
        popup = null;

        _order--;
    }

    public void CloseAllPopupUI() // 모든 팝업을 닫는다.
    {
        while (_popupStack.Count > 0)
            ClosePopupUI();
    }

    public void Clear() // 초기화
    {
        CloseAllPopupUI();
        _sceneUI = null;
    }
}

 

Util.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Util
{
    public static T GetOrAddComponent<T>(GameObject go) where T : UnityEngine.Component
    {
        T component = go.GetComponent<T>(); // 인자로 받은 게임오브젝트에서 T에 해당하는 컴포넌트를 가져온다
        if (component == null) // 없을 경우
            component = go.AddComponent<T>(); // 붙여준다.
        return component;
    }
    public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
    {
        Transform transform =  FindChild<Transform>(go, name, recursive); //인자로 받은 게임 오브젝트에서 name에 해당하는 자식을 찾는다
        if (transform == null)
            return null; // 찾지못하면 null리턴
        return transform.gameObject; // 자식의 게임 오브젝트를 반환
    }
        public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object
    {
        if (go == null) // 없는 게임 오브젝트를 인자로 받는다며 null 리턴
            return null;

        if (recursive == false) // 바로 아래 자식만 탐색한다.
        {
            for (int i = 0; i < go.transform.childCount; i++) // 인자로 받은 자식의 수 만큼 반복
            {
                Transform transform = go.transform.GetChild(i); // 모든 자식을 가져온다.
                if (string.IsNullOrEmpty(name) || transform.name == name) // null이 아니거나, 자식의 이름이 인자로 받은 이름과 같은 경우
                {
                    T component = transform.GetComponent<T>(); // 자식에서 컴포넌트를 받아온다.
                    if (component != null) // 컴포넌트가 존재할 경우
                        return component; // 반환
                }
            }
        }
        else // recursive가 true 인 경우
        { // 재귀적으로 찾는다는 의미
            foreach (T component in go.GetComponentsInChildren<T>())
            { // GetComponentsInChildren 작동 방식이 DFS 랑 비슷하기 때문에 모든 자식 컴포넌트를 불러온다.
                if (string.IsNullOrEmpty(name) || component.name == name)
                    return component;
            }
        }

        return null;
    }


}

'UNITY' 카테고리의 다른 글

UNITY - UI 자동화(2) - 인벤토리 구현  (0) 2024.01.11
UNITY - UI 자동화(1) - 인벤토리 구현  (0) 2024.01.11
UNITY - Camera - 3인칭 시점(2)  (4) 2024.01.08
UNITY - Camera - 3인칭 시점(1)  (3) 2024.01.08
UNITY - InputManager  (1) 2024.01.08

보통 게임에서 플레이어와 3인칭 카메라 사이에 물체가 존재한다면 플레이어가 보이도록 카메라를 물체 앞에 둔다.

이번 글에서는 RayCast를 사용하여 플레이어와 카메라 사이에 물체가 감지된다면 카메라의 위치를 바꾸는 실습을 할 것이다.

RayCast는 오브젝트를 기준으로 한 방향으로 레이저를 발사하여, 레이저에 물체 감지 시 RayCastHit 구조체에 감지 정보들을 넣어주는 기능이다. 

따라서 플레이어와 카메라 사이의 물체를 감지하기 위해서, 카메라에서 플레이어 방향으로 RayCast를 보낸다.

RayCastHit 구조체에서 플레이어 오브젝트가 아닌 다른 오브젝트가 감지되었다면 레이저가 충돌한 지점으로 카메라를 이동함으로써 기능을 구현할 수 있다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    private Camera cam;
    public GameObject _player = null;

    public Vector3 _delta = new Vector3(0.0f, 6.0f, -6.0f);
    // 플레이어와 카메라 사이의 위치를 Vector3로 선언
    void Start()
    {
        cam = Camera.main; // main camera 객체를 불러온다.
        _player = GameObject.Find("TestCube"); // 플레이어 오브젝트를 불러온다.
    }

    // Update is called once per frame
    void LateUpdate()
    {
        cam.transform.position = _player.transform.position + _delta;
        // 카메라의 위치를 플레이어 위치와 선언 위치를 더하여 업데이트한다.
        
        Debug.DrawRay(transform.position, -_delta, Color.yellow);
        // 레이저를 발사하는 경로를 그린다.

        RaycastHit hit;

        if (Physics.Raycast(transform.position, -_delta, out hit , Vector3.Distance(transform.position, _player.transform.position)))
        { // 레이저를 발사하여 감지되는 오브젝트가 존재시 조건문 발동
            if (hit.transform.gameObject.name != _player.transform.gameObject.name)
            { // 레이저와 부딪힌 물체의 이름과 플레이어의 이름이 다를 시
                cam.transform.position = hit.point; // 부딪힌 위치로 카메라 이동
            }
        }

    }
}

'UNITY' 카테고리의 다른 글

UNITY - UI 자동화(1) - 인벤토리 구현  (0) 2024.01.11
UNITY - UIManager  (2) 2024.01.11
UNITY - Camera - 3인칭 시점(1)  (3) 2024.01.08
UNITY - InputManager  (1) 2024.01.08
UNITY - Manager  (1) 2024.01.03

게임 씬에서 화면에 보여지는 부분은 Camera 오브젝트를 이용해 볼 수 있다. 꼭 한 개 이상의 Camera가 존재해야한다.

이 글에서는 이동하는 Player의 오브젝트를 따라서 카메라도 이동하는 기능을 구현해 볼 것이다. 

원리는 간단하다. 화면에 보여질 오브젝트를 원하는 위치에 카메라를 놓은 상태로 두 오브젝트 간의 Vector3 값의 차이를 이용해 Update문으로 위치를 바꿔준다.

 

CameraController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    private Camera cam;
    public GameObject _player = null;

    public Vector3 _delta = new Vector3(0.0f, 6.0f, -6.0f);
    // 플레이어와 카메라 사이의 거리를 저장하여 시점을 설정한다.
    void Start()
    {
        cam = Camera.main; // main camera를 가져온다.
        _player = GameObject.Find("TestCube"); // TestCube 객체를 Player로 설정한다.
    }

    // Update is called once per frame
    void LateUpdate() // 가장 마지막에 실행되는 함수로, 플레이어의 위치가 변경된 후
    {				  // 실행되기 때문에 자연스러운 화면 연출이 가능하다.
        cam.transform.position = _player.transform.position + _delta;
        // 업데이트가 될 때마다 camera의 위치를 플레이어위치 + 설정값으로 변경한다
    }
}

 

main camera에 해당 코드를 붙여주면 된다.

'UNITY' 카테고리의 다른 글

UNITY - UI 자동화(1) - 인벤토리 구현  (0) 2024.01.11
UNITY - UIManager  (2) 2024.01.11
UNITY - Camera - 3인칭 시점(2)  (4) 2024.01.08
UNITY - InputManager  (1) 2024.01.08
UNITY - Manager  (1) 2024.01.03

+ Recent posts