[Unity] 존 레몬의 공포 체험 (2)

Kim Yuhyeon·2023년 2월 13일
0

게임개발

목록 보기
72/135

스크립트 작성


참고

https://learn.unity.com/tutorial/kaerigteoyi-umjigimeul-wihan-seukeuribteu-jagseong?uv=2020.3&projectId=633563fcedbc2a7b67ba2f8f


명명 규칙

    Vector3 m_Movement;

하지만 m 접두사로 시작하고 모든 단어가 대문자로 시작하는 non-public 멤버 변수는 예외이며, 이를 파스칼 대문자(PascalCase)이라고 합니다.
멤버 변수는 특정 메서드가 아닌 클래스에 속한 변수입니다.
non-public 멤버 변수의 m
부분은 '멤버(member)' 변수라는 데서 유래한 것입니다.

    public float turnSpeed;

m_ 접두사가 붙는 파스칼 표기법이 아닌 낙타 표기법을 사용했습니다.
이는 변수가 public이기 때문이며, Unity 명명 규칙에서는 public 멤버 변수에 이 형식을 사용합니다.
명명 규칙은 기술적 근거는 없지만 매우 유용하게 사용할 수 있습니다.

변수 값 설정

 m_Movement.Set(horizontal, 0f, vertical);

3D 공간의 벡터에는 3개의 값이 있는데, 이 Set 메서드를 통해 각각에 값이 할당됩니다.
여기에는 벡터의 각 좌표에 해당하는 파라미터가 3개 있습니다.
이제 이동 벡터에서 x축에는 수평 입력 값, y축에는 0, z축에는 수직 입력 값이 들어갑니다.
두 번째 파라미터의 0 다음에 오는 f는 이 수를 플로트로 처리하도록 컴퓨터에 지시합니다.

이제 작은 문제 하나를 해결해야 합니다.
이동 벡터는 최대 1의 값을 가질 수 있는 2개의 숫자로 구성되어 있습니다.
피타고라스 정리에 따라 2개의 숫자 모두 값이 1이면 벡터의 길이(크기)가 1을 초과합니다.

따라서 캐릭터가 단일 축을 따라 이동하는 것보다 대각선 방향으로 더 빨리 이동하게 됩니다.
이를 방지하기 위해서는 이동 벡터가 항상 같은 크기를 유지할 수 있도록 크기를 정규화하면 됩니다.
벡터를 정규화한다는 것은 벡터의 방향을 동일하게 유지하면서 크기를 1로 변경하는 것을 말합니다.

Mathf.Approximately(horizontal, 0f)

이 메서드는 2개의 플로트 파라미터를 취하여 부울 1개를 반환합니다
(2개의 플로트 값이 유사하면 참, 그렇지 않으면 거짓을 반환함).
horizontal 변수가 0에 가까우면 메서드가 참을 반환합니다.

정적 메서드

정적 메서드는 클래스의 인스턴스가 아니라 클래스의 유형에 대해 호출됩니다.
입력은 글로벌 컨셉에 더 가깝기 때문에 축 값을 판단하기 위해 Input 클래스의 단일 인스턴스가 필요하지 않습니다.
따라서 이러한 값을 구하는 메서드는 정적 메서드가 됩니다.

마찬가지로, Mathf 클래스는 헬퍼 메서드(다른 메서드의 작업 수행을 지원하는 메서드)로 가득 차 있는데, 헬퍼 메서드에는 Mathf의 특정 인스턴스에 대한 구체적인 데이터가 포함되어 있지 않아 이들 역시 정적 메서드가 됩니다.

반대로 m_Movement 변수를 생각해 보세요. 특정 Vector3 인스턴스에 값을 설정해야 했으므로 이들은 정적 메서드가 아닙니다.

이처럼 정적 메서드는 유형 이름을 사용하여 호출되고,
비정적 메서드(또는 '인스턴스' 메서드)는 인스턴스 이름을 사용하여 호출됩니다.

제너릭 메서드

m_Animator = GetComponent<Animator>();

GetComponent 앞에 클래스가 없는 이유는 무엇일까요?
이전에는 다른 오브젝트에 대한 메서드에 접근하기 위해 이를 추가했습니다(예: 이동 벡터에 대한 정규화 메서드).

하지만 GetComponent는 MonoBehaviour의 일부이고, 현재 코드를 작성하고 있는 클래스가 MonoBehaviour이므로 이미 접근 권한이 주어진 상태입니다.

다음으로 홑화살괄호(<>)를 살펴보겠습니다.
GetComponent 메서드가 제네릭 메서드이기 때문에 홑화살괄호가 추가되었습니다.

제네릭 메서드는 2개의 파라미터 세트, 즉 일반 파라미터와 유형 파라미터로 구성된 메서드를 말합니다.
홑화살괄호 사이에 나열된 파라미터는 유형 파라미터입니다.

이제 GetComponent에 우리가 찾고 있는 컴포넌트의 유형을 알려 줘야 합니다.
Animator 컴포넌트를 찾고 있으므로 유형 파라미터는 Animator가 됩니다.

캐릭터 회전

이 게임에서 캐릭터는 앞으로만 걸을 수 있으므로 몸체가 이동 방향과 같은 방향을 향해야 합니다.
하지만 입력에 따라 너무 빠르게 몸을 회전하면 어색해 보일 수 있으니 속도를 적절히 제어하도록 합니다.

하지만 속도를 얼마나 줄여야 할까요?
먼저 캐릭터의 회전하는 속도가 어느 정도여야 하는지 고려해 보세요.

캐릭터의 전방 벡터 계산

캐릭터가 이동 방향을 향해야 한다는 것을 기억하세요.
모든 Transform 컴포넌트에는 전방 벡터가 있기 때문에 먼저 캐릭터의 전방 벡터로 원하는 값을 계산하도록 합니다.

Vector3 desiredForward = Vector3.RotateTowards (transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);

RotateTowards에는 4개의 파라미터(처음 두 파라미터는 Vector3로 현재 회전 값과 목표 회전 값임)가 필요합니다.

코드는 transform.forward로 시작하여 m_Movement 변수를 목표로 합니다.
transform.forward를 이용하면 Transform 컴포넌트에 바로 접근하여 전방 벡터를 구할 수 있습니다.

다음 두 파라미터는 시작 벡터와 목표 벡터 사이의 변화량으로,
첫 번째는 각도의 변화(단위: 라디안)이고 두 번째는 크기의 변화입니다.

이 코드는 각도를 turnSpeed * Time.deltaTime만큼, 크기를 0만큼 변경합니다.

프레임

Time.deltaTime은 이전 프레임으로부터 경과된 시간(프레임 간 시간)입니다.
그러면 turnSpeed에 이 값을 곱해야 하는 이유는 무엇일까요?

프레임마다 Update가 호출되므로 게임이 초당 60프레임으로 실행된다면
이 메서드는 1초에 60번 호출됩니다.

호출 간의 변화량은 극히 작기 때문에 60프레임 동안 1초에 대해 원하는 변화량을 얻을 수 있습니다.

하지만 게임이 초당 30프레임으로 실행되는 경우는 어떨까요?
동일한 시간 동안 절반의 메서드 호출만 이루어지기 때문에 절반만큼만 회전하게 됩니다. 초당 프레임 수가 캐릭터의 회전 속도에 영향을 미쳐서는 안 되므로, 이는 바람직하지 않습니다.

그렇다면 프레임 단위가 아니라 초 단위로 변경을 수행하는 것은 어떨까요?
그러면 훨씬 쉬워질 겁니다.
이를 위해서는 초당 원하는 만큼의 변경에 한 프레임에 소요된 시간을 곱해야 합니다.

회전 생성 및 저장

   Quaternion m_Rotation = Quaternion.identity;

쿼터니언으로 회전을 저장할 수 있으며,
이를 통해 회전을 3D 벡터로 저장할 때 발생하는 몇 가지 문제를 해결할 수 있습니다.

쿼터니언에 기본값 Quaternion.identity를 부여했습니다.
보통, 특정 메서드가 아니라 클래스의 일부인 변수(멤버 변수)는 클래스의 인스턴스가 생성되면 기본값으로 설정됩니다.
예를 들어 Vector3의 기본값은 x, y, z 모두 0이며 쿼터니언도 동일합니다.
하지만 이동이 없을 때 제로 벡터는 일리가 있지만 제로 쿼터니언은 뭔가 이상합니다.
따라서 Quaternion.identity로 설정하면 제로 쿼터니언이 아니라 아예 회전 값을 주지 않게 되어 더 적절합니다.

     m_Rotation = Quaternion.LookRotation (desiredForward);

LookRotation 메서드를 호출하여 해당 파라미터 방향으로 바라보는 회전을 생성합니다.

캐릭터에 이동 및 회전 적용

마지막 단계에서는 캐릭터에 이동과 회전을 적용하면 됩니다.
이를 적용할 수 있는 여러 가지 방법이 있지만, 캐릭터가 물리 시스템의 일부여야 하기 때문에 다른 기법을 사용하는 대신 리지드바디를 이동해야 합니다.

OnAnimatorMove

하지만 이 애니메이션에는 회전 동작이 없으며 Update 메서드에서 Rigidbody를 회전하려 하면 애니메이션에 의해 오버라이드되어 캐릭터가 필요한 시점에 회전하지 않을 수 있습니다.

따라서 애니메이션의 루트 모션 일부만 사용하여 회전이 아니라 이동을 적용해야 합니다.
그러면 애니메이터에서 루트 모션이 적용되는 방식을 어떻게 변경할 수 있을까요?
MonoBehaviour에는 애니메이터에서 루트 모션의 적용 방식을 변경하는 특수 메서드가 있습니다.

 void OnAnimatorMove ()
    {

    }

이 메서드를 통해 원하는 대로 루트 모션을 적용하여 이동과 회전을 개별적으로 적용할 수 있습니다.

캐릭터의 이동 설정

  m_Rigidbody.MovePosition (m_Rigidbody.position + m_Movement * m_Animator.deltaPosition.magnitude);

먼저 Rigidbody 컴포넌트에 대한 레퍼런스를 사용하여 MovePosition 메서드를 호출하고 새로운 위치인 단일 파라미터를 전달했습니다.

캐릭터의 새 위치는 리지드바디의 현재 위치에서 시작하며, 이동 벡터에 애니메이터의 deltaPosition 크기를 곱한 값을 추가했습니다.

애니메이터의 deltaPosition은 루트 모션으로 인한 프레임당 위치의 이동량을 말합니다.
여기서는 deltaPosition의 크기(길이)에 캐릭터의 이동 방향을 나타내는 이동 벡터를 곱했습니다.

캐릭터의 회전 설정

   m_Rigidbody.MoveRotation (m_Rotation);

이는 회전에 적용된다는 점을 제외하고 MovePosition 호출과 매우 유사합니다.
이번에는 회전에 변경을 적용하는 것이 아니라 회전을 직접 설정하고 있습니다.
이것이 여러분이 처음으로 작성한 스크립트의 마지막 코드 라인입니다.
하지만 조정해야 할 것이 하나 남았습니다.

FixedUpdate

  • Update 루프 : 렌더링에 사용
  • FixedUpdate 루프 : 물리 작업 실행에 사용

OnAnimatorMove를 사용하여 루트 모션을 오버라이드합니다.
그러면 OnAnimatorMovUpdate 메서드와 같은 렌더링이 아니라 물리에 맞추어 적시에 호출됩니다.

이동 벡터와 회전은 Update에서 설정됩니다.
OnAnimatorMove가 처음으로 호출되면 쿼터니언에 값이 설정되어 있지 않아 문제가 발생합니다.

OnAnimatorMove에 맞추어 이동 벡터와 회전을 적시에 설정하기 위해
Update -> FixedUpdate 메서드로 변경합니다.

0개의 댓글