Chapter 2. Transform (+ Input Manager)
카테고리: Unity Lesson 2
태그: Unity Game Engine
인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
🙋♀️ Scale
오브젝트의 크기. 확대/축소
거대한 모델을 필요하다고 가정하면, 모델을 만들 때 처음부터 거대한 모델을 만들 필요 없이 모델은 작게 만들어주고 유니티에 와서 Scale을 통해 크기를 늘리면 된다.
🙋♀️ Position
오브젝트의 위치. 이동과 관련.
거리 = 시간 * 속력
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private float _speed = 10.0f;
void Start()
{
}
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.D))
transform.position += new Vector3(1.0f, 0.0f, 0.0f) * Time.deltaTime * _speed;
if (Input.GetKey(KeyCode.A))
transform.position -= new Vector3(1.0f, 0.0f, 0.0f) * Time.deltaTime * _speed;
}
}
- 매 프레임마다
W
키를 누르는 중이라면 현재 위치로부터 한 프레임동안 Vector3(0.0f, 0.0f, Time.deltatime * _speed) 만큼 더 이동한다.- 방향(벡터) 👉 Vector3(0.0f, 0.0f, 1.0f)
- Z 축 positive 방향이니
Vector3.forward
와 방향이 같다.
- Z 축 positive 방향이니
- 거리(스칼라) 👉 시간 X 속력 = Time.deltaTime * _speed
- 프레임간의 간격 시간동안
_speed
크기만큼 이동한다.
- 프레임간의 간격 시간동안
- 방향(벡터) 👉 Vector3(0.0f, 0.0f, 1.0f)
S
키를 누르는 중이라면 현재 위치로부터 한 프레임동안 -Vector3(0.0f, 0.0f, Time.deltatime * _speed) 만큼 더 이동한다.- 방향(벡터) 👉 -Vector3(0.0f, 0.0f, 1.0f)
- Z 축 negative 방향이니
Vector3.back
와 방향이 같다.
- Z 축 negative 방향이니
- 거리(스칼라) 👉 시간 X 속력 = Time.deltaTime * _speed
- 프레임간의 간격 시간동안
_speed
크기만큼 이동한다.
- 프레임간의 간격 시간동안
- 방향(벡터) 👉 -Vector3(0.0f, 0.0f, 1.0f)
D
키를 누르는 중이라면 현재 위치로부터 한 프레임동안 Vector3(Time.deltatime * _speed, 0.0f, 0.0f) 만큼 더 이동한다.- 방향(벡터) 👉 Vector3(1.0f, 0.0f, 0.0f)
- X 축 positive 방향이니
Vector3.Right
와 방향이 같다.
- X 축 positive 방향이니
- 거리(스칼라) 👉 시간 X 속력 = Time.deltaTime * _speed
- 프레임간의 간격 시간동안
_speed
크기만큼 이동한다.
- 프레임간의 간격 시간동안
- 방향(벡터) 👉 Vector3(1.0f, 0.0f, 0.0f)
A
키를 누르는 중이라면 현재 위치로부터 한 프레임동안 -Vector3(Time.deltatime * _speed, 0.0f, 0.0f) 만큼 더 이동한다.- 방향(벡터) 👉 -Vector3(1.0f, 0.0f, 0.0f)
- X 축 negative 방향이니
Vector3.Left
와 방향이 같다.
- X 축 negative 방향이니
- 거리(스칼라) 👉 시간 X 속력 = Time.deltaTime * _speed
- 프레임간의 간격 시간동안
_speed
크기만큼 이동한다.
- 프레임간의 간격 시간동안
- 방향(벡터) 👉 -Vector3(1.0f, 0.0f, 0.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.D))
transform.position += Vector3.right * Time.deltaTime * _speed;
if (Input.GetKey(KeyCode.A))
transform.position += Vector3.left * Time.deltaTime * _speed;
}
위와 같이 바꿀 수 있다.
World 좌표계 / Local 좌표계
현재
Player
오브젝트를 Y 축으로 45 회전한 상태다.
- 위 사진은 World 좌표계, 즉 Global 을 기준으로 한
Player
오브젝트의 좌표계를 나타낸다.- 플레이어는 월드 좌표계의 Y 축을 기준으로 45 도 회전한 곳을 바라보고 있지만 이와 상관없이 절대적인 월드 세상 기준에서의 ‘앞’방향은 파란색 화살표(Z축 Positive)임을 알려준다.
- 아래 사진은 Local 좌표계를 기준으로 한
Player
오브젝트의 좌표계를 나타낸다.- 플레이어는 월드 좌표계의 Y 축을 기준으로 45 도 회전해 있지만 플레이어 기준에선 앞 쪽 방향이 현재 플레이어가 바라보고 있는 월드 좌표계의 Y 축을 기준으로 45 도 방향이기 때문에, 이 방향이 Local 좌표계로선 플레이어의 ‘앞’방향이다. 플레이어가 바라보고 있는 방향이 앞쪽임을 나타내는 파란색 화살표(Z축 Positive)임을 알 수 있다.
- 로컬 좌표계는 모델을 디자인 하는 분들이 주로 사용한다.
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.D))
transform.position += transform.TransformDirection(Vector3.right * Time.deltaTime * _speed);
if (Input.GetKey(KeyCode.A))
transform.position += transform.TransformDirection(Vector3.left * Time.deltaTime * _speed);
}
- TransformDirection(Vector3) 함수
- 인수로 받은 좌표 Vector3 를, 함수를 호출한 오브젝트의 Vector3 로컬 좌표계 기준에서 월드 좌표계 기준으로 방향만 변환하여 이를 리턴해준다. (벡터 길이는 변하지 않음)
- InverseTransformDirection(Vector3) 함수
- 인수로 받은 좌표 Vector3 를 월드 좌표계 기준에서 로컬 좌표계 기준으로 방향만 변환하여 이를 리턴해준다.
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.D))
transform.Translate(Vector3.right * Time.deltaTime * _speed);
if (Input.GetKey(KeyCode.A))
transform.Translate(Vector3.left * Time.deltaTime * _speed);
}
- Translate 함수
- Local 좌표계를 기준으로 인수로 들어온 Vector3 만큼 이동한다.
Space.World
를 두 번째 인수로 넘기면 World 좌표계를 기준으로 이동시킬 수 있다.
🙋♂️ Vector3
// Vector3 와 유사하게 만들어 봄
struct MyVector
{
public float x;
public float y;
public float z;
public float magnitude { get { return Mathf.Sqrt(x * x + y * y + z * z); } }
public MyVector normalized { get { return new MyVector(x / magnitude, y / magnitude, z / magnitude); } }
public MyVector(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
public static MyVector operator + (MyVector a, MyVector b)
{
return new MyVector(a.x + b.x, a.y + b.y, a.z + b.z);
}
public static MyVector operator - (MyVector a, MyVector b)
{
return new MyVector(a.x - b.x, a.y - b.y, a.z - b.z);
}
public static MyVector operator * (MyVector a, MyVector b)
{
return new MyVector(a.x * b.x, a.y * b.y, a.z * b.z);
}
}
- Vector3 는 3 개의 float 을 원소로 가지기 때문에 3 가지의 값이 필요한 곳이라면 Positiom, Scale, Rotation 등등 여러가지 곳에서 사용이 가능하다.
Vector3 의 용도
1️⃣ 위치 벡터 를 표현
말 그대로 어떤 좌표. 상태.
2️⃣ 방향 벡터 를 표현
예를 들어 A 위치 벡터에서 B 벡터 만큼 더해서 C 위치 벡터가 된다는 의미에 주목해 보면, B 벡터는 A 위치 벡터가 C 위치 벡터로 향하는 '방향 벡터'가 된다. X 방향으로, Y 방향으로, Z 방향으로 이만큼만 이동 해 보아라 이런 뜻이니까!
void Start()
{
MyVector to = new MyVector(10.0f, 0.0f, 0.0f);
MyVector from = new MyVector(5.0f, 0.0f, 0.0f);
MyVector dir = to - from; // (5.0f, 0.0f, 0.0f); 👉 방향의 크기(거리)는 5, 실제 방향은 오른쪽
}
- 목적지 위치 벡터 - 출발지 위치 벡터 = 방향 벡터
- 출발지로부터 목적지로 향하는 방향을 의미하게 된다.
- 방향 벡터
- 1️⃣ 방향에 대한 크기(스칼라). 즉, 방향에 대한 거리를 알 수 있다.
- ✨
magnitude
public float magnitude { get { return Mathf.Sqrt(x * x + y * y + z * z); } }
- ✨
- 2️⃣ 실제 방향을 알 수 있다.
- ✨
normalized
- 크기가 1 이라 오직 방향에만 무게를 둔 단위 벡터
- 각 요소를 크기(
magnitude
)로 나눠주면 된다.public MyVector normalized { get { return new MyVector(x / magnitude, y / magnitude, z / magnitude); } }
- ✨
- 1️⃣ 방향에 대한 크기(스칼라). 즉, 방향에 대한 거리를 알 수 있다.
void Start()
{
MyVector to = new MyVector(10.0f, 0.0f, 0.0f);
MyVector from = new MyVector(5.0f, 0.0f, 0.0f);
MyVector dir = to - from;
dir = dir.normalized;
MyVector newPos = from + dir * _speed;
}
🙋♀️ Rotation
오브젝트의 회전 값.
회전 값을 절대적으로 설정할 때 👉 eulerAngles
유니티에선 Transform 의 rotation
은 Vecotr3 가 아닌 Quaternion 이다.(x,y,z,w 이렇게 4개의 값이 필요) 따라서 Vector3 형태로 X, Y, Z 축 이렇게 3 개의 회전 값으로 회전 값을 설정하고 싶다면 eulerAngles
를 사용해야 한다.
transform.eulerAngles = new Vector3(0.0f, _yAngle, 0.0f); // Y 축으로 _yAngle 각도 만큼 회전한다.
eulerAngles
- 오일러 각도의 회전 값을 나타내며 Vector3 를 사용해 회전 값을 설정할 수 있다.
- 📢 주의 사항
- 밑에 코드와 같이 절대적인 회전 Vector3 값으로 설정하는 것이 아닌, 이만큼 더 회전해라! 하는 델타 값 의미로
eulerAngles
에 Vector3를 더하고 빼주는건 안된다. - 오일러 각도는 360도를 넘어가면 값의 계산에 실패하기 때문에
eulerAngles
를 얼만큼 더 회전할지의 델타 회전값으로 사용하는 것은 권장하지 않는다.transform.eulerAngles += new Vector3(0.0f, _yAngle, 0.0f); // '+=' ❌❌❌
- 밑에 코드와 같이 절대적인 회전 Vector3 값으로 설정하는 것이 아닌, 이만큼 더 회전해라! 하는 델타 값 의미로
회전 값을 상대적으로 설정할 때 👉 Rotate
eulerAngles
로는 얼만큼 더 회전할지의 델타 회전값으로 사용하는 것이 불가능 하기 때문에, 얼만큼 더 회전할지의 델타 회전 값을 설정해주는 것은 Rotate
함수를 사용하여야 한다. 현재 회전값으로부터 인수로 넘기는 Vector3 만큼 더 회전한다. eulerAngles
와 비슷하게 Vector3를 인수로 받는다. (내부적으로 eulerAngles
만큼 더 회전시킨다.)
transform.Rotate(0.0f, Time.deltaTime * _speed, 0.0f);
쿼터니언 함수 소개
Quaternion.Euler(Vector3)
인수로 받은 Vector3 를 쿼터니언으로 변환하고 이를 리턴해준다. Euler() 함수로 Vector3를 쿼터니언으로 변환하면, 쿼터니언 타입은 transform.rotation
에 할당이 가능해진다.
transform.rotation = Quaternion.Euler(new Vector3(0.0f, _yAngle, 0.0f));
Quaternion.LookRotation()
우리가 원하는 방향을 쳐다 보는 회전 값을 쿼터니언으로 리턴한다. Vector3를 인수로 넘기면 인수로 넘긴 Vector3의 방향을 쳐다보는 회전값(쿼터니언)을 리턴한다.
if (Input.GetKey(KeyCode.S))
{
transform.rotation = Quaternion.LookRotation(Vector3.back);
}
S
키를 누르면 Vector3.back 방향을 바라보는 회전 상태를 쿼터니언으로 반환한다. 이를 transform.rotation
으로 설정하면 그 방향으로 오브젝트가 회전할 것이다.
Quaternion.Slerp(Vector3 a, Vector3 b, float t)
A 에서 B 까지 0.0 ~ 1.0 퍼센트 비율로 보간. Lerp 와 같다!
- 🎈 Lerp 와의 차이점
- Lerp
- 선형 보간법
- Slerp
- 구면 선형 보간법
- 회전이나 방향을 보간할 때 주로 쓰인다.
- Lerp
좀 더 부드럽게 회전시킬 수 있다. A 벡터와 B 벡터 사이를 t
퍼센트로 보간한 결과를 쿼터니언으로 리턴한다.
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), Time.time * speed);
WASD 이동하면서 자연스럽게 그 쪽으로 회전하는 코드
void Update()
{
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.D))
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 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);
}
}
- WASD 키를 누를 때마다 해당하는 방향으로 회전한다.
- 따라서 Translate 으로 이동을 할 때는 상대적인 방향으로 이동 하기 때문에, Translate 의 인수로 넣어주는 Vector3의 방향은 Vector3.forward 가 되야 한다.
- 쿼터니언의 LookRotation 함수 덕에 향하고자 하는 방향으로 회전을 먼저 했기 때문에 자기 자신의 입장에선 상대적으로 그저 앞으로 가면 되기 때문이다.
- 이처럼 회전과 이동을 병행할 땐 방향, 절대적, 상대적 방향을 잘 생각해야 한다.
void Update()
{
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.D))
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
transform.position += Vector3.right * 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;
}
}
Translate 을 썼을 때는 Slerp를 통해 보간 중에도 상대적인, 자기 기준에서의 forward 방향으로 계속 이동을 했어서 조금 부자연스러웠었다. 따라서 이렇게 transform.position += Vector3.forward * Time.deltaTime * _speed; 그냥 절대적인 좌표로 위치를 설정해주면 더 자연스러워질 것이다. Translate 과 다르게 절대적인 좌표를 대입하기 때문에 모두 Vector3.forward 였었던 것과 달리 각각 4 가지의 절대 방향을 다시 설정해주었다.
🙋♀️ Input Manager
Update() 함수내에서 직접 입력이 들어오는 것을 관리하는건 비추다. 공용으로 사용할 수 있는, 입력을 총괄 관리하는 Input Manager 같은 것을 따로 만들면 좋다.
- 게임에 입력 받을 수 있는 키보드 Key 종류가 예를 들어 100개라면, Update() 함수내에서 입력을 관할 했을 때 매 프레임마다 매번 100개의 Key 입력을 전부 검사해야 하므로 비효율적이다.
- 코드를 수정하기도 어렵다.
- 따라서 Update 문에 넣지 않고 공용으로 이곳 저곳에서 사용될 수 있는 입력 검사 관할 함수로서 따로 빼는게 좋다.
- 디버깅도 쉬워지고 수정도 쉬워진다.
📜InputManager.cs
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) // 어떤 키던 간에 false 면 그냥 return
return;
if (KeyAction != null)
KeyAction.Invoke();
}
}
리스너 패턴 (디자인 패턴)
- 📜InputManager.cs
- 일반 C# 스크립트로 구현했다.
- MonoBehavior 을 상속 받지 않고
using System
필요
- 일반 C# 스크립트로 구현했다.
- OnUpdate()
- 어떤 입력이든 입력이 들어온게 아예 없다면 그냥 함수를 실행하지 않고
return
KeyAction
액션에 등록된 함수가 있다면 실행시킨다.if (KeyAction != null) KeyAction.Invoke();
- 어떤 입력이든 입력이 들어온게 아예 없다면 그냥 함수를 실행하지 않고
📜Managers.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Managers : MonoBehaviour
{
static Managers s_instace;
static Managers Instance { get { Init(); return s_instace; } }
InputManager _input = new InputManager();
public static InputManager Input { get { return Instance._input; } }
void Start()
{
Init();
}
void Update()
{
_input.OnUpdate();
}
static void Init()
{
if (s_instace == null)
{
GameObject obj = GameObject.Find("@Managers");
if (obj == null)
{
obj = new GameObject { name = "@Managers" };
obj.AddComponent<Managers>();
}
DontDestroyOnLoad(obj);
s_instace = obj.GetComponent<Managers>();
}
}
}
- 📜Managers.cs 인스턴스(
Instance
)는 싱글톤. - 📜Managers.cs 안에서 📜InputManager.cs 타입의
_input
인스턴스를 생성했으니 이제_input
는 📜Managers.cs의 멤버가 된다.- 따라서
_input
이 📜Managers.cs에 속한 멤버이니 📜InputManager.cs 도 싱글톤인게 보장된다.
- 따라서
Input
는 이 📜Managers.cs 인스턴스의_input
을 불러오는 프로퍼티다. (싱글톤 불러다 주는 static 함수 같은 역할. 프로퍼티로 구현한 것 뿐.)get { return Instance._input; }
- Update() 유니티 이벤트 함수
- 매프레임마다
_input.OnUpdate()
을 실행시켜주기만 하면 땡이다.- 📜InputManager.cs의 OnUpdate() 함수
- 여기서는 또
KeyAction
만 Invoke 시키는 일을 한다.
- 여기서는 또
- 📜InputManager.cs의 OnUpdate() 함수
- 매프레임마다
📜PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
float _speed = 10.0f;
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.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.D))
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
transform.position += Vector3.right * 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;
}
}
}
- 📜InputManager.cs의
KeyAction
에 플레이어의 입력을 검사하는 함수인 OnKeyboard()을 등록해둔다.- 📜Managers.cs와 📜InputManager.cs 에서는
KeyAction
에 어떤 함수들이 등록되어 있는지 알 필요 없이 그저 실행만 해줄 뿐이다. 실행은 매니저에게만 맡긴다.- 입력 키에 대한 어떤 추가 삭제가 있을시 코드 수정을 할 필요가 없다.
- 📜PlayerController.cs의 OnKeyboard() 내용만 수정하면 될 뿐이다. 그리고 이를 그저
KeyAction
등록만 해두면 된다.
- 📜PlayerController.cs의 OnKeyboard() 내용만 수정하면 될 뿐이다. 그리고 이를 그저
- 입력 키에 대한 어떤 추가 삭제가 있을시 코드 수정을 할 필요가 없다.
- 📜Managers.cs와 📜InputManager.cs 에서는
void Start()
{
Managers.Input.KeyAction -= OnKeyboard;
Managers.Input.KeyAction += OnKeyboard;
}
- 📜PlayerController.cs 입장에서는 플레이어의 입력을 검사하는 함수인 OnKeyboard() 내용만 작성하고 이를
KeyAction
에 등록만 해두면 될 뿐이다. 실행은 📜Managers.cs가 해준다. - 등록하기(+=)에 앞서 삭제(-=)를 한번 시켜주는 이유는 실수 방지!! 실수로 다른 곳에서 OnKeyboard() 를 이미 등록했다면 두 번 등록이 되는거라 OnKeyboard() 이 두번 호출되게 된다. 그래서 혹시나 실수했을 경우를 대비해 한번 빼고 시작해주는 것.
📜InputManager.cs 에서 KeyAction
를 Invoke 시킬 때, 등록된 함수인 OnKeyboard()가 📜PlayerController.cs에서 등록되었으므로 이를 기억하고 📜PlayerController.cs 객체의 OnKeyboard() 함수를 호출하게 된다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기