-
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 static Managers GetInstance() { Init(); return Instance; }
- 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 이라고 써도 된다.
-
- Player
- 컴포넌트 hierarchy 를 보면
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.Translate 활용!!! ⭐ (이었던 것 같지만 마지막에 캐릭터 튀는 현상으로 다시 transform.position으로 롤백하게 된다)//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
- 작동방식: 로컬을 기준으로 연산한다.
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
- 위치 벡터
- 좌표
- 방향 벡터
- 내가 상대방의 위치로 이동할 때: 상대방 위치 - 나의 위치
- 크기: 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()으로 수정
이제는 Managers 스크립트 수정**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 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