전 챕터처럼 애니메이션의 전환을 하나의 AnimGraph에서 if-else로 관리하는 것은 명확한 한계가 있다.

 

 

스테이트 머신으로 기존의 구조를 바꿀 수 있다.

 

 

 

스테이트 머신을 더블 클릭하면 안의 구조로 들어갈 수 있고 스테이트를 추가하여 연결시킬 수 있다.

 

 

 

또 스테이트 내부로 들어가게 되면 전 챕터에서 봤던 그 구조를 볼 수 있다.

 

// MyAnimInstance.h
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
	bool IsFalling;

// ----------

// MyAnimInstance.cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeUpdateAnimation(DeltaSeconds);

	auto Pawn = TryGetPawnOwner();

	if (IsValid(Pawn))
	{
		Speed = Pawn->GetVelocity().Size();

		auto Character = Cast<ACharacter>(Pawn);
		if (Character)
		{
        		// 캐릭터가 추락중인가?
			IsFalling = Character->GetMovementComponent()->IsFalling();
		}
	}
}

// -------------

// MyCharacter.cpp
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

		// 별도로 Jump 메소드를 선언하지 않아도 부모 클래스에서 이미 정의되었기 때문에 사용할 수 있다
	PlayerInputComponent->BindAction(TEXT("Jump"), EInputEvent::IE_Pressed, this, &AMyCharacter::Jump);

 

Space바 키를 입력 받으면 점프모션으로 변화시켜 애니메이션을 재생하기위해 MyCharacter에는 점프키에 대한 바인딩을, MyAnimInstance에는 IsFalling을 추가하여 캐릭터가 떨어지고 있는 상태인지를 받아온다.

 

 

 

더블 클릭을 눌러서 들어가면 상태 전환간의 룰을 지정할 수 있게된다.

 

 

상태 전환간의 룰까지 지정하고 나면 의도한 대로 애니메이션이 정상적으로 작동하는 것을 볼 수 있다.

 

 

특정 상태일때의 처리, 한 상태에서 다른 상태로 넘어갈때의 처리. 이렇게 두 가지로 관리한다고 보면 된다.

C++ 코드로 관리하는것은 정말 어렵다.

 

 

 

이런 식으로 점프 모션도 세분화해서 만들수 있다.

 

특정 애니메이션이 한번만 재생되길 원하는 경우 애니메이션 애셋의 디테일에서 Loop Animation 체크를 해제해주면 된다.

특정 애니메이션 재생이 끝난 직후 다음 상태로 넘어가길 원한다면 상태 전환간의 룰을 누르고 디테일 탭에서 Automatic Rule Based를 체크해주면 된다. (JumpEnd->Ground)

 

 

C++로 개발을 한다고 해도 애니메이션 상태 변환 만큼은 툴을 활용하는 것이 일반적이다.

언리얼에서의 애니메이션은 별도의 클래스에 몰빵해서 관리한다.

 

C++ 클래스로 만들 때, 부모 클래스를 AnimInstance로 고른다.

 

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Pawn, Meta=(AllowPrivateAccess=true))
float Speed;

 

UPROPERTY의 각 인자는 필요할 때마다 구글링하면 되므로 우선은 넘어간다.

 

 

AnimInstance C++ 클래스를 만들고,

 

 

 

해당 C++ 클래스를 상속받는 블루프린트 애니메이션을 만들고, (블루프린트 클래스가 아닌것에 유의)

 

 

 

해당 애니메이션을 적용시킬 BP_MyCharacter의 Animation Bluprint를 위의 ABP_MyAnim으로 설정한다.

 

실행 도중에 애니메이션 블루프린트에 들어가보면 현재 실행중인 애니메이션이 어떤 것인지 확인할 수 있다.

 

 

 

위에서 선언한 Speed는 애님 프리뷰 에디터에서 확인할 수 있다.

 

 

 

AnimGraph를 통해서 관리할 수 있다.

 

 

애니메이션에 관련된것은 AnimInstance에서 몰빵해서 관리하게 되는데, 실제로 어떤 입력값에 의해 이동이 발생해서 그 값으로 애니메이션의 상태가 결정되어야 하는 경우 AnimInstance의 값에 접근할 수 밖에 없다.

MyCharacter에서 움직임이 발생할때 마다 필요한 정보를 GetAnimInstance 등등 접근해서 수단과 방법을 가리지 않고 세팅하는 방법, 반대로 AnimInstance에서 매 틱마다 외부의 정보를 수집해서 자신에게 세팅하는 방법이 있을 것이다.

 

전자의 경우 코드가 뒤섞일 우려가 있다.

그래서 후자의 경우를 사용하는 게 일반적이다.

 

// 헤더
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

	virtual void NativeUpdateAnimation(float DeltaSeconds) override;

private:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Pawn, Meta=(AllowPrivateAccess=true))
	float Speed;
};

// ------

// cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeUpdateAnimation(DeltaSeconds);
    
    // 현재 AnimInstance를 소유중인 Pawn을 가져오는 것을 시도함
	auto pawn = TryGetPawnOwner();
    
    // '시도' 이기 때문에 못 가져올 수도 있으므로 유효한 값인지 검사
	if (IsValid(pawn))
	{
		Speed = pawn->GetVelocity().Size();
	}
}

 

애니메이션 블루프린트를 만들면 테스트 하기가 편하다.

C++로 만들게 될 경우 블루프린트를 굳이 사용할 필요는 없지만 대략적으로 어떻게 돌아가는지는 알아야 하고 경우에 따라 블루프린트를 적절히 조합해서 사용하는게 더 편한 경우가 있다. 케바케이다.

 

큰 프로젝트이고 성능에 중요함이 있다고 하면 C++로 무조건 가야하고,

블루프린트는 프로토타이핑 할때는 좋으나 성능이 10배가량 느리기 때문에 남발하면 안된다.

 

 

블루프린트 클래스도 생성을 누르면 C++클래스와 마찬가지로 부모 클래스를 선택하게 된다.

C++클래스와 다르게 rename이나 삭제도 자유롭게 할 수 있다.

 

마치 비주얼 코딩을 하는 느낌이다.

블루프린트 클래스의 컴파일은 가볍지만 블루프린트 클래스 자체가 가벼운건 절대 아니다.

 

 

GameModeBase 클래스에서 블루프린트 클래스를 기본값으로 설정하려면 ConstructorHelpers로 불러와야 한다.

 

AMyGameModeBase::AMyGameModeBase()
{
	// 기존 문법
    //DefaultPawnClass = AMyCharacter::StaticClass();

	static ConstructorHelpers::FClassFinder<ACharacter>
    BP_Char(TEXT("Blueprint'/Game/Blueprints/BP_MyCharacter.BP_MyCharacter_C'"));

	if (BP_Char.Succeeded())
	{
		DefaultPawnClass = BP_Char.Class;
	}

 

경로 뒤에 _C를 붙이게 되는데 규칙이다.

 

 

  

void AMyCharacter::UpDown(float Value)
{
	AddMovementInput(GetActorForwardVector(), Value);
}

void AMyCharacter::LeftRight(float Value)
{
	AddMovementInput(GetActorRightVector(), Value);
}

void AMyCharacter::Yaw(float Value)
{
	AddControllerYawInput(Value);
}

 

둘의 결과는 동일하다.

물론 블루프린트 클래스와 아래의 코드는 서로 다른 클래스이긴 하지만 똑같이 작동하게 구현할 수 있다는 것을 확인할 수 있다.

 

블루프린트 클래스는 C++ 클래스를 상속 받아서 사용한다. 당연히 내가 만들어둔 클래스를 상속받아 사용하는 것도 가능하다.

하지만 반대로 블루프린트 클래스를 상속받는 C++ 클래스를 만드는 것은 불가능하다.

 

 

꼭 캐릭터만 블루프린트로 만들라는 법은 없다. 게임 모드도 블루프린트 클래스로 만들어서 편하게 관리할 수도 있다.

 

만들어진 블루프린트 클래스는 유니티의 prefab과 같은 것이라고 보면 될 것 같다.

Pawn에서 Character로 넘어갈 때 생각보다 기능이 많이 추가된다.

Character는 기본적으로 SkeletalMesh가 붙어있다.

CharacterMoveComponent가 기본적으로 상속되어있다.

 

액터나 폰은 생성자에서 메시나 루트 컴포넌트를 직접 설정해줬어야 하는데 캐릭터는 부모 클래스가 기본값으로 다 설정을 해둔다.

 

 

// MyCharacter.h
	void UpDown(float Value); // 이전시간 Pawn에 있던 것과 같음
	void LeftRight(float Value); // cpp에서도 똑같이 복붙 해주면 된다. 클래스명만 바꿔서.

private:
	UPROPERTY(VisibleAnywhere)
	class USpringArmComponent* SpringArm; // CoreMinimal에 없으므로 전방선언

	UPROPERTY(VisibleAnywhere)
	class UCameraComponent* Camera; // 마찬가지로 전방선언
};


// MyCharacter.cpp
#include "MyCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"

// Sets default values
AMyCharacter::AMyCharacter()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	// 보이지 않는 일종의 셀카봉
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SPRINGARM"));
    // 카메라
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("CAMERA"));

	// 셀카봉을 캐릭터의 하위 구조에 넣음
	SpringArm->SetupAttachment(GetCapsuleComponent());
    // 카메라를 셀카봉의 하위 구조에 넣음
	Camera->SetupAttachment(SpringArm);

	// 셀카봉을 늘린다
	SpringArm->TargetArmLength = 500.f;
    // 캐릭터의 뒤쪽으로 뻗어나가게 회전 시킨다
	SpringArm->SetRelativeRotation(FRotator(-35.f, 0.f, 0.f));

	// 캐릭터 메시 위치를 조정한다 (위치 및 회전 동시 조정)
	GetMesh()->SetRelativeLocationAndRotation(
		FVector(0.f, 0.f, -88.f), FRotator(0.f, -90.f, 0.f));

	static ConstructorHelpers::FObjectFinder<USkeletalMesh> SM(TEXT("SkeletalMesh'/Game/ParagonGreystone/Characters/Heroes/Greystone/Meshes/Greystone.Greystone'"));
	
	if (SM.Succeeded())
	{
    	// StaticMesh가 아닌 SkeletalMesh를 가지고 있으므로 맞춰서 변경해줌
		GetMesh()->SetSkeletalMesh(SM.Object);
	}
}

 

 

상속 구조도 그대로 표현되는 것을 확인할 수 있다.

 

마우스 좌우 움직임으로 캐릭터 회전을 추가하는 경우, 기존 키 입력처럼 프로젝트 세팅에서 마우스 축을 추가하고,

SetupPlayerInputComponent에서 BindAxis를 해준 후 메소드를 구현해주면 된다.

프로젝트에서 아무것도 설정하지 않았다면 월드 세팅 Game Mode의 게임모드 오버라이드는 None으로 설정되어 있을 것이다.

이것을 C++ 클래스의 Game Mode Base를 만들어서 바꿔주면 GameMode의 속성들을 정의해서 사용할 수 있다.

 


MyActor를 만들었던 때 처럼 Pawn의 생성자를 선언 및 정의하고 MyGameModeBase에서 DefaultPawnClass를 해당 폰으로 지정해주면 실행 시 해당 폰으로 설정 및 기본 생성이 된다.

 

// MyPawn
AMyPawn::AMyPawn()
{
 	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
	Movement = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("MOVEMENT"));

	RootComponent = Mesh;

	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM(TEXT("StaticMesh'/Game/StarterContent/Props/SM_Couch.SM_Couch'"));
	if (SM.Succeeded())
	{
		Mesh->SetStaticMesh(SM.Object);
	}
}

// ---------------

#include "MyPawn.h"

// MyGameModeBase
AMyGameModeBase::AMyGameModeBase()
{
	DefaultPawnClass = AMyPawn::StaticClass();
}

 

 

Pawn과 Actor의 다른점은 Pawn은 입력을 받을 수 있다. (빙의 가능)

 

Pawn 클래스에는 Actor와 다르게 입력에 대한 부분을 처리하는 SetupPlayerInputComponent 메소드가 기본적으로 포함되어있다.

 

void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(TEXT("UpDown"), this, &AMyPawn::UpDown);
	PlayerInputComponent->BindAxis(TEXT("LeftRight"), this, &AMyPawn::LeftRight);
    // C#의 delegate 문법과 유사하다

}

 

UpDown과 LeftRight를 작성하고 해당 메소드도 클래스에 선언 및 정의해준다.

그리고 Text의 UpDown과 LeftRight는 그냥 적은게 아니고 엔진 세팅과 맞춰주어야 한다.

 

 

Tick에서 입력 받는거나 위의 함수에서 받는거나 어차피 매 틱마다 실행되는거면 무슨 차이가 있는건가? 싶을 수 있지만 차후 PlayerController에 이전시켜 줄 수도 있다. 그러면 위의 함수에 들어올것도 없이 PlayerControll에서 이벤트를 선점해서 그쪽에서 관리할 수도 있다.

아직은 무슨 얘기인지 잘 모르겠지만 그렇다고 한다.

 

 

입력과 별개로 이동에 관련된 부분은 또 다른 컴포넌트로 관리된다.

 

UPROPERTY(VisibleAnywhere)
class UFloatingPawnMovement* Movement;
// 해당 컴포넌트는 CoreMinimal 헤더에 포함되어있지 않기 때문에 헤더에서는 전방선언 해주고
// cpp파일에서 "GameFramework/FloatingPawnMovement.h" 를 포함시켜주면 사용할 수 있다

// ----

// 생성자
Movement = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("MOVEMENT"));

 

 

이후에는 UpDown, LeftRight 메소드에서 해당 컴포넌트의 이동 메소드를 사용하면 된다.

 

void AMyPawn::UpDown(float Value)
{
	UE_LOG(LogTemp, Warning, TEXT("UpDown %f"), Value)

	AddMovementInput(GetActorForwardVector(), Value);
}

void AMyPawn::LeftRight(float Value)
{
	UE_LOG(LogTemp, Warning, TEXT("LeftRight %f"), Value)

	AddMovementInput(GetActorRightVector(), Value);
}

+ Recent posts