ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Section 1 ~ 4
    👾 Unity 2023. 5. 13. 07:22

    Section1: Basics

    • 컴포넌트 화 할 c#
    • 아닌 c# 스크립트를 구분하자

    매니저 만들기

    • Monobehavior 가 붙으면 컴포넌트 화 (gameOjbect)를 상속 받기 때문이다.
    • 매니저스에서 monoBehavior 삭제한다
      • Start 와 Update 는 컴포넌트로 인식했을때 호출
      • 삭제했을때 이 둘은 호출되지 않는다.
      • 이를 다시 해결하기 위해서 unity editor에서 빈 게임오브젝트 생성해서 매니저 스크립트를 붙인다→ 그렇담 다시 monobehavior 상속을 받아야겠지…

    구현내용:

    • gameobject “@Managers” 추가
    • 폴더 hierarchy

    Singleton 패턴

    • 싱글톤 패턴이란: 어플리케이션이 시작될 때, 어떤 클래스가 최초 한번만 메모리를 할당하고 (static) 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴이다
      • 하나의 인스턴스를 메모리에 등록해서 여러 쓰레드가 동시에 해당 인스턴스를 공유하여 사용할 수 있게끔 할 수 있기 때문에 요청이 많은 곳에서 사용하면 효율을 높일 수 있다
      • 장점:
        • 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비 방지
        • 싱글톤으로 만들어진 클래스의 인스턴스는 전역이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다
        • 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 경우 사용한다
        • 두번째 이용시 부터는 객체 로딩 시간이 줄어 성능이 좋아지는 장점이 있다
      • 단점
        • 싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우, 다른 클래스의 인스턴스들 간에 결합도가 높아져 개방-폐쇄 원칙을 위배하게 된다
          • 수정이 어려워지고 유지보수 비용이 높아질 수 있다
          • 멀티쓰레드 환경에서 동기화 처리를 안하면 인스턴스가 2개가 생성될 수 있는 가능성
    • 플레이어 게임오브젝트 생성, 플레이어 스크립트 생성하여 플레이어 오브젝트에 부착
    • 플레이어 스크립트에서 매니저스에 액세스 할 필요 있다
      • hierarchy에서 우선 매니저스 게임 오브젝트를 찾아서 컴포넌트에 액세스
      void Start()
          {
              GameObject go = GameObject.Find("@Managers");
              Managers mg = go.GetComponent<Managers>();
          }
      
      → 게임 오브젝트를 이름으로 찾는 것은 굉장히 부하가 심하기 때문에 개선 필요

    싱글턴:

    • 특정 클래스의 인스턴스가 하나만 있을때
    • 대표적으로 static 으로 만들어서 유일성 보장한다
      • 외부에서 사용하고 싶을 경우 GetInstance 로 → 유일한 매니저를 갖고온다
      static Managers Instance;
          public static Managers GetInstance() { return Instance; }
          // Start is called before the first frame update
          void Start()
          {
              GameObject go = GameObject.Find("@Managers");
              Instance = this;
          }
      

    → 하지만 문제 해결되지않는다: 왜냐하면 매니저스 게임오브젝트가 여러게 복사 되었을때 각각의 객체별로 매니저스라는 컴포넌트가 있기때문에, Instance = this 를 덮어 쓰게된다

    GameObject go = GameObject.Find("@Managers");
    Instance = go.GetComponent<Managers>();
    
    • 요렇게 변경하여 하나만의 매니저의 인스턴스만 인정해준다
    • 그에따라 player 스크립트 변경
    void Start()
        {
            Managers mg = Managers.GetInstance();
        }
    
    • 만약 @Managers 가 없으면?
      • public static Managers GetInstance() { Init(); return Instance; }
        • 여기에 Init() 을 넣은 이유: 다른 객체가 이 매니저에 이니셜라이즈 되기전 액세스 할 수 도 있으므로
      • DontDestroyOnLoad(go);
        • 어지간해서는 삭제되지않게. scene이동을 하더라도 삭제되지않는다.
        • 안전보장
    • public class Managers : MonoBehaviour { static Managers Instance; public static Managers GetInstance() { Init(); return Instance; } // Start is called before the first frame update void Start() { Init(); } // Update is called once per frame void Update() { } static void Init() { if (Instance == null) { GameObject go = GameObject.Find("@Managers"); if (go == null) { go = new GameObject { name = "@Managers" }; go.AddComponent<Managers>(); } DontDestroyOnLoad(go); Instance = go.GetComponent<Managers>(); } } }
    • 플레이어 스크립트에서 : Managers mg = Managers.GetInstance();
      • 항상 GetInstance() 요렇게 괄호까지 써야하니까 이부분을 더 간편하게 만들어보자..
      public class Managers : MonoBehaviour
      {
          static Managers s_intance;
          public static Managers Instance { get { Init(); return s_intance; } }
          // Start is called before the first frame update
          void Start()
          {
              Init();
          }
      
          // Update is called once per frame
          void Update()
          {
              
          }
      
          static void Init()
          {
              if (s_intance == null)
              {
                  GameObject go = GameObject.Find("@Managers");
                  if (go == null)
                  {
                      go = new GameObject { name = "@Managers" };
                      go.AddComponent<Managers>();
                  }
      
                  DontDestroyOnLoad(go);
                  s_intance = go.GetComponent<Managers>();
              }
          }
      }
      
      • 프로퍼티 형식으로 만들어준다.
      • 그렇담 이제는 플레이어 스크립트에서…
        • 요렇게 간편하게!
      • void Start() { Managers mg = Managers.Instance; }

     

    Section2: Transform

    플레이어 설정

    • 우선 Unity-chan 에셋 (art 와 audio 만) 임포트 → Player로 이름 변경
    • 새로운 스크립트 생성하여 플레이어에 부착 : PlayerController
    • 좌우 이동을 시키는 스크립트를 우선 작성해보자면:
      • 컴포넌트 hierarchy 를 보면
        • Player
            • transform
            • PlayerController
          • 순서일텐데, PlayerController에서 transform 컴포넌트를 변경하기위해서는 부모 컴포넌트에서 내려오는 방식으로 해야할 듯 싶지만 transform 은 자주쓰이기때문에 그냥 transform 이라고 써도 된다.
    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

    • 지금 플레이어가 너무 빨리 이동하는 것을 볼 수 있다
      • 업데이트는 프레임 마다 호출되기 때문
      • Time.deltaTime 을 써서 수정한다
      • 속도는 _speed 로 조정
      public class PlayerController : MonoBehaviour
      {
          [SerializeField]
          float _speed = 10.0f;
          // Start is called before the first frame update
          void Start()
          {
              
          }
      
          // Update is called once per frame
          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;
              //transform
          }
      }
      
      • 코드를 더 개선하자면…
      • Vector3(0.0f, 0.0f, 1.0f) 대신 Vector3.forward
      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;
      
    • 지금은 캐릭터가 회전해도 월드 스페이스로 이동하는걸 알수있다. (비스듬히 이동)
      • 캐릭터가 회전했을시에도 캐릭터 기준으로 (로컬좌표) 앞뒤좌우로 움직이는것이 필요하다
      • 두 가지 좌표계 (x로 toggle)
        • 월드 좌표계
        • 로컬 좌표계
      • transform.TransformDirection을 이용해서 수정해준다
        • 로컬에서 월드로 좌표계 변환해준다
        //transform.TransformDirection는 로컬에서 월드로 좌표계 변환
                //transform.InverseTransformDirection 월드에서 로컬로 좌표계 변환
                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);
                //transform
        
        더 간략한 방법 transform.Translate 활용!!! ⭐ (이었던 것 같지만 마지막에 캐릭터 튀는 현상으로 다시 transform.position으로 롤백하게 된다)
        • 작동방식: 로컬을 기준으로 연산한다.
        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);
        

    Vector3

    1. 위치 벡터
      • 좌표
    2. 방향 벡터
      • 내가 상대방의 위치로 이동할 때: 상대방 위치 - 나의 위치
      • 크기: magnitude
      • 실제 방향 (단위벡터: 크기가 1) : normalized

    Rotation

    • transform.rotation 은 벡터3 타입이 아니고 Quatertnion (4개의 값)
      • gimbal lock과 관련있다
    • 그렇담 Vector3 을 사용하는 transform.eulerAngles 사용 가능….
      • 하지만 아래와 같이 eulerAngles는 absolute value만 넣을 수 있지 점점 증가나 감소 등 변화하는 값을 넣을 수 없다….
      • 예를 들면 내가 지금 필요한 transform.eulerAngles += new Vector3(0.0f, Time.deltaTime * 100, 0.0f);
    float _yAngle = 0.0f;
        // Update is called once per frame
        void Update()
        {
            _yAngle += Time.deltaTime * _speed;
            transform.eulerAngles = new Vector3(0.0f, _yAngle, 0.0f);
            //transform.rotation
    				...
        }
    
    • 그러므로…transform.Rotate() 을 쓴다
    transform.Rotate(new Vector3(0.0f, Time.deltaTime * 100.0f, 0.0f));
    
    • 다른 방법으로는
    transform.rotation = Quaternion.Euler(new Vector3(0.0f, Time.deltaTime * 100.0f, 0.0f));
    

    이제 캐릭터가 이동할때 이동하는 방향으로 바라보게 수정하자면…

    • Quaternion.LookRotation 활용
            if (Input.GetKey(KeyCode.W))
            {
                transform.rotation = Quaternion.LookRotation(Vector3.forward);
                // transform.Translate(Vector3.forward * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.S))
            {
                transform.rotation = Quaternion.LookRotation(Vector3.back);
                //transform.Translate(Vector3.back * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.A))
            {
                transform.rotation = Quaternion.LookRotation(Vector3.left);
                //transform.Translate(Vector3.left * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.D))
            {
                transform.rotation = Quaternion.LookRotation(Vector3.right);
                //transform.Translate(Vector3.right * Time.deltaTime * _speed);
            }
    
    • 현재: 부드럽지 않게 회전한다. 이러한 경우 Lerp / Slerp 로 수정한다
      • Quaternion.Slerp(시작값, 끝값, 정도/비율?)
    if (Input.GetKey(KeyCode.W))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.2f);
                // transform.Translate(Vector3.forward * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.S))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.2f);
                //transform.Translate(Vector3.back * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.A))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.2f);
                //transform.Translate(Vector3.left * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.D))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
                //transform.Translate(Vector3.right * Time.deltaTime * _speed);
            }
    
    • 캐릭터가 이제 회전함으로, 이동할때 방향벡터는 항상 forward 여야한다
    if (Input.GetKey(KeyCode.W))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.2f);
                transform.Translate(Vector3.forward * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.S))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.2f);
                transform.Translate(Vector3.forward * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.A))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.2f);
                transform.Translate(Vector3.forward * Time.deltaTime * _speed);
            }
            if (Input.GetKey(KeyCode.D))
            {
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
                transform.Translate(Vector3.forward * Time.deltaTime * _speed);
            }
    
    • 현재 캐릭터가 튀는 현상이 있다 (혹은 커브 형으로 이동)
      • 왜냐하면 Slerp 때문에: 캐릭터가 바로 도는것이 아니라 중간 지점을 바라보는 때가 있다.
      • 그렇기 때문에 transform.Translate (로컬) 말고 transform.position (월드 좌표) 을 사용한다
      • 방향도 다시 롤백
      • 회전 * 이동 완성! 😀
      if (Input.GetKey(KeyCode.W))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.2f);
                  transform.position += Vector3.forward * Time.deltaTime * _speed;
              }
              if (Input.GetKey(KeyCode.S))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.2f);
                  transform.position += Vector3.back * Time.deltaTime * _speed;
              }
              if (Input.GetKey(KeyCode.A))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.2f);
                  transform.position += Vector3.left * Time.deltaTime * _speed;
              }
              if (Input.GetKey(KeyCode.D))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
                  transform.position += Vector3.right * Time.deltaTime * _speed;
              }
      

    Input Manager

    • 현재 플레이어 컨트롤러에서 업데이트문마다 키보드 인풋 체크를 하는데 → 다른 많은 곳에서 업데이트 문마다인풋 체크를 하면 성능부하 걸릴수 있다
    • 공용으로 사용할수 있는 매니저에 넣어 이벤트를 건너 받아 구현한다
    • 새로운 매니저: InputManager 스크립트 생성
      • 싱글톤 패턴으로 매니저스 있으니까 컴포넌트로 만들 필요없기때문에 MonoBehavior 삭제
      • Action 사용 (using System 필요) : 일종의 delegate
        • 대표적인 리스너 패턴
    • Input Manager 는 컴포넌트/모노비헤이비어 아닌 누군가가 호출해야하는 것이기때문에
      • Update() 를 public void OnUpdate()으로 수정
      **using System;**
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      public class InputManager 
      {
          **public Action KeyAction = null;**
      
          // Update is called once per frame
          **public void OnUpdate()
          {
              if (Input.anyKey == false)
                  return;
              if (KeyAction != null)
                  KeyAction.Invoke();
          }**
      }
      
      이제는 Managers 스크립트 수정
      • Managers Instance 외부 호출 못하게 public 삭제
      • 인풋 매니저 연결
      • Managers.Input 하면 인풋 매니저 반환 할수 있는 코드
      • 업데이트 문에 _input.OnUpdate 도: 인풋 있는지 없는지 계속 확인
      • 이에 맞춰 플레이어 컨트롤 수정
        • 인풋 관련은 따로 함수로 빼고
        • Input 매니저의 KeyAction 에 구독 신청 한다
          • 입력이 있으면 → 이 함수 호출
      void Start()
          {
              Managers.Input.KeyAction += OnKeyboard;
          }
      
          // Update is called once per frame
          void Update()
          {
           
          }
      
          void OnKeyboard()
          {
              if (Input.GetKey(KeyCode.W))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.2f);
                  transform.position += Vector3.forward * Time.deltaTime * _speed;
              }
              if (Input.GetKey(KeyCode.S))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.2f);
                  transform.position += Vector3.back * Time.deltaTime * _speed;
              }
              if (Input.GetKey(KeyCode.A))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.2f);
                  transform.position += Vector3.left * Time.deltaTime * _speed;
              }
              if (Input.GetKey(KeyCode.D))
              {
                  transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
                  transform.position += Vector3.right * Time.deltaTime * _speed;
              }
          }
      
      두번 구독 되어있을 수 있으니…
    • Managers.Input.KeyAction -= OnKeyboard; Managers.Input.KeyAction += OnKeyboard;
    • public class Managers : MonoBehaviour { static Managers s_intance; static Managers Instance { get { Init(); return s_intance; } } **InputManager _input = new InputManager(); public static InputManager Input { get { return Instance._input; } }** // Start is called before the first frame update void Start() { Init(); } // Update is called once per frame void Update() { **_input.OnUpdate();** } ... }

     

    Section3: Prefab

    Prefab

    • Prefabs 폴더 생성해서 이 안으로 드래그앤 드랍하면 프리팹 생성
    • 프리팹 오버라이드: 프리팹 인스턴스가 기본값 사용하지않을 때
      • 인스펙터 창에서 오버라이드 창에서 모든 변경 값 볼 수 있다
    • Nested Prefab: 중첩된 프리팹
    • Prefab Variants: 상속의 개념
    • Instantiate(프리팹) 요렇게 코드상으로 프리팹 인스턴스 생성 → 게임 오브젝트 반환
    • Destroy(게임오브젝트) 로 없앨 수도 있다

    Resource Manager

    public GameObject prefab;
    GameObject tank;
    void Start()
    {
    	tank = Instantiate(prefab);
    	Destroy(tank, 3.0f);
    }
    
    • 요렇게 public을 써서 했을 경우 규모가 작은 게임에서는 ok
    • 코드로 관리하려면… 우선…
    • Resource 폴더 만들고 그 안에 Prefabs 폴더
    GameObject prefab;
    GameObject tank;
    void Start()
    {
      prefab = Resources.Load<GameObject>("Prefabs/Tank");
    	tank = Instantiate(prefab);
    	Destroy(tank, 3.0f);
    }
    

    → 요런식으로 구현

    • 그런데 여기저기 이런식으로 구현하면 도대체 어떤 컴포넌트가 언제 인스턴스 생성하는지 관리 어렵다 → Resource Manager 필요성
    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);
        }
    }
    
    • return Object.Instantiate(prefab, parent)가 return Instantiate(prefab, parent) 아닌 이유는
      • Instantiate 함수가 재귀적으로 호출될까봐
    • Manager 코드도 수정
    public class Managers : MonoBehaviour
    {
        static Managers s_intance;
        static Managers Instance { get { Init();  return s_intance; } }
    
        InputManager _input = new InputManager();
        ResourceManager _resource = new ResourceManager();
        public static InputManager Input { get { return Instance._input; } }
        public static ResourceManager Resource { get { return Instance._resource; } }
    		...
    }
    

    폴더정리

    • 에셋도 정리해서 Art 폴더 안에 넣어준다

     

    Section4: Collision

    Collider

    • 많이 쓰는 4가지
      • Mass (kg)
      • Use Gravity
      • Is Kinematic
      • Constraints
    • 양쪽 물체 모두 Collider 필요하다

    '👾 Unity' 카테고리의 다른 글

    델리게이트 & 코루틴 파헤쳐보기 & 실행 순서  (0) 2023.08.04