[Unreal 4] 애니메이션 - 애니메이션 기초, 스테이트 머신
1. 애니메이션 기초
MyCharacter C++클래스를 부모로 만든 블루프린트 클래스 BP_MyCharacter클래스를 기본 DefaultPawn으로 하고 시작하자.
그럼 이제💥 애니메이션을 어디에 넣냐 ❓❓❗
👉 Character클래스에는 Actor클래스에서는 StaticMesh컴포넌트를 이용했던 반면 SkeletalMesh컴포넌트를 가지고 있었다.
BP_MyCharacter를 블루프린트 에디터로 열어서 Mesh 를 누르면 디테일에 Animation란이 있는게 보인다.
Animation Mode콤보박스에서 두번째 Use Animation Asset을 선택하면 그냥 바로 애니메이션을 지정해주면 된다. 그런데 한 동작을 반복할때 쓰는것이고 실제로는 동작에 따라 애니메이션이 달라야한다. 그래서 블루프린트로 이를 제어하기는 쉽지 않다.
👉 움직이는 함수-예) UpDown, LeftRight-에서 해당하는 애니메이션을 동작시키는 방법이 쉬우며 일반적인 방법이다.
그런데 이런 방법은 또 모두 내 캐릭터의 움직임이라면 MyCharacter 클래스 내에서 관리하는 것을 쉽지 않다. 특정 UI가 켜졌을 때 동작하는 애니메이션이라면 UI관련 코드랑 연관이 있을 것이고 그럼 복잡하게 된다.
👉 언리얼에서 제안하는 방법은 애니메이션 관련 클래스를 만들어서 관리하도록 한다. ⭐AnimInstance⭐
위 에디터 사진을 보면 콤보박스에서 첫번째로 Use Animation Blueprint를 선택할 경우 AnimClass를 지정하게 되어있다. AnimClass란 사용할 애니메이션 블루프린트의 클래스로 먼저 애니메이션을 상속받은 블루프린트클래스를 만들어 지정해보자.
1. 우선 C++클래스로 AnimInstace를 상속받은 클래스 MyAnimInstance를 만들어보자.
그리고 AnimInstance내에서 사용할 것들을 클래스 내에 추가해보자.
private:
// 멈춰있다가 움직이면 동작을 시작을 나타내기 위한 변수 Speed
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Pawn, Meta=(AllowPrivateAccess=true)
float Speed;
2. 위에서 만든 MyAnimInstance 클래스를 부모로 애니메이션블루프린트를 생성해보자.
Blueprints\에서 우클릭으로 애니메이션>애니메이션블루프린트 를 선택해야한다. 이름은 ABP_MyAnim이라 해보자.
*AnimInstance를 사용할 때는 일반블루프린트로 생성하는게 아니고 애니메이션 블루프린트라는 별도의 창으로 생성해야한다.* *스켈레탈 메쉬를 선택해줘야한다.*
생성한 애니메이션블루프린트를 열어보면 최종 애니메이션 포즈를 보여준다.
우측 하단의 에셋브라우저에 보면 가지고 있는 애니메이션들의 목록을 보여준다. 여기서 앞으로 걷는것(Jog_Fwd)와 Idle포즈를 가져왔다. 또 Speed값을 GetSpeed(speed만 검색해도 나오는 노드)로 가져와 >노드에서
(Speed > 0) ? TRUE : FASLE 처럼 bool로 포즈를 섞을 수 있도록 TRUE면 Jog_Fwd, FALSE면 Idle포즈 재생하도록 연결해줬다.
3. 캐릭터의 애니메이션에서 AnimInstance를 지정해주자.
-하나의 방법으로 내 캐릭터가 블루프린트로 만들어져 있다면(예)BP_MyChar) 블루프린트 에디터에서 컴포넌트> 메쉬 Animation을 UseAnimationBlueprint로 하고 Anim Class를 지정해준다.(ABP_MyAnim으로)
게임을 실행한 후 애니메이션 블루프린트 에디터로 가면 스폰되어 있는 액터의 AnimGraph가 흘러가듯이 최종애니메이션 포즈로 들어가는게 보인다.
그럼 이제 Speed값을 움직이면 바뀌도록 세팅해야하는데
고려할수 있는 방법은 우선 1) MyCharacter에서 움직일때 필요한 정보를 메쉬에서 GetAnimInstance해서 가져와 설정하는 방법이 있고 2) MyAnimInstance클래스에서 매 틱마다 모든 정보를 역으로 수집해서 애니메이션 관련된 값들을 설정하는 방법이 있다. 두번째가 좀 더 정석적인 방법으로 AnimInstance에 있는 함수로 NativeUpdateAnimation(float DeltaSeconds) 를 사용한다.
이 함수는 엔진에서 매프레임마다 호출이 되는 순서상 입력 시스템을 호출하고 Tick같은 컨텐츠 로직을 호출하고 최종적으로 NativeUpdateAnimation를 호출된다. 즉, 매 프레임마다 들어오는 함수이다.
//MyAnimInstance.h
UCLASS()
class [프로젝트이름]_API UMyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Pawn, Meta=(All))
float Speed;
}
//MyAnimInstance.cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
auto pawn = TryGetPawnOwner(); // 이 AnimInstance를 가지고 있는 pawn 객체정보를 가져온다.
if(IsValid(pawn))
{
Speed = pawn->GetVelocity().Size();
}
}
이 AnimInstsance에 있는 함수를 재정해서 그 안에서 값을 설정해 보았다.
그런데 이 애님인스턴스가 사용되는지(진짜로 폰안에서 쓰이는지) 알수 없는 상태에서 매 프레임마다 이함수를 호출하는게 맞는지 비효율적이라고 생각할수도 있지만 1번방법도 매프레임 꺼내서 확인하고 덮어씌워야하는데 더복잡해질수 있으므로 위 방법이 조금 더 정석적인 방법이다.
이제 이를 컴파일하고 실행해보면 지정해준 입력이 들어오면 움직임이 생기고 DefaultPawn에 속도가 생겨 Speed값이 들어와 ABP_AnimInstance에서 연결해준대로 동작하는걸 볼 수 있었다. 위 Bool로 블렌딩 노드를 보면 더 자연스럽게 하기 위해 블렌딩 시간 등도 설정할 수 있다.
이렇게 애니메이션 기초를 봤는데
- 스켈레탈 메쉬에 애니메이션 블루프린트로 지정하는데 애니메이션 블루프린트는 AnimInstance를 상속해서 애니메이션 관련 로직은 모두 AnimInstance에서 관리한다는것.
- 관련 변수들도 AnimInstance에서 관리하여 NativeUpdateAnimation에 매 프레임마다 들어오며 필요한경우 다른 클래스에서 갖다쓰는 방식으로 동작하는게 정석이라는것.
두 가지 정도는 기억하면 좋을 것같다.
2. 스테이트 머신
상태기계는 간단하게 상태를 이용해 관리한다는 개념인데 유니티에도 있다.
애니메이션 블루프린트로는 어떤 기능이 부족해서 스테이트 머신이 생겼을까..?
한 캐릭터가 가질수 있는 동작애니메이션은 매우 많다.
예를들어 위에서 뛰는것과 멈춰있는 상태만 있다면 애니메이션 블루프린트에서 애님 그래프로 간단하게 만들수 있지만
나아가고 있으며(Speed값이 true이며) 점프하거나 수영하거나 기어가거나 등 여러 동작을 생각할 수있다.
그러면 중첩으로 스페이스키가 눌렷는지 등을 확인해 if문과같은 노드를 추가해서 구현할 수 있겟지만 굉장히 복잡해진다.
그래서 if-else가 아니라 상태로 관리하는게 간단하고 기본적으로 자리잡게 되었다.
게임코드 로직 외에도 애니메이션이나 AI에 대해서 가질수 있는 상태의 경우의 수를 체크하는 식으로 관리하면 더 편하게 된다.
예를 들어 벽을 타고 있는 상태라면 벽을 타면서 할수 있는 다를 동작을 재생시키기때문에 코드가 섞일일이 없어진다.
저번시간에 만들었던 ABP_MyAnimInstance에 스테이트머신 노드를 검색해 추가해보자. F2로 이름을 바꿀수 있다. Action이라는 이름을짓고 최종 애니메이션 포즈에 연결해 주었다. 이 스테이트 머신은 일종의 어떤 상태를 관리하는 기계이다. 이런 스테이트 머신은 이처럼 애니메이션 외에도 ATM이나 자판기에서도 사용하는 유용한 패턴이다ㅏ.
스테이트 머신을 더블클릭으로 들어가서 스테이트를 추가해 연결할 수 있다.
그리고 우클릭으로 스테이트를 두개 추가한 후 이름을 각각 Ground, Jumping이라 지은뒤 오른쪽과 같이 연결해 주었다. 처음에 들어오면 Ground 스테이트이며 상황에 따라 Ground에서 Jumping으로도, 그 역으로도 돌아갈 수 있다는 뜻이다.
그리고 왼쪽처럼 Ground스테이트안으로 들어가 전에 만들었던 걷는 애니메이션 블루프린트의 내용을 옮겼다. 땅바닥에 있을 때만 실행되므로 Ground상태에 있을 때만 실행될 로직이라는 뜻이다. Jumping상태에서는 전혀 들어올 일이 없을것이다.
이렇게 설정하고 컴파일 한 후 실행해보면 이전과 같이 동작한다. Action상태머신에서 기본으로 Ground상태에 놓이고 그 안에서의 동작은 전과 같으니 가능하다.
이제 Jumping 상태에서의 동작을 바꿔보자.
Jumping중인것은 스페이스 키 입력으로 변수를 바꿔 그 변수를 확인하고 동작하도록 할 것이다. 애니메이션 블루프린트의 에디터에서 왼쪽에 눈 모양을 누르면 상속된 변수 표시를 눌러 가지고 있는 변수들을 볼 수 있다.
MyAnimInstance클래스로 돌아가
// MyAnimInstance.h
{
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Pawn, Meta=(All))
float Speed;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Pawn, Meta=(All))
bool IsFalling; // 함수 이름과 동일하게 지었다.
}
//MyAnimInstance.cpp
#include "GameFramework/Character.h"
#include "GameFramework/PawnMovementComponent.h"
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
auto pawn = TryGetPawnOwner(); // 이 AnimInstance를 가지고 있는 pawn 객체정보를 가져온다.
if(IsValid(pawn))
{
Speed = pawn->GetVelocity().Size();
auto Character = Cast<ACharacter>(Pawn);
if(Character)
{
IsFalling = Character->GetMovementComponent()->IsFalling();
}
}
}
위와 같이 수정하고 위클래스를 상속받는 ABP_MyAnimInstance에 가면 IsFalling변수가 추가되어있다.
GetMovementComponent는 이미 만들어진 함수로 IsFalling외에도 기어가는것, 숙이는 것 등 FPS에서 많이들 사용하는 동작들을 판별하는 함수를 가지고 있다. 언리얼이 FPS에서 시작되었기에..!
이제 점프키를 받는 로직을 작성해보자.
MyCharacter클래스로 돌아가서 입력을 감지하는 SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)에서
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 계속해서 들어오는 축맵핑이 아니라 일시적으로 들어오는 액션맵핑으로
// 두번째 인자에 어떻게 들어올지도 지정해준다.- 예)눌렀을때 , 클릭, 더블클릭 등
// Jump함수는 따로 만들지 않아도 ACharacter::Jump()가 이미 있다.
PlayerInputComponent->BindAction(TEXT("Jump"), EInputEvent::IE_Pressed, this, &AMyCharacter::Jump);
PlayerInputComponent->BindAxis(TEXT("UpDown"), this, &AMyCharacter::UpDown);
PlayerInputComponent->BindAxis(TEXT("LeftRight"), this, &AMyCharacter::LeftRight);
PlayerInputComponent->BindAxis(TEXT("Yaw"), this, &AMyCharacter::Yaw);
}
위와 같이 액션맵핑을 Jump함수와 해주었다.
또 에디터에서는 프로젝트 세팅>입력에서
스페이스바와 함께 바인딩해줬다.
이제 다시 애니메이션 블루프린트로 돌아가보자. 아직 Jump상태에서 애니메이션을 지정해주지 않았는데 만약 이대로 컴파일 후 실행하면 캐릭터가 그래도 위로 가기는 하나 애니메이션이 바뀌지는 않는다..!
이제는 Ground상태에서 Jumping상태로 넘어가는 조건을 만들어야할 텐데
Action 스테이트 머신에서 Ground->Jumping 에서 ->을 누르면 'Ground->Jumping(룰)' 로 들어간다.
여기서 IsFalling을 드래그 드롭해 가져와 이미 있는 CanEnterTransaction으로 연결한다. 컴파일, 저장하고
다시 밖에서 'Jumping->Ground(룰)' 로 들어간다. 여기에선 거꾸로 IsFalling을 가져온다음에 not노드를 추가해 연결하고 not노드를 CanEnterTransaction에 연결한다. 마찬가지로 컴파일, 저장하고 나가면 이제 Jumping과 Ground를 연결하는 상태가 활성화 된다.
마지막으로 Jumping상태에서 Jump관련 애니메이션 시퀀스를 연결해주면 끝난다.
다시 모두 컴파일하고 실행해보면 스페이스바를 눌렀다가 IsFalling상태가 활성화 되면 연결한 애니메이션이 동작한다.
정리해보면 Action스테이트 머신을 ABP_MyAnim애니메이션 블루프린트의 애님그래프에 추가하고 > Action 스테이트 머신에서 Jumping, Ground 스테이트를 추가하고 연결한 뒤 > 각 스테이트 내에서 또 조건에 따라 동작할 애니메이션시퀀스를 만들고 > 스테이트들을 왔다갔다할 룰을 지정(입력에 따라 달라지는 변수 등을 이용해)해주면
복잡한 애니메이션을 관리하는 스테이트 머신을 이용하는 간단한 방법이다.
상태별로 관리하는데
특정 상황에 대한 처리/ 한 상황에서 다른 상황으로 넘어갈 때 처리.
만약에 Ground 상태에 있다가 피격을 받으면 hit를 판정하는 변수로 넘어가고도 할 수 있다. 이를 C++코드에서 관리하는 것을 더 쉽지 않은데 특히나 어떤 애니메이션 동작 후 다른 애니메이션 동작을 부드럽게 연결하는 하는게 복잡하다.
예를 들어 Ground에서 바로 Jumping상태로 넘어가지 않고 JumpingStart와 JumpingEnd 스테이트를 추가해보자.
Ratio를 검색해서 Jump_start 스테이트의 애니메이션 남은 비율을 가져오는 노드를 추가하고 비율이 0.1보다 작으면으로 결과 노드에 연결한다. 즉, Jump_start스테이트에서 남은 동작이 10%미만이면 JumpStart->Jumping으로 넘어간다는 것이다.
Jumping->JumpingEnd로 가는 룰은 IsJumping노드의 값이 NOT이면 CanEnterTransaction에 연결하였다.
또 마찬가지로 JumpingEnd 상태에서는 관련 애니메이션 (예)-JumpRecovery) 하나만 연결한 후 루프를 껐다. 그리고 최종적으로 JumpingEnd->Ground(룰)에서는 착지가 잘 되었을 것이니 JumpStart->Jumping처럼 애니메이션 남은 비율로 연결해도 되고 간단하게 화살표누르고 디테일에서 Automatic Rule Based를 키면 자동으로 애니메이션이 켜지면 넘어가게 된다.
만약 위 동작들을 비주얼적으로 아니고 코드로 관리하면 굉장히 힘들게 된다. 애니메이션의 경로를 받아와서 동작 정도도 받아와야하고.. 또.. 이렇게 변수 하나씩 Speed나 IsFalling을 추가해 애니메이션 트랜잭션을 스테이트머신에서
애님그래프에서 상태를 지정하고 상태에 들어가서 그 상태일 때 어떤 애니메이션을 틀어줄지 정하고 또 상태에서 다른 상태로 언제 넘어갈지 조건을 세팅하여야 맞물려 돌아간다.
스테이트 머신을 이용한 애니메이션 관리는 이렇다..
애니메이션은 계속된다.