본문 바로가기

개발 공부 기록/UnrealEngine5

이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 5

개요

'이득우의 언리얼 C++ 게임 개발의 정석'의 7장의 내용을 바탕으로 하고 있습니다.

알라딘 책 구매 링크

 

이득우의 언리얼 C++ 게임 개발의 정석

언리얼 엔진 학습에 목말라하는 게임 개발자에게 단비 같은 언리얼 엔진 프로그래밍 책이다. 에픽게임즈 본사의 개발자 프로그램 언리얼 데브 그랜트를 수상한 저자의 책으로, 언리얼 엔진의

www.aladin.co.kr

 

본문

애니메이션 블루프린트는 시각적 도구(Visual Scripting)를 사용해 애니메이션 시스템을 제작하도록 설계된 애님 그래프(Anim Graph)와
애니메이션 블루프린트의 기반을 이루는 애님 인스턴스(Anim Instance)로 구성된다.

  • 애님 인스턴스: 스켈레탈 메시를 소유하는 폰의 정보를 받아 애님 그래프가 참조할 데이터를 제공한다.
    블루프린트와 C++로 제작할 수 있다.
  • 애님 그래프: 애님 인스턴스의 변수 값에 따라 변화하는 애니메이션 시스템을 설계하는 공간이다.
    블루프린트로만 제작할 수 있다.

게임 엔진은 틱마다 입력 시스템 → 게임 로직 → 애니메이션 시스템 순으로 로직을 실행한다. 그래서 애니메이션 시스템에서
폰에 접근을 할 때 먼저 폰 객체가 유효한 상태인지를 점검해야 한다.
이를 검사하는 명령어가 TryGetPawnOwner이다.

아래는 AnimInstance 클래스의 TryGetPawnOwner의 함수 내부이다.
먼저 애니메이션 객체에 적용된 스켈레탈 메시 컴포넌트를 가져와 본 후 액터가 유효한지를 체크하여 소유한 폰으로 변환해서 반환해준다.

 

void UABAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	// AnimInstance 내부에 구현되어 있는 업데이트 함수, 애니메이션의 속도와 방향 등의 정보를 계속 업데이트 해준다.
	Super::NativeUpdateAnimation(DeltaSeconds);

	// 소유한 폰이 현재 유효한 상태인지를 체크하여 반환하는 함수
	auto Pawn = TryGetPawnOwner();
	// 폰이 유효한 상태라면(TryGetPawnOwner에서 유효하지 않으면 NULL을 반환함) 폰의 속도를 받아와서 넣어준다.
	if (::IsValid(Pawn))
	{
		CurrentPawnSpeed = Pawn->GetVelocity().Size();
	}
}

 

애님 인스턴스의 틱에서 폰에 접근해 애니메이션을 생성하는 방식이 일반적이지만 반대로 폰에서 스켈레탈 메시 컴포넌트의 GetAnimInstance 함수를 사용해서 폰에서 애님 인스턴스로 접근할 수도 있음.
다만 전자의 방식으로 설계하면 애니메이션 로직과 폰의 로직을 분리할 수 있으므로 애니메이션이 필요 없는 서버 코드에도 문제없이 동작한다. 따라서 전자의 방식이 주로 사용됨.

유니티 엔진이나 기타 게임을 만들 때 Tick함수(혹은 Update함수)와 같이 프레임 단위로 자주 호출되는 함수에서
Get등을 사용하는 것이 최적화에도 좋은 방법은 아닌데 이런 부분에 있어서 더 찾아봐야 할 것 같다.

 

애님 그래프는 스테이트 머신을 제공해주어 액터의 상황에 맞는 애니메이션을 재생해줄 수 있도록 도와준다.
아래와 같이 BaseAction State Machine 아래로 진입하면 Ground에서의 애니메이션으로 마지막 Idle과 Run 애니메이션으로 설계할 수 있다.

 

ACharacter 클래스에는 Jump 멤버 함수가 존재하고 이를 입력과 바인딩 할 수 있게 인자와 반환이 없는 void 함수로 선언되어 있다.
이를 Jump 입력과 연동하면 캐릭터 점프 기능이 바로 완성된다.

다른 액션들과 마찬가지로 InputAction을 만들고 불러온 다음 ACharacter의 Jump 함수를 연동한다.
(어차피 상속되어 있기 때문에 AABCharacter::Jump라고 써도 되긴하다. 상속된 함수를 사용함을 강조하기 위해서 작성해놨다.)

점프의 높이는 GetCharacterMovement의 JumpZVelocity를 통해 조절할 수 있다.

// 최초 속도를 빠르게 하여 높게 뛴다.
GetCharacterMovement()->JumpZVelocity = 800.0f;

 

폰이 점프나 수영, 달리기 등의 움직임을 하고 있는 상태인지 파악하기 위해서 폰의 무브먼트 컴포넌트에서는
현재 움직임을 파악하는 함수들을 제공해준다.

  • IsFalling(): 현재 공중에 떠있는지 알려준다.
  • IsSwimming(): 현재 수영 중인지 알려준다.
  • IsCrouching(): 현재 쭈그리고 앉아있는지 알려준다.
  • IsMoveOnGround(): 땅 위에서 이동 중인지 알려준다.

이 중 IsFalling을 사용해보면서 캐릭터가 점프 중일 때는 달리는 애니메이션을 수행하지 않게 적용해본다.

// ABAnimInstance.cpp

#include "ABAnimInstance.h"

UABAnimInstance::UABAnimInstance()
{
	CurrentPawnSpeed = 0.0f;
	
	// 헤더에서 bool 타입 private 변수로 선언해주었다.
	IsInAir = false;
}

void UABAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	// AnimInstance 내부에 구현되어 있는 업데이트 함수, 애니메이션의 속도와 방향 등의 정보를 계속 업데이트 해준다.
	Super::NativeUpdateAnimation(DeltaSeconds);

	// 소유한 폰이 현재 유효한 상태인지를 체크하여 반환하는 함수
	auto Pawn = TryGetPawnOwner();
	// 폰이 유효한 상태라면(TryGetPawnOwner에서 유효하지 않으면 NULL을 반환함) 폰의 속도를 받아와서 넣어준다.
	if (::IsValid(Pawn))
	{
		CurrentPawnSpeed = Pawn->GetVelocity().Size();
		// 폰을 캐릭터로 변환해서 있는 지 확인한다.
		auto Character = Cast<ACharacter>(Pawn);
		if (Character)
		{
			// 캐릭터가 존재한다면 무브먼트 컴포넌트에 접근하여 현재 공중에 있는 지(떨어지는 중인지) 체크한다.
			IsInAir = Character->GetMovementComponent()->IsFalling();
		}
	}
}

 

애니메이션 블루프린트에서 새로운 스테이트 Jump를 추가하고 Ground와 서로 연결하여 전환되는 상태 관계를 추가한다.

 

각 화살표 위의 방향 노드를 클릭하면 Ground → Jump / Jump → Ground 트랜지션 설정 화면이 출력된다.
Ground → Jump에는 왼쪽 하단의 Is In Air 변수를 끌어다 놓고 Result 노드와 연결하고 Jump → Ground 트랜지션에서는 사이에
Not Boolean을 반환하는 노드를 두어 Is In Air 변수가 false일 경우에 트랜지션 되도록 설정한다.

 

다음은 스켈레톤 간 애니메이션 공유를 위한 IK 릭 리타기팅이다.

간단하게 설명하면 A 스켈레톤 모델에게 B 스켈레톤의 여러 애니메이션들을 적용시킬 수 있게 해주는 시스템이다.

 

IK Rig 공식 문서 링크

언리얼 엔진의 IK 릭과 리타기팅 공식문서 링크

 

콘텐츠 브라우저 우클릭 → 애니메이션 → 리타기팅을 가면 IK 리타기터IK 릭이 존재한다.

우선 애니메이션을 적용할 스켈레탈 메시와 애니메이션이 적용되어 있는 스켈레탈 메시 두 가지의 IK 릭을 생성해준다.

 

생성한 IK 릭을 열고 우측 프리뷰 스켈레탈 메시에 스켈레탈 메시를 넣어준다.

그 후 왼쪽 계층구조에서 우클릭하여 원하는 리타깃 루트를 설정해준다.

 

아래 예시의 경우 pelvis를 리타깃 루트 설정하고 상단의 리타깃 체인 자동 생성을 해주었다.
원하는 양식의 리타깃 체인이 존재할 경우 직접 왼쪽이나 우하단 IK 리타기팅 창에서 설정해줄 수 있다.

리타깃 체인이란 기존 본 구조를 조인트 체인이라는 관절 단위로 엮어서 아예 다른 본 구조를 가진 스켈레탈 메시끼리도
리타기팅을 할 때 유연성을 제공해주는 시스템이다.

 

똑같이 애니메이션을 제공할 스켈레탈 메시용 IK 릭도 만든 다음은 IK_리타기터를 생성해서 리타기팅을 진행하면 된다.

 

우측의 소스와 타깃의 IK 릭 애셋 부분에 방금 생성한 IK 릭 파일들을 넣어주면 뷰포트에 리타깃 되는 메시들이 보이게 된다.
두 메시들간의 거리는 우측 타깃 메시 오프셋에서 설정 가능하다.

 

이 상태에서 우하단 애셋 브라우저에서 애니메이션들을 더블클릭해서 실행해보면 애니메이션이 어떻게 수행되는지 프리뷰를 볼 수 있다.
이를 비교해보면서 이상한 부분이 있다면 직접 리타깃 체인을 수정해서 복사할 수 있다.

 

원하는 애니메이션들을 선택한 다음 선택된 애니메이션 익스포트를 하면 애니메이션을 리타기팅해서 사용할 수 있게 된다.

 

점프의 동작을 보통 구현할 때 점프 시작 동작, 체공 동작, 착지 동작 3가지로 나눠서 적용한다.

이를 구현하기 위해 스테이트를 JumpStart, JumpLoop, JumpEnd로 나눠서 구현한다.

 

각 스테이트의 애니메이션에 리타기팅 했던 Jump, Fall_Loop, Land를 넣어서 설정을 마무리했다.

 

Ground → JumpStart 와 JumpLoop → JumpEnd 는 앞서 설정한 것과 동일하게 설정하지만 JumpStart → JumpLoop, JumpEnd → Ground의 경우 땅에서 떨어졌다, 땅에 닿았다의 개념이 아닌 직전 애니메이션 동작이 마무리 되었는가를 판단해서 자연스럽게 전환되도록 진행도를 비교하여 다음 애니메이션이 실행되도록 적용해준다.

이는 Time Remaining (ratio)를 통해 알아낼 수 있고 해당 값이 0.1이면 재생 시간이 10% 남았다는 의미이다.
0.1보다 작으면 사실상 애니메이션이 다 끝난 것이므로 0.1보다 작으면 트랜지션이 되도록 한다.

이 방법과 같이 구현할 이유가 없이 애니메이션 재생이 모두 종료가 된 이후 스테이트를 이동하려면 트랜지션 노드에서 제공하는
“스테이트의 시퀀스 플레이어에 따른 자동 규칙(Automatic Rule Based on Sequence Player in State)” 옵션을 체크하면 된다.