애니메이션을 섞는다.

 

 

생성시 스켈레톤을 선택한다.

 

 

 

각각의 애니메이션을 축에 맞게 배치 후, Shift를 눌러서 이동시켜보면 배치한 애니메이션들이 적절하게 섞이는것을 확인할 수 있다.

 

 

// MyAnimInstance.h
private:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
    float Horizontal;

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

// ------------
// MyAnimInstance.cpp
void UMyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
		// .. 생략
		auto Character = Cast<AMyCharacter>(Pawn); // ACharacter에서 AMyCharacter로 변경
		if (Character)
		{
			IsFalling = Character->GetMovementComponent()->IsFalling();

			Vertical = Character->UpDownValue; // 추가된 코드
			Horizontal = Character->LeftRightValue; // 추가된 코드
		}
}

// ------------
// MyCharacter.h
public: // 따로 Get 메소드를 만들기 귀찮아서 public으로 열어둠
	UPROPERTY()
	float UpDownValue = 0;

	UPROPERTY()
	float LeftRightValue = 0;


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

// MyCharacter.cpp
void AMyCharacter::UpDown(float Value)
{
	//UE_LOG(LogTemp, Warning, TEXT("UpDown %f"), Value)

	UpDownValue = Value;
	AddMovementInput(GetActorForwardVector(), Value);
}

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

	LeftRightValue = Value;
	AddMovementInput(GetActorRightVector(), Value);
}

 

 

블렌드 스페이스, 코드 추가, 스테이트 수정까지 완료하고 나면 이동 애니메이션이 방향에 따라 적절하게 섞인것을 볼 수 있다.

ADAD 연타를 하거나 대각선끼리 이동시 부자연스러움이 느껴지는데 이부분은 Interpolation Time을 0.1정도 줘서 자연스럽게 만들어 줄 수 있다.

 

 

애니메이션 처리는 서버에 넘길 필요없이 클라에서 처리하면 되는 기술이다.

 

참고로 블렌드 스페이스 1D는 축이 하나이다.

애니메이션 재생중 특정 시점이 되었을 때 코드에 알람(노티파이)을 주는 개념

애니메이션, 몽타주 둘다 설정 가능하다. 유니티에도 거의 동일한 기능이 있다.

 

 

노티파이 설정

 

// MyAnimInstance.h
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

private:
	UFUNCTION()
	void AnimNotify_AttackHit();
}

// -------

// MyAnimInstance.cpp
void UMyAnimInstance::AnimNotify_AttackHit()
{
	UE_LOG(LogTemp, Log, TEXT("AnimNotify_AttackHit"));
}

 

코드에서도 추가해 주는데 메소드명을 막 짓는것이 아니라 AnimNotify_"노티파이명" 으로 지정해 주어야 한다.

 

 

 

실행 후 공격 시 애니메이션의 노티파이 때마다 한 번씩 호출되는 것을 확인할 수 있다.

 

 

 

 

 

 

지금까지는 한번의 공격으로 평타 세번이 연속해서 나갔지만 각각의 모션을 끊어서 재생하고 싶다면 몽타주 섹션으로 구분하고 링크를 끊어주면 된다. 최상단이 몽타주 섹션이고 우클릭으로 추가할 수 있다.

 

// MyAnimInstance.h
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
// .. 생략
public:
	void JumpToSection(int32 SectionIndex);

	FName GetAttackMontageName(int32 SectionIndex);
};

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

// MyAnimInstance.cpp
void UMyAnimInstance::JumpToSection(int32 SectionIndex)
{
	FName Name = GetAttackMontageName(SectionIndex); // 섹션 이름 변환
	Montage_JumpToSection(Name, AttackMontage); // 해당 섹션 재생
}

FName UMyAnimInstance::GetAttackMontageName(int32 SectionIndex)
{
	// 언리얼에서 사용하는 이름 타입이라고 생각됨
	return FName(*FString::Printf(TEXT("Attack%d"), SectionIndex));
}

// ---------

// MyCharacter.h

// ---------

// MyCharacter.cpp
void AMyCharacter::Attack()
{
	if (IsAttacking)
		return;

	AnimInstance->PlayAttackMontage();

	AnimInstance->JumpToSection(AttackIndex); // 공격시마다 0, 1, 2, 0.. 을 넘겨줌
	AttackIndex = (AttackIndex + 1) % 3;

	IsAttacking = true;
}

 

구분된 섹션을 코드로 제어할 수 있다.

여태까지 InputComponent를 받는 방식이 델리게이트 라고 볼수 있다. (바인딩)

'어떤 행동이 끝났을 때, ~~을 호출해줘' -> 콜백처리

 

// MyCharacter.h
class TESTUNREALENGINE_API AMyCharacter : public ACharacter
{
	
public:	
	// .. 생략
	UFUNCTION()
	void OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);

private:
	// .. 생략
    UPROPERTY(VisibleAnywhere)
	bool IsAttacking = false;
    
	UPROPERTY()
	class UMyAnimInstance* AnimInstance;
};

// --------

// MyCharacter.cpp
void AMyCharacter::BeginPlay()
{
	Super::BeginPlay();

	AnimInstance = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());
    // 몽타주 재생이 끝나면 OnAttackMontageEnded를 실행해 달라고 요청
	AnimInstance->OnMontageEnded.AddDynamic(this, &AMyCharacter::OnAttackMontageEnded);
}

void AMyCharacter::Attack()
{
	if (IsAttacking)
		return;

	AnimInstance->PlayAttackMontage();

	IsAttacking = true;
}

// 델리게이트에 등록된 메소드
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	IsAttacking = false;
}

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

// MyAnimInstance.cpp
void UMyAnimInstance::PlayAttackMontage()
{
	// MyCharacter에서 조건을 모두 검사하기 때문에 이제 여기서 검사할 필요는 없다
	Montage_Play(AttackMontage, 1.f);
}

 

이런 패턴이 정말 자주 일어난다.

애니메이션 편집기라고 보면 된다.

 

 

메시를 더블클릭해서 들어온 후 해당 메뉴에서 애님 몽타주를 만들면 편집 창으로 들어올 수 있다.

애셋 브라우저에서 애니메이션 몇 개를 끌어오면 연속되어 재생되는것을 확인할 수 있다.

원한다면 애니메이션 세그먼트에서 재생 시간, 재생 속도, 루프 횟수를 조절할 수 있다.

 

 

점프같은 경우는 AnimInstance에서 캐릭터의 상태를 매 틱 체크하는 방식이었지만 반대로 공격은 캐릭터에서 발생 시 AnimInstance에 접근해서 Attack Montage를 실행 시킬 것이다.

 

// MyAnimInstance.h
class TESTUNREALENGINE_API UMyAnimInstance : public UAnimInstance
{
	GENERATED_BODY()

public:
	UMyAnimInstance(); // 생성자 추가

	virtual void NativeUpdateAnimation(float DeltaSeconds) override;

	void PlayAttackMontage(); // 메소드 추가

private:
	// .. 생략
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
	UAnimMontage* AttackMontage; // Montage 추가
};

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

// MyAnimInstance.cpp
UMyAnimInstance::UMyAnimInstance()
{
	static ConstructorHelpers::FObjectFinder<UAnimMontage> AM(TEXT("AnimMontage'/Game/Animations/Greystone_Skeleton_Montage.Greystone_Skeleton_Montage'"));

	if (AM.Succeeded())
	{
		AttackMontage = AM.Object;
	}
}

void UMyAnimInstance::PlayAttackMontage() // 실제 Montage 실행부
{
	if (!Montage_IsPlaying(AttackMontage))
	{
		Montage_Play(AttackMontage, 1.f);
	}
}

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

// MyCharacter.cpp
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	// .. 생략
	PlayerInputComponent->BindAction(TEXT("Attack"), EInputEvent::IE_Pressed, this, &AMyCharacter::Attack);
}

void AMyCharacter::Attack()
{
		// MyAnimInstance에 접근
	auto AnimInstance = Cast<UMyAnimInstance>(GetMesh()->GetAnimInstance());

	if (AnimInstance)
	{
    		// 메소드 직접 실행
		AnimInstance->PlayAttackMontage();
	}
}

 

Attack을 바인딩 했으니 당연히 입력에서 Attack을 마우스 왼클릭으로 설정해주면 된다.

 

 

코드 및 AnimGraph를 완성하면 좌클릭시 설정한 애니메이션 몽타주가 재생된다.

전 챕터처럼 애니메이션의 전환을 하나의 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++로 개발을 한다고 해도 애니메이션 상태 변환 만큼은 툴을 활용하는 것이 일반적이다.

+ Recent posts