[ 섹션 13. 미니 RPG ]

 환경세팅

  - Terrain은 Unity에서 개발자가 직접 지형을 제작할 수 있도록 제공한 Tool이다.

  - Terrain은 [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 3D Object ] - [ Terrain ] 을 통해 생성한다.

 

  - Light의 Type에는 Realtime, Mixed, Baked가 있다.

   > Realtime은 조명이 씬에 직접적인 빛을 제공하고 매 프레임마다 업데이트되어 게임 오브젝트가 씬 내에서 이동시

     조명은 즉시 업데이트된다. (상당한 부하를 유발)

   > Baked는 Lightmap을 Baking할 때 Scene의 Static Object에 대한 조명 효과가 계산되고 Texture에 기록된다.

     (즉, 움직이지 않는 물체의 빛을 미리 계산해두는 것)

   > 물체가 많아질수록 Baking 시간이 상당히 오래 걸린다. 따라서 개발 도중에 물체를 추가할때마다 자동으로 Baking

      하는 것을 막기 위해 [ Window ] - [ Rendering ] - [ Lighting Settings ] - [ Lightmapping Setting ] 의 Auto Generate의

      체크를 해제한다. (최신 버전에서 에러 발생시 Compress Lightmaps의 체크를 해제)

 

 이동

  - NavMesh는 Navigation Mesh의 줄임말로, 게임 월드에서 걸을 수 있는 표면을 뜻한다.

  - NavMesh는 [ Window ] - [ AI ] - [ Navigation ] 에서 설정 가능하다.

   > [ Bake ] 에서 미리 갈 수 있는 영역 또는 제한 사항들을 Bake한다.

     (계산에 포함되는 Mesh는 Navigation Static 속성을 가진다.)

  - 설정된 NavMesh를 이용하여 목적지까지 경로를 찾고 이동하는 캐릭터에 부착되는 컴포넌트가 Nav Mesh Agent이다.

NavMesh를 이용한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    //...
    
    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
        if (dir.magnitude < 0.1f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            _state = PlayerState.Idle;
        }
        else
        {
            // NavMesh를 사용
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            float moveDist = Mathf.Clamp(Time.deltaTime * _speed, 0, dir.magnitude);
            nma.Move(dir.normalized * moveDist);

            Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green); // Ray를 시각적으로 표현
            if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"))) // 건물, 사물과 인접시 Player가 멈추도록 설정
            {
                _state = PlayerState.Idle;
                return;
            }

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
        }

        // 애니매이션
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("speed", _speed);
    }

    //...
}

 

 스탯

스탯 관리를 위한 공용 Stat Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Stat : MonoBehaviour
{
    [SerializeField]
    protected int _level;
    [SerializeField]
    protected int _hp;
    [SerializeField]
    protected int _maxHp;
    [SerializeField]
    protected int _attack;
    [SerializeField]
    protected int _defense;
    [SerializeField]
    protected float _moveSpeed;

    public int Level { get { return _level; } set { _level = value; } }
    public int Hp { get { return _hp; } set { _hp = value; } }
    public int MaxHp { get { return _maxHp; } set { _maxHp = value; } }
    public int Attack { get { return _attack; } set { _attack = value; } }
    public int Defense { get { return _defense; } set { _defense = value; } }
    public float MoveSpeed { get { return _moveSpeed; } set { _moveSpeed = value; } }

    private void Start()
    {
        _level = 1;
        _hp = 100;
        _maxHp = 100;
        _attack = 10;
        _defense = 5;
        _moveSpeed= 5.0f;
    }
}​

 

Player의 스탯 관리를 위해 Stat를 상속받는 PlayerStat Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStat : Stat
{
    [SerializeField]
    protected int _exp;
    [SerializeField]
    protected int _gold;

    public int Exp { get { return _exp; } set { _exp = value; } }
    public int Gold { get { return _gold; } set { _gold = value; } }

    private void Start()
    {
        _level = 1;
        _hp = 100;
        _maxHp = 100;
        _attack = 10;
        _defense = 5;
        _moveSpeed = 5.0f;
        _exp = 0;
        _gold = 0;
    }
}​

 

마우스 커서

  - Cursor로 사용하기 위한 Image는 Inspector 창에서 Texture Type을 Cursor로 설정한다. 

마우스 커서를 위한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    Texture2D _attackIcon;
    Texture2D _handIcon;

    enum CursorType 
    {
        None,
        Attack,
        Hand,
    }

    CursorType _cursorType = CursorType.None;

    void Start()
    {
        _attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack"); // Image 파일을 Texture2D를 통해 불러온다.
        _handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand"); // Image 파일을 Texture2D를 통해 불러온다.
    }
    void Update()
    {
        UpdateMouseCursor();
    }

    void UpdateMouseCursor()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }
    
    // ...
}

 

+ 추가 검색

 - Cursor.SetCursor() 함수는 Cursor의 Image를 사용자 지정 Texture로 바꿔준다.

  > 첫번째 인자는 Cursor에 지정할 Texture, 두번째 인자는 Cursor의 Spot, 세번째 인자는 최적화 방법이다.

 

 타게팅 락온

타게팅 락온을 위해 Define의 MouseEvent 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    // ...

    public enum MouseEvent
    {
        Press, 
        PointerDown, // 마우스를 Click하는 순간
        PointerUp, // 마우스 Click을 떼는 순간
        Click, // 순간적인 Click (굉장히 짧은 시간)
    }

    // ...
}​

 

InputManager 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputManager
{
    // ...
    
    float _pressedTime = 0; // Click과 PointerUp을 구분하기 위한 것

    // 체크하는 부분이 유일해짐
    public void OnUpdate()
    {
        // ...

        if (MouseAction != null)
        {
            if (Input.GetMouseButton(0))
            {
                if (!_pressed) // 마우스를 Click하는 순간
                {
                    MouseAction.Invoke(Define.MouseEvent.PointerDown);
                    _pressedTime = Time.time;
                }
                MouseAction.Invoke(Define.MouseEvent.Press); // Press Event를 발생시켜 알려준다.
                _pressed = true;
            }
            else
            {
                if (_pressed)
                {
                    if (Time.time < _pressedTime + 0.2f) // 0.2초 내에 Mouse Click을 뗀 경우
                        MouseAction.Invoke(Define.MouseEvent.Click); // Click Event를 발생시켜 알려준다.
                    MouseAction.Invoke(Define.MouseEvent.PointerUp);
                } 
                _pressed = false;
                _pressedTime = 0; // 초기화
            }
        }
    }
}

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    void UpdateMouseCursor()
    {
        if (Input.GetMouseButton(0)) // 만약 마우스를 누르고 있다면 커서가 변경되면 안되기 때문
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }

    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    GameObject _lockTarget;
    void OnMouseEvent(Define.MouseEvent evt)
    {
        if (_state == PlayerState.Die)
            return;

        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        _state = PlayerState.Moving;

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if(_lockTarget != null)
                        _destPos = _lockTarget.transform.position;
                    else if (raycastHit)
                        _destPos = hit.point;
                }
                break;
            case Define.MouseEvent.PointerUp:
                _lockTarget = null;
                break;
        }
    }
}​

 

+ 추가 검색 (https://unity-beginner.tistory.com/17)

 - GetMouseButtonDown( )은 마우스 버튼을 클릭하고 있을때 계속해서 발생,

   GetMouseButton( )은 마우스 버튼을 클릭했을 때 한 번 발생,

   GetMouseButtonUp( )은 마우스 버튼을 놓았을 때 한 번 발생

  > 괄호안에 입력한 숫자가 0 : 마우스 왼쪽 버튼, 1 : 마우스 오른쪽 버튼, 2 : 마우스 휠 버튼에 해당한다.

 

 공격 #1

PlayerController에서 마우스 Cursor와 관련된 부분을 따로 빼 CursorController 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CursorController : MonoBehaviour
{
    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    Texture2D _attackIcon;
    Texture2D _handIcon;

    enum CursorType
    {
        None,
        Attack,
        Hand,
    }

    CursorType _cursorType = CursorType.None;

    void Start()
    {
        _attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack"); // Image 파일을 Texture2D를 통해 불러온다.
        _handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand"); // Image 파일을 Texture2D를 통해 불러온다.
    }

    void Update()
    {
        if (Input.GetMouseButton(0)) // 만약 마우스를 누르고 있다면 커서가 변경되면 안되기 때문
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }
}​

 

공격 Animation을 위한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    public enum PlayerState
    {
        Die,
        Moving,
        Idle,
        Skill,
    }

    [SerializeField]
    PlayerState _state = PlayerState.Idle;

    public PlayerState State // PlayerState가 변함과 동시에 Animation도 변경하기 위한 프로퍼티 생성
    {
        get { return _state; }
        set
        {
            _state = value;

            Animator anim = GetComponent<Animator>();
            switch (_state)
            {
                case PlayerState.Die:
                    anim.SetBool("attack", false);
                    break;
                case PlayerState.Idle:
                    anim.SetFloat("speed", 0);
                    anim.SetBool("attack", false);
                    break;
                case PlayerState.Moving:
                    anim.SetFloat("speed", _stat.MoveSpeed);
                    anim.SetBool("attack", false);
                    break;
                case PlayerState.Skill:
                    anim.SetBool("attack", true);
                    break;
            }
        }
    }

    // ...

    void UpdateMoving()
    {
        // 몬스터가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= 1)
            {
                State = PlayerState.Skill;
                return;
            }
        }

        // ...
    }

    // ...

    void OnHitEvent()
    {
        State = PlayerState.Moving;
    }

    void Update()
    {
        switch (State)
        {
            case PlayerState.Die:
                UpdateDie();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
            case PlayerState.Skill:
                UpdateSkill();
                break;
        }
    }

    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    GameObject _lockTarget;
    void OnMouseEvent(Define.MouseEvent evt)
    {
        if (_state == PlayerState.Die)
            return;

        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

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

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if(_lockTarget != null)
                        _destPos = _lockTarget.transform.position;
                    else if (raycastHit)
                        _destPos = hit.point;
                }
                break;
        }
    }
}​

 

 공격 #2

  - Animation 전환시 반드시 Unity의 Tool을 사용하라는 법은 없다.

   > Parameter의 변경을 통한 Animation의 전환보다 Play( ), CrossFade( ) 사용이 더 편리할때도 있다.

   > CrossFade( )는 심지어 Blending도 가능하다. (두번째 매개변수를 통해)

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    public PlayerState State // PlayerState가 변함과 동시에 Animation도 변경하기 위한 프로퍼티 생성
    {
        get { return _state; }
        set
        {
            _state = value;

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

    // ...

    void UpdateSkill()
    {
       if (_lockTarget != null)
        {
            Vector3 dir = _lockTarget.transform.position - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
            Quaternion quat = Quaternion.LookRotation(dir); // 목적지를 바라보기 위해서
            transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // 자연스럽게 연결되도록
        }
    }

    void OnHitEvent()
    {
        if (_stopSkill)
        {
            State = PlayerState.Idle;
        }
        else
        {
            State = PlayerState.Skill;
        }
    }

    // ...

    bool _stopSkill = false;
    void OnMouseEvent(Define.MouseEvent evt)
    {
        switch (State)
        {
            case PlayerState.Idle:
                OnMouseEvent_IdleRun(evt);
                break;
            case PlayerState.Moving:
                OnMouseEvent_IdleRun(evt);
                break;
            case PlayerState.Skill:
                {
                    if (evt == Define.MouseEvent.PointerUp)
                        _stopSkill = true;
                }
                break;
        }
    }

    void OnMouseEvent_IdleRun(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

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

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if (_lockTarget == null && raycastHit)
                        _destPos = hit.point;
                }
                break;
            case Define.MouseEvent.PointerUp:
                _stopSkill = true;
                break;
        }
    }
}​

 

+ 추가 검색 (https://ansohxxn.github.io/unitydocs/anim/)

 - CrossFade( )는 Play( ) 에 대한 결과를 보다 더 부드럽고 자연스럽게 Blending 하여 Animation을 재생한다.

 > 첫번째 Parameter는 재생하고자 하는 Clip의 이름

 > 두번째 Parameter는 지연 시간 (다음 Animation으로 바뀌는데 걸리는 Fade 시간)

 > 세번째 Parameter는 Animation Layer

 > 네번째 Parameter는 Animation의 재생 시작 시점 (0 ~ 1 비율)

  ~> 0.0f로 설정시 다시 처음으로 돌아가 재생한다.

  ~> Clip의 Loop Time이 체크되어 있지 않더라도 반복 재생시킬 수 있다.

 

체력 게이지 #1

  - UI는 2D UI와 (게임 세상과 별개), 3D UI (게임 세상과 공존)로 나눌 수 있다.

   > 3D UI를 만들기 위해서는 Canvas의 Render Mode를 World Space로 변경한다.

   > 3D UI를 만들때 Canvas의 Scale을 약 1/100 로 줄이면 적당한 크기로 변경이 가능하다.

 

- Slider의 Value가 0과 1일때 텅 비어 있지도, 가득 차있지도 않다.

 > 이는 Fill Area의 [ Inspector ] - [ Rect Transform ] 에서 Left와 Right의 값을 전부 0으로 설정하면 해결이 가능하다.

설정 전의 모습
Fill Area의 Rect Transform의 Left와 Right의 값을 전부 0으로 설정
설정 후의 모습

 

- Slider의 Fill이 Background의 경계선을 넘어 튀어나와 있다.

 > 이는 Fill의 [ Inspector ] - [ Rect Transform ] 에서 Left와 Right의 값을 전부 0으로 설정하면 해결이 가능하다.

설정 전의 모습
Fill의 Rect Transform의 Left와 Right의 값을 전부 0으로 설정
설정 후의 모습

 

HP_Bar의 Canvas를 Prefab으로 저장한 뒤 UI_HPBar Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_HPBar : UI_Base
{
    enum GameObjects
    {
        HPBar
    }
    public override void Init()
    {
        Bind<GameObject>(typeof(GameObjects));
    }

    void Update()
    {
        // HP_Bar를 캐릭터의 머리 위로 위치시키기 위해서
        Transform parent = transform.parent;
        transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y); // 부모님 위치를 기준으로 Collider만큼의 높이를 올려 저장
    }
}​

 

UIManager 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIManager
{
    // ...

    public T MakeWorldSpaceUI<T>(Transform parent = null, string name = null) where T : UI_Base
    {
        if (string.IsNullOrEmpty(name))
            name = typeof(T).Name;

        GameObject go = Managers.Resource.Instantiate($"UI/WorldSpace/{name}");

        if (parent != null)
            go.transform.SetParent(parent);

        Canvas canvas = go.GetOrAddComponent<Canvas>();
        canvas.renderMode = RenderMode.WorldSpace;
        canvas.worldCamera = Camera.main;

        return Util.GetOrAddComponent<T>(go);
    }

    // ...
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    void Start()
    {
        _stat = gameObject.GetComponent<PlayerStat>();

        Managers.Input.MouseAction -= OnMouseEvent; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.MouseAction += OnMouseEvent;

        Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    // ...
}​

 

 

 체력 게이지 #2

상속받는 Class들이 Start 함수 선언 없이 Init 함수를 실행하도록 UI_Base 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public abstract class UI_Base : MonoBehaviour
{
    // ...
    
    public abstract void Init();

    void Start()
    {
        Init();
    }

    // ...
}​

 

Billboard 기능 구현 및 실시간 HP 감소를 위한 UI_HPBar 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_HPBar : UI_Base
{
    enum GameObjects
    {
        HPBar
    }

    Stat _stat;

    public override void Init()
    {
        Bind<GameObject>(typeof(GameObjects));
        _stat = transform.parent.GetComponent<Stat>();
    }

    void Update()
    {
        // HP_Bar를 캐릭터의 머리 위로 위치시키기 위해서
        Transform parent = transform.parent;
        transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y); // 부모님 위치를 기준으로 Collider만큼의 높이를 올려 저장
        transform.rotation = Camera.main.transform.rotation; // 자신이 바라보는 방향을 카메라가 바라보는 방향으로 설정 (Billboard의 개념)

        float ratio = _stat.Hp / (float)_stat.MaxHp;
        SetHpRatio(ratio);
    }

    public void SetHpRatio(float ratio)
    {
        GetObject((int)GameObjects.HPBar).GetComponent<Slider>().value = ratio;
    }
}

 

Hit Event 발생시 상대방의 HP를 감소시키기 위한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            Stat myStat = gameObject.GetComponent<PlayerStat>();
            int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
            Debug.Log(damage);
            targetStat.Hp -= damage;
        }

        if (_stopSkill)
        {
            State = PlayerState.Idle;
        }
        else
        {
            State = PlayerState.Skill;
        }
    }

    // ...
}​

 

+ 추가 검색

  - Unity에서 3D 오브젝트가 카메라를 바라보도록 만드는 기술을 Billboard라고 한다.

   > UI인 Slider를 카메라를 바라보도록 수정할 경우 좌우 반전이 된 채로 표시된다.

      ( transform.LookAt(Camera.main) 을 통해 )

    ~> 따라서 UI인 Slider에 Billboard 기술을 적용하기 위해서는 아래와 같이 수정한다.

         ( transform.rotation = Camera.main.transform.rotation 으로 수정 )

 몬스터 AI #1

  - 새로 생성하고자 하는 MonsterController가 PlayerController와 겹치는 부분이 상당히 많아서 이럴때는 BaseController를

    새로 생성하여 MonsterController와 PlayerController가 BaseController를 상속받도록 하는 것이 좋다.

Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum State
    {
        Die,
        Moving,
        Idle,
        Skill,
    }

    // ...
}​

 

Base Controller 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class BaseController : MonoBehaviour
{
    [SerializeField]
    protected Vector3 _destPos; // 목적지 좌표를 저장하기 위한 변수

    [SerializeField]
    protected Define.State _state = Define.State.Idle;

    [SerializeField]
    protected GameObject _lockTarget;

    public virtual Define.State State // State가 변함과 동시에 Animation도 변경하기 위한 프로퍼티 생성
    {
        get { return _state; }
        set
        {
            _state = value;

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

    private void Start() 
    {
        Init();
    }

    void Update()
    {
        switch (State)
        {
            case Define.State.Die:
                UpdateDie();
                break;
            case Define.State.Moving:
                UpdateMoving();
                break;
            case Define.State.Idle:
                UpdateIdle();
                break;
            case Define.State.Skill:
                UpdateSkill();
                break;
        }
    }

    public abstract void Init();
    protected virtual void UpdateDie() { }
    protected virtual void UpdateMoving() { }
    protected virtual void UpdateIdle() { }
    protected virtual void UpdateSkill() { }
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    PlayerStat _stat;
    bool _stopSkill = false;

    public override void Init()
    {
        _stat = gameObject.GetComponent<PlayerStat>();

        Managers.Input.MouseAction -= OnMouseEvent; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.MouseAction += OnMouseEvent;

        if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateMoving()
    {
        // 몬스터가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= 1)
            {
                State = Define.State.Skill;
                return;
            }
        }

        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
        if (dir.magnitude < 0.1f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            State = Define.State.Idle;
        }
        else
        {
            // NavMesh를 사용
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            float moveDist = Mathf.Clamp(Time.deltaTime * _stat.MoveSpeed, 0, dir.magnitude);
            nma.Move(dir.normalized * moveDist);

            Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green); // Ray를 시각적으로 표현
            if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"))) // 건물, 사물과 인접시 Player가 멈추도록 설정
            {
                if (Input.GetMouseButton(0) == false)
                    State = Define.State.Idle;
                return;
            }

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
        }
    }

    protected override void UpdateSkill()
    {
       if (_lockTarget != null)
        {
            Vector3 dir = _lockTarget.transform.position - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
            Quaternion quat = Quaternion.LookRotation(dir); // 목적지를 바라보기 위해서
            transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // 자연스럽게 연결되도록
        }
    }

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            Stat myStat = gameObject.GetComponent<PlayerStat>();
            int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
            Debug.Log(damage);
            targetStat.Hp -= damage;
        }

        if (_stopSkill)
        {
            State = Define.State.Idle;
        }
        else
        {
            State = Define.State.Skill;
        }
    }

    void OnMouseEvent(Define.MouseEvent evt)
    {
        switch (State)
        {
            case Define.State.Idle:
                OnMouseEvent_IdleRun(evt);
                break;
            case Define.State.Moving:
                OnMouseEvent_IdleRun(evt);
                break;
            case Define.State.Skill:
                {
                    if (evt == Define.MouseEvent.PointerUp)
                        _stopSkill = true;
                }
                break;
        }
    }

    void OnMouseEvent_IdleRun(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

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

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if (_lockTarget == null && raycastHit)
                        _destPos = hit.point;
                }
                break;
            case Define.MouseEvent.PointerUp:
                _stopSkill = true;
                break;
        }
    }
}​

 

몬스터 AI #2

MonsterController 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : BaseController
{
    Stat _stat;

    [SerializeField]
    float _scanRange = 10;

    [SerializeField]
    float _attackRange = 2;

    public override void Init()
    {
        _stat = gameObject.GetComponent<Stat>();

        if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateIdle()
    {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player == null)
            return;

        float distance = (player.transform.position - transform.position).magnitude;
        if (distance <= _scanRange) // 사정 거리 안으로 들어올 경우 움직이기 시작
        {
            _lockTarget = player;
            State = Define.State.Moving;
            return;
        }
    }

    protected override void UpdateMoving()
    {
        // 플레이어가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= _attackRange)
            {
                NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
                nma.SetDestination(transform.position);
                State = Define.State.Skill;
                return;
            }
        }

        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
        if (dir.magnitude < 0.1f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            State = Define.State.Idle;
        }
        else
        {
            // NavMesh를 사용
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            nma.SetDestination(_destPos);
            nma.speed = _stat.MoveSpeed;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
        }
    }

    protected override void UpdateSkill()
    {
        if (_lockTarget != null)
        {
            Vector3 dir = _lockTarget.transform.position - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
            Quaternion quat = Quaternion.LookRotation(dir); // 목적지를 바라보기 위해서
            transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // 자연스럽게 연결되도록
        }
    }

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            Stat myStat = gameObject.GetComponent<Stat>();
            int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
            targetStat.Hp -= damage;

            if (targetStat.Hp > 0)
            {
                float distance = (_lockTarget.transform.position - transform.position).magnitude;
                if (distance <= _attackRange)
                    State = Define.State.Skill;
                else
                    State = Define.State.Moving;
            }
            else
            {
                State = Define.State.Idle;
            }
        }
        else
        {
            State = Define.State.Idle;
        }
    }
}​

 

 Destroy #1

  - Destroy된 Object는 사실 실제로 삭제된건 C++ Native Object이고, UnityEngine.Object C# Class 부분은 아직 Memory

    상에 존재하기 때문에 실제로 null이 된 것은 아니지만 없어진 것처럼 행동해야 하기 때문에 "null"로 처리된다.

   > 이것이 바로 fake null 이다.

   > 이에 대한 처리가 UnityEngine.Object의 == 연산자 오버로딩에 구현되어 있다.

      (진짜 null이 된게 아님에도 Destroy된 Object == null 을 하면 true를 리턴하도록 구현됨)

  - Destroy된 Object의 Component를 참조하는 것들을 잘 확인해야 한다.

   > Object가 해제되면 당연히 해당 Object의 Component도 참조할 수 없다.

MosterController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : BaseController
{
    // ...

    void OnHitEvent()
    {
        // ...

            if (targetStat.Hp <=0)
            {
                GameObject.Destroy(targetStat.gameObject);
            }

        // ...
    }
}​

 

CameraController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    // ...

    // Player의 움직임이 실행된 뒤 Camera의 위치를 움직여야 떨리는 현상이 줄어든다.
    void LateUpdate() // 게임 Logic에서 LateUpdate()는 Update()보다 늦게 실행된다.
    {
        if (_mode == Define.CameraMode.QuaterView)
        {
            if (_player == null)
            {              
                return;
            }

            // ...
        }
    }

    // ...
}​

 

+ 추가 검색 (https://ansohxxn.github.io/unitydocs/fakenull/)

  - 해결 방법 1 : Object를 bool로 묵시적 형변환

  - 해결 방법 2 : Destroy 후 진짜 null을 대입

  - 해결 방법 3 : System.Object로 형변환 후 비교

 

 Destroy #2

Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum WorldObject
    {
        Unknown,
        Player,
        Monster,
    }
    
    // ...
}​

 

BaseController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class BaseController : MonoBehaviour
{
    // ...

    public Define.WorldObject WorldObjectType { get; protected set; } = Define.WorldObject.Unknown;

    // ...
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    // ...

    public override void Init()
    {
        WorldObjectType = Define.WorldObject.Player;

        // ...
    }

    // ...
}​

 

MonsterController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : BaseController
{
    // ...

    public override void Init()
    {
        WorldObjectType = Define.WorldObject.Monster;

        // ...
    }

    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            // ...

            if (targetStat.Hp <=0)
            {
                Managers.Game.Despawn(targetStat.gameObject);
            }

            // ...
        }
        // ...
    }
}​

 

GameManagerEx 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManagerEx : MonoBehaviour
{
    // 스폰된 Object들은 ID로 관리하는 것이 좋다 => Dictionary가 적당
    // 서버에서는 해당 ID를 통해 통신하기 때문이다.
    //Dictionary<int, GameObject> _players = new Dictionary<int, GameObject>();
    //Dictionary<int, GameObject> _monsters = new Dictionary<int, GameObject>();
   
    // 현재는 멀티가 아닌 싱글 게임이기 때문에 아래와 같이 관리한다.
    GameObject _player;
    HashSet<GameObject> _monsters = new HashSet<GameObject>(); // HashSet은 Dictionary와 비슷하지만 Key가 X

    public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
    {
        GameObject go = Managers.Resource.Instantiate(path, parent);

        switch (type)
        {
            case Define.WorldObject.Monster:
                _monsters.Add(go);
                break;
            case Define.WorldObject.Player:
                _player = go;
                break;
        }

        return go;
    }

    public Define.WorldObject GetWorldObjectType(GameObject go)
    {
        BaseController bc = go.GetComponent<BaseController>();
        if (bc == null)
            return Define.WorldObject.Unknown;
        return bc.WorldObjectType ;
    }

    public void Despawn(GameObject go) 
    {
        Define.WorldObject type = GetWorldObjectType(go);

        switch (type)
        {
            case Define.WorldObject.Monster:
                {
                    if (_monsters.Contains(go))
                        _monsters.Remove(go);
                }
                break;
            case Define.WorldObject.Player:
                {
                if (_player == go)
                    _player = null;      
                }
                break;
        }

        Managers.Resource.Destroy(go);
    }
}​

 

Managers 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    // ...
    
    GameManagerEx _game = new GameManagerEx();

    // ...

    public static GameManagerEx Game { get { return Instance._game; } }

    // ...
}​

 

Extension 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public static class Extension
{
    // ...

    // 폴링하는 Object일 경우 Destroy가 아닌 비활성화 처리가 된다.
    // 따라서 null 체크뿐만이 아닌 활성화 상태 여부도 체크해줘야 한다.
    public static bool IsValid(this GameObject go) 
    {
        return go != null && go.activeSelf;
    }
}​

 

CameraController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    // ...

    public void SetPlayer(GameObject player) { _player = player; }

    // Player의 움직임이 실행된 뒤 Camera의 위치를 움직여야 떨리는 현상이 줄어든다.
    void LateUpdate() // 게임 Logic에서 LateUpdate()는 Update()보다 늦게 실행된다.
    {
        if (_mode == Define.CameraMode.QuaterView)
        {
            if (_player.IsValid() == false)
            {              
                return;
            }

            // ...
        }
    }

    // ...
}​

 

GameScene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

        GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "Player");
        Camera.main.gameObject.GetOrAddComponent<CameraController>().SetPlayer(player);
        Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
    }

    // ...
}​

 

+ 추가 검색 (https://wlsdn629.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-Dictionary-HashTable-HastSet-%EA%B0%84%EB%8B%A8-%EC%84%A4%EB%AA%85)

  - HashSet은 Key가 존재하지 않으며 Value(들)의 구성으로 이루어져 있다.

   > 순서가 존재하지 않는다.

   > 각 항목의 중복을 허용하지 않는다. (즉, Value 중복을 허용 X)

   > Add( ), Remove( ), Clear( ), UnionWith( ), IntersectWith( ), ExceptWith( ) 등의 함수가 존재한다.

 

 레벨업

  - Damage와 관련된 부분은 공격을 하는 객체가 아닌 공격을 받는 객체에서 처리해주는 것이 좋다.

   (공격을 받는 객체의 방어력, 버프 등이 존재하기 때문)

Stat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Stat : MonoBehaviour
{
    // ...

    public virtual void OnAttacked(Stat attacker) 
    {
        int damage = Mathf.Max(0, attacker.Attack - Defense);
        Hp -= damage;
        if (Hp <= 0) {
            Hp = 0;
            OnDead();
        }
    }

    protected virtual void OnDead() 
    {
        Managers.Game.Despawn(gameObject);
    }
}​

 

PlayerStat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStat : Stat
{
    // ...

    protected override void OnDead() 
    {
        Debug.Log("Player Dead");
    }
}​

 

MonsterController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            targetStat.OnAttacked(_stat);
        }

        // ...
    }

    // ...
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            targetStat.OnAttacked(_stat);
        }

        // ...
    }

    // ...
}​

 

  - Monster를 죽일 경우 Monster에 따른 경험치 휙득량을 데이터를 통해 관리해주는 것이 좋다.

  - Level Up 확인 코드는 유일한 것이 좋다. (그렇지 않으면 고된 복붙 작업)

StatData json 파일 수정
{
    "stats": [
        {
            "level": "1",
            "maxHp": "200",
            "attack": "20",
            "totalExp": "0"
        },
        {
            "level": "2",
            "maxHp": "250",
            "attack": "25",
            "totalExp": "10"
        },
        {
            "level": "3",
            "maxHp": "300",
            "attack": "30",
            "totalExp": "20"
        }
    ]
}​

 

Data.Contents 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Data
{
    #region Stat
    [Serializable]
    public class Stat
    {
        public int level;
        public int maxHp;
        public int attack;
        public int totalExp;
    }

    // ...
    #endregion
}​

 

Stat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Stat : MonoBehaviour
{
    // ...

    public virtual void OnAttacked(Stat attacker) 
    {
        int damage = Mathf.Max(0, attacker.Attack - Defense);
        Hp -= damage;
        if (Hp <= 0) {
            Hp = 0;
            OnDead(attacker);
        }
    }

    protected virtual void OnDead(Stat attacker) 
    {
        PlayerStat playerStat = attacker as PlayerStat;
        if (playerStat != null)
        {
            playerStat.Exp += 5;
        }

        Managers.Game.Despawn(gameObject);
    }
}​

 

PlayerStat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStat : Stat
{
    // ...

    public int Exp { 
        get { return _exp; } 
        set 
        { 
            _exp = value;

            // Level Up 체크
            int level = Level;
            while (true)
            {
                Data.Stat stat;
                if (Managers.Data.StatDict.TryGetValue(level + 1, out stat) == false) // 다음 Level이 존재하지 않을 경우
                    break;
                if (_exp < stat.totalExp)
                    break;
                level++;
            }

            if (level != Level) // Level에 변화가 있을 경우
            {
                Level = level;
                SetStat(Level);
            }
        } 
    }

    // ...

    private void Start()
    {
        _level = 1;
        _exp = 0;
        _defense = 5;
        _moveSpeed = 5.0f;
        _gold = 0;

        SetStat(_level);
    }

    public void SetStat(int level)
    {
        Dictionary<int, Data.Stat> dict = Managers.Data.StatDict;
        Data.Stat stat = dict[level];
        _hp = stat.maxHp;
        _maxHp = stat.maxHp;
        _attack = stat.attack;
    }

    protected override void OnDead(Stat attacker) 
    {
        Debug.Log("Player Dead");
    }
}

 

몬스터 자동 생성

SpawningPool 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class SpawningPool : MonoBehaviour
{
    [SerializeField]
    int _monsterCount = 0; // 현재 몬스터 수
    int _reserveCount = 0; // 현재 Spawn 예약 수

    [SerializeField]
    int _keepMonsterCount = 0; // 유지시켜야 하는 몬스터 수

    [SerializeField]
    Vector3 _spawnPos; // Spawn 중심점

    [SerializeField]
    float _spawnRadius = 15.0f; // Spawn 중심점을 기준으로 랜덤 생성을 위한 생성 거리 범위 제한

    [SerializeField]
    float _spawnTime = 5.0f; // 랜덤 생성을 위한 생성 시간 범위 제한

    public void AddMonsterCount(int value) { _monsterCount += value; }
    public void SetkeepMonsterCount(int count) { _keepMonsterCount = count; }

    void Start()
    {
        Managers.Game.OnSpawnEvent -= AddMonsterCount;
        Managers.Game.OnSpawnEvent += AddMonsterCount;
    }

    void Update()
    {
        while (_reserveCount + _monsterCount < _keepMonsterCount)
        {
            StartCoroutine("ReserveSpawn");
        }
    }

    IEnumerator ReserveSpawn()
    {
        _reserveCount++;

        yield return new WaitForSeconds(Random.Range(0, _spawnTime));
        GameObject obj = Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");

        NavMeshAgent nma = obj.GetOrAddComponent<NavMeshAgent>();
        Vector3 randPos;
        while(true) // randPos가 갈 수 있는 길이 나올때까지 반복
        {
            Vector3 randDir = Random.insideUnitSphere * Random.Range(0, _spawnRadius); // 랜덤으로 뽑힌 방향벡터가 randDir에 담긴다. (insideUnitCircle은 2D)
            randDir.y = 0; // 땅을 뜷고 Spawn 되지 않도록
            randPos = _spawnPos + randDir;

            // randPos가 갈 수 있는 길인지 체크
            NavMeshPath path = new NavMeshPath();
            if (nma.CalculatePath(randPos, path)) // 갈 수 있는 길이면 True, 갈 수 없는 길이면 False를 반환
                break;
        }
        obj.transform.position = randPos;

        _reserveCount--;
    }
}​

 

GameManagerEx 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManagerEx : MonoBehaviour
{
    // ...
    
    public Action<int> OnSpawnEvent; // int는 증가/감소 수

    // ...

    public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
    {
        // ...

        switch (type)
        {
            case Define.WorldObject.Monster:
                // ...
                if (OnSpawnEvent != null)
                    OnSpawnEvent.Invoke(1);
                break;
            // ...
        }

        // ...
    }

    // ...
    
    public void Despawn(GameObject go) 
    {
        // ...

        switch (type)
        {
            case Define.WorldObject.Monster:
                {
                    if (_monsters.Contains(go)) {
                        _monsters.Remove(go);
                        if (OnSpawnEvent != null)
                            OnSpawnEvent.Invoke(-1);
                    } 
                }
                break;
            // ...
        }

        // ...
    }
}

 

GameScene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

        //Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
        GameObject go = new GameObject { name = "SpawningPool" };
        SpawningPool pool = go.GetOrAddComponent<SpawningPool>();
        pool.SetkeepMonsterCount(5);
    }

    // ...
}​

 

+ 추가 검색 (  )

  - Unity는 난수 생성과 관련된 함수들이 있는 집합인 Random 이라는 기능을 제공한다.

   ~> 가장 대표적인 기능이 Random.Range(min, max) 이며 이는 min과 max 범위 내에서 랜덤한 값을 반환한다.

        (이때 max의 Type이 int일 경우 max의 값은 포함되지 X)

   ~> Random.insideUnitSphere은 반경 1을 갖는 구 안의 임의의 위치(Vector3)를 반환하는 프로퍼티이다.

   ~> Random. insideUnitCircle 은 반경 1을 갖는 구 안의 임의의 위치(Vector2)를 반환하는 프로퍼티이다.

 

 

 

 

 

 

[ 섹션 7. UI ]

 UI 기초

  - UI 추가시 자동으로 Canvas가 생성되는데 이는 도화지 역할을 한다.

  - UI는 Rect Transform Component를 통해 좌표를 설정한다.

  - UI는 원근법을 적용받지 않는다.

  - Shift를 누른채로 UI 크기를 조절할 경우 비율을 유지하며 크기를 조절할 수 있다.

좌 : Pivot / 우: Anchor

  -   Anchor는 UI에서 상당히 중요한 역할을 한다.

 

 Rect Transform

  - 디바이스의 종류에 따라 Screen의 크기는 제각각이다. 이때 Anchor 기능이 중요하게 작용한다.

  - Anchor는 Rect Transform을 Component로 가진 부모를 가져야 활성화가 된다.

Anchor의 개념

  -   부모와 Anchor 사이의 거리는 비율로, Anchor와 본인 사이의 거리는 고정 거리로 연산을 한다.

Anchor Presets

  -   Rect Transform의 Anchor Presets을 통해 쉽고 간단하게 Anchor 설정을 할 수 있다.

  -   Anchor Presets에서 Shift를 누른 상태로 Anchor 설정시 Pivot도 함께 이동한다.

  -   Anchor Presets에서 Alt를 누른 상태로 Anchor 설정시 본인의 위치도 함께 이동한다.

  -   Anchor Presets에서 Shift와 Alt를 누른 상태로 Anchor 설정시 Pivot과 본인의 위치도 함께 이동한다.

 

 Button Event

  - Canvas에 UI를 모두 추가한 뒤 최종적으로 해당 Canvas를 Prefab 형태로 저장하곤 한다.

  - 해당 Canvas내의 UI에 관한 Event는 Event 관리를 하는 담당하는 Script를 생성한 뒤 해당 Script를 Canvas의

    Component로 추가하고 UI의 On Click()의 객체로 Canvas를 추가, 실행하고자 하는 Event 함수를 선택한다.

    (Event 함수를 Public으로 선언해야 함수 선택 가능)

  - Canvas를 Prefab 형태로 저장할 경우 코드를 통해 필요할때마다 사용할 수 있다.

  - 여러개의 Canvas로 인해 겹치는 현상이 발생할 경우 이는 Canvas Component의 Sort Order을 통해 해결 가능하다.

     (팝업창 구현시 유용)

Button Event를 위한 UI_Button Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_Button : MonoBehaviour
{
    [SerializeField]
    Text _text;

    int _score = 0;

    public void OnButtonClicked()
    {
        _score++;
        _text.text = $"점수 : {_score}";
    }
}​

 

UI Click시 캐릭터 이동은 제한되도록 InputManager 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputManager
{
    public Action KeyAction = null;
    public Action<Define.MouseEvent> MouseAction = null;

    bool _pressed = false;

    // 체크하는 부분이 유일해짐
    public void OnUpdate()
    {
        // using UnityEngine.EventSystems; 추가시 사용 가능
        if (EventSystem.current.IsPointerOverGameObject()) // UI가 Click된 상황일 경우 
            return;

        if (Input.anyKey && KeyAction != null)
            KeyAction.Invoke();

        if (MouseAction != null)
        {
            if (Input.GetMouseButton(0))
            {
                MouseAction.Invoke(Define.MouseEvent.Press); // Press Event를 발생시켜 알려준다.
                _pressed = true;
            }
            else
            {
                if (_pressed)
                    MouseAction.Invoke(Define.MouseEvent.Click); // Click Event를 발생시켜 알려준다.
                _pressed = false;
            }
        }
    }
}​

 

Prefab으로 저장된 UI를 코드를 통해 불러오기 위한 것
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        Managers.Resource.Instantiate("UI_Button"); // 다음과 같이 UI Prefab을 불러올 수 있다.
    }
}​

 

UI 자동화 #1

  - 게임 규모가 커질수록 UI의 Event 연결을 Tool을 이용하여 On Click()을 통해 연결하는 것이 힘들어진다.

     (뿐만 아닌 [SerializeField]에 선언한 UI를 Tool을 이용하여 연결하는 것도 힘들어진다.)

UI 자동화를 위한 UI_Button 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_Button : MonoBehaviour
{
    Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>(); // Dictionary 사용

    enum Buttons
    {
        PointButton
    }

    enum Texts
    {
        PointText,
        ScoreText
    }

    private void Start()
    {
        Bind<Button>(typeof(Buttons)); // 넘기고자 하는 enum은 Buttons이며, Button이라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<Text>(typeof(Texts)); // 넘기고자 하는 enum은 Texts이며, Text라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
    }

    // C#의 Reflection 기능을 통해 enum을 해당 함수의 인자로 넘겨줄 수 있다.
    // enum 값을 인자로 넘겨주면 enum 안의 이름에 해당하는 객체를 찾아 자동으로 연결해주는 역할을 하는 함수이다.
    void Bind<T>(Type type) where T : UnityEngine.Object // using System;을 추가해야 Type 사용이 가능하다.
    {
        // 해당 enum에 존재하는 모든 이름들을 names에 담는 것
        string[] names = Enum.GetNames(type); // String 배열을 반환 (C#에서 제공하는 기능)

        // Unity 모든 객체의 최상위 부모가 UnityEngine.Object
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects); // Dictionary에 추가

        for (int i = 0; i < names.Length; i++)
        {
            objects[i] = Util.FindChild<T>(gameObject, names[i], true); // enum에 존재하는 이름에 해당하는 객체를 찾아 objects에 저장
        }
    }

    int _score = 0;

    public void OnButtonClicked()
    {
        _score++;
    }
}​

 

Util 성격을 가진 함수들을 관리하는 Util Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Util
{
    // 최상위 부모를 받고, 이름을 입력하지 않을 경우 타입만 일치해도 리턴, 재귀적으로 찾을거냐? (즉, 자식의 자식도)
    public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object // 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>())
            {
                if (string.IsNullOrEmpty(name) || component.name == name)
                    return component;
            }
        }

        return null;
    }
}​

 

+ 추가 검색 (https://engineer-mole.tistory.com/174)
 - Dictionary는 Index 대신 중복 불가능한 Key를 사용하고 Value와 함께 다룬다. 이처럼 Key와 Value 세트로 다루는 배열을

   "연관 배열"이라고 한다.

 - C#에서 연관 배열을 다루기 위한 Class가 바로 Dictionary Class이다. Dictionary Class는 Key를 통해 Value의 값을 얻을

   수 있다.

 - 요소를 추가하기 위해서는 Add Method를 사용한다.

 

+ 추가 검색 (https://velog.io/@yongseok1000/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98)
 - C#은 Reflection 기능을 지원한다. 이는 객체(Instance)를 토대로 데이터타입의 메타적인 정보를 가져오는 기법이다.

 - Reflection은 조사, Instance 생성, 기존 개체에서 형식을 가져와 호출, 접근 기능을 제공한다.

 

+ 추가 검색 (https://www.csharpstudy.com/CSharp/CSharp-generics.aspx)
 - C#은 Generics 기능을 지원한다. 이는 데이터의 Type을 확정하지 않고 데이터 Type 자체를 Parameter로 받아들인다.

 - 이는 여러 데이터 형식에 동일한 Logic을 적용해야 할 때 유용하다.

 - Generic Type 선언시 where T : ~ 를 통해 Type Parameter에 제약 조건을 설정할 수 있다. 

 

UI 자동화 #2

  - Bind 함수는 enum에 존재하는 이름에 해당하는 객체를 찾아 저장한다. 이때 저장된 객체들 중 index를 인자로 받아

    해당하는 객체를 반환하는 Get 함수를 추가할 수 있다.

  - Bind 함수처럼 Component를 찾는 것이 아닌 Object 자체를 Mapping 하는 경우가 있을 수 있는데, 이에 GameObject를

    위한 FindChild 함수를 추가할 수 있다.

Get 함수를 추가한 후 UI_Base Script를 생성하여 UI_Button과 분리(UI_Button 이 UI_Base를 상속받도록)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_Base : MonoBehaviour
{
    protected Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>(); // Dictionary 사용

    // C#의 Reflection 기능을 통해 enum을 해당 함수의 인자로 넘겨줄 수 있다.
    // enum 값을 인자로 넘겨주면 enum 안의 이름에 해당하는 객체를 찾아 자동으로 연결해주는 역할을 하는 함수이다.
    protected void Bind<T>(Type type) where T : UnityEngine.Object // using System;을 추가해야 Type 사용이 가능하다.
    {
        // 해당 enum에 존재하는 모든 이름들을 names에 담는 것
        string[] names = Enum.GetNames(type); // String 배열을 반환 (C#에서 제공하는 기능)

        // Unity 모든 객체의 최상위 부모가 UnityEngine.Object
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects); // Dictionary에 추가

        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true);
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true); // enum에 존재하는 이름에 해당하는 객체를 찾아 objects에 저장
        }
    }

    // index를 인자로 받아 해당하는 객체를 반환
    protected T Get<T>(int idx) where T : UnityEngine.Object
    {
        // Key 값을 이용하여 추출
        UnityEngine.Object[] objects = null;
        if (_objects.TryGetValue(typeof(T), out objects) == false) // 만약 추출에 실패한 경우
            return null;

        return objects[idx] as T; // 추출에 성공한 경우 T로 Casting하여 반환 (objects는 UnityEngine.Object 이므로)
    }

    // 자주 사용하는 것들은 굳이 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); }
}​

 

UI_Base를 상속 받는 UI_Button 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_Button : UI_Base
{
    enum Buttons
    {
        PointButton
    }

    enum Texts
    {
        PointText,
        ScoreText
    }

    enum GameObjects
    {
        TestObject,
    }

    private void Start()
    {
        Bind<Button>(typeof(Buttons)); // 넘기고자 하는 enum은 Buttons이며, Button이라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<Text>(typeof(Texts)); // 넘기고자 하는 enum은 Texts이며, Text라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<GameObject>(typeof(GameObjects)); // Component를 찾는 것이 아닌 Object 자체를 Mapping하는 경우

        GetText((int)Texts.ScoreText).text = "Bind Test"; // 다음과 같이 사용
    }

    int _score = 0;

    public void OnButtonClicked()
    {
        _score++;
    }
}

 

GameObject를 위한 FindChild 함수를 추가한 Util 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Util
{
    // GameObject를 위한 FindChild 생성
    public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
    {
        Transform transform = FindChild<Transform>(go, name, recursive); // 모든 GameObject는 Transform Component를 가지므로
        if (transform == null)
            return null;
        return transform.gameObject;
    }

    // 최상위 부모를 받고, 이름을 입력하지 않을 경우 타입만 일치해도 리턴, 재귀적으로 찾을거냐? (즉, 자식의 자식도)
    public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object // 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>())
            {
                if (string.IsNullOrEmpty(name) || component.name == name)
                    return component;
            }
        }

        return null;
    }
}​

 

UI 자동화 #3

  - UI Event 연동을 위해서는 Event System을 이용하는 것이 중요하다.

UI Event를 관리하는 UI_EventHandler Script 생성
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

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

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (OnBeginDragHandler != null)
            OnBeginDragHandler.Invoke(eventData);
    }

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

 

Event 추가를 위한 UI_Button 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class UI_Button : UI_Base
{
    enum Buttons
    {
        PointButton
    }

    enum Texts
    {
        PointText,
        ScoreText
    }

    enum GameObjects
    {
        TestObject,
    }

    enum Images
    {
        ItemIcon,
    }

    private void Start()
    {
        Bind<Button>(typeof(Buttons)); // 넘기고자 하는 enum은 Buttons이며, Button이라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<Text>(typeof(Texts)); // 넘기고자 하는 enum은 Texts이며, Text라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<GameObject>(typeof(GameObjects)); // Component를 찾는 것이 아닌 Object 자체를 Mapping하는 경우
        Bind<Image>(typeof(Images));

        GetText((int)Texts.ScoreText).text = "Bind Test"; // 다음과 같이 사용

        // 이벤트 추가
        GameObject go = GetImage((int)Images.ItemIcon).gameObject; // ItemIcon을 찾아 Bind한 것의 Image Component를 뽑아온 뒤, 해당 객체 자체를 가져온 것 (GameObject)
        UI_EventHandler evt = go.GetComponent<UI_EventHandler>(); // 해당 객체의 UI_EventHandler Component를 뽑아온 것
        evt.OnDragHandler += ((PointerEventData data) => { evt.gameObject.transform.position = data.position; });
    }

    int _score = 0;

    public void OnButtonClicked()
    {
        _score++;
    }
}

 

+ 추가 검색 

 - Delegate, 무명 Method, 람다식의 개념을 아래 유튜브 영상을 통해 쉽게 이해할 수 있었다.

( https://www.youtube.com/watch?v=6FomZi4QiRY&ab_channel=%EC%BC%80%EC%9D%B4%EB%94%94  )

 - Delegate는 Method Parameter와 Return Type에 대한 정의 후, 동일한 Parameter와 Return Type을 가진 Method를 서로

   호환해서 불러 사용할 수 있도록 만들어준다.

 - 무명 Method는 어떤 Method가 일회용으로 단순한 문장들로 구성된 경우 별도의 Method로 정의하지 않고 사용할 수 있

   도록 만들어준다. 이는 delegate 키워드와 함께 선언하며 이름은 지정하지 않는다. (Delegate를 통해서만 호출 가능)
 - 람다식은 무명 함수를 만들기 위해 사용하며 간결하고 직관적인 형태로 함수를 정의하는 방식이다. 이는 C# 컴파일러가

   형식 유추 기능을 제공하므로 Parameter의 Type을 굳이 지정하지 않아도 된다.

   (즉, 람다식을 통해 무명 Method보다 더 짧은 코드로 무명 Method를 만들 수 있다.)

Delegate, 무명 Method, 람다식 예제
int a = 5;
int b = 5;

int sum;

void Add() {
	sum = A + b;
}

void Back() {
	sum = 0;
}

delegate void MyDelegate();
MyDelegate() myDelegate;

void Start() {
    myDelegate = Add;
    myDelegate += delegate() { print(sum); }; // 무명 메소드
    myDelegate += () => print(sum); // 람다식
    myDelegate += Back;
    
    myDelegate();
}

 

UI 자동화 #4

  - UI Event 추가 코드를 정리

OnBeginDrag 삭제 후 OnPointerClick을 추가한 UI_EventHandler 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class UI_EventHandler : MonoBehaviour, IPointerClickHandler, IDragHandler
{
    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);
    }
}​

 

UIEvent enum을 추가한 Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum UIEvent
    {
        Click,
        Drag,
    }

    public enum MouseEvent
    {
        Press,
        Click,
    }

    public enum CameraMode
    {
        QuaterView, 
    }
}​

 

GetOrAddComponent 함수를 추가한 Util 수정
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>();
        if (component == null)
            component = go.AddComponent<T>();
        return component;
    }

    // GameObject를 위한 FindChild 생성
    public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
    {
        Transform transform = FindChild<Transform>(go, name, recursive); // 모든 GameObject는 Transform Component를 가지므로
        if (transform == null)
            return null;
        return transform.gameObject;
    }

    // 최상위 부모를 받고, 이름을 입력하지 않을 경우 타입만 일치해도 리턴, 재귀적으로 찾을거냐? (즉, 자식의 자식도)
    public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object // 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>())
            {
                if (string.IsNullOrEmpty(name) || component.name == name)
                    return component;
            }
        }

        return null;
    }
}​

 

AddUIEvent 함수를 추가한 UI_Base 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class UI_Base : MonoBehaviour
{
    protected Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>(); // Dictionary 사용

    // C#의 Reflection 기능을 통해 enum을 해당 함수의 인자로 넘겨줄 수 있다.
    // enum 값을 인자로 넘겨주면 enum 안의 이름에 해당하는 객체를 찾아 자동으로 연결해주는 역할을 하는 함수이다.
    protected void Bind<T>(Type type) where T : UnityEngine.Object // using System;을 추가해야 Type 사용이 가능하다.
    {
        // 해당 enum에 존재하는 모든 이름들을 names에 담는 것
        string[] names = Enum.GetNames(type); // String 배열을 반환 (C#에서 제공하는 기능)

        // Unity 모든 객체의 최상위 부모가 UnityEngine.Object
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects); // Dictionary에 추가

        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true);
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true); // enum에 존재하는 이름에 해당하는 객체를 찾아 objects에 저장
        }
    }

    // index를 인자로 받아 해당하는 객체를 반환
    protected T Get<T>(int idx) where T : UnityEngine.Object
    {
        // Key 값을 이용하여 추출
        UnityEngine.Object[] objects = null;
        if (_objects.TryGetValue(typeof(T), out objects) == false) // 만약 추출에 실패한 경우
            return null;

        return objects[idx] as T; // 추출에 성공한 경우 T로 Casting하여 반환 (objects는 UnityEngine.Object 이므로)
    }

    // 자주 사용하는 것들은 굳이 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 AddUIEvent(GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
    {
        UI_EventHandler evt = Util.GetOrAddComponent<UI_EventHandler>(go);

        switch (type)
        {
            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_Button Event 호출 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class UI_Button : UI_Base
{
    enum Buttons
    {
        PointButton
    }

    enum Texts
    {
        PointText,
        ScoreText
    }

    enum GameObjects
    {
        TestObject,
    }

    enum Images
    {
        ItemIcon,
    }

    private void Start()
    {
        Bind<Button>(typeof(Buttons)); // 넘기고자 하는 enum은 Buttons이며, Button이라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<Text>(typeof(Texts)); // 넘기고자 하는 enum은 Texts이며, Text라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<GameObject>(typeof(GameObjects)); // Component를 찾는 것이 아닌 Object 자체를 Mapping하는 경우
        Bind<Image>(typeof(Images));

        GetText((int)Texts.ScoreText).text = "Bind Test"; // 다음과 같이 사용

        // 이벤트 추가
        GameObject go = GetImage((int)Images.ItemIcon).gameObject; // ItemIcon을 찾아 Bind한 것의 Image Component를 뽑아온 뒤, 해당 객체 자체를 가져온 것 (GameObject)
        AddUIEvent(go, (PointerEventData data) => {go.transform.position = data.position; }, Define.UIEvent.Drag);
    }

    int _score = 0;

    public void OnButtonClicked()
    {
        _score++;
    }
}​

 

+ 추가 검색 (https://developer-talk.tistory.com/477)

 - 확장 Method는 Class 또는 Interface를 상속하거나 재구성하지 않고 Class에 Method를 추가할 수 있는 기능이다.

 - 확장 Method는 기존 Class에 존재하지 않으며 Static Class에서 정의해야 한다. 또한 확장 Method의 첫번째 Parameter

   를 Binding Parameter라고 하며, 해당 Parameter의 Type은 Binding 되어야 하는 Class이다. 이때 Class 이름 앞에 this

   Keyword가 존재해야한다.

 

UI Manager #1

  - UI는 PopUp UI와 Scene UI로 나눌 수 있다.

  - UI_Manager를 통해 PopUp UI Canvas Component의 Sort Order를 관리하고, 이를 Stack을 통해 관리한다.

UI 관리를 위한 UIManager 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIManager
{
    int _order = 0; // 최근에 사용한 order를 저장하기 위한 변수

    // Stack을 통해 Popup을 관리 (가장 마지막에 띄운 PopUp이 가장 먼저 삭제되어야 하므로)
    Stack<UI_Popup> _popupStack = new Stack<UI_Popup>(); // 사실상 GameObject는 빈 깡통과 같다. 해당 GameObject의 Component에 많은 정보들이 담겨있는것 (때문에 UI_PopUp Component 정보를 담는 것)

    public T ShowPopupUI<T>(string name = null) where T : UI_Popup // name은 Prefab의 이름과, T는 Script와 관련이 있다. (name은 필수가 아닌 옵션으로)
    {
        if (string.IsNullOrEmpty(name))
            name = typeof(T).Name;

        GameObject go = Managers.Resource.Instantiate($"UI/Popup/{name}");
        T popup = Util.GetOrAddComponent<T>(go);
        _popupStack.Push(popup);

        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();
    }
}​

 

Managers에 ResourceManager 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    InputManager _input = new InputManager();
    ResourceManager _resource = new ResourceManager();
    UIManager _ui = new UIManager();

    public static InputManager Input { get { return Instance._input; } }
    public static ResourceManager Resource { get { return Instance._resource; } }
    public static UIManager UI {  get { return Instance._ui; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();
        }
    }
}​

 

+ 추가 검색 (https://jeong-f.tistory.com/3)

 - Stack에서의 Peek() 함수는 빼내고자 하는 데이터를 확인하기 위한 함수이다.

 

UI Manager #2

  - Unity의 Hierarchy에는 폴더가 없기 때문에 Create Empty를 통한 GameObject를 폴더로 사용한다.

Virtual Method인 Init() 함수를 추가한 UI_Popup 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_Popup : UI_Base
{
    public virtual void Init()
    {
        Managers.UI.SetCanvas(gameObject, true);
    }

    public virtual void ClosePopupUI()
    {
        Managers.UI.ClosePopupUI(this);
    }
}

 

Virtual Method인 Init() 함수를 추가한 UI_Scene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

 

UI_Popup의 Init() 함수를 Override한 UI_Button 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class UI_Button : UI_Popup
{
    enum Buttons
    {
        PointButton
    }

    enum Texts
    {
        PointText,
        ScoreText
    }

    enum GameObjects
    {
        TestObject,
    }

    enum Images
    {
        ItemIcon,
    }

    private void Start()
    {
        Init();
    }

    public override void Init()
    {
        base.Init();

        Bind<Button>(typeof(Buttons)); // 넘기고자 하는 enum은 Buttons이며, Button이라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<Text>(typeof(Texts)); // 넘기고자 하는 enum은 Texts이며, Text라는 Component를 가진 객체를 찾아 Mapping 해달라는 것
        Bind<GameObject>(typeof(GameObjects)); // Component를 찾는 것이 아닌 Object 자체를 Mapping하는 경우
        Bind<Image>(typeof(Images));

        GetButton((int)Buttons.PointButton).gameObject.AddUIEvent(OnButtonClicked);

        // 이벤트 추가
        GameObject go = GetImage((int)Images.ItemIcon).gameObject; // ItemIcon을 찾아 Bind한 것의 Image Component를 뽑아온 뒤, 해당 객체 자체를 가져온 것 (GameObject)
        AddUIEvent(go, (PointerEventData data) => { go.transform.position = data.position; }, Define.UIEvent.Drag);
    }

    int _score = 0;

    public void OnButtonClicked(PointerEventData data)
    {
        _score++;


        GetText((int)Texts.ScoreText).text = $"점수 : { _score}";
    }
}​

 

Sort Order 관리를 위한 UIManager 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIManager
{
    int _order = 10; // 최근에 사용한 order를 저장하기 위한 변수

    // Stack을 통해 Popup을 관리 (가장 마지막에 띄운 PopUp이 가장 먼저 삭제되어야 하므로)
    Stack<UI_Popup> _popupStack = new Stack<UI_Popup>(); // 사실상 GameObject는 빈 깡통과 같다. 해당 GameObject의 Component에 많은 정보들이 담겨있는것 (때문에 UI_PopUp Component 정보를 담는 것)
    UI_Scene _sceneUI = null;

    public GameObject Root
    {
        get
        {
            GameObject root = GameObject.Find("@UI_Root");
            if (root == null)
                root = new GameObject { name = "@UI_Root" };
            return root;
        }
    }

    public void SetCanvas(GameObject go, bool sort = true) // 외부에서 Popup UI 생성시 자신의 Canvas에 존재하는 UI의 우선순위를 결정
    {
        Canvas canvas = Util.GetOrAddComponent<Canvas>(go);
        canvas.renderMode = RenderMode.ScreenSpaceOverlay;
        canvas.overrideSorting = true; // Canvas가 중첩된 경우 부모와 독립하여 자신만의 sortingOrder를 갖는다는 것

        if (sort)
        {
            canvas.sortingOrder = _order;
            _order++;
        }
        else // sort 요청을 안 한 경우는 Popup UI와 관련이 없는 일반 UI라는 것
        {
            canvas.sortingOrder = 0;
        }    
    }

    public T ShowSceneUI<T>(string name = null) where T : UI_Scene // name은 Prefab의 이름과, T는 Script와 관련이 있다. (name은 필수가 아닌 옵션으로)
    {
        if (string.IsNullOrEmpty(name))
            name = typeof(T).Name;

        GameObject go = Managers.Resource.Instantiate($"UI/Scene/{name}");
        T sceneUI = Util.GetOrAddComponent<T>(go);
        _sceneUI = sceneUI;

        go.transform.SetParent(Root.transform);

        return sceneUI;
    }

    public T ShowPopupUI<T>(string name = null) where T : UI_Popup // name은 Prefab의 이름과, T는 Script와 관련이 있다. (name은 필수가 아닌 옵션으로)
    {
        if (string.IsNullOrEmpty(name))
            name = typeof(T).Name;

        GameObject go = Managers.Resource.Instantiate($"UI/Popup/{name}");
        T popup = Util.GetOrAddComponent<T>(go);
        _popupStack.Push(popup);

        go.transform.SetParent(Root.transform);

        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();
    }
}​

 

Blocker 생성

  - 팝업창 뒤 UI들의 Event 발생 방지를 위해서는 일종의 Blocker를 생성해야 한다.

  - Blocker는 [ Hierarchy ] - [ UI ] - [ Image 또는 Panel ] 을 생성하고 Alpha 값을 0으로 설정한 뒤 크기를 크게 늘려준다. 

    이때 Blocker의 Raycast Target은 반드시 설정되어 있어야 한다.

   > Hierarchy 창에서 Blocker 아래에 위치한 UI들은 Blocker가 대신 Raycast를 받기 때문에 Event 발생이 방지된다.

 

+ 추가 검색 (https://developer-talk.tistory.com/469)

 - Virtual Method란 자식 클래스에서 부모 Method의 Parameter 및 Return Type 재정의를 허용하는 것이다.

 - C#에서 Method 재정의를 허용하기 위해 부모 클래스의 Method를 Virtual 키워드로 선언한다. 이때 Virtual 키워드로 선언

   된 Method를 Virtual Method라고 한다.

 - 자식 클래스는 Override 키워드를 통해 Property 또는 Method를 재정의한다.

 

인벤토리 실습 #1

  -  Layout Group Component를 통해 Inventory Item을 배치를 변경할 수 있다.

     (Grid Layout Group, Horizontal Layout Group, Vertical Layout Group)

 

 인벤토리 실습 #2

abstract Method인 Init() 함수를 추가한 UI_Base 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public abstract class UI_Base : MonoBehaviour
{
    protected Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>(); // Dictionary 사용

    public abstract void Init();

    // C#의 Reflection 기능을 통해 enum을 해당 함수의 인자로 넘겨줄 수 있다.
    // enum 값을 인자로 넘겨주면 enum 안의 이름에 해당하는 객체를 찾아 자동으로 연결해주는 역할을 하는 함수이다.
    protected void Bind<T>(Type type) where T : UnityEngine.Object // using System;을 추가해야 Type 사용이 가능하다.
    {
        // 해당 enum에 존재하는 모든 이름들을 names에 담는 것
        string[] names = Enum.GetNames(type); // String 배열을 반환 (C#에서 제공하는 기능)

        // Unity 모든 객체의 최상위 부모가 UnityEngine.Object
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects); // Dictionary에 추가

        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true);
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true); // enum에 존재하는 이름에 해당하는 객체를 찾아 objects에 저장
        }
    }

    // index를 인자로 받아 해당하는 객체를 반환
    protected T Get<T>(int idx) where T : UnityEngine.Object
    {
        // Key 값을 이용하여 추출
        UnityEngine.Object[] objects = null;
        if (_objects.TryGetValue(typeof(T), out objects) == false) // 만약 추출에 실패한 경우
            return null;

        return objects[idx] as T; // 추출에 성공한 경우 T로 Casting하여 반환 (objects는 UnityEngine.Object 이므로)
    }

    // 자주 사용하는 것들은 굳이 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 AddUIEvent(GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
    {
        UI_EventHandler evt = Util.GetOrAddComponent<UI_EventHandler>(go);

        switch (type)
        {
            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를 상속받는 UI_Popup 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    public virtual void ClosePopupUI()
    {
        Managers.UI.ClosePopupUI(this);
    }
}​

 

UI_Base를 상속받는 UI_Scene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

 

UI_Scene을 상속받는 UI_Inven 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_Inven : UI_Scene
{
    enum GameObjects
    {
        GridPanel
    }

    // Start is called before the first frame update
    void Start()
    {
        Init();
    }

    public override void Init()
    {
        base.Init();

        Bind<GameObject>(typeof(GameObjects));

        GameObject gridPanel = Get<GameObject>((int)GameObjects.GridPanel);
        foreach (Transform child in gridPanel.transform)
            Managers.Resource.Destroy(child.gameObject);

        // 실제 인벤토리 정보를 참고해서
        for (int i = 0; i < 8; i++)
        {
            GameObject item = Managers.Resource.Instantiate("UI/Scene/UI_Inven_Item");
            item.transform.SetParent(gridPanel.transform);

            UI_Inven_Item invenItem = Util.GetOrAddComponent<UI_Inven_Item>(item);
            invenItem.SetInfo($"집행검{i}번");
        }
    }
}​

 

UI_Base를 상속받는 UI_Inven_Item 생성
 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));
        Get<GameObject>((int)GameObjects.ItemNameText).GetComponent<Text>().text = _name;
        Get<GameObject>((int)GameObjects.ItemIcon).AddUIEvent((PointerEventData) => { Debug.Log($"아이템 클릭! {_name}"); });
    }

    public void SetInfo(string name)
    {
        _name = name;
    }
}​

 

+ 추가 검색 (https://eboong.tistory.com/63)

 - foreach문은 배열을 순회하면서 각각의 데이터 요소들에 순서대로 접근하며 배열의 끝에 도달할 경우 자동으로 반복이

   종료된다.

 

+ 추가 검색 (https://jshzizon.tistory.com/entry/C-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%B6%94%EC%83%81-%ED%95%A8%EC%88%98-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4-Abstract-Mehtod-Abstract-Class)

 - Abstract Method는 몸체가 없는 함수를 의미한다. Abstract Method 사용시 몸체 앞에 반드시 abstract 키워드를 붙여야 

  하며, 이러한 Abstract Method를 1개라도 포함하고 있는 Class 또한 반드시 abstract 키워드를 붙여 Abstract Class임을

  명시해야 한다.

 - Abstract으로 선언된 Abstract Method는 상속 받은 하위 Class에서 반드시 Override 키워드를 통해 오버라이딩 해준다. 



[ 섹션 8. Scene ]

 Scene Manager #1

Scene과 관련된 enum을 추가한 Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum Scene{
        Unknown,
        Login,
        Lobby,
        Game,
    }

    public enum UIEvent
    {
        Click,
        Drag,
    }

    public enum MouseEvent
    {
        Press,
        Click,
    }

    public enum CameraMode
    {
        QuaterView, 
    }
}​

 

BaseScene 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public abstract class BaseScene : MonoBehaviour
{
    public Define.Scene SceneType {get; protected set;} = Define.Scene.Unknown;

    void Awake() {
        Init();
    }

    protected virtual void Init() {
        // Event System Component를 들고 있는 GameObject를 찾는 것
        Object obj = GameObject.FindObjectOfType(typeof(EventSystem));
        if (obj == null)
            Managers.Resource.Instantiate("UI/EventSystem").name = "@EventSystem";
    }

    public abstract void Clear();
}​

 

BaseScene을 상속받는 GameScene 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

        SceneType = Define.Scene.Game;

        Managers.UI.ShowSceneUI<UI_Inven>();
    }

    public override void Clear() {

    }
}​

 

BaseScene을 상속받는 LoginScene 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

        SceneType = Define.Scene.Login;
    }
    
    public override void Clear() {

    }
}​

 

 Scene Manager #2

SceneManagerEx 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneManagerEx
{
    public BaseScene CurrentScene { get { return GameObject.FindObjectOfType<BaseScene>(); } }

    public void LoadScene(Define.Scene type) {
        CurrentScene.Clear();
        SceneManager.LoadScene(GetSceneName(type));
    }

    string GetSceneName(Define.Scene type) {
        // C#의 Reflection 기능을 사용
        string name = System.Enum.GetName(typeof(Define.Scene), type);
        return name;
    }
}​

 

Managers에 SceneManagerEx 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    InputManager _input = new InputManager();
    ResourceManager _resource = new ResourceManager();
    SceneManagerEx _scene = new SceneManagerEx();
    UIManager _ui = new UIManager();

    public static InputManager Input { get { return Instance._input; } }
    public static ResourceManager Resource { get { return Instance._resource; } }
    public static SceneManagerEx Scene {get { return Instance._scene; }}
    public static UIManager UI {  get { return Instance._ui; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();
        }
    }
}​

 

LoginScene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; 

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

        SceneType = Define.Scene.Login;
    }

    private void Update() {
        if (Input.GetKeyDown(KeyCode.Q)){
            Managers.Scene.LoadScene(Define.Scene.Game);
        }
    }
    
    public override void Clear() {
        Debug.Log("LoginScene Clear!");
    }
}​

 

+ 추가 검색 (https://rucira-tte.tistory.com/115)

 - Find("~~")는 Object의 이름을 통해 찾는 함수, FindObjectOfType< ~~>()는 Script 이름을 통해 찾는 함수, 

   FindGameObjectWithTag("~~")는 Tag를 통해 찾는 함수이다.


 
[ 섹션 9. Sound ]

 Sound Manager #1

  - Sound를 위해서는 Sound를 재생하기 위한 Player, Sound를 위한 음원, 이를 듣는 관객 총 3가지가 필요하다.

  - 차례대로 AudioSource, AudioClip, AudioListener가 위를 담당한다.

 

 Sound Manager #2

Sound와 관련된 enum을 추가한 Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum Scene{
        Unknown,
        Login,
        Lobby,
        Game,
    }
    
    public enum Sound
    {
        Bgm,
        Effect,
        MaxCount,
    }

    public enum UIEvent
    {
        Click,
        Drag,
    }

    public enum MouseEvent
    {
        Press,
        Click,
    }

    public enum CameraMode
    {
        QuaterView, 
    }
}​

 

SoundManager 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SoundManager
{
    AudioSource[] _audioSources = new AudioSource[(int)Define.Sound.MaxCount];

    public void Init()
    {
        GameObject root = GameObject.Find("@Sound");
        if (root == null)
        {
            root = new GameObject { name = "@Sound" };
            Object.DontDestroyOnLoad(root);

            string[] soundNames = System.Enum.GetNames(typeof(Define.Sound));
            for (int i = 0; i < soundNames.Length - 1; i++)
            {
                GameObject go = new GameObject { name = soundNames[i] };
                _audioSources[i] = go.AddComponent<AudioSource>();
                go.transform.parent = root.transform; // UI는 Rect Transform이므로 SetParent를 사용, 일반적인 경우에는 parent를 사용
            }

            _audioSources[(int)Define.Sound.Bgm].loop = true;
        }
    }

    public void Play(string path, Define.Sound type = Define.Sound.Effect, float pitch = 1.0f)
    {
        if (path.Contains("Sounds/") == false)
            path = $"Sounds/{path}";

        if (type == Define.Sound.Bgm)
        {
            AudioClip audioClip = Managers.Resource.Load<AudioClip>(path);
            if (audioClip == null)
            {
                Debug.Log($"AudioClip Missing! {path}");
                return;
            }

            AudioSource audioSource = _audioSources[(int)Define.Sound.Bgm];
            if (audioSource.isPlaying)
                audioSource.Stop();

            audioSource.pitch = pitch;
            audioSource.clip = audioClip;
            audioSource.Play();
        }
        else
        {
            AudioClip audioClip = Managers.Resource.Load<AudioClip>(path);
            if (audioClip == null)
            {
                Debug.Log($"AudioClip Missing! {path}");
                return;
            }

            AudioSource audioSource = _audioSources[(int)Define.Sound.Effect];
            audioSource.pitch = pitch;
            audioSource.PlayOneShot(audioClip);
        }
    }
}​

 

Manager에 SoundManager 추가 및 SoundManager의 Init 함수 실행
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    InputManager _input = new InputManager();
    ResourceManager _resource = new ResourceManager();
    SceneManagerEx _scene = new SceneManagerEx();
    SoundManager _sound = new SoundManager();
    UIManager _ui = new UIManager();

    public static InputManager Input { get { return Instance._input; } }
    public static ResourceManager Resource { get { return Instance._resource; } }
    public static SceneManagerEx Scene {get { return Instance._scene; }}
    public static SoundManager Sound { get { return Instance._sound; }}
    public static UIManager UI {  get { return Instance._ui; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();

            s_instance._sound.Init();
        }
    }
}​

 

 Sound Manager #3

  - BGM 변경은 자주 발생하지 않아 자주 실행되지 않으나, Effect는 자주 실행된다. 이를 경로를 통해 불러오는 것은

    부하를 유발할 수 있으므로 Cashing을 통해 해결하고자 한다.

Cashing을 위한 SoundManager 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SoundManager
{
    AudioSource[] _audioSources = new AudioSource[(int)Define.Sound.MaxCount];
    Dictionary<string, AudioClip> _audioClips = new Dictionary<string, AudioClip>(); // Cashing을 위한 Dictionary (경로와 AudioClip을 가짐)

    public void Init()
    {
        GameObject root = GameObject.Find("@Sound");
        if (root == null)
        {
            root = new GameObject { name = "@Sound" };
            Object.DontDestroyOnLoad(root);

            string[] soundNames = System.Enum.GetNames(typeof(Define.Sound));
            for (int i = 0; i < soundNames.Length - 1; i++)
            {
                GameObject go = new GameObject { name = soundNames[i] };
                _audioSources[i] = go.AddComponent<AudioSource>();
                go.transform.parent = root.transform; // UI는 Rect Transform이므로 SetParent를 사용, 일반적인 경우에는 parent를 사용
            }

            _audioSources[(int)Define.Sound.Bgm].loop = true;
        }
    }

    public void Clear() // 메모리 낭비 방지를 위해 Scene 이동시 초기화하기 위한 함수
    {
        foreach (AudioSource audioSource in _audioSources)
        {
            audioSource.clip = null;
            audioSource.Stop();
        }
        _audioClips.Clear();
    }

    public void Play(string path, Define.Sound type = Define.Sound.Effect, float pitch = 1.0f)
    {
        if (path.Contains("Sounds/") == false)
            path = $"Sounds/{path}";

        if (type == Define.Sound.Bgm)
        {
            AudioClip audioClip = Managers.Resource.Load<AudioClip>(path);
            if (audioClip == null)
            {
                Debug.Log($"AudioClip Missing! {path}");
                return;
            }

            AudioSource audioSource = _audioSources[(int)Define.Sound.Bgm];
            if (audioSource.isPlaying)
                audioSource.Stop();

            audioSource.pitch = pitch;
            audioSource.clip = audioClip;
            audioSource.Play();        
        }
        else
        {
            AudioClip audioClip = GetOrAddAudioClip(path);
            if (audioClip == null)
            {
                Debug.Log($"AudioClip Missing! {path}");
                return;
            }

            AudioSource audioSource = _audioSources[(int)Define.Sound.Effect];
            audioSource.pitch = pitch;
            audioSource.PlayOneShot(audioClip);
        }
    }

    AudioClip GetOrAddAudioClip(string path) // Cashing을 위한 함수
    {
        AudioClip audioClip = null;
        if (_audioClips.TryGetValue(path, out audioClip) == false)
        {
            audioClip = Managers.Resource.Load<AudioClip>(path);
            _audioClips.Add(path, audioClip);
        }
        return audioClip;
    }
}​

 

+ 추가 검색

 - Dictionary는 특정 Key를 통해 데이터를 삭제하는 Remove() Method와 모든 데이터를 삭제하는 Clear() Method를 

   제공한다.

 - Dictionary는 특정 Key가 포함되어 있는지의 여부를 확인하는 ContainsKey() Method와 TryGetValue() Method를

   제공한다. 이때 out Keyword를 통해 별도의 변수를 선언하지 않아도 반환값을 사용할 수 있다.

   (둘 다 반환값은 Bool Type이며, TryGetValue() Method가 더 효율적) 

 

 Sound Manager #4

  - Audio Source Component의 Spatial Blend를 2D에서 3D로 변경할 경우 3D Sound를 지원한다.

    (해당 Object 자체가 소리의 진원지가 되는 것)

  - PlayClipAtPoint(AudioClip, Vector3) Method를 통해 특정 좌표에서 Sound를 재생시킬 수 있다.


 
[ 섹션 10. Object Pooling ]

Pool Manager #1

  - Object를 생성하거나 파괴하는 작업은 꽤나 무거운 작업으로 분류된다. Object 생성은 Memory를 새로 할당하고 

    Resource를 Load하는 등의 초기화하는 과정으로, Object 파괴는 파괴 이후에 발생하는 Garbage Collecting으로 인한

    Frame Drop이 발생할 수 있다.

  - 이러한 문제를 해결하기 위해 사용되는 기법이 Object Pooling이다.

  - Pooling할 Object를 담은 Object Pool을 구성한 뒤 외부에서 해당 Object가 필요하면 Object Pool에서 꺼내 사용한다.

  - Object Pool에서 꺼낸 Object의 사용이 끝나면 Object를 Pool에 돌려준다.

  - 만약 Object Pool에서 Object를 가져오려고 할 때, 모든 Object가 이미 사용중이라면 새로운 Object를 생성한다.

 

+ 추가 검색 (https://wergia.tistory.com/203)

 

 Pool Manager #2

  - Poolable Script를 Component로 들고 있다면 Memory Pooling 대상이 된다.

 Poolable 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Poolable : MonoBehaviour
{
    // Poolable Script를 Component로 들고 있다면 Memory Pooling 대상이 된다.
    public bool IsUsing; // 현재 Pooling이 된 상태인지
}​

 

PoolManager 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolManager
{
    #region Pool
    class Pool{
        public GameObject Original {get; private set;}
        public Transform Root {get; set;}

        Stack<Poolable> _poolStack = new Stack<Poolable>();

        public void Init(GameObject original, int count = 5){
            Original = original
            Root = new GameObject().transform;
            Root.name = $"{original.name}_Root";

            for (int i = 0; i < count; i++)
                Push(Create());
        }

        Poolable Create() {
            GameObject go = object.Instantiate<GameObject>(Original);
            go.name = Original.name;
            return go.GetOrAddComponent<Pollable>();
        }

        public void Push(Poolable poolable) {
            if (poolable == null)
                return;
            
            poolable.transform.parent = Root;
            poolable.gamoObject.SetActive(false);
            poolable.IsUsing = false;

            _poolStack.Push(poolable);
        }

        public Poolable Pop(Transform parent) {
            Poolable poolable;

            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop();
            else
                poolable = Create();

            poolable.gameObject.SetActive(true);
            poolable.transform.parent = parent;
            poolable.IsUsing = true;

            return poolable;
        }
    }
    #endregion

    // PoolManager는 여러개의 Pool을 가지며 각각의 Pool들은 이름을 통해 관리를 할 것이다.
    Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();

    Transform _root;
    // Pool들의 Root를 만드는 것 (대기실 역할)
    public void Init() {
        if (_root == null) {
            _root = new GameObject { name = "@Pool_Root" }.transform;
            object.DontDestroyOnLoad(_root);
        }
    }

    public void CreatePool(GameObject original, int count = 5) {
        Pool pool = new Pool();
        pool.Init(original, count);
        pool.Root.parent = _root;

        _pool.Add(original.name, pool);
    }

    // Pool에 Object를 집어넣는 것
    public void Push(Poolable poolable) {
        string name = poolable.gameObject.name;
        if(_pool.ContainsKey(name) == false) {
            GameObject.Destroy(poolable.gameObject);
            return;
        }

        _pool[name].Push(poolable);
    }

    public Poolable Pop(GameObject original, Transform parent = null) {
        if (_pool.ContainsKey(original.name) == false)
            CreatePool(original);
        return _pool[original.name].Pop(parent);
    }

    public GameObject GetOriginal(string name) {
        if (_pool.ContainsKey(name) == false)
            return null;
        return _pool[name].Original;
    }

    public void Clear() {
        foreach (Transform child in _root)
            GameObject.Destroy(child.gameObject);
        
        _pool.Clear();
    }
}​

 

Manager에 PoolManager 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    InputManager _input = new InputManager();
    PoolManager _pool = new PoolManager();
    ResourceManager _resource = new ResourceManager();
    SceneManagerEx _scene = new SceneManagerEx();
    SoundManager _sound = new SoundManager();
    UIManager _ui = new UIManager();

    public static InputManager Input { get { return Instance._input; } }
    public static PoolManager Pool { get { return Instance._pool; } }
    public static ResourceManager Resource { get { return Instance._resource; } }
    public static SceneManagerEx Scene {get { return Instance._scene; }}
    public static SoundManager Sound { get { return Instance._sound; }}
    public static UIManager UI {  get { return Instance._ui; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();

            s_instance._sound.Init();
        }
    }

    public static void Clear()
    {
        Input.Clear();
        Sound.Clear();
        Scene.Clear();
        UI.Clear();
    }
}​

 

+ 추가 검색  (https://crazykim2.tistory.com/540)

 - C#은 #region과 #endregion을 통해 코드를 정리할 수 있다.

 - #region과 #endregion 옆에 Comment를 붙여 부가 설명을 추가할 수 있다.

 

+ 추가 정리

 Pool Manager #3

PoolManager를 통한 ResourceManager 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ResourceManager
{
    public T Load<T>(string path) where T : Object
    {
        if(typeof(T) == typeof(GameObject)) { // 만약 같다면 Prefab일 확률이 높다.
            string name = path;
            int index = name.LastIndexOf('/');
            if (index >= 0)
                name = name.Substring(index + 1);

            GameObject go = Managers.Pool.GetOriginal(name);
            if (go != null)
                return go as T;
        }

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

        if(original.GetComponent<Poolable>() != null) // 만약 해당 GameObject가 Pooling 대상이라면 Pool에서 꺼내온다.   
            return Managers.Pool.Pop(original, parent).gameObject;    

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

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

        Poolable poolable = go.GetComponent<Poolable>();
        if (poolable != null) { // 만약 해당 GameObject가 Pooling 대상이라면 다시 Pool에 집어넣는다.
            Managers.Pool.Push(poolable);
            return;
        }

        Object.Destroy(go);
    }
}

 

DontDestroyOnLoad 해제를 위한 코드를 추가한 PoolManager 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PoolManager
{
    #region Pool
    class Pool{
        public GameObject Original {get; private set;} // Pool 안에 생성할 GameObject
        public Transform Root {get; set;} // Pool의 Root

        Stack<Poolable> _poolStack = new Stack<Poolable>();

        public void Init(GameObject original, int count = 5){
            Original = original;
            Root = new GameObject().transform;
            Root.name = $"{original.name}_Root";

            for (int i = 0; i < count; i++)
                Push(Create());
        }

        Poolable Create() {
            GameObject go = Object.Instantiate<GameObject>(Original); // GameObject를 동적으로 생성
            go.name = Original.name;
            return go.GetOrAddComponent<Poolable>();
        }

        public void Push(Poolable poolable) {
            if (poolable == null)
                return;
            
            poolable.transform.parent = Root;
            poolable.gameObject.SetActive(false);
            poolable.IsUsing = false;

            _poolStack.Push(poolable);
        }

        public Poolable Pop(Transform parent) {
            Poolable poolable;

            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop();
            else
                poolable = Create();

            poolable.gameObject.SetActive(true);

            // DontDestroyOnLoad 해제 용도
            if (parent == null)
                poolable.transform.parent = Managers.Scene.CurrentScene.transform;

            poolable.transform.parent = parent;
            poolable.IsUsing = true;

            return poolable;
        }
    }
    #endregion

    // PoolManager는 여러개의 Pool을 가지며 각각의 Pool들은 이름을 통해 관리를 할 것이다.
    Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();

    Transform _root;
    // Pool들의 Root를 만드는 것 (대기실 역할)
    public void Init() {
        if (_root == null) {
            _root = new GameObject { name = "@Pool_Root" }.transform;
            Object.DontDestroyOnLoad(_root);
        }
    }

    public void CreatePool(GameObject original, int count = 5) {
        Pool pool = new Pool();
        pool.Init(original, count);
        pool.Root.parent = _root;

        _pool.Add(original.name, pool);
    }

    // Pool에 Object를 집어넣는 것
    public void Push(Poolable poolable) {
        string name = poolable.gameObject.name;
        if(_pool.ContainsKey(name) == false) {
            GameObject.Destroy(poolable.gameObject);
            return;
        }

        _pool[name].Push(poolable);
    }

    public Poolable Pop(GameObject original, Transform parent = null) {
        if (_pool.ContainsKey(original.name) == false)
            CreatePool(original);
        return _pool[original.name].Pop(parent);
    }

    public GameObject GetOriginal(string name) {
        if (_pool.ContainsKey(name) == false)
            return null;
        return _pool[name].Original;
    }

    public void Clear() {
        foreach (Transform child in _root)
            GameObject.Destroy(child.gameObject);
        
        _pool.Clear();
    }
}​

 


 
[ 섹션 11. Coroutine ]

Coroutine #1

  - Coroutine을 통해 함수의 상태를 저장/복원할 수 있다. 따라서 원하는 타이밍에 함수를 잠시 중단/복원이 가능하다.

  - Coroutine을 통해 시간 관리가 가능하다. (Ex : 몇 초 후 실행)

 

+ 추가 검색  (https://codeposting.tistory.com/entry/Unity-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EC%A7%80-Coroutine-%EC%9D%B4%EC%9C%A0-%EC%B5%9C%EC%A0%81%ED%99%94)

 - Coroutine이란 코드 내에서 구문 실행 도중에 처리를 대기시키거나 순차 처리에 함수를 병렬로 동시에 처리할 수 있도록

   만들어 준다. (Thread와는 다른 개념)

 - Coroutine은 IEnumerator 형태의 데이터를 반환하는 함수이며, yield Keyword를 통해 return을 필수적으로 해야한다.

 

 Coroutine #2

Coroutine 예제
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CoroutineEx : MonoBehaviour
{
    Coroutine co;
    
    void Awake()
    {
    	co = StartCoroutine("ExplodeAfterSeconds", 4.0f);
        StartCoroutine("StopExplode", 2.0f);
    }
    
    IEnumerator ExplodeAfterSeconds(float seconds)
    {
    	Debug.Log("Explode Enter");
        yield return new WaitForSeconds(seconds);
        Debug.Log("Explode Execute!");
    }
    
    IEnumerator StopExplode(float seconds)
    {
    	Debug.Log("Stop Enter");
        yield return new WaitForSeconds(seconds);
        Debug.Log("Stop Execute!");
        if (co != null)
        {
            Stopcoroutine(co);
            co = null;
        }
    }
}

 
[ 섹션 12. Data ]

Data Manager #1

  - 보통 Assets/Resources/Data 경로에 Data 파일들을 저장한다.

  - json 파일은 Unity에서 [ 오른쪽 마우스 ] - [ Create ] 를 통해 만들 수 없고 아래와 같이 만든다.

Resources 폴더 선택 후 [ 오른쪽 마우스 ] - [ Show In Explorer ] -> Resources/Data 경로에 [ 오른쪽 마우스 ] - [ 새로 만들기 ] - [ 텍스트 문서 ]  -> 확장자를 txt에서 json으로 수정

 

  - json에서 []는 리스트를, {}는 구조체를 나타낸다.

  - json 파일의 데이터를 저장할 Class는 반드시 [Serializable]을 붙여줘야 하며, 또한 해당 Class의 변수 역시 public 또는

    [SerializeField]를 붙여줘야 한다. 이때 Class의 변수명이 json 파일의 구조체 안 변수명과 같아야 한다. 

Stat과 관련된 데이터를 저장하는 StatData를 json 파일로 생성
{
    "stats": [
        {
            "level": "1",
            "hp": "100",
            "attack": "10"
        },
        {
            "level": "2",
            "hp": "150",
            "attack": "15"
        },
        {
            "level": "3",
            "hp": "200",
            "attack": "20"
        }
    ]
}​

 

DataManager 생성
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class Stat {
    public int level;
    public int hp;
    public int attack;
}

[Serializable]
public class StatData {
    public List<Stat> stats = new List<Stat>();
}

public class DataManager
{
    public void Init() {
        // StatData.json 파일을 불러오기 위한 것
        TextAsset textAsset = Managers.Resource.Load<TextAsset>($"Data/StatData");

        // Unity에서 제공하는 json Parsing
        StatData data = JsonUtility.FromJson<StatData>(textAsset.text);
    }
}​

 

Manager에 DataManager 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    DataManager _data = new DataManager();
    InputManager _input = new InputManager();
    PoolManager _pool = new PoolManager();
    ResourceManager _resource = new ResourceManager();
    SceneManagerEx _scene = new SceneManagerEx();
    SoundManager _sound = new SoundManager();
    UIManager _ui = new UIManager();

    public static DataManager Data { get { return Instance._data; }}
    public static InputManager Input { get { return Instance._input; } }
    public static PoolManager Pool { get { return Instance._pool; } }
    public static ResourceManager Resource { get { return Instance._resource; } }
    public static SceneManagerEx Scene {get { return Instance._scene; }}
    public static SoundManager Sound { get { return Instance._sound; }}
    public static UIManager UI {  get { return Instance._ui; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();

            s_instance._data.Init();
            s_instance._pool.Init();
            s_instance._sound.Init();
        }
    }

    public static void Clear()
    {
        Input.Clear();
        Sound.Clear();
        Scene.Clear();
        UI.Clear();
        Pool.Clear();
    }
}​

 

 Data Manager #2

DataManager 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ILoader<Key, Value> 
{
    Dictionary<Key, Value> MakeDict();
}

public class DataManager
{
    public Dictionary<int, Stat> StatDict { get; private set; } = new Dictionary<int, Stat>();

    public void Init() 
    {
        StatDict = LoadJson<StatData, int, Stat>("StatData").MakeDict();
    }

    Loader LoadJson<Loader, Key, Value>(string path) where Loader : ILoader<Key, Value>
    {
        // json 파일을 불러오기 위한 것
        TextAsset textAsset = Managers.Resource.Load<TextAsset>($"Data/{path}");

        // Unity에서 제공하는 json Parsing
        return JsonUtility.FromJson<Loader>(textAsset.text);
    }
}​

 

Data.Contents 생성
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#region Stat

[Serializable]
public class Stat
{
    public int level;
    public int hp;
    public int attack;
}

[Serializable]
public class StatData : ILoader<int, Stat>
{
    public List<Stat> stats = new List<Stat>();

    public Dictionary<int, Stat> MakeDict()
    {
        Dictionary<int, Stat> dict = new Dictionary<int, Stat>();
        foreach(Stat stat in stats)
            dict.Add(stat.level, stat);
        return dict;
    }
}

#endregion​

 

+ 추가 검색  (https://seroi-programming.tistory.com/entry/C-%EC%96%B8%EC%96%B4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95)

 - Interface는 객체 지향 프로그래밍의 핵심 개념 중 하나이다.

 - Interface는 Class와 비슷하지만, Class와 달리 구현되지 않은 Method와 Property를 가질 수 있다.

 - Interface는 접근 제한 한정자를 사용할 수 없고, 모든 것이 public으로 선언된다.

 - Interface는 Class가 따라야하는 규약을 정의하는데 사용되며, Class는 Interface에 정의된 모든 멤버를 구현해야한다.

 - Interface를 구현하는 Class는 Class 이름 뒤에 콜론(:)을 붙이고 구현하고자 하는 Interface의 이름을 지정한다.

 - Interface는 Instance를 만들 수 없지만, Interface를 상속받는 Class의 Instance를 만드는 것은 가능하다.

 - Class는 여러개의 Interface를 상속받을 수 있다.

 

 

 

 

 

[ 섹션 0. 개론 ]

# 환경설정

  - [ Window ] - [ Layouts ] 에서 2 by 3 또는 Tall 배치가 가장 유용하다.
  - 단축키
   ~> Ctrl + Shift + N : Create Empty
   ~> Ctrl + P : Play
   ~> Ctrl + Shift + C : Console 창


 
[ 섹션 1. 유니티 기초 ]

# 에디터 입문

  - 3D 템플릿
   ~> Scene에서 오른쪽 마우스를 누른 상태로 WASD와 QE를 통해 상하좌우로 움직일 수 있다.
   ~> Scene에서 Alt를 누른 상태로 오른쪽 마우스를 누른채 마우스를 움직일 경우 화면을 조작할 수 있다.
   ~> Scene에서 Alt를 누른 상태로 마우스 휠을 통해 가상 카메라가 이동하는 속도를 조절할 수 있다.
   ~> 객체 선택 후 Q, W, E, R을 통해 Tool 변경이 가능하다.
   ~> Main Camera 선택 후 Ctrl + Shift + F를 누르면 Main Camera가 현재 Scene에서 바라보고 있는 방향과
        똑같은 방향을 바라보도록 설정이 가능하다.


Component 패턴

  - Unity로 게임 개발시 디자인 패턴이 중요한데 그 중 하나가 Component 패턴이다.
  - Component라는 부품을 만들고 해당 부품들을 관리하는 것이 중요하다.


Manager 만들기

  - Component로 사용될 C# 스크립트와 일반적인 C# 스크립트를 구분하는것이 중요하다. (Tip)
  - F12를 통해 C# 코드의 Class 세부 내용을 확인할 수 있다.
  - Manager 구현 방법
   ~> Manager C# 스크립트는 Component로 사용될 C# 스크립트가 아니므로 MonoBehaviour를 상속받을 필요가 없다.
        (상속 받지 않을 경우 Unity 기본 함수인 Start(), Update()와 같은 것들이 자동으로 호출되지 않는 문제 발생)
     => 즉, MonoBehaviour를 상속받지 않고 Hierarchy에 존재하지 않게 하는 것
   ~> Scene에 배치하는 Object는 꼭 실체가 있는 사물일 필요가 없기 때문에 (Tip) MonoBehaviour를 상속받은 Manager
        C# 스크립트를 실체가 없는 사물의 Component로 사용한다.
     => 즉, Singleton Class가 여느 Unity C# 스크립트처럼 MonoBehaviour를 상속받아 Hierarchy에 존재하게 하는 것


#Singleton 패턴

  - 특정 Class에 Instance가 1개만 있기를 원할때 자주 사용한다.
   ~> Static을 통해 유일성을 보장, GetInstance()를 통해 가져올 수 있다.

Managers Class 구현 방법 1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers Instance; // 유일성 보장
    public static Managers GetInstance() { return Instance; } // 유일한 매니저를 가져온다.

    void Start()
    {
        // 초기화
        Instance = this;
        DontDestroyOnLoad(this.gameObject);
    }
}​

 

Managers Class 구현 방법 2
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers Instance; // 유일성 보장
    public static Managers GetInstance() { return Instance; } // 유일한 매니저를 가져온다.

    void Start()
    {
        // 초기화
        GameObject go = GameObject.Find("Managers");
        Instance = go.GetComponent<Managers>();
        DontDestroyOnLoad(go);
    }
}​

 

Managers Class 구현 방법 3 (Property의 get을 이용)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    public static Managers Instance { get { return s_instance; } } // 유일한 매니저를 가져온다

    void Start()
    {
        Init();
    }

    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>();
        }
    }
}​

 

PlayerController Class 구현 방법 1 ( Managers Class 구현 방법 1, 2일때)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        Managers mg = Managers.GetInstance();
    }
}​

 

PlayerController Class 구현 방법 2 ( Managers Class 구현 방법 3일때)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        Managers mg = Managers.Instance;
    }
}​


+ 추가 검색 (https://art-life.tistory.com/130)
 - Singleton의 역할
  1. 게임 시스템 전체를 관장하는 스크립트 (단일 시스템 자원 관리 차원)
  2. 게임 시스템상 전역 변수의 역할을 하는 스크립트
  3. Scene Load시 데이터가 파괴되지 않고 유지
  4. 여러 Object가 접근을 해야하는 스크립트의 역할
  5. 단 1개의 객체만 존재

 

+ 추가 검색 (https://developer-talk.tistory.com/39)
 - Property란?

  ~> 정보 은닉을 위해 Private로 선언하더라도 Property는 get, set을 통해 접근이 가능하다.

  ~> 굳이 사용하는 이유는 변수의 값을 변경하거나 가져올때, 조건을 걸어서 변수의 접근을 제어할 수 있다.

  ~> Property는 Field와 동일한 이름으로 하되 첫글자는 대문자로 하는 것이 일반적이다.


 
[ 섹션 2. Transform ]

# 플레이어 설정


  - Asset Store는 [ Window ] - [ Asset Store ] 에서 사용이 가능하다.
  - Player라는 game Object에 Transform Componet와 직접 만들어 추가한 PlayerController Component가 있을때
    PlayerController Component에서 Transform Component로 접근하고자 할 경우 transform.position과 같이 바로
    접근할 수 있다.
   ~> 마찬가지로 PlayerController Component에서 부모인 gameObject에 접근하고자 할 경우 transform.gameObject와
        같이 바로 접근할 수 있다.

 

간단한 Player 움직임 구현 방법
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKey(KeyCode.W))
            transform.position += new Vector3(0.0f, 0.0f, 1.0f);
        if (Input.GetKey(KeyCode.S))
            transform.position -= new Vector3(0.0f, 0.0f, 1.0f);
        if (Input.GetKey(KeyCode.A))
            transform.position -= new Vector3(1.0f, 0.0f, 0.0f);
        if (Input.GetKey(KeyCode.D))
            transform.position += new Vector3(1.0f, 0.0f, 0.0f);
    }
}​

 

# Position

  - Position에서의 Rotation은 해당 축만을 고정한채 Degree 단위로 회전하는 것이다.
   ~> 예로 Transform Component의 Rotation y에 90을 입력하면 y축을 고정한채 90° 만큼 회전한다.
  - 이전 Frame과 현재 Frame의 시간 차이를 구해 동작을 하도록 만드는 것이 중요하다.
   ~> 이는 Time.deltaTime을 이용한다. (너무 느릴 경우 거리 = 시간 * 속도 식을 이용하여 속도에 해당하는 상수 값을 곱해
        해결이 가능하다.)

 

Time.deltaTime과 상수 값을 이용한 Player 움직임 구현 방법 1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    float speed = 10.0f;

    void Update()
    {
        if (Input.GetKey(KeyCode.W))
            transform.position += new Vector3(0.0f, 0.0f, 1.0f) * Time.deltaTime * speed;
        if (Input.GetKey(KeyCode.S))
            transform.position -= new Vector3(0.0f, 0.0f, 1.0f) * Time.deltaTime * speed;
        if (Input.GetKey(KeyCode.A))
            transform.position -= new Vector3(1.0f, 0.0f, 0.0f) * Time.deltaTime * speed;
        if (Input.GetKey(KeyCode.D))
            transform.position += new Vector3(1.0f, 0.0f, 0.0f) * Time.deltaTime * speed;
    }
}​

 

Time.deltaTime과 상수 값을 이용한 Player 움직임 구현 방법 2
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    float speed = 10.0f;

    void Update()
    {
        if (Input.GetKey(KeyCode.W))
            transform.position += Vector3.forward * Time.deltaTime * speed;
        if (Input.GetKey(KeyCode.S))
            transform.position += Vector3.back * Time.deltaTime * speed;
        if (Input.GetKey(KeyCode.A))
            transform.position += Vector3.left * Time.deltaTime * speed;
        if (Input.GetKey(KeyCode.D))
            transform.position += Vector3.right * Time.deltaTime * speed;
    }
}​

 

  - Unity는 World 좌표와 Local 좌표가 존재한다.
   ~> World 좌표는 Unity의 가상 공간의 기준 좌표를, Local 좌표는 Object 기준 시점의 좌표를 의미한다.
   ~> Player Object 기준의 Local 좌표를 보고 싶은 경우 Player Object를 누른뒤 x를 눌러 확인할 수 있다.
  - 위의 코드는 Player가 바라보는 방향을 forward 방향으로 인식하지 않는 문제점이 발생하는데 이는 위의 코드가 World
    좌표를 기준으로 돌아가고 있기 때문이다. 이는 TransformDirection() 함수를 통해 해결할 수 있다.
    ~> 참고로 World 좌표에서 Local 좌표는 InverseTransformDirection()

TransformDirection 함수를 통해 Local 좌표 기준의 Player 움직임 구현 방법
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    float speed = 10.0f;

    void Update()
    {
        if (Input.GetKey(KeyCode.W))
            transform.position += transform.TransformDirection(Vector3.forward * Time.deltaTime * speed);
        if (Input.GetKey(KeyCode.S))
            transform.position += transform.TransformDirection(Vector3.back * Time.deltaTime * speed);
        if (Input.GetKey(KeyCode.A))
            transform.position += transform.TransformDirection(Vector3.left * Time.deltaTime * speed);
        if (Input.GetKey(KeyCode.D))
            transform.position += transform.TransformDirection(Vector3.right * Time.deltaTime * speed);
    }
}​

 

Translate 함수를 통해 Local 좌표 기준의 Player 움직임 구현 방법
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    float speed = 10.0f;

    void Update()
    {
        if (Input.GetKey(KeyCode.W))
            transform.Translate(Vector3.forward * Time.deltaTime * speed);
        if (Input.GetKey(KeyCode.S))
            transform.Translate(Vector3.back * Time.deltaTime * speed);
        if (Input.GetKey(KeyCode.A))
            transform.Translate(Vector3.left * Time.deltaTime * speed);
        if (Input.GetKey(KeyCode.D))
            transform.Translate(Vector3.right * Time.deltaTime * speed);
    }
}​


# Vector

  - Vector는 게임에서 2가지 용도로 사용된다. 이는 위치 벡터와 방향 벡터이다.
   ~> 방향 벡터를 통해 얻을 수 있는 정보는 2가지가 존재한다. 이는 거리(크기)와 실제 방향이다.
    => 거리(크기)는 magnitude 기능을 통해 추출이 가능하다.
    => 실제 방향은 normalized 기능을 통해 추출이 가능하다.
         (단위 벡터인 normalized는 방향의 크기는 무시한채 방향에 대한 정보만을 추출할 수 있다.)  


# Rotation

  - Euler Angle은 x, y, z 3개의 축을 기준으로 0~360° 만큼 회전시키는 우리에게 친숙한 좌표이나 Gimbal Lock이라는 
    문제를 가진다. (너무 어려워서 이해할 필요는 X)
   ~> 이러한 문제를 해결하기 위해 고안된 것이 Quaternion이다. Unity는 모든 Rotation에는 내부적으로 Quaternion이
        사용되며 우리는 이를 친숙한 Euler Angle로 바꿔 사용한다.

 

rotation 구현 방법들
float _yAngle += Time.deltaTime * 100.0f;

void Update()
{
    // 오류 발생
    transform.rotation = new Vector3(0.0f, _yAngle, 0.0f);

    // 정상적인 동작
    transform.eulerAngles = new Vector3(0.0f, _yAngle, 0.0f); // 덧셈, 뺄셈 지양 (즉, 절대 회전값)
    transform.Rotate(new Vector3(0.0f, _yAngle, 0.0f)); // += delta
    transform.rotation = Quaternion.Euler(new Vector3(0.0f, _yAngle, 0.0f));
}

 

LookRotation, Slerp 함수를 통한 Player 움직임 구현 방법
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    float speed = 10.0f;

    void Update()
    {
        if (Input.GetKey(KeyCode.W))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * speed);
        }
        if (Input.GetKey(KeyCode.S))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * speed);
        }
        if (Input.GetKey(KeyCode.A))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * speed);
        }
        if (Input.GetKey(KeyCode.D))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * speed);
        }
    }
}

 

-  위의 코드에서 LookLotation()는 특정 방향을 바라보도록 만들어주는 함수이며,
     Slerp()는 일정 시간을 두고 목표 방향으로 회전하도록 만들어주는 함수이다. (즉, 회전이 부드럽게 처리되도록)
  -  Translate() 함수의 벡터를 전부 forward로 바꾼 이유는 Player가 고개를 특정 방향을 바라보도록 만들었기 때문이다.
     (특정 방향을 바라보도록 만들었기 때문에 바라보는 방향이 항상 forward이기 때문)  

 

# Input Manager

  -  Action을 담당하는 Input Manager를 통해 코드 과부화를 줄이는 것이 중요하다.
  - Delegate (함수 포인터)
   ~> Action (반환 형식 X)
   ~> Func (반환 형식 O)

InputManager Class에서 KeyAction 선언, Key 입력이 감지되면 KeyAction을 Invoke
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InputManager
{
    public Action KeyAction = null;

    // 체크하는 부분이 유일해짐
    public void OnUpdate()
    {
        if (Input.anyKey == false)
            return;

        if (KeyAction != null)
            KeyAction.Invoke();
    }
}​

 

Manager Class의 Update() 함수에서 InputManager Class OnUpdate() 함수를 실행
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    InputManager _input = new InputManager();
    public static InputManager Input { get { return Instance._input; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();
        }
    }
}​

 

PlayerController Class의 OnKeyboard() 함수를 KeyAction으로 구독 신청
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        Managers.Input.KeyAction -= OnKeyboard; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.KeyAction += OnKeyboard;
    } 

    void OnKeyboard()
    {
        if (Input.GetKey(KeyCode.W))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * 0.5f);
        }
        if (Input.GetKey(KeyCode.S))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * 0.5f);
        }
        if (Input.GetKey(KeyCode.A))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * 0.5f);
        }
        if (Input.GetKey(KeyCode.D))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * 0.5f);
        }
    }
}​


+ 추가 검색
 - Action 개념을 모른 상태로 강의를 시청하여 이해가 어려웠는데 아래 유튜브 영상을 통해 쉽게 이해할 수 있었다.
( https://www.youtube.com/watch?v=dUuQ_q9H2_g&t=427s&ab_channel=%EA%B3%A0%EB%9D%BC%EB%8B%88TV-%EA%B2%8C%EC%9E%84%EA%B0%9C%EB%B0%9C%EC%B1%84%EB%84%90 )
 

Delegate 개념 예시

  - Delegate 사용시 코드가 너무 길어서 이를 간편화해서 만든 것이 Action과 Func이다.

Action 개념 예시

  -  System.Action을 통해 사용 가능하며 System.을 생략하려면 using System을 선언한다.
  - += 를 통해 함수를 등록(구독 신청)할 수 있다.
  - 다른 스크립트들에서 Action 대리자를 구독시 Action 대리자를 호출할 경우 해당 대리자를 구독한 모든 스크립트의 함수
    는 자동으로 작동하게 된다.
  -  Action 대리자를 구독한 함수 실행을 위해서는 Invoke() 함수를 사용한다.
    (?.Invoke()는 nullable 문법으로 null 확인을 자동으로 수행하여 null이 아닌 경우에만 Invoke를 실행한다.)


 
[ 섹션 3. Prefab ]

# Prefab

  - Prefab만 수정해도 인스턴스화된 객체들 모두 수정이 반영된다. (오버라이딩시 반영 X)
  - Nested Prefab은 다른 Prefab 내에 Prefab 인스턴스를 포함할 수 있는 것이다.
   ~> 즉, 중첩된 Prefab (예를 들어 Tank, Player Prefab을 합쳐 Player In Tank Prefab 생성)
  - Prefab Variant는 다른 Prefab의 Property를 상속받는 것이다.
   ~> 예를 들어 Tank Prefab의 Property를 상속받아 Fast Tank Prefab을 생성
 
+ 추가 검색 (https://notyu.tistory.com/35)
 - Prefab은 미리 만들어 놓은 게임 오브젝트, 템플릿이다.
  ~> 원하는 만큼의 Prefab 인스턴스를 생성할 수 있으며, 생성된 Prefab 인스턴스는 서로 독립적으로 작동하고,
        Prefab 인스턴스의 Property를 수정해도 다른 Prefab 인스턴스에는 영향을 주지 않는다. 
 

# Resource Manager

  - Prefab은 Instantiate() 함수를 통해 인스턴스를 생성, Destroy() 함수를 통해 인스턴스를 삭제한다.
  - Prefab 뿐만이 아닌 온갖 Art, Sound Resource는 Assets 폴더 및 Resources 폴더에 일반적으로 넣어둔다.
   ~> 규모가 큰 게임에서 Prefab을  Tool로 하나하나 연결하는 것은 힘들기 때문에 코드를 통해 Prefab을 불러온다.

코드를 통해 Prefab을 불러오는 코드 구현
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PrefabTest : MonoBehaviour
{
    GameObject prefab;
    GameObject tank;

    void Start()
    {
        prefab = Resources.Load<GameObject>("Prefabs/Tank");
        tank = Instantiate(prefab);

        Destroy(tank, 3.0f);
    }
}​

 

  - Prefab을 생성/삭제하는 Instantiate, Destroy 코드들이 여기저기 흩어져 있으면 추적이 굉장히 어려워지므로
    Resource Manager를 만들어 관리하는 것이 좋다.

 

Resource Manager 생성
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 prefab = Load<GameObject>($"Prefabs/{path}");
        if (prefab == null)
        {
            Debug.Log($"Failed to load prefab : {path}");
            return null;
        }

        return Object.Instantiate(prefab, parent);
    }

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

        Object.Destroy(go);
    }
}​

 

Managers에 ResourceManager 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    static Managers s_instance; // 유일성 보장
    static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 가져온다

    InputManager _input = new InputManager();
    ResourceManager _resource = new ResourceManager();

    public static InputManager Input { get { return Instance._input; } }
    public static ResourceManager Resource { get { return Instance._resource; } }

    void Start()
    {
        Init();
    }

    void Update()
    {
        _input.OnUpdate();
    }

    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>();
        }
    }
}

 

코드를 통해 Prefabs을 불러오는 코드의 변화
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PrefabTest : MonoBehaviour
{
    GameObject prefab;
    GameObject tank;

    void Start()
    {  
        tank = Managers.Resource.Instantiate("Tank");
        Managers.Resource.Destroy(tank);
    }
}​

 
[ 섹션 4. Collision ]

#Collider

  - RigidBody Component를 통해 물리 적용을 받는다.
  - Collider Component를 통해 물리적 충돌을 위한 게임 오브젝트의 모양을 정의한다.
  - RigidBody Component에서의 is Kinematic은 외부에서 가해지는 물리적 힘에 반응하지 않도록 하는 기능이다.
    (RigidBody Component를 추가하는 이유가 물리 적용을 받기 위해서도 있지만 충돌 판정을 위해서도 있기 때문에)
 


#Collision

  - Collision 발생 조건
   ~> Player 또는 상대방/물체에게 RigidBody 존재 (Is kinematic : off)
   ~> Player 또는 상대방/물체에게 Collider 존재 (Is Trigger : off)

collision에는 Player와 충돌한 상대방/물체에 대한 정보가 담긴다.

 
#Trigger

  - Trigger는 Collision과 달리 충돌 처리에 물리 연산이 필요하지 않아 물체끼리 충돌이 일어나도 관통할 수 있다.
    (스킬 피격 판정, Player 범위 도달 등에 유용)
  - Trigger 발생 조건
   ~> Player와 상대방/물체 모두 Collider 존재
   ~> Player와 상대방/물체 둘 중 하나는 Is Trigger : On
   ~> Player와 상대방/물체 둘 중 하나는 RigidBody 존재

other에는 Player와 충돌한 상대방/물체에 대한 정보가 담긴다.

 
#RayCasting #1

  - RayCasting은 다음과 같이 구현이 가능하다.

RayCasting 구현 방법
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestCollision : MonoBehaviour
{
    void Update()
    {
        Vector3 look = transform.TransformDirection(Vector3.forward); // Player가 바라보는 방향으로 Ray 발사
        Debug.DrawRay(transform.position + Vector3.up, look * 3, Color.red); // Ray를 시각적으로 표현

        RaycastHit hit;
        if (Physics.Raycast(transform.position + Vector3.up, look, out hit, 3)) // Physics.Raycast의 반환값은 Bool
        {
            Debug.Log($"Raycast {hit.collider.gameObject.name}"); // hit는 Player와 충돌한 물체
        }
    }
}​

  
#투영의 개념

  - Local / World / Viewport / Screen
   ~> Screen 좌표는 Pixel 좌표라고 할 수 있으며, View Port 좌표는 Screen 좌표와 굉장히 유사하나 이는 비율로 표시한다
        (즉, 값이 0.0과 1.0 사이)
  - World 좌표 (즉, 3D)에서 Screen 좌표 (즉, 2D)로 어떻게 왔다 갔다 할 수 있을까?
   ~> 이때 투영 개념이 적용된다. (즉, 축이 1개 사라지게 된다.) 투영시 중요한 법칙 중 하나는 비율을 지킨다는 것이다.
 


#RayCasting #2

  - 카메라를 기준으로 한 RayCasting은 다음과 같이 구현이 가능하다.

마우스로 클릭시 Ray를 Main Camera에서 클릭한 위치로 쏘기 위한 것
핑크색으로 칠한 평면이 Main Camera의 작은 평면을, 녹색 선이 Main Camera와 작은 평면 사이의 거리를 나타낸다. (아래 코드 주석 참고)

카메라를 기준으로 한 RayCasting 구현 방법 1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestCollision : MonoBehaviour
{
    void Update()
    {
        // 우리가 현재 알고 싶은건 Main Camera 작은 평면의 World 좌표
        if (Input.GetMouseButtonDown(0)) // GameScene 특정 부분을 마우스 왼쪽 버튼으로 눌렀을때
        {
            // nearClipPlane은 Main Camera와 작은 평면 사이의 거리
            Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));
            Vector3 dir = mousePos - Camera.main.transform.position; // dir는 Main Camera와 작은 평면 사이의 방향 벡터
            dir = dir.normalized; // dir는 Main Camera와 작은 평면 사이의 단위 벡터
            Debug.DrawRay(Camera.main.transform.position, dir * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

            RaycastHit hit;
            if (Physics.Raycast(Camera.main.transform.position, dir, out hit, 100.0f))
            {
                Debug.Log($"Raycast Camera {hit.collider.gameObject.name}");
            }
        }
    }
}​

 

카메라를 기준으로 한 RayCasting 구현 방법 2
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestCollision : MonoBehaviour
{
    void Update()
    {
        // 우리가 현재 알고 싶은건 Main Camera 작은 평면의 World 좌표
        if (Input.GetMouseButtonDown(0)) // GameScene 특정 부분을 마우스 왼쪽 버튼으로 눌렀을때
        {
            // nearClipPlane은 Main Camera와 작은 평면 사이의 거리
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100.0f))
            {
                Debug.Log($"Raycast Camera {hit.collider.gameObject.name}");
            }
        }
    }
}​

 
#LayerMask

  - Layer를 통해 LayerMask가 가능하다. (이는 최적화와 관련있다.)
   ~> 이를 통해 연산하고자 하는 애들만 골라서 Ray Casting을 할 수 있다.

LayerMask를 통한 선택적 Ray Casting 구현 방법
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestCollision : MonoBehaviour
{
    void Update()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

        int mask = (1 << 8) | (1 << 9); // Bit Flag를 사용한 Layer Mask (8번, 9번 Layer에 해당하는 Object만 Ray Casting

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, mask))
        {
            Debug.Log($"Raycast Camera {hit.collider.gameObject.name}");
        }
    }
}

 
+ 추가 검색
 - Bit Flag 개념을 모른 상태로 강의를 시청하여 이해가 어려웠는데 아래 글을 통해 쉽게 이해할 수 있었다. 
(https://velog.io/@gkswh4860/%EB%B9%84%ED%8A%B8-%EC%97%B0%EC%82%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-Unity-Layer-%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90)


 
[ 섹션 5. Camera ]

#Camera #1

  - Camera Component의 Culling Mask는 특정 Layer만 Camera에 찍히도록 할 수 있다.
  - Camera Component의 Target Texture는 게임 내의 CCTV와 같은 기능 구현시 유용하게 사용된다.

  - Main Camera를 Player의 자식으로 넣게될 경우 Player가 고개를 돌릴때마다 Main Camera도 같이 돌아가 시각적으로

    어지러움을 유발하는 문제가 발생한다. 이러한 문제를 아래와 같이 해결할 수 있다.

게임에서 정의하고자 하는 것들을 위한 Define Class를 만들어 CameraMode 정의
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum CameraMode
    {
        QuaterView, 
    }
}​

 

MainCamera에 CameraController 스크립트를 Component로 추가한 뒤
Tool을 통해 Inspector에서 Delta의 y값은 6, z값은 -5로 설정, Player는 Player 오브젝트를 드래그&드롭하여 연결
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField]
    Define.CameraMode _mode = Define.CameraMode.QuaterView;

    [SerializeField]
    Vector3 _delta = new Vector3(0.0f, 6.0f, -5.0f);

    [SerializeField]
    GameObject _player = null;

    // Player의 움직임이 실행된 뒤 Camera의 위치를 움직여야 떨리는 현상이 줄어든다.
    void LateUpdate() // 게임 Logic에서 LateUpdate()는 Update()보다 늦게 실행된다.
    {
        if (_mode == Define.CameraMode.QuaterView)
        {
            transform.position = _player.transform.position + _delta;
            transform.LookAt(_player.transform); // 항상 player를 쳐다보도록
        }
    }

    public void SetQuaterView(Vector3 delta)
    {
        _mode = Define.CameraMode.QuaterView;
        _delta = delta;
    }
}

 

+ 추가 검색 (https://luv-n-interest.tistory.com/352)
 - [SerializeField]를 사용하는 이유는 Inspector에서는 접근이 가능하지만 외부 스크립트에서는 접근이 불가능하도록

   하기 위함이다.

  ~> 즉, Private 변수이지만 Inspector에서 접근이 가능하도록 직렬화하는 것이다.

  ~> 이때 직렬화라는 것은 쉽게 말하자면 추상적인 데이터를 전송 가능하고 저장 가능한 형태로 바꾸는 것을 의미한다.

 

#Camera #2

  - 마우스를 통한 Player의 움직임을 구현하기 위한 코드는 아래와 같다.

우선 Define Class에 다음과 같이 MouseEvent를 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum MouseEvent
    {
        Press,
        Click,
    }

    public enum CameraMode
    {
        QuaterView, 
    }
}​

 

Input Manager를 다음과 같이 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InputManager
{
    public Action KeyAction = null;
    public Action<Define.MouseEvent> MouseAction = null;

    bool _pressed = false;

    // 체크하는 부분이 유일해짐
    public void OnUpdate()
    {
        if (Input.anyKey && KeyAction != null)
            KeyAction.Invoke();

        if (MouseAction != null)
        {
            if (Input.GetMouseButton(0))
            {
                MouseAction.Invoke(Define.MouseEvent.Press); // Press Event를 발생시켜 알려준다.
                _pressed = true;
            }
            else
            {
                if (_pressed)
                    MouseAction.Invoke(Define.MouseEvent.Click); // Click Event를 발생시켜 알려준다.
                _pressed = false;
            }
        }
    }
}​

 

PlayerController를 다음과 같이 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    bool _moveToDest = false; // 목적지까지 이동을 해야하는지의 여부
    Vector3 _destPos; // 목적지 좌표를 저장하기 위한 변수

    void Start()
    {
        Managers.Input.KeyAction -= OnKeyboard; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.KeyAction += OnKeyboard;
        Managers.Input.MouseAction -= OnMouseClicked; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.MouseAction += OnMouseClicked;
    } 

    void Update()
    {
        if (_moveToDest)
        {
            Vector3 dir = _destPos - transform.position; // 목적지까지의 방향 벡터를 알 수 있다.
            if (dir.magnitude < 0.0001f) // 만약 목적지까지 거의 도착을 완료했다면
            {
                _moveToDest = false;
            }
            else
            {
                float moveDist = Mathf.Clamp(Time.deltaTime * _speed, 0, dir.magnitude);
                transform.position += dir.normalized * moveDist;

                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
                transform.LookAt(_destPos); // Player가 목적지를 바라보면서 이동하도록
            }
        }
    }

    void OnKeyboard()
    {
        if (Input.GetKey(KeyCode.W))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * _speed);
        }
        if (Input.GetKey(KeyCode.S))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * _speed);
        }
        if (Input.GetKey(KeyCode.A))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * _speed);
        }
        if (Input.GetKey(KeyCode.D))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.5f);
            transform.Translate(Vector3.forward * Time.deltaTime * _speed);
        }

        _moveToDest = false;
    }

    void OnMouseClicked(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); // Ray를 시각적으로 표현

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f))
        {
            _destPos = hit.point; // hit의 좌표로 목적지 설정
            _moveToDest = true;
        }
    }
}​

 

#Camera #3

  - Player가 벽과 가까이 만났을때의 카메라 View를 설정하기 위한 코드는 아래와 같다.

CameraController를 다음과 같이 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField]
    Define.CameraMode _mode = Define.CameraMode.QuaterView;

    [SerializeField]
    Vector3 _delta = new Vector3(0.0f, 6.0f, -5.0f);

    [SerializeField]
    GameObject _player = null;

    // Player의 움직임이 실행된 뒤 Camera의 위치를 움직여야 떨리는 현상이 줄어든다.
    void LateUpdate() // 게임 Logic에서 LateUpdate()는 Update()보다 늦게 실행된다.
    {
        if (_mode == Define.CameraMode.QuaterView)
        {
            RaycastHit hit;
            // Player가 벽과 가까이 만났을때의 카메라 View를 설정하기 위한 것
            // 시작 위치는 Player의 위치, _delta는 카메라의 위치, 최대 거리는 _delta.magnitude
            if (Physics.Raycast(_player.transform.position, _delta, out hit, _delta.magnitude, LayerMask.GetMask("Wall"))) 
            {
                // .magnitude를 통해 방향벡터의 크기를 구한다.
                float dist = (hit.point - _player.transform.position).magnitude * 0.8f;
                transform.position = _player.transform.position + _delta.normalized * dist;
            }
            else
            {
                transform.position = _player.transform.position + _delta;
                transform.LookAt(_player.transform); // 항상 player를 쳐다보도록
            } 
        }
    }

    public void SetQuaterView(Vector3 delta)
    {
        _mode = Define.CameraMode.QuaterView;
        _delta = delta;
    }
}​

 
[ 섹션 6. Animation ]

#Animation

  - Animator는 Unity에서 제공하는 Animation System 중 하나인 Mecanim이라고 불리는 핵심 Component이다.

PlayerAnimator

PlayerController에 Animation 부분 추가
public class PlayerController : MonoBehaviour
{
	void Update()
    {
		if (_moveToDest)
        {
            Animator anim = GetComponent<Animator>();
            anim.Play("RUN");
        }
        else
        {
            Animator anim = GetComponent<Animator>();
            anim.Play("WAIT");
        }
    }
}

 

#Animation Blending

  - Animation 전환이 보다 더 자연스럽도록 만들어 주는 것이 Animation Blending이다.

 

Float형 Parameter 추가 후, Blend Tree에 Motion 추가와 Threshold 및 Parameter 설정

PlayerController에 Animation 부분 수정
public class PlayerController : MonoBehaviour
{
	float wait_run_ratio = 0;
    void Update()
    {
        if (_moveToDest)
        {
            wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime);
            Animator anim = GetComponent<Animator>();
            anim.SetFloat("wait_run_ratio", wait_run_ratio);
            anim.Play("WAIT_RUN");
        }
        else
        {
            wait_run_ratio = Mathf.Lerp(wait_run_ratio, 0, 10.0f * Time.deltaTime);
            Animator anim = GetComponent<Animator>();
            anim.SetFloat("wait_run_ratio", wait_run_ratio);
            anim.Play("WAIT_RUN");
        }
    }
}

 

#State 패턴

  - 게임 규모가 커질수록 Animation의 종류는 상당히 많아지는데 위의 방법과 같이 Animation Blending을 구현하게 되면 

    스파게티 코드가 되버린다. (수많은 변수와 If/Else문) 이때 효과적인게 바로 State 패턴이다.

State 패턴을 이용한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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
    {
        Die,
        Moving,
        Idle,
    }

    PlayerState _state = PlayerState.Idle;

    void UpdateDie()
    {

    }

    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향 벡터를 알 수 있다.
        if (dir.magnitude < 0.0001f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            _state = PlayerState.Idle;
        }
        else
        {
            float moveDist = Mathf.Clamp(Time.deltaTime * _speed, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
            transform.LookAt(_destPos); // Player가 목적지를 바라보면서 이동하도록
        }

        // 애니매이션
        wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime);
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("wait_run_ratio", wait_run_ratio);
        anim.Play("WAIT_RUN");
    }

    void UpdateIdle()
    {
        // 애니매이션
        wait_run_ratio = Mathf.Lerp(wait_run_ratio, 0, 10.0f * Time.deltaTime);
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("wait_run_ratio", wait_run_ratio);
        anim.Play("WAIT_RUN");
    }

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

    void OnMouseClicked(Define.MouseEvent evt)
    {
        if (_state == PlayerState.Die)
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

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

 

  -  다음과 같이 수정한 PlayerController는 2가지 Animation을 동시에 실행하지 못한다는 단점이 존재한다.

     (예를 들면 움직이면서 Skill 사용시 움직이는 Animation과 Skill 사용 Animation을 동시에 실행하지 못함)

 

#State Machine #1

 

  -   위와 같이 Make Transition을 통해 State 전환 조건을 만들 수 있다.

 

  -   Make Transition을 통한 State 전환시 좋은 점은 Blending을 코드상에서가 아닌 Tool을 통해 쉽게 조절할 수 있다.

  -   Has Exit Time이 체크된 경우 Animation을 한번 실행한 뒤 빠져나올 수 있다.

      (즉, 체크 해제된 경우 해당 Animation을 무한반복한다.)

  -  Fixed Duration이 체크된 경우 Exit Time에 입력한 값은 절대 시간을, Fixed Duration이 체크 해제된 경우 Exit Time에

     입력된 값은 %를 의미한다.

     (즉, 절대시간인 경우 0.6875 후 Animation을 넘어가는 것, %인 경우 해당 Animation의 68%정도가 실행된 경우 넘어

      가는 것)

 

State Machine #2

  - Parameter를 추가하여 Parameter를 통해 State 전환이 가능하다.

Float형 Parameter 추가 후,  Transition에 해당 Parameter를 이용한 Contions 추가

Parameter를 이용한 PlayerController 수정
public class PlayerController : MonoBehaviour
{    
    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향 벡터를 알 수 있다.
        if (dir.magnitude < 0.0001f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            _state = PlayerState.Idle;
        }
        else
        {
            float moveDist = Mathf.Clamp(Time.deltaTime * _speed, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
            transform.LookAt(_destPos); // Player가 목적지를 바라보면서 이동하도록
        }

        // 애니매이션
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("speed", _speed);
    }

    void UpdateIdle()
    {
        // 애니매이션
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("speed", 0);
    }
}​

 

 KeyFrame Animation

  - 이미 만들어진, 모델에 종속된 Animation이 아닌 게임에 의존적인 Animation을 직접 만들기 위한 것이다.

  -  [ Window ] - [ Animation ] 또는 Ctrl + 6을 통해 Animation을 만들 수 있다. 이때 Animation을 만들 Object를 선택해야만

     활성화된다.

 

  -   첫번째 줄은 시간(Frame), 두번째 줄은 Animation Event, 세번째 줄은 KeyFrame과 관련있다.

  -   이때의 Key는 해당 Frame에서 어떤 속성이 변할 경우 생성되며, 2개의 Key 사이의 중간 과정은 보정되어 부드럽게

      이어지게 된다. (왼쪽 하단의 Curves에서 세밀하게 보정할 수도 있다.)

 

 Animation Event

  - 특정 기능을 Animation의 특정 동작에 실행되도록 하기 위한 것이다. (Call Back Event, Sound, Effect...)

Animation Event 추가 후, Inspector 창에서 실행하고자 하는 Function 선택이 가능하다.

 

 

  -   이미 만들어진 Animation도 Inspector - Animation - Events에서 Add Event를 통해 Animation Event를 추가할 수 있다.

 

 

+ Recent posts