Transform
모든 오브젝트는 기본적으로 Tranform 컴포넌트를 가지고 있고, 많은 위치 이동은 이 컴포넌트에서 지원하는 기능을 통해 구현된다. 따라서 물체의 이동을 구현하기 위해 Transform에 대해 자세히 알 필요가 있다.
이번에는 기능을 파악하기 위해 게임수학을 이해하고 원하는 움직임을 구현해보자.
Transform Component
모든 게임 오브젝트가 가지고 있는 컴포넌트로 추가 및 제거가 불가능하다. Transform은 벡터와 행렬과 밀접한 관련이 있으며, 해당 개념을 명확히 이해하고 적절히 사용할 수 있어야한다.
position
물체의 위치에 대한 정보로 x, y, z축을 기준으로 하는 3차원 좌표값을 가지고 있으며, 유니티에서 지원하는 Vector3 클래스로 쉽게 위치 이동 구현이 가능하다.
public class Tank
{
[Serializefield] private Vector3 targetPos;
[Serializefield] private float moveSpeed;
[Serializefield][Range(0f,1f)] private float rate;
[Serializefield] private Transform target;
[Serializefield] private Transform source;
private void Update()
{
// 1. Vector3를 이용한 위치 설정
transform.position = new Vector3(1,2,3);
transform.position = targetPos;
// 2. 방향으로 이동하기
// 2-1. 1프레임에 1번 이동 → 순식간에 시점에서 벗어난다.
transform.Translate(Vector3.forward);
// 2-2. Time.deltaTime을 활용하여 프레임과 상관없이 동일한 속도로 이동
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
// 3. 일정한 속도로 목적지로 직선 이동하기
transform.position = Vector3.MoveTowards
(transform.position, target.position, moveSpeed * Time.deltaTime);
}
// 4. 보간해서 이동하기 (선분의 내분 공식과 동일)
// → 부드러운 이동을 표현할 때 사용
// 2개 이상의 러프를 활용하여 베지에 곡선을 구현할 수 있다.
transform.position = Vector3.Lerp(source.position, target.position, rate)
// 5. Space에 따른 position
Vector3 worldPos = transform.position;
Vector3 localPos = transform.localPosition;
// 5-1. 로컬 기준 이동(바라보는 방향)
transform.Translate(Vector3.forward * Time.deltaTime, Space.Local);
// 5-2. 월드 기준 이동(북쪽)
// Space.World의 경우 생략 가능
transform.Translate(Vector3.forward * Time.deltaTime, Space.World);
}
Rotation
인스펙터의 Transform에서는 position과 동일한 x,y,z축으로 보여지나 내부 동작은 4원수를 사용하는 quternion을 통해 이루어진다. 이는 연산속도가 빠르고 3축을 사용하는 Euler를 사용할 때의 짐벌락 현상을 방지하기 위함이다.
public class Tank
{
[Serializefield] private float speed;
public void Update()
{
// 구문오류 → 유니티에서 회전은 Quaternion으로 구현되어있기 때문
transform.rotation = new Vector3(0, 60, 0); ❌
// 1. 회전 직접 지정 : Euler를 이용하여 Quaternion으로 변환하여 사용 권장
transform.rotation = Quaternion.Euler(0, 60, 0);
// 1-1. Euler → Quaternion
Quaternion a = Quaternion.Euler(0, 60, 0);
// 1-2 Quaternion → Euler
Vector3 b = transform.rotation.eulerAngles;
// 1-3 방향벡터를 쿼터니언으로 변환
Quaternion c = Quaternion.LookRotation(Vector3.right);
// 1-4 현재 회전을 방향벡터로 전환
Vector3 d = transform.right;
// 2. 축을 지정하여 회전
transform.Rotate(Vector3.forward, speed * Time.deltaTime);
// 3. 지점을 지정하여 회전
transform.RotateAround(target.position, Vector3.up, speed * Time.deltaTime);
// 4. 지점을 바라보도록 회전
transform.LookAt(target.position);
// 4-1. 회전 제한 응용
Vector3 targetPos = target.position;
targetPos.y = transform.position.y; // y좌표 고정
transform.LookAt(targetPos);
// 5. 보간해서 회전하기
// 이동과 동일하게 Lerp 사용이 가능하며, 부드럽게 회전할 수 있다.
Vector3 targetDir = (target.position - transform.position).normalized
Quaternion targetRot = Quaternion.LookRotation(targetDir);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRot, rate);
// 6. Space에 따른 rotation
// 월드 기준 회전
Vector3 worldPos = transform.rotation;
// 상위 계층 혹은 자신 기준 회전
Vector3 localPos = transform.localrotation;
// 6-1. Tank(부모)가 회전해도 turret(자식)은 항상 북쪽을 바라본다.
// 월드 기준 상대 회전이 0이기 때문에 회전하지 않는다.
turret.rotation = Quaternion.identity;
// 6-2. Tank(부모)가 회전하면 turret(자식)의 방향도 따라간다.
// 부모 기준 상대 회전이 0이기 때문에 부모와 같은 회전값을 가져
// 따라가는 것처럼 보인다.
turret.localRotation = Quaternion.identity;
// 6-3. turret의 개별 회전
if(Input) rotate -= 30 * Time.deltaTime;
if(Input) rotate += 30 * Time.deltaTime;
if(Input)
{
transform.Rotation = Quaternion.Euler(0, rotate, 0);
transform.localRotation = Quaternion.Euler(0, rotate, 0);
}
// 6-4 자기 자신 기준 회전(정수리 방향)
transform.Rotate(Vector3.up * Time.deltaTime);
// 6-5 월드 기준 회전(하늘 방향)
transform.Rotate(Vector3.up * Time.deltaTime, Space.World);
}
}
Scale
x, y, z축을 기준으로 물체의 크기 비율을 조절할 수 있다. 기본값은 (1, 1, 1)로, (2, 1, 1)은 x축 방향으로 2배 확대한 크기이다. 음수 값도 가능하며, 이 경우 방향 기준으로 반전이 된다.
GetKey
public class InputTest
{
private void Update()
{
// 특정한 장치의 입력을 기준으로 입력 감지 → 여러 플랫폼 대응이 어려움
// 누르고 있는 동안 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!");
}
// 누르고 있지 않은 동안 true
if(!Input.GetKey(KeyCode.Space))
{
Debug.Log("!Get Key");
}
}
}
public class InputTest
{
private void Update()
{
// 마우스 입력 감지
if(Input.GetMouseButton(0)){}
}
}
GetAxis
puvlic class InputManager
{
public float result;
private void Update()
{
<Input Manager>
// 여러 장치의 입력을 입력매니저에 이름과 입력을 정의
// 정의된 이름으로 입력의 변경사항을 확인
// 에디터 → project Settings, Input Manager 에서 관리
// 단 초창기 방식이기에 키보드, 마우스, 조이스틱만 정의되어있음.
if(Input.GetButtonDown("Fire1"))
{
Debug.Log("Fire1 버튼 눌림");
}
// -1 ~ 1까지의 float값을 부드럽게 변환(부드러운 이동 구현)
result = Input.GetAxis("Horizontal");
}
}
GetAxisRaw
public class InputManager
{
public float result;
private void Update()
{
// -1, 0, 1의 값을 반환 → 즉각적인 반응 구현 가능
result = Input.GetAxisRaw("Horizontal");
// 위와 아래의 코드 결과는 동일하다.
float axis = 0;
if(Input.GetKey(KeyCode.LeftArrow)
{
axis -= 1;
}
if(Input.GetKey(KeyCode.RightArrow)
{
axis += 1;
}
}
}
public class Tank
{
[Serializefield] float moveSpeed;
[Serializefield] float rotateSpeed;
private void Update()
{
Move();
Rotate();
}
private void Move()
{
float input = Input.GetAxis("Vertical");
transform.Translate
(Vector3.forward * moveSpeed * input * Time.deltaTime);
}
private void Rotate()
{
float input = Input.GetAxis("Horizontal");
transform.Rotate
(Vector3.up, rotateSpeed * input * Time.deltaTime);
}
}
Frame
프레임은 현대 PC 사양을 나타내는 지표 중 하나로 FPS(초당 프레임)으로도 표현하기도 한다. 다만 게임을 제작하는 개발자 입장에서 frame은 1초마다 Update함수 동작 횟수를 의미하는데, frame이 높을수록 Update가 많이 실행되며, 동일한 스크립트여도 컴퓨터 사양에 따라 성능이 달라지는 문제가 발생한다.
이렇게 호출 횟수가 PC의 사양에 따라 달라지게 된다면 사용자는 불편을 느끼게 될 것이고, 따라서 우리는 이러한 문제를 미리 해결할 필요가 있다.
public void Update()
{
transform.Translate(Vector3.forward);
}
만약 이렇게 스크립트를 짰다면 100프레임에서는 1초에 100번 이동하고, 10프레임에서는 1초에 10번 움직이기에 프레임이 낮으면 더 적은 거리를 이동하게 되는 것이다.
따라서 이를 해결하기위해 추가적인 조치가 필요하며, 대표적인 방법으로 프레임을 고정시키거나, Time.deltaTime을 활용하는 방법 등이 있다.
Time.deltaTime
프레임에 역수를 취한 값으로, 이번 Update 이후 다음 Update가 동작하는데까지 걸린 시간 즉 1프레임당 걸린 시간을 의미한다. 이걸 활용하면 다른 프레임 환경에서도 동일한 속도로 움직이도록 만드는 것이 가능하다.
public void Update()
{
transform.Translate(Vector3.forward * Time.deltaTime);
}
해당 스크립트를 보면 100프레임에서는 1초에 100번 업데이트가 실행되고 10프레임에서는 1초에 10번 업데이트가 실행되지만 Time.deltaTime
이 각각 1/100, 1/10의 값을 가지기 때문에 1초 동안 이동한 거리가 동일하므로, 다양한 프레임 환경에서 속도가 모두 같도록 만들 수 있는 것이다.
FixedUpdate
주기적인 업데이트가 필요한 물리 엔진의 경우 Update가 아닌 FixedUpdate에 넣는 것이 좋다. Update는 프레임에 따라 횟수가 달라지지만 FixedUpdate는 동일한 주기로 실행되기 때문이다. 가장 대표적인 예시로 AddForce가 있다.
마찬가지 이유로 transform.Translate을 Update에서도 사용할 수 있지만, Rigidbody를 사용한 정교한 이동은 FixedUpdate에서 처리해야 안정적으로 구현이 가능하다.
World Space vs Local Space
World Space
게임 환경 내의 고정된 글로벌 좌표계(절대 위치)
씬의 월드 기준으로 고정된 방향으로 이동 및 회전을 하기에 상위 계층, 프리팹의 transform의 월드 기준 이동(북쪽 이동)을 구현할 때 사용한다.
부모의 좌표계를 기준으로 하는 좌표계(상대 위치)
상위 계층 혹은 자신이 바라보는 방향 기준으로 이동 및 회전하기 때문에, 하위 계층의 계층 내의 독립적인 움직임을 구현할 때 사용한다.