[3D,Transform]2-1. Position과 좌표계

0

유니티 엔진

목록 보기
19/21

플레이어 설정

유니티 상에서 움직일 캐릭터를 에셋스토어에서 다운받고,테스트를 위해 플레이어 컨트롤러 스크립트를 포함시켜서 간단하게 움직이는 설정을 해보겠습니다.

  1. 에셋 스토어에서 마음에 드는 캐릭터를 다운받고 유니티에 컴포트합니다.

  2. 모델을 하이이얼아키에 가져오고 오브젝트 이름을 Player로 바꿔줍니다. PlayerController 스크립트를 만들어서 모델의 게임 오브젝트에 붙여줍니다.

  3. 현재 구조는 다음과 같습니다.(모델의 파츠는 생략)

  • Player(최상단 게임 오브젝트)
    • transform 컴포넌트
    • Animator 컴포넌트
    • PlayerController 스크립트 컴포넌트
  1. PlayerController에서 다음 내용을 작성합니다.
public class PlayerController : MonoBehaviour
{
    [SerializeField]
    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.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;
    }
    
}

결과

하나씩 파헤쳐 보기

코드가 각각 어떤 역할을 하는지 살펴보겠습니다.

[SerializeField]

유니티는 변수를 public으로 설정했을때 인스펙터 창에서 변수가 노출되고 인스펙터상에서 변경할 수 있습니다.
하지만 모든 변수를 public으로 설정하는것은 은닉성과 캡슐화를 생각했을때 좋지 않다는것을 알고있습니다.
유니티는 C#에서 제공하는 리플렉션 기능으로 변수는 private지만 인스펙터에 노출할 수 있는 방법을 구현했습니다.
방법은 간단하게 private로 설정한 변수 위에 [SerializeField]를 써주면 됩니다.

Input.GetKey(KeyCode.W)

게임 런타임중 키보드를 입력받았을때 메시지로 알려줍니다.
update 이벤트 함수에서 if문안에 넣을 경우 키가 눌려있는 프레임동안 true를 반환합니다.

transform.position

게임에서 다른 컴포넌트와 달리 transform은 매우 많이 접근해야하는컴포넌트이므로 유니티에서는 간략하게 접근하는 방법을 제공합니다.
transform.position은 현재 스크립트가 속한 오브젝트의 transform의 position에 접근합니다.
position은 현재 오브젝트의 월드 좌표를 의미합니다. 반환값은 Vector3입니다.

new Vector3(1.0f, 0.0f, 0.0f)

벡터는 게임에서 두가지 목적으로 쓰입니다.
1. 방향벡터
2. 위치벡터
현재는 방향벡터의 목적으로 쓰였고, 생성자 매개변수는 각각 x,y,z축을 나타냅니다. 따라서 new Vector3(1.0f, 0.0f, 0.0f)는 x축을 가리키고있는 방향을 뜻합니다.
참고로 유니티에서는 방향을 나타내는 벡터를 따로 정의해 두었습니다.
따라서 위 코드를 아래와 같이 변경하여 가독성을 높여주겠습니다.

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;
}

_speed

속력(speed)이란 '같은 시간만큼 이동한 거리'를 뜻합니다.
사람은 1분동안 240미터를 이동하고,
자동차는 1분동안 600미터를 이동합니다.
자동차가 1분이라는 같은 시간동안 더 많은 거리를 이동한 이유는 속력이 다르기 때문입니다.
결국 속력은 같은 시간동안 이동한 거리를 구하면 되기 때문에 다음과 같은 공식이 나옵니다.

속력(speed) = 이동거리 / 걸린시간

이를 이항하여 플레이어를 움직이는 이동거리를 구하면 다음과 같습니다.

이동거리 = 걸린시간 * 속력(speed)

따라서 _speed는 임의의 숫자로 10.0f를 주었습니다.

Time.deltaTime

여기서 문제는 걸린 시간입니다. 왜냐하면 게임의 프레임 속도는 일정하지 않으며, Update 함수 콜 사이의 시간 간격 역시 일정하지 않다는 점 때문입니다.

게임을 하면서 FPS(Frames Per Second)라는 단어를 자주 들어보았을 것입니다.
이는 '초당 프레임 수'라는 의미를 가지고 있습니다.

프레임이란 게임내에서 모든 조작입력, 판정 계산, 동작에 들어가는 일련의 시간을 수량화하는 최소의 시간 단위입니다. 간단하게 말하면 업데이트 함수를 한번 돌려서 걸린 시간 지표라고 봐도 무방하고,
그러니 FPS는 1초에 업데이트 함수를 몇번돌렸는지 나타내는 지표라고 생각해도 괜찮습니다.

Time.deltaTime은 '지난 프레임을 진행하는데 걸린 시간'을 의미합니다.

보통 30프레임 기준의 게임에서는 한 프레임당 deltaTime이 0.033초 정도가 나와야하고 60프레임 기준의 게임에서는 한 프레임당 0.016초 정도가 나와야 합니다.

코드로 확인해 보자면 유니티 에디터에서 스크립트를 하나 생성하고 Start 함수에서 Application.targetFrameRate를 30으로 지정한 다음 Update 함수에서 Time.deltaTime을 디버그로 출력시키도록 코드를 작성합니다.

public class FrameRate : MonoBehaviour
{
    void Start()
    {
        Application.targetFrameRate = 30;	//게임의 프레임 속도를 제어하는 데 사용합니다. 
    }

    void Update()
    {
        Debug.Log(Time.deltaTime);
    }
}

실행해보면 앞에서 말한대로 1프레임마다 약 0.033초에 가까운 값이 매번 갱신되는 것을 볼 수 있습니다.

여기서 프레임마다 속도가 다른 이유는 프로세서(cpu)의 성능 때문입니다.
프로세서가 좋을 수록 0.03초에 가까운 속도로 성능을 내며, 프로세서가 다른 작업을 하느라 게임 연산 로직에 쏟을 자원이 없다던가 단순히 프로세서의 연산속도가 떨어지면 프레임마다 걸리는 속도가 늦춰집니다.

예를들어
프레임을 30으로 제한하고 캐릭터를 1미터 움직이는 로직을 update()에 작성합니다.
여기서 걸리는 delta Time을 통해 컴퓨터 A,B에서 측정할겁니다.

컴퓨터 A는 성능이 좋아서 Time.deltaTime 평균 0.03초를 내었고,
컴퓨터 B는 성능이 좋지 못해 평균 0.06초를 내었다고 가정하겠습니다.(두 배의 성능차이)

단순 계산으로 0.06초동안
A컴퓨터는 업데이트 함수를 2번 돌릴 수 있고(연산을 두 번할수 있다는 뜻),
B컴퓨터는 1번만 돌릴 수 있습니다.

이 상태에서 캐릭터를 전진 한다면 같은 시간 내에 A는 2미터 거리에 있고,
B 캐릭터는 1미터 거리에 있게 됩니다.

이 문제를 해결하기 위한 방법이 바로 이 이동 값에 delta time을 곱해주는 겁니다.
이렇게 이동 벡터에 delta time을 곱해주고 나면, 게임이 몇 프레임으로 진행되는지에 전혀 상관없이 오브젝트는 동일한 속도로 움직이게 됩니다.

A : 0.03f(delta time) ✖ speed(10.0f) ✖ FPS(30) = 9.0f (1초 동안 이동한 거리)
B : 0.06f(delta time 성능차로 인해↑) ✖ speed(10.0f) ✖ FPS(성능차로 인해↓ 15) = 9.0f
성능이 좋지 않아서 FPS가 떨어져도 delta time이 늘어나서 그 값이 가중치로 계산됨.

이렇게 deltaTime을 곱해서 흘러간 프레임 시간만큼만 가중치를 줌으로써 프로세서 속도의 영향에서 자유로워 지게됩니다.

이동 방향과 좌표계

플레이어를 인스펙터 상에서 90도로 돌려보겠습니다.

그리고 실행 해보면 원하는 방향과는 다르게 움직입니다.
w(앞)키를 눌러도 캐릭터가 바라보는 방향으로 전진하는것이 아니라 월드 좌표계의 Front방향으로 움직이고있습니다.
여기서 월드 좌표계와 로컬 좌표계의 개념을 알아야합니다.

월드 좌표계란?

Unity에서 월드 좌표계는 씬 내에서 게임 오브젝트의 위치 및 방향을 지정하는 기준이 되는 전역 좌표계를 말합니다. 모든 오브젝트에 일관되고 고정된 기준 프레임을 제공하며 위치, 회전, 스케일 등을 저장하는데 사용됩니다.

사실 실제 세상에서 세상을 중심으로 어떠한 객체가 어느 위치에 있느냐 하는 것은 그 세상의 중심이 어디인지는 사람마다 생각이 다르고 절대적이라고 할 수 있는 중심이 없기 때문에 세상의 중심을 기준으로 한 위치라는 것은 구할 수 없겠지만, 유니티 엔진에서는 X, Y, Z가 한 점에서 모이는 (0, 0, 0)이 바로 게임 안에서의 세상의 중심이 됩니다.

월드 좌표계의 특징

  • 데카르트 좌표계: Unity의 월드 좌표계는 세 개의 축을 사용하여 위치를 지정하는 데카르트 좌표계를 따릅니다: X축은 가로 방향, Y축은 세로 방향, Z축은 깊이 또는 앞/뒤 방향을 나타냅니다.

  • 원점: 세계 좌표계의 원점은 X, Y, Z 축이 교차하는 지점(0, 0, 0)입니다. 모든 위치가 측정되는 기준점 역할을 합니다.

  • 절대 위치: Unity의 게임 오브젝트에는 월드 좌표계에서 절대 좌표로 지정된 위치가 있습니다. 게임 오브젝트의 위치는 월드 원점을 기준으로 씬에서 해당 게임 오브젝트의 위치를 나타냅니다.

  • 글로벌 회전: 월드 좌표계에서 게임 오브젝트의 회전은 전역 축에 대한 게임 오브젝트의 방향을 결정합니다. 회전 값은 오일러 각도 또는 쿼터니언으로 게임 오브젝트가 X, Y, Z 축을 중심으로 회전하는 방식을 정의합니다.

  • 글로벌 스케일: 월드 좌표계에서 게임 오브젝트의 스케일은 전역 참조를 기준으로 게임 오브젝트의 크기를 결정합니다. 스케일링은 게임 오브젝트와 그 자식 게임 오브젝트의 전체 크기에 모든 방향에서 균일하게 영향을 줍니다.

  • 부모-자식 계층 구조와는 무관합니다: 부모 게임 오브젝트에 상대적인 로컬 좌표계와 달리 월드 좌표계는 부모-자식 관계와 무관합니다. 이는 계층 구조에 관계없이 씬의 모든 게임 오브젝트에 일관된 참조 프레임을 제공합니다.

  • Unity 에디터 및 트랜스폼 기즈모: Unity 에디터에서 월드 좌표계는 트랜스폼 기즈모라는 시각적 보조 도구로 표시됩니다. 트랜스폼 기즈모를 사용하면 Unity 에디터의 트랜스폼 툴을 사용하여 월드 좌표계에서 게임 오브젝트의 위치, 회전, 배율을 조작할 수 있습니다.

로컬 좌표계란?

Unity에서 로컬 좌표계는 씬 내의 특정 게임 오브젝트를 기준으로 하는 좌표계를 의미합니다. Unity의 각 게임 오브젝트에는 부모 게임 오브젝트 또는 월드 원점을 기준으로 위치, 회전, 배율을 정의하는 고유한 로컬 좌표계가 있습니다. 로컬 좌표계는 오브젝트의 이동과 회전을 계산하는 데도 사용됩니다.

위의 이미지를 보자. 스피어 오브젝트 하나가 월드 좌표를 설명할 때 사용했던 큐브 오브젝트보다 XZ좌표가 각각 1씩 월드의 중심에 가깝게 존재하고 있습니다. 큐브 오브젝트의 위치가 {-6, 0, -4}였으니, 스피어 오브젝트는 {-5, 0, -3}의 위치에 있습니다. 만약에 추가된 이 스피어 오브젝트를 큐브 스피어를 중심으로 공전하게 만들고 싶다면 어떻게 해야할까요?

만약 월드 좌표만으로 처리하려고 한다면 위의 이미지와 같이 좌표가 복잡하게 바뀌게됩니다.
이 방식으로 구현하려면 위성의 공전 궤도를 계산하듯 스피어 오브젝트의 위치가 바뀔 때마다 복잡한 계산을 해야합니다.

하지만 스피어 오브젝트를 큐브 오브젝트의 자식 오브젝트로 만들면 포지션이 월드의 중심 좌표를 기준으로한 월드 좌표인 {-5, 0, -3}이 아니라 큐브 오브젝트를 중심으로한 로컬 좌표 로 표시되는 것을 확인할 수 있습니다.

이렇게 하고 나면 간단하게 큐브 오브젝트를 회전시키는 것만으로도 궤도를 따라서 스피어 오브젝트가 간단하게 공전하는 것을 볼 수 있습니다. 물론 큐브도 함께 자전한다는 문제가 있기는 하지만 이런 문제는 간단하게 해결하고 스피어 오브젝트만 궤도를 따라서 공전하게도 만들 수 있습니다.

로컬 좌표계의 특징

  • 위치: 로컬 좌표계에서 게임 오브젝트의 위치는 부모 게임 오브젝트 또는 월드 원점을 기준으로 한 게임 오브젝트의 위치를 나타냅니다. 위치는 세 가지 값을 사용하여 정의됩니다: X, Y, Z 좌표입니다.

  • 회전: 로컬 좌표계에서 게임 오브젝트의 회전은 부모 게임 오브젝트 또는 월드에 대한 게임 오브젝트의 방향을 결정합니다. 회전은 오일러 각도(X, Y, Z 축을 중심으로 회전을 나타내는 세 가지 값) 또는 쿼터니언(회전을 수학적으로 표현한 것)으로 표현할 수 있습니다.

  • 스케일: 로컬 좌표계에서 게임 오브젝트의 스케일은 부모 게임 오브젝트 또는 월드에 대한 상대적인 크기를 결정합니다. 스케일은 세 가지 값을 사용하여 정의됩니다: X, Y, Z 스케일 인자입니다. 배율은 게임 오브젝트와 그 자식 게임 오브젝트의 크기에는 영향을 미치지만 위치나 회전에는 영향을 미치지 않습니다.

  • 부모-자식 계층 구조: Unity의 게임 오브젝트는 부모-자식 관계를 설정하여 계층 구조로 구성할 수 있습니다. 게임 오브젝트가 다른 게임 오브젝트의 자식으로 설정되면 해당 게임 오브젝트의 로컬 좌표계는 부모의 좌표계에 상대적인 좌표계가 됩니다. 이를 통해 자식 게임 오브젝트가 부모의 위치, 회전 및 배율을 상속하는 계층적 변환이 가능합니다.

  • 트랜스폼 순서: Unity에서 트랜스폼(이동, 회전, 크기 조정)은 TRS 순서라고 하는 특정 순서로 적용됩니다. 순서는 다음과 같습니다: 스케일, 회전, 마지막으로 변환 순입니다. 이 순서에 따라 변환이 결합되는 방식이 결정되며 로컬 좌표계에서 게임 오브젝트의 최종 위치, 회전 및 배율에 영향을 줍니다.

  • 로컬에서 월드로의 변환: Unity는 로컬 좌표를 월드 좌표로 변환하는 함수와 프로퍼티를 제공합니다. 로컬에서 월드로의 변환은 게임 오브젝트와 그 부모 게임 오브젝트의 로컬 변환을 고려하여 게임 오브젝트의 글로벌 위치, 회전, 스케일을 생성합니다.

문제 해결하기

우리가 하고 싶은것은 플레이어 캐릭터가 바라보는 기준으로 앞으로 움직이게 하는것 입니다.
여기서 위에서 작성한 코드를 보면

transform.position += //여기는 월드 좌표계의 방향을 생각하며 작성
Vector3.forward * Time.deltaTime * _speed; //여기는 로컬 좌표계의 방향을 생각하며 작성

즉, 두 좌표계를 혼동해서 사용했기에 원하지 않는 방식으로 움직이는걸 알 수 있습니다.
그러므로 로컬 좌표계를 월드 좌표계로 변환하는 과정이 필요합니다.

1. transform.TransformDirection

유니티는 로컬->월드로 방향을 변환하는 함수를 제공합니다.
이것을 사용해서 방향을 변환합니다.

(참고로 transform.InverseTransformDirection는 월드->로컬로 방향을 변환하는 함수입니다.)

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.A))
        transform.position += transform.TransformDirection(Vector3.left * Time.deltaTime * _speed);
    if (Input.GetKey(KeyCode.D))
        transform.position += transform.TransformDirection(Vector3.right * Time.deltaTime * _speed);
}

2. transform.Translate

이 함수는 캐릭터가 바라보고있는 로컬 좌표계 기준으로 연산을 해줍니다.

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.A))
        transform.Translate(Vector3.left * Time.deltaTime * _speed);
    if (Input.GetKey(KeyCode.D))
        transform.Translate(Vector3.right * Time.deltaTime * _speed);
    
}

0개의 댓글