미니 RPG 만들기_총정리

개발조하·2024년 1월 12일
0

Unity

목록 보기
30/30
post-thumbnail

1. 기본 틀 구성 (Assets 폴더)

1.1 Manager 총괄_Managers.cs

  • 기능:
    Scene, Resource, Pool, UI, Input, Sound, Data, Network 등 게임의 전반적인 구조를 관리한다.

  • 특징: 싱글톤으로 구현
public class Managers : MonoBehaviour
{
    static Managers s_instance; //유일성 보장
    static Managers Instance { get { Init(); return s_instance; } }
    private void Start()
    {
        //만약 운좋게 Start()가 실행될 경우 대비
        Init();
    }
    private static void Init()
    {
        if(s_instance == null)
        {
            GameObject go = GameObject.Find("@Managers");
            if(go == null)
            {
                go = new GameObject { name = "@Managers" };
                go.AddComponent<Managers>();
            }
            DontDestroyOnLoad(go);
            s_instance = go.GetComponent<Managers>();
        }
    }
}

static 메서드 및 속성 보충 설명

1.2 사용자 입력 받기_InputManager.cs

  • 기능:
    사용자 입력을 처리하고 해당 입력에 기반하여 특정 작업을 수행하는 코드를
    PlayerController.cs의 Update()에서 프레임마다 체크하는 방식보다는 중앙에서 이벤트 통지하는 것이 유리하다.
    즉, Action에 메서드를 등록하고 구독신청하고 Input이 발생하면 Event를 전파해주는 매니저이다. (Listener 패턴)
  • 특징:
    Define.MouseEvent로 다양한 마우스 이벤트를 enum으로 정의하여 사용한다.

1.2.1 Mouse Event

public class Define
{
    public enum MouseEvent
    {
        Press,
        PointerDown,
        PointerUp,
        Click,
    }
}
public class InputManager
{
    public Action KeyAction = null;
    public Action<Define.MouseEvent> MouseAction = null;

    bool _pressed = false;
    float _pressedTime = 0;
    public void OnUpdate()
    {
        //KEY로 조작
        if (Input.anyKey && KeyAction != null)
            KeyAction.Invoke();

        //Mouse로 조작
        if(MouseAction != null)
        {
            if (Input.GetMouseButton(0)) //마우스 왼쪽을 누르는 동안
            {
                if (!_pressed) //마우스가 처음 눌렸을 때
                {
                    MouseAction.Invoke(Define.MouseEvent.PointerDown);
                    _pressedTime = Time.time;

                }
                //마우스가 계속 눌려있을 때
                MouseAction.Invoke(Define.MouseEvent.Press);
                _pressed = true;
            }
            else //마우스 왼쪽을 떼면
            {
                if (_pressed)
                {
                    if(Time.time < _pressedTime + 0.2f)
                    {
                        MouseAction.Invoke(Define.MouseEvent.Click);
                    }
                    MouseAction.Invoke(Define.MouseEvent.PointerUp);
                }
                _pressed = false;
                _pressedTime = 0;
            }
        }
    }

    public void Clear()
    {
        KeyAction = null;
        MouseAction = null; 
    }
}
  • Input.GetMouseButton() : 0- 마우스 왼쪽 | 1- 마우스 오른쪽 | 2-마우스 가운데(스크롤)

  • 델리게이트(Delegate) :
    델리게이트는 C#에서 메서드에 대한 참조를 보유할 수 있는 타입이다. 이를 통해 메서드를 변수처럼 전달하고 저장할 수 있으며, 나중에 이 델리게이트를 사용하여 해당 메서드를 호출할 수 있다. 델리게이트는 특정 시그니처(매개변수와 반환 타입)를 가진 메서드만 참조할 수 있다.

  • Invoke() :
    Invoke()는 델리게이트가 참조하는 메서드(들)를 실행하는 데 사용된다.

  • Managers.cs에 등록

  • PlayerController.cs에서 MouseAction에 이벤트 구독

1.3 자원 관리_ResourceManager.cs

  • Assets 폴더 산하에 'Resources' 폴더 생성 (명칭 주의)
    ㄴ 그 안에 프로젝트에 사용될 모든 자원들을 보관한다.
  • ResoueceManager의 인터페이스 구성
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 prefab = Load<GameObject>($"Prefabs/{path}");
        if(prefab == null)
        {
            Debug.Log($"Failed to load prefab : {path}");
            return null;
        }

        //Object를 붙여야 Object의 Instantiate()가 호출된다. 안쓰면 재귀함수가 되어버림.
        return Object.Instantiate(prefab, parent);
    }

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

        Object.Destroy(go);
    }
}

  • 호출 코드 (예시)
GameObject go = Managers.Resource.Instantiate($"UI/WorldSpace/{name}");

2. Player 설정

2.1 Player 컴포넌트

  • 필수 컴포넌트
  1. 'PlayerController.cs' : UnityChan의 움직임 등을 실행
  2. 'Capsule Collider' : 충돌 기능 (Edit Collider로 충돌범위 설정)
  3. 'Rigidbody' : 물리력

2.2 마우스 클릭 위치로 Player 이동

  • Raycasting: Main Camera에서 Ray를 쏴서 닿은 부분이 "Ground"일 경우, Player가 해당 위치로 이동한다.
public class PlayerController : MonoBehaviour
{
    [SerializeField] float _speed = 10.0f;

    bool _moveToDest = false;
    Vector3 _destPos;

    void Start()
    {
        Managers.Input.MouseAction -= OnMouseEvent;
        Managers.Input.MouseAction += OnMouseEvent;
    }

    void Update()
    {
        if(_moveToDest)
        {
            Vector3 dir = _destPos - transform.position;
            if(dir.magnitude < 0.0001f)
            {
                _moveToDest = false;
            }
            else
            {
                //이동하는 값(moveDist)가 남은 거리(dir.magnitude)보다는 작아야 한다.
                //-> 안그래면 목적지에 도달했을 때 캐릭터가 와리가리함.
                float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);

                transform.position += dir.normalized * moveDist;
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
                transform.LookAt(_destPos);
            }
        }
    }

    void OnMouseEvent(Define.MouseEvent evt)
    {
        if (evt != Define.MouseEvent.Click)
            return;

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

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Ground")))
        {
            _destPos = hit.point;
            _moveToDest = true;
            //Debug.Log($"Raycast Camera @{hit.collider.gameObject.name}");
        }
    }
}

2.3 Animation

2.3.1 Idle <-> Moving

public class PlayerController : MonoBehaviour
{
    [SerializeField] float _speed = 10.0f;
    Vector3 _destPos;
    Define.State _state = Define.State.Idle;

    public Define.State State
    {
        get { return _state; }
        set
        {
            _state = value;

            Animator anim = GetComponent<Animator>();
            switch (_state)
            {
                case Define.State.Idle:
                    anim.CrossFade("WAIT", 0.1f);
                    break;
                case Define.State.Moving:
                    anim.CrossFade("RUN", 0.1f);
                    break;
            }
        }
    }

    void Start()
    {
        Managers.Input.MouseAction -= OnMouseEvent;
        Managers.Input.MouseAction += OnMouseEvent;
    }

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

    private void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position;
        dir.y = 0; //y축으로는 이동하지 않도록

        //player가 destination에 도착하면
        if (dir.magnitude < 0.1f)
        {
            State = Define.State.Idle;
        }
        else
        {
            //장애물 판별 Raycast
            Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green);
            bool hitBlock = Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"));
            if (hitBlock && Input.GetMouseButton(0)==false)
            {
                State = Define.State.Idle;
                return;
            }

            //이동하는 값(moveDist)가 남은 거리(dir.magnitude)보다는 작아야 한다.
            //-> 안그래면 목적지에 도달했을 때 캐릭터가 와리가리함.
            float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);

            transform.position += dir.normalized * moveDist;
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
            transform.LookAt(_destPos);
        }
    }
    private void UpdateIdle()
    {
        
    }

    void OnMouseEvent(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Ground"));
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        State = Define.State.Moving;  
                    }
                }
                break;

            case Define.MouseEvent.Press:
                if (raycastHit)
                {
                    _destPos = hit.point;
                }
                break;
                
            case Define.MouseEvent.PointerUp:
                break;
        }
    }
}

3. 카메라

3.1 캐릭터 Follow

  • 카메라가 QuaterView로 캐릭터를 따라다니도록 설정
public class CameraController : MonoBehaviour
{
    [SerializeField] Define.CameraMode _mode = Define.CameraMode.QuarterView;
    [SerializeField] Vector3 _delta = new Vector3(0.0f, 6.0f, -5.0f); //player를 기준으로 카메라가 얼만큼 떨어져있는지
    [SerializeField] GameObject _player = null;

    void Start()
    {
        
    }

    void LateUpdate()
    {
        if (_mode == Define.CameraMode.QuarterView)
        {
            transform.position = _player.transform.position + _delta;

            //player 주시
            transform.LookAt(_player.transform);
        }
    }

    //코드로 _delta값 세팅하는 메서드
    public void SetQuaterView(Vector3 delta)
    {
        _mode = Define.CameraMode.QuarterView;
        _delta = delta;
    }
}

ㄴ 우선 현재는 player를 인스펙터창에 드래그앤드롭으로 연결해주도록 해놨음. 나중에 코드로 player 불러오도록 변경 예정

3.2 장애물에 카메라 시야가 가려질 경우

장애물 앞으로 카메라를 이동시켜 Player를 촬영할 수 있도록 한다. ㄴ 장애물로 인식할 Object의 Layer를 꼭 "Block"으로 설정해줘야 한다.

4. UI 자동화

4.1 UI 요소 자동 맵핑 및 사용

  • 기능:
  1. UI 요소 바인딩
    : Start()에서 Bind<Button>(typeof(Buttons))Bind<Text>(typeof(Texts))를 호출하여 UI버튼과 텍스트 요소를 자동으로 매핑한다. 이때 'enum'을 사용하여 수동으로 각 요소의 이름을 지정하는 번거로움을 줄인다.
  2. UI 요소 검색 및 매핑
    : Bind<T>()로 지정된 타입의 모든 UI요소를 찾아 '_objects' 딕셔너리에 추가한다. FindChild<T>()를 사용하여 GameObject의 자식 요소들 중에서 이름이 일치하는 요소를 찾는다.
  3. UI 요소 사용
    : Get<T>()를 통해 필요할 때 특정 UI요소를 쉽게 가져오고, 요소의 내용을 업데이트할 수 있다. (text의 내용 설정 등)

  • 툭징:
  1. FindChild<T>()처럼 UI 외에도 기능적으로 사용할 수 있는 메서드는 Util.cs에서 관리한다.
  2. 특정 UI만이 아닌 모든 UI에 적용되는 내용들은 UI_Base.cs에서 관리하며, 각 UI들은 UI_Base를 상속받아 사용한다.
public class UI_Base : MonoBehaviour
{
    Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();

    //UI 맵핑 자동화
    protected void Bind<T>(Type type) where T : UnityEngine.Object
    {
        //enum값들을 string 타입의 배열로 반환
        string[] names = Enum.GetNames(type);

        //Dictionary에 넣을 공간 생성
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects);

        for (int i = 0; i < names.Length; i++)
        {
            //UI_Btn(캔버스)의 하위 객체들을 순회하면서 일치하는 object를 찾는다.
            if (typeof(T) == typeof(GameObject))
            {
                objects[i] = Util.FindChild(gameObject, names[i], true);
            }
            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))
        {
            return objects[idx] as T; //UnityEngine.Object타입을 T타입으로 캐스팅
        }
        return null;
    }

    //자주 사용하는 컴포넌트 메서드화
    protected GameObject GetObject(int idx) { return Get<GameObject>(idx); }  
    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 class UI_Button : UI_Base
{ 
    int _score = 0;

    enum Buttons
    {
        PointButton,
    }

    enum Texts
    {
        PointText,
        ScoreText,
    }
    enum GameObjects
    {

    }

    void Start()
    {
        Bind<Button>(typeof(Buttons));
        Bind<Text>(typeof(Texts));
        Bind<GameObject>(typeof(GameObjects));

        GetText((int)Texts.ScoreText).text = $"점수: {_score}점!!";
    }
}
public class Util //기능성 메서드 모음
{
    //지정된 GameObject의 자식 오브젝트 중에서 특정 이름을 가진 컴포넌트를 찾는다.
    public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object
    {
        if (go == 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)
                {
                    T component = transform.GetComponent<T>();
                    if (component != null)
                        return component;
                }
            }
        }
        else //자식의 자식까지 스캔
        {
            foreach(T component in go.GetComponentsInChildren<T>())
            {
                return component;
            }
        }

        //못찾았으면
        return null;
    }

    //GameObject용 FindChild메서드
    public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
    {
        Transform transform = FindChild<Transform>(go, name, recursive);
        if(transform != null)
        {
            return transform.gameObject;
        }
        return null;
    }
}

4.2 Event() 자동 연동

4.2.1 UI_EventHandler.cs / AddUIEvent() 이벤트 연결하기

인스펙터에서 UI별로 실행할 Event를 드래그앤드롭으로 연결하지 않고, 스크립트를 생성하여 자동화하자.

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

    public void OnDrag(PointerEventData eventData)
    {
        if (onDragHandler != null)
        {
            onDragHandler.Invoke(eventData);
        }
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        if (onClickHandler != null)
        {
            onClickHandler.Invoke(eventData);
        }
    }
}

  • PointButton을 클릭하면 _score가 1점씩 증가하는 로직 구현
public class UI_Button : UI_Base
{ 
    int _score = 0;

    enum Buttons
    {
        PointButton,
    }

    enum Texts
    {
        PointText,
        ScoreText,
    }
    enum GameObjects
    {

    }

    void Start()
    {
        Bind<Button>(typeof(Buttons));
        Bind<Text>(typeof(Texts));
        Bind<GameObject>(typeof(GameObjects));

        GameObject go = GetButton((int)Buttons.PointButton).gameObject;
        AddUIEvent(go, OnClickPointButton, Define.UIEvent.Click);
    }

    void OnClickPointButton(PointerEventData data)
    {
        _score++; // 점수 증가
        GetText((int)Texts.ScoreText).text = $"점수: {_score}점!!";
    }
}

4.2.2 Extension.cs로 AddUIEvent() 활용코드 간소화

ㄴ 지금은 PointButton에 클릭 시 실행할 이벤트를 연결하려면 이렇게 총 2줄의 코드를 작성해야 한다. Extension.cs에 AddUIEvent()메서드를 가공하여 간결한 구현을 해보자.

public static class Extension 
{
    public static void AddUIEvent
    (this GameObject go, Action<PointerEventData> action, 
    Define.UIEvent type = Define.UIEvent.Click)
    {
        UI_Base.AddUIEvent(go, action, type);
    }
}

this GameObject go: 확장 메서드를 적용할 GameObject이다. this 키워드는 이 메서드가 확장 메서드임을 나타낸다.

  • GetOrAddComponent()도 자주 사용하기 때문에 Extension.cs에 추가해주자.
public static class Extension 
{
    public static void AddUIEvent(this GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
    {
        UI_Base.AddUIEvent(go, action, type);
    }

    public static T GetOrAddComponent<T>(this GameObject go) where T : UnityEngine.Component
    {
        return Util.GetOrAddComponent<T>(go);
    }
}

4.3 인벤토리 실습

4.3.1 Grid Layout Group

  • 인벤토리 기본 설정
  • 프리팹보기 모드에서 UI_Inven_Item이 제대로 나오지 않는다면?
    : Rect Transform - Reset 해줌

4.3.2 Item 맵핑하기

4.3.3 Item별 수정하기

📄참고자료
[인프런] c#과 유니티로 만드는 MMORPG 게임 개발 시리즈_3. 유니티 엔진
static 메서드 및 속성 보충 설명
Input.GetMouseButton(0)
델리게이트(Delegate)
Invoke()
Animator.CrossFade()
IsPointerOverGameObject()

profile
Unity 개발자 취준생의 개발로그, Slow and steady wins the race !

0개의 댓글