게임 오브젝트의 위치, 회전, 크기를 저장하는 컴포넌트임과 동시에 부모-자식 상태를 저장하는 컴포넌트이다. (따라서 자기 자신의 Transform은Transform은 GameObject ~ 를 쓰지 않고 바로 정보를 가져올 수 있다.)
게임 오브젝트는 반드시 하나의 트랜스폼 컴포넌트를 가지고 있으며 추가 & 제거할 수 없다.
트랜스폼에 있는 요소에 대해 알아보고자 한다.
유니티에서 오브젝트의 위치 정보는 x, y, z 3축으로 구성된다. 이는 유니티에서 Vector3 클래스를 통해 간편하게 다룰 수 있으며, 이를 사용해 게임 오브젝트의 위치 이동을 구현할 수 있다.
주요 코드
public class TransformTester : MonoBehaviour
{
[Range(0f, 1f)]
[SerializeField] float rate;
[SerializeField] Transform target;
[SerializeField] float moveSpeed;
private void Update()
{
// 1. 벡터를 이용한 위치 설정
transform.position = new Vector3(1, 2, 3);
// 2. 방향으로 이동시키기
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
// 3. 목적지로 이동시키기
transform.position = Vector3.MoveTowards(transform.position, target.position, moveSpeed * Time.deltaTime);
// 4. linearly interpolate 보간해서 이동시키기
transform.position = Vector3.Lerp(transform.position, target.position, rate);
}
}
여기서 4. 보간을 사용해서 이동시키는 방법의 경우, rate를 조절하는 방식 등을 사용하여 이동 속도를 조절할 수 있다. 또한 보간을 사용하는 경우 오차로 인해 정확하게 도달하지 못하는 경우가 생길 수도 있어 해당 부분은 감안해야 한다.
인스펙터의 Transform 컴포넌트에서는 회전값이 포지션과 동일하게 x, y, z축으로 보여지지만 내부 동작은 4원소를 사용하는 쿼터니언을 통해 이뤄진다. 이는 연산속도가 빠르고 3축을 사용했을 때의 짐벌락 현상을 방지하는 데 효과적이다.
쿼터니언은 연산적으로 효과적이지만, 유니티 입문 시점에서는 이해하기 복잡한 개념이므로, 보다 직관적인 Quaternion클래스의 Euler를 사용한다.
x축, y축, z축으로 회전 시킬 때, 한 축을 회전시키는 과정에서 다른 두 가지 축이 겹쳐버리고, 나머지 두 축 중 어떠한 것을 움직여도 똑같이 회전하는 현상을 말한다.
내용을 자세히 다루진 않겠지만, 오일러 각 공식과 함께 아래 영상의 내용을 보고 이해할 수 있을 것 같다는 생각이 들었다.
[짐벌락은 왜 생기는가?]
https://www.youtube.com/watch?v=vHr77Dre25Q
public class TransformTester : MonoBehaviour
{
[Range(0f, 1f)]
[SerializeField] float rate;
[SerializeField] Transform target;
[SerializeField] float rotateSpeed;
private void Update()
{
// 1. 회전 직접 지정 : Euler를 이용하여
transform.rotation = Quaternion.Euler(0, 60, 0);
// 1-1. 오일러를 쿼터니언으로 변환
Quaternion a = Quaternion.Euler(0, 60, 0);
// 1-2. 쿼터니언을 오일러로 변환
Vector3 b = transform.rotation.eulerAngles;
// 1-3. 방향벡터를 쿼터니언으로 변환
Quaternion c = Quaternion.LookRotation(Vector3.right);
// 1-4. 현재 회전을 방향벡터로 변환
Vector3 d = transform.right;
// 2. 축을 기준으로 회전
transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
// 3. 지점을 기준으로 회전
transform.RotateAround(target.position, Vector3.up, rotateSpeed * Time.deltaTime);
// 4. 지점을 바라보도록 회전
transform.LookAt(target.position);
// 바로 각도가 바뀌는 게 아니고 점차적으로 회전
Vector3 targetDir = (target.position - transform.position).normalized;
Quaternion targetRot = Quaternion.LookRotation(targetDir);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRot, rate);
}
}
직관적인 이름 그대로 게임 오브젝트의 크기를 비율로 지정할 수 있는 요소이다
// Scale 조절
Transform.localScale = new Vector3(x, y, z);
// lossyScale은 부모 오브젝트의 스케일에 상관 없이 자신 고유의 Scale 수치를 반환
Transform.lossyScale
월드 스페이스는 게임 환경 내의 고정된 글로벌 좌표계 ( 절대 좌표 )를 의미한다.
이 좌표계는 게임의 시작부터 끝까지 변하지 않으며, 모든 오브젝트들이 이 월드 스페이스 내에서 위치를 가지게 된다.
월드 스페이스에서의 오브젝트의 위치, 회전, 크기는 월드 포지션(World Position), 월드 회전(World Rotation), 월드 스케일(World Scale)로 표현된다.
로컬 스페이스는 오브젝트나 컴포넌트의 개별적인 좌표계 ( 상대 좌표 )를 의미한다.
특히 부모-자식 관계에서 중요하며, 자식 오브젝트의 로컬 스페이스는 부모 오브젝트에 대해 상대적이다.
Ex) 자식 오브젝트의 로컬 포지션이 (0, 0, 0)이라면, 이는 부모 오브젝트의 위치와 동일하다.
로컬 스페이스에서의 오브젝트의 위치, 회전, 크기는 로컬 포지션(Local Position), 로컬 회전(Local Rotation), 로컬 스케일(Local Scale)로 표현된다.
// 월드 기준 위치
Vector3 position = transform.position;
Quaternion rotation = transform.rotation;
// 로컬 기준 위치
Vector3 localPosition = transform.localPosition;
Quaternion localrotation = transform.localRotation;
// Translate는 설정하지 않으면 local 스페이스 기준으로 이동시키지만, world 스페이스로 이동시킬 수도 있다.
transform.Translate(Vector3.forward * Time.deltaTime, Space.World);
PC의 사양을 나타내는 지표 중 하나로서 FPS(Frames Per Second) 라는 것이 있다. 동영상이나 애니메이션에서 자연스러운 영상을 보여주기 위한 프레임 수로도 들어본 적 있을 것이다.
여기서 컴퓨터에서의 FPS는, 게임의 출력 속도 및 연산속도, 그러니까 1초에 몇 번 연산할 수 있는지에 대한 지표라고 할 수 있다. 그렇다면 이런 문제가 발생할 수도 있다.
FPS가 다른 컴퓨터에서 동일한 게임을 플레이할 때, 똑같이 방향키를 누르더라도 방향키 입력이 반영된 횟수가 다를 수도 있지 않을까?
이와 같이 컴퓨터의 성능에 따라 캐릭터의 이동속도가 달라지고, 게임 판정이 달라질 수 있는 문제가 생길 수 있다.
세상에 게임을 플레이하는 모든 컴퓨터의 성능을 동일하게 맞출 수는 없으니, FPS가 다른 컴퓨터에서도 동일한 판정이 될 수 있도록 조정이 필요하다.
유니티에서는 프레임에 따른 차이를 줄이기 위한 Delta Time이란 기능을 제공한다.
Delta Time의 공식은 아래와 같이 설정되어 있다.
Delta Time = 1 / FPS
즉, 이 공식을 반영하여 아래와 같이 두 컴퓨터가 있다고 할 때, 연산량을 계산하면 이렇게 나온다.
1초에 연산을 10회 하길 원한다고 할 때
- FPS가 10인 컴퓨터 : 10(FPS) * deltaTime * 10(회) = 10 * 1/10 * 10 = 10
- FPS가 100인 컴퓨터 : 100(FPS) * deltaTime * 10(회) = 100 * 1/100 * 10 = 10
이와 같이 다른 성능의 컴퓨터에서도 동일한 연산을 진행하여, 게임 플레이에 컴퓨터의 성능의 영향이 없도록 할 수 있다.
단일 키 입력에 대한 여부를 반환받기 위한 함수이다. 키가 눌리는 판정에 따라 세 가지 bool 타입 반환 함수를 적절하게 사용한다.
// Key가 눌린 내내 True 판정
Input.GetKey()
// Key가 눌린 순간 1회 True 판정
Input.GetKeyDown()
// 눌려 있던 키를 뗀 순간 1회만 True를 반환
Input.GetKeyUp()
// 사용 예시
// 누르고 있는 동안 true
if (Input.GetKey(KeyCode.Space))
{
Debug.Log("Get Key!");
}
// 누른 순간에 true
if(Input.GetKeyDown(KeyCode.Space))
{
Debug.Log("Get Key Down!");
}
// 눌렀다 뗀 순간에 true
if (Input.GetKeyUp(KeyCode.Space))
{
Debug.Log("Get Key Up!");
}
GetKey로 불러올 수 있는 키의 종류는 키보드, 방향키, 마우스, 조이스틱 등 다양하다.
마우스의 경우 0 : 좌클릭, 1 : 우클릭, 2 : 휠클릭이다.
해당 내용에 대해 알아보기에 앞서, InputManager에 대해 알아보자.
Edit에 Project Settings에 들어가보자.
그러면 위와 같이 Input Manager의 내용을 확인할 수 있는데, 이것은 유니티에서 기본적으로 제공해주는 키 입력 매니저다.
여기에서 Horizontal과 Vertical의 경우를 살펴 보면 아래와 같이 내용이 적혀 있다.
Horizontal을 사용하면 기본적으로 키입력 좌우 방향키, 혹은 A/D로 좌우 이동 입력(X축 이동)을 반환해주는 것이다. Vertical의 경우는 Z축 이동을 반환하며 상하 방향키, 혹은 W/S로 사용할 수 있다. 이를 이용하여 플레이어의 이동을 간단하게 구현할 수 있다.
// 예시 코드 - 앞 뒤 움직임 구현하기
private void Move()
{
float input = Input.GetAxis("Vertical");
transform.Translate(Vector3.forward * moveSpeed * input * Time.deltaTime);
}
여기에서 GetAxis와 GetAxis가 어떠한 것인지 자세히 알아보자.
- GetAxis
유니티에서 제공하는 Input Manager에 대응하는 코드다.
유니티 좌표상의 X(Horizontal), Z(Vertical)축에 해당하는 입력 데이터이며, 미 입력시 0, 방향과 입력에 따라 -1.0 ~ 1.0 사이의 float 타입을 반환한다.
- GetAxisRaw
GetAxis와 마찬가지로 Input Manager에 대응하고 유니티 좌표상의 X, Z축에 해당하는 입력 데이터를 -1, 0, 1로 반환한다.
실제로 점차적인 움직임 보다는 입력값이 딱 떨어지는 GetAxisRaw 쪽이 움직임 부분에서는 자연스러워 보이기도 했다.
아까의 InputManager를 다시 살펴 보자.
InputManager는 Size를 키울 수 있고, 칸을 추가하여 원하는 입력값과 세부사항을 추가할 수 있다.
이런 기능이 필요할 만한 상황은, 게임 자체가 다양한 플랫폼으로 출시되어야 할 때 유용할 것이라는 걸 알 수 있다.
컴퓨터는 키보드 입력을 받아야 하고, 휴대폰은 터치나 스크린 버튼을 지원해야 하고, 콘솔의 경우는 게임 패드로 키 입력을 받아야 하는 등 다양한 상황이 존재할 수 있다.
이러한 상황을 생각하여 공통적으로 하는 '행동 키입력'을 정한 뒤에, 키 배정은 InputManager를 통해 각각 처리할 수 있는 것이다.
이렇게 하면 게임 상의 행동을 코드로 표현할 때 더 간단하게 표현할 수 있다.
if(Input.GetButtonDown("Fire1"))
{
Debug.Log("Fire1 버튼 눌림");
}
using UnityEngine;
public class TankMovement : MonoBehaviour
{
[SerializeField] float moveSpeed;
[SerializeField] float rotateSpeed;
void Update()
{
Move();
Rotate();
}
private void Move()
{
if (Input.GetKey(KeyCode.UpArrow))
{
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.DownArrow))
{
transform.Translate(Vector3.back * moveSpeed * Time.deltaTime);
}
}
private void Rotate()
{
// 운전하는 시점으로 했을 때 후진 시 회전 방향이 부자연스럽다고 느껴 코드를 추가했습니다.
// 좌회전 핸들을 꺾은 상태에서 후진하면 ㄱ자(?) 방향으로 회전하고
// 우회전 핸들을 꺾은 상태에서 후진하면 역ㄱ자(?) 방향으로 회전한다고 생각함
if ((!Input.GetKey(KeyCode.UpArrow) && !Input.GetKey(KeyCode.DownArrow)) && Input.GetKey(KeyCode.LeftArrow))
{
transform.Rotate(Vector3.up, -rotateSpeed * Time.deltaTime);
}
if ((!Input.GetKey(KeyCode.UpArrow) && !Input.GetKey(KeyCode.DownArrow)) && Input.GetKey(KeyCode.RightArrow))
{
transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.UpArrow))
{
transform.Rotate(Vector3.up, -rotateSpeed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.DownArrow))
{
transform.Rotate(Vector3.up, rotateSpeed* Time.deltaTime);
}
if (Input.GetKey(KeyCode.RightArrow) && Input.GetKey(KeyCode.UpArrow))
{
transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.RightArrow) && Input.GetKey(KeyCode.DownArrow))
{
transform.Rotate(Vector3.up, -rotateSpeed * Time.deltaTime);
}
}
}
using UnityEngine;
public class TankMovementUpgrade : MonoBehaviour
{
[SerializeField] float moveSpeed;
[SerializeField] float rotateInterPolate;
void Update()
{
SetPosition();
}
private void SetPosition()
{
Vector3 direction = Movement();
if (direction == Vector3.zero)
{
return;
}
transform.rotation = Quaternion.Lerp
(
transform.rotation,
Quaternion.LookRotation(direction),
rotateInterPolate * Time.deltaTime
);
transform.position += moveSpeed * Time.deltaTime * direction;
}
private Vector3 Movement()
{
Vector3 inputDirection = Vector3.zero;
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.RightArrow))
{
inputDirection.x = Input.GetAxisRaw("Horizontal");
}
if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.DownArrow))
{
inputDirection.z = Input.GetAxisRaw("Vertical");
}
return inputDirection.normalized;
}
}
이 와중에 코드를 작성하면서 알게 된 사실이 있는데, GetAxis를 사용할 경우 따로 월드 스페이스라고 명시해주지 않아도 월드 스페이스 단위로 움직인다는 사실이었다.
아무래도 정의에 명시되어 있다시피 '유니티 좌표상의 X(Horizontal), Z(Vertical)축에 해당하는 입력 데이터'이다 보니, 게임 오브젝트의 입장에서의 좌표가 아닌 월드 좌표를 기준으로 움직임이 반영되는 것이라는 걸 확인할 수 있었다.