Search
⚙️

Unity 엔진 정리 (1)

1. 개론

layout - tall
Game 창을 밑으로 연결시켜서 2 by 3 같이 창을 만들어놓음.
이 것이 작업하기 가장 좋은 상태.
hierarchy, inspector, resource 간 드래그 거리가 제일 가까움.

Hello Unity 출력하기

콘솔창 단축키 ctrl + shift + c
debug 에 용이
Assets 에 script 작성 후 GameObject에 drag&drop

Scene 저장

File - save
ctrl + s
SampleScene은 Scenes에 default로 있음.

2. 유니티 기초

에디터 기초

영화를 찍는다고 생각을 하자.
Scene은 영화 촬영장이다.
배우들이 있고, 소품들이 있고, 감독이 있는.
Direction Light는 말 그대 조명.
Main Camera는 말 그대로 카메라.
Hierarchy는 영화 세트장에 나와있는 모든 것.
Project에서
Asset 배우들, 의류.. 음악들.. 영화 관련 된 모든 소품들.
언제든지 꺼내쓸 수 있는 도구들 정도
Inspector
소품에 관한 상세 설명서.
camera - ctrl shift f 현재 시점으로 카메라 설정

Component 패턴

Update 함수가 있다 하자.
여기에
-이동 쿨타임 체크
-애니메이션 갱신
-스킬 쿨타임 체크
-물리 적용(중력 등)
이렇게 다 때려박으면 한 함수가 5000줄 넘어가는 현상이 벌어질 수 도 있고,
나중에 폭탄이 되어 버린다.
한 코드를 변경하면, 다른 버그 5개가 생겨나는..
이걸 예쁘게 관리하는 것 중 하나가 Component 패턴이다.
예를 들면 애니메이션 갱신은
AnimationComponent _anim = new AnimationComponent();
애니메이션 클래스를 새로 만들어서,
update 엔 _anim.Update(deltaTick); 이런 식으로만 적는 것임.
AnimationComponent _anim = new AnimationComponent();
SkillComponent _skill = new SkillComponent();
PhysicsComponent _physics = new PhysicsComponent();
이런 식으로 부품을 만들어서 관리하는 것이 좋다.
unity는 모든 것이 component 패턴으로 만들어진 것이다

매니저 만들기

게임은 매니저 구조가 있는 것이 관리하기도 편함
밑 그림 같은 구조를 유지하는 것이 좋음

Singleton 패턴

@Manager의 Managers 스크립트에 항상 접근이 가능했으면 좋겠는데.. 어떻게 하지?
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { // Start is called before the first frame update void Start() { GameObject go = GameObject.Find("@Managers"); Managers mg = go.GetComponent<Managers>(); } // Update is called once per frame void Update() { } }
C#
복사
이렇게 GameObject 의 find를 이용해서, getComponent로 가져오는 방법이 있음
근데 이것은 맘에 들지 않는다. 부하가 많이 걸리기 때문
Managers 에서 유일성을 보장해주고, @Managers라는 이름을 가진 것만 인정해주고
외부에선 GetInstance()를 통해서만 받을 수 있게 한다음
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Managers : MonoBehaviour { 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 = go.GetComponent<Managers>(); } // Update is called once per frame void Update() { } }
C#
복사
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { // Start is called before the first frame update void Start() { Managers mg = Managers.GetInstance(); } // Update is called once per frame void Update() { } }
C#
복사
Player 에선 이렇게 받아오는 법이 있다.
이것도 살짝 아쉬움
using System.Collections; using System.Collections.Generic; using UnityEngine; 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>(); } } }
C#
복사
최종 Manager 코드를 보면
#Managers.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Managers : MonoBehaviour { static Managers s_instance; // 유일성이 보장된다. public static Managers Instance { get { Init(); return s_instance; } } // 유일한 매니저를 갖고 온다. // Start is called before the first frame update void Start() { //초기화 Init(); } // Update is called once per frame void Update() { } 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>(); } } }
C#
복사
#Player.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { // Start is called before the first frame update void Start() { Managers mg = Managers.Instance; } // Update is called once per frame void Update() { } }
C#
복사

3. Transform(트랜스폼)

플레이어 설정 / Position

asset 에서 유니티짱을 가져와봤다
지금부터 얘를 Player로 하겠다.
public class PlayerController : MonoBehaviour { void Start() { } //GameObject (Player) //Transform //PlayerController (*) //Transform에 접근하려면 원래는 GameObject에 먼저 접근해야하는데 //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); //transform } }
C#
복사
거의 분신사바 급 무빙이 되어버렸다.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { public float _speed = 10.0f; void Start() { } //GameObject (Player) //Transform //PlayerController (*) //Transform에 접근하려면 원래는 GameObject에 먼저 접근해야하는데 transform으로 바로 접근 가능 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 } } }
C#
복사
좀 나아졌다.
Time.deltatime과 speed를 설정해주었다.
신기한 것은 speed 선언 시 public을 붙여주면,
여기에서도 수정이 가능하게 된다.
[SerializeField] float _speed = 10.0f;
C#
복사
이렇게 써도 public을 쓰지 않고도 방금처럼 할 수 있음
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; //transform }
C#
복사
이렇게 forward, back, left, right 로 예약된 명령어도 사용 가능하다.
그리고 좌표계는 월드좌표계와, 각 물체의 좌표계가 다르다.
이건 월드 좌표계
X키를 누르면 좌표계 변경 가능
요건 Player의 좌표계.
그래서 월드좌표계에서 로컬좌표계로 사용하고 싶다면
void Update() { //Local -> World //TransformDirection //World -> Local // 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 }
C#
복사
과 같이 적어주는 방법이 있음.
이것도 귀찮다.
translate이라는 함수는 로컬좌표계를 기준으로 하는 함수.
void Update() { //Local -> World //TransformDirection //World -> Local // InverseTransformDirection 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); //transform }
C#
복사
이렇게 할 수도 있다.
플레이어를 기준으로 움직이는 것을 볼 수 있다.

Rotation

float _yAngle = 0.0f; void Update() { _yAngle += Time.deltaTime * _speed; transform.eulerAngles = new Vector3(0.0f, _yAngle, 0.0f);
C#
복사
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); } //transform }
C#
복사
저 Quaternion 에 대한 것은 자세하게 알고 싶으면.. 유튜브를 봐야함.
wasd 누를 때마다 캐릭터가 바라보는 방향이 바뀌는 모습
그런데 너무 딱딱하게 움직인다.
lerp계열의 함수를 쓰면 해결할 수 있음.
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); }
C#
복사
여기에 아까 움직이게 코드도 작성해주면.
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); }
C#
복사
이동함수를 쓸 때에는 방향 전환과 함께 어떻게 동작하는 지 생각해봐야함.
처음처럼 transform.Translate 과 같이 하면 이상하게 작동하는 것을 볼 수 있음.

Input manager

using System.Collections; using System.Collections.Generic; using UnityEngine; using System; public class InputManager { public Action KeyAction = null; public void OnUpdate() { if (Input.anyKey == false) return; if (KeyAction != null) KeyAction.Invoke(); } }
C#
복사
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; } }
C#
복사
Manager에서 InputManager도 관리.

4. Prefab(프리팹)

prefab #1

산하에 부모 자식 같이 만들어 놓으면 같이 움직이게 됨.
탱크를 근데 여러 개 만들고 싶으면?
이렇게 ctrl + d를 이용해서 무한정 복사하는 것은 무식한 방법임.
hierarchy 에 있던 tank를 asset에 옮겨주면, prefab 이 생성됨.
위에 hierarchy에서도 파랑색 상자로 바뀐 것을 볼 수 있음
이 상태에서는 hierarchy에 있는 Tank를 삭제해도 무방함. 프리팹이 만들어졌기 때문에.
프리팹은 한 번에 수정이 용이함.
프리팹 수정을 하고 싶다. 프리팹 두 번 더블 클릭

prefab #2

근데 프리팹마다 값을 다르게 하고 싶은 게 있을 수 있음
tank1은 speed 10, tank2는 speed 15로 하고 싶은데요
여기서 바꿔주면 됨.
Override하는 것인데, 임의로 바꾼 값에 대해서는 하늘색 선과 함께 굵게 Speed 표시되있는 것을 볼 수 있음.
내역을 보고 싶으면 Overrides 눌러서 확인해볼수 있음
Nested Prefab 이라는 것도 있음.

Resource Manager

using System.Collections; using System.Collections.Generic; using UnityEngine; public class PrefabTest : MonoBehaviour { public GameObject prefab; GameObject tank; void Start() { tank = Instantiate(prefab); Destroy(tank, 3.0f); } }
C#
복사
Instantiate를 통해 생성하고, Destroy를 이용해 원하는 프리팹을 몇 초 후에 삭제시킬건지 할 수 있음
근데 이렇게 하나하나 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); } }
C#
복사
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); } }
C#
복사
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; } }
C#
복사

5. Collision(충돌)

collider

Rigidbody
Use Gravity는 중력 사용여부
Mass 질량(kg)
Collider를 통해 충돌 여부를 탐지할 수 있게 할 수 있음.

collision

public class TestCollision : MonoBehaviour { // 1) 나 혹은 상대한테 RigidBody 있어야 한다. (isKinematic : 0ff) // 2) 나한테 Collider가 있어야 한다 (isTrigger : off) // 3) 상대한테 Collider가 있어야 한다(isTrigger : off) private void OnCollisionEnter(Collision collision) { Debug.Log("Collision !"); }
C#
복사
//1) 둘다 Collider 가 있어야 한다. //2) 둘 중 하나는 IsTrigger : On //3) 둘 중 하나는 RigidBody가 있어야 한다. private void OnTriggerEnter(Collider other) { Debug.Log("Trigger !"); }
C#
복사
Trigger는 물리현상이 벌어지지 않는 대신 피격판정 이런걸로 응용할 수 있다.

RayCasting

플레이어를 클릭하고.. 우리가 Unity화면은 2D인데(컴퓨터 화면) 3D를 제어하고 있잖아?
어떻게 화면을 클릭했는데 캐릭터가 선택되지?
이때 들어가는 기술이 Raycasting
Ray 광선을 casting 쏘다
레이저를 쏴서 캐릭터를 맞춘다고 생각하면 됨.
void Update() { Debug.DrawRay(transform.position, Vector3.forward, Color.red); if(Physics.Raycast(transform.position, Vector3.forward)) { Debug.Log("Raycast"); } } }
C#
복사
여기서 레이저가 1칸만 나간 이유는 forward 는 단위벡터라
void Update() { Vector3 look = transform.TransformDirection(Vector3.forward); Debug.DrawRay(transform.position + Vector3.up, look * 10, Color.red); RaycastHit hit; if(Physics.Raycast(transform.position + Vector3.up, look , out hit, 10)) { Debug.Log($"Raycast {hit.collider.gameObject.name}"); } }
C#
복사
유니티짱 기준으로 좌표계 변경해주고 발사
그런데 선이 큐브 2개를 관통해도 Console엔 하나만 뜸
광선이 통과하는 모든 것을 확인하려면 다른 명령어가 있다.
RaycastHit[] hits; hits = Physics.RaycastAll(transform.position + Vector3.up, look, 10); foreach (RaycastHit hit in hits) { Debug.Log($"Raycast {hit.collider.gameObject.name}"); } }
C#
복사
이런건 많이 활용될 수 있음
예를 들어 카메라
캐릭터가 어떤 물체에 가려질 때(캐릭터에서 나온 레이저가 카메라보다 다른 물체에 먼저 닿는 경우) 카메라 위치를 바꿔서 캐릭터 앞까지 찍어주는.. 이런 기술

투영의 개념

//Local <-> World <-> Viewport <-> Screen(화면) Debug.Log(Input.mousePosition);
C#
복사
말 그대로 Screen의 mousePosition.
Debug.Log(Camera.main.ScreenToViewportPoint(Input.mousePosition));
C#
복사
카메라에서 좌표 하나는 사라짐
가두리 양식 장 안에 있는 것들이 보이고, 점점 조그매져서 저 카메라 앞 사각형에 이른다 생각하면 편한 것 같음

Raycasting#2

if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane)); //Vector3 dir = mousePos - Camera.main.transform.position; //dir = dir.normalized; Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); RaycastHit hit; if(Physics.Raycast(ray, out hit, 100.0f)) { Debug.Log($"Raycast Camera @{hit.collider.gameObject.name}"); } }
C#
복사
화면을 누르면 카메라에서 레이저가 나가게됨.
이걸 응용한다면, 롤이나 와우 같은 곳에서 바닥을 클릭했을 때 해당 좌표로 이동할 수 있게 만들 수 있다.

LayerMask

raycasting은 무거운 작업은 맞음
연산이 많이 들어감.
layer를 이용할 수 있음
원하는 애들만 raycasting을 하도록 하는 것임 부하 줄이기
if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); int mask = (1 << 8); RaycastHit hit; if(Physics.Raycast(ray, out hit, 100.0f, mask)) { Debug.Log($"Raycast Camera @{hit.collider.gameObject.name}"); } }
C#
복사

6. Camera(카메라)

Camera #1

유니티짱의 산하로 카메라를 이동시켰을 때
잘 따라다니긴 하는데 너무 어지러움
유니티짱의 로테이션에 따라 움직여서 그럼
public class CameraController : MonoBehaviour { [SerializeField] Define.CameraMode _mode = Define.CameraMode.QuarterView; [SerializeField] Vector3 _delta; [SerializeField] GameObject _player; void Start() { } void Update() { transform.position = _player.transform.position + _delta; } }
C#
복사
근데 덜덜덜 거리는 것 볼 수 있다.
왜 그럴까 update에서 동시에 이동과 카메라 이동을 관리하는데, 누가 더 먼저 실행되는지는 미정이다
그래서 이동과 카메라 이동 간에 간극이 생기는 것이다
이것을 해결하려면 이동이 끝난 다음에 카메라 이동이 이루어지면 된다.
void LateUpdate() { transform.position = _player.transform.position + _delta; transform.LookAt(_player.transform); }
C#
복사
void LateUpdate() { if (_mode == Define.CameraMode.QuarterView) { transform.position = _player.transform.position + _delta; transform.LookAt(_player.transform); } } public void SetQuarterView(Vector3 delta) { _mode = Define.CameraMode.QuarterView; _delta = delta; }
C#
복사

Camera #2

마우스 클릭을 했을 때 유니티짱이 움직이도록 설정.
그리고 이제 카메라와 유니티짱 사이에 벽이 있으면, 카메라가 벽 바로 앞으로 오게 만들었다.

7. Animation(애니메이션)

Animation 기초

일일이 코드로 팔 다리가 움직이는 것을 하는 것은 너무 힘듦.
애니메이터가 만들어주는 애니메이션을 이용함.
이런 식으로.
지금 유니티짱은 Humanoid 기반으로 만들어졌음.
기초적인 Animator Controller
WAIT 과 RUN을 넣어주고
Update문에서 움직이고 있다면 Run을 , 그렇지 않다면 Wait을 표출하면 될 것이다
if (_moveToDest) { Animator anim = GetComponent<Animator>(); anim.Play("RUN"); } else { Animator anim = GetComponent<Animator>(); anim.Play("WAIT"); }
C#
복사

Animation Blending

그런데 너무 뚝뚝 멈추는 것 같다.
애니메이션을 서서히 변환시키는 것이 더 좋을 것 같음.
그럴 땐 Blending을 통해서 섞어주는 것 이 좋음
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"); }
C#
복사
훨씬 부드럽게 멈추는 것을 볼 수 있다.

State 패턴

근데 나중에 엄청나게 애니메이션이 많아지면 이걸 일일히 Blending 하기가 힘들 것이다.
그리고 If - else 문으로 계속 처리하는 것도 힘들다
bool isjumping; bool isFalling; bool isSkillCasting; bool isSkillChanneling;
나중에 이런 것이 쭉쭉 늘어난다고 해보자.
이것을 일일히 if else문으로 쭉쭉.. 스파게티 코드가 되버리게 된다.
폭탄 해제하는 느낌이 되버린다. 버그 투성이
따라서 boolean으로 상태를 관리하는 것은 그렇게 좋지 않음
이런 식으로 만드는 것이 좋음.
어떤 state가 끝나면, state를 전환해주는 식이다.
예를 들면 Moving이 끝나면 Idle로 state 전환
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(_speed * Time.deltaTime, 0, dir.magnitude); transform.position += dir.normalized * moveDist; transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); } //애니메이션 처리 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; } }
C#
복사
하지만 이것은 만병통치약은 아님.
왜냐면 state를 하나만 지정할 수 있기 때문에
스킬 시전 중 이동. 이런 건 어려울 수 있음.

State Machine

그런데 애니메이션의 상태 관리를 이렇게 모두 코드로 할 수 있을까?
나중에 가면 엄청나게 복잡해진다.
예시이다.
이럴 땐, 그림과 같이 시각적인 표를 이용해서 나타내는 것이 훨씬 쉽다.
RUN 과 WAIT을 서로 연결시켜 놓으면, 오른쪽에서와 같이 자동으로 Blending이 이루어진다.
그리고 Parameter를 전달해주면 된다. Condition을 통해 speed가 몇 이상이면, 몇 이하면 state를 변경하는 식으로.
Animator anim = GetComponent<Animator>(); //현재 게임 상태에 대한 정보를 넘겨준다. anim.SetFloat("speed", _speed); } void UpdateIdle() { //애니메이션 처리 Animator anim = GetComponent<Animator>(); anim.SetFloat("speed", 0);
C#
복사

Keyframe animation

8. UI

UI기초, ButtonEvent

UI는 우선 원근법이 적용이 되지 않는다. 버튼이 뒤에 있든, 앞에 있든 같은 크기면 같은 크기로 나온다. UI 는 2D로 작업하는 것이 편하다.
앵커라는 것이 있는데, 앵커를 통해 컴퓨터나 핸드폰의 디스플레이 크기가 달라져도 일정 비율만큼 UI가 나오게 할 수 있다.
Button을 눌렀을 때 이벤트가 발생하게 하려면 어떻게 해야할까
우선 버튼을 눌렀을 때 나오는 On Click() 과 관련이 있는 것 같다.
UI관련 script를 작성해보자.
우선 UI_Button 의 Prefab을 만들어주었다. 그리고 UI_Button 스크립트를 넣어준 뒤, On Click() 이벤트와 연결해준다. 눌렀을 때 시행되는 함수를 만들어주기 위해 스크립트를 작성해보면
public class UI_Button : MonoBehaviour { public void OnButtonClicked() { Debug.Log("ButtonClicked"); } }
C#
복사
이렇게 된다.
근데 버튼을 누르면 플레이어가 움직이니까 수정을 좀 해주자.
public void OnUpdate() { //UI를 클릭한다면 바로 return if (EventSystem.current.IsPointerOverGameObject()) return;
C#
복사
[SerializeField] TMP_Text _text; int _score = 0; public void OnButtonClicked() { _score++; _text.text = $"Score : {_score}"; }
C#
복사
버튼을 클릭할 때마다 숫자가 오르게 해봤다.
SerializeField를 통해 text를 unity 툴에서 바로 변경할 수 있게 하고, text를 드래그앤 드롭으로 버튼과 연결시켜준다.
근데 신버전(21버전) 은 text라 쓰면 안되고, TMP_Text라 써야한다.
위에서 using TMPro도 해주어야 하고.
근데 게임 규모가 커진다면 저렇게 하나하나 OnClick() 과 같이 툴로 연결해줄 순 없다.

UI 자동화