
이번엔 전에 생성한 캐릭터에 움직이는 애니메이션과 공격모션을 부여해보았다.

애니메이션 블루프린트(Animation Blueprints)는스켈레탈 메시(Skeletal Mesh)의 애니메이션을 제어하는 블루프린트로애니메이션 블루프린트 에디터(Animation Blueprint Editor)안에서그래프(Graphs)를 편집하여 애니메이션을 블렌딩하거나 스켈레톤의 본을 제어하거나, 각 프레임에 사용할 스켈레탈 메시의 최종 애니메이션 포즈를 정의할 로직을 생성할 수 있다.
Anim Class에 AnimationBlueprint를 넣어줘서 해당 애니메이션 블루프린트가 이 캐릭터의 애니메이션을 관리하도록 한다.
우리가 사용할 Animation Blueprint를 만들기전에 AnimInstance를 상속받는 `UABAnimInstance'를 C++로 생성한다.
<Header>
// 초기화
virtual void NativeInitializeAnimation() override;
// 업데이트
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
// AnimInstance를 들고있는 Owner
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category = Character)
TObjectPtr<class ACharacter> Owner;
// Character의 MovementComponent(Velocity등의 값을 가져오기 위함)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Character)
TObjectPtr<class UCharacterMovementComponent> Movement;
// Character's Speed
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
FVector Velocity;
// Character's Speed on Ground
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
float GroundSpeed;
// Idle 상태값
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
uint8 bIsIdle : 1;
// 움직이고 있는지 아닌지를 확인하기 위한 경계값
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
float MovingThreshold;
// 떨어지고 있는지 확인하기 위한 값
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
uint8 bIsFalling : 1;
// 점프하는 중인지 나타내는 값
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
uint8 bIsJumping : 1;
// 점프중인지 확인하기위한 경계값
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Character)
float JumpingThreshold;
<cpp>
// 초기화
void UABAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
// Character의 Movement가져옴
Owner = Cast<ACharacter>(GetOwningActor());
if (Owner)
{
Movement = Owner->GetCharacterMovement();
}
}
// 주기적으로 호출
void UABAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
if (Movement)
{
Velocity = Movement->Velocity;
// 하늘 방향(z)를 제외한 속도
GroundSpeed = Velocity.Size2D();
bIsIdle = GroundSpeed < MovingThreshold;
bIsFalling = Movement->IsFalling();
bIsJumping = bIsFalling & (Velocity.Z > JumpingThreshold);
}
}
다음 함수와 변수들을 선언해주고 UABAnimInstance를 상속받는 Animation Blueprint를 만들어준다.
SkeletalMesh의 Anim Class에 우리가 만든 Animation Blueprint를 넣어주면 다음과 같이 캐릭터가 A포즈를 취한 상태인것을 볼 수 있다. 아직 애니메이션을 넣어주지 않았기 때문이다.
우선 Idle,Walk,Run 상태의 애니메이션을 넣어주는데 Blend Space를 사용해서 애니메이션을 적절히 보간시켜준다.

이를 이용하면 캐릭터의 특정한 값에 따라 애니메이션들을 보간하는데 여기서는 GroundSpeed 값을 이용하였다.
그 이후 Animation Blueprint를 이용해서 캐릭터의 상태에 따른 애니메이션을 부여해보겠다.

Anim Graph에서 내부에 StateMachine을 생성해주고 Cache로 받는다.

Locomotion 내부에 각각 Idle과 블렌드 스페이스를 이용한 IdleWalkRun을 넣어주고 Idle일 때와 Idle이 아닐 때로 전환 규칙을 지정해주면 된다.

Locomotion State에는 앞에서 만들어둔 Cache를 받도록 설정해주고 Jump,Falling,Land 에 애니메이션을 넣어준다.
ToJump,ToLand와 같은 StateAlias에는 전환 받을 State를 설정해줘야하는데 ToJump는 Idle, Walk, Run에서 전환되도록 Locomotion에 체크해주고 ToLand는 Jump,Falling에서 전환되도록 각각 체크해주면된다.

그러면 달리고 뛰는 모션이 잘 동작하는 것을 볼 수 있다.
Animation Montage를 이용해서 공격모션을 만들어보자
애니메이션 몽타주는 여러 애니메이션을 하나의 애셋으로 합칠 수 있고 이를 섹션으로 나누어 각각 재생하거나 합쳐서 재생할 수 있다.

공격모션 1,2,3,4를 각각 넣어주고 몽타주 섹션을 각각 추가해준다.

다음과 같이 Link를 제거해서 따로따로 사용하도록 한다.

공격에 사용할 InputAction을 추가하고 InputMappingContext에 추가해준다.
<Header>
void Attack();
<cpp>
AABCharacterPlayer::AABCharacterPlayer()
{
...
static ConstructorHelpers::FObjectFinder<UInputAction> InputActionAttackRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Attack.IA_Attack'"));
if (InputActionAttackRef.Object)
{
AttackAction = InputActionAttackRef.Object;
}
}
...
void AABCharacterPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
...
EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Attack);
...
}
그리고 생성한 IA_Attack을 등록하고 함수 Attack()과 묶어준다.
// Animation Montage의 Section의 접두사
UPROPERTY(EditAnywhere,Category = Name)
FString MontageSectionNamePrefix;
// 최대 콤보
UPROPERTY(EditAnywhere, Category = Name)
uint8 MaxComboCount;
// 프레임의 기준 재생속도
UPROPERTY(EditAnywhere, Category = Name)
float FrameRate;
// ComboAttack 별 입력을 받을 시간
UPROPERTY(EditAnywhere, Category = Name)
TArray<float> EffectiveFrameCount;

ComboAttack에 필요한 데이터를 DataAsset에 넣어서 관리해준다.
<header>
// Combo Action Section
protected:
// Animation Montage 슬롯
UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = Animation)
TObjectPtr<class UAnimMontage> ComboActionMontage;
// ComboAction DataAsset
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UABComboActionData> ComboActionData;
// CharacterPlayer의 Attack()에서 호출되는 함수
// ComboAttack을 시작하거나 입력 여부를 확인한다.
void ProcessComboCommand();
void ComboActionBegin();
// UAnimMontage 내부에 FOnMontageEnded라는 델리게이트가 있다. 그것의 파라미터와 맞춰준다.
void ComboActionEnd(class UAnimMontage* TargetMontage, bool IsProperlyEnded);
// ComboCheck()을 일정 시간 뒤에 호출
void SetComboCheckTimer();
// 시간내에 입력이 들어왔으면 다음 섹션의 애니메이션으로 점프시킨다.
void ComboCheck();
// 콤보 시작 전에는 0, 콤보가 시작되면 1,2,3,4 의 값을 가진다.
int32 CurrentCombo = 0;
// 입력이 들어올 시간을 체크하는 Timer Handle
FTimerHandle ComboTimerHandle;
bool HasNextComboCommand = false;
ABACharacterBase에 다음과 같은 변수와 함수들을 추가해준다.
<cpp>
void AABCharacterBase::ProcessComboCommand()
{
if (CurrentCombo == 0)
{
ComboActionBegin();
return;
}
// ComboEffectiveTime이 지나면 ComboCheck에서 타이머를 초기화시킨다.
// 타이머가 유효하면 ComboCheck()하기 이전에 커맨드가 발동했다는 뜻이다.
if (!ComboTimerHandle.IsValid())
{
HasNextComboCommand = false;
}
else
{
HasNextComboCommand = true;
}
}
void AABCharacterBase::ComboActionBegin()
{
// Combo Status
CurrentCombo = 1;
// Movement Setting
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
//Animation Setting
const float AttackSpeedRate = 1.0f;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);
// EndDelegate에 ComboActionEnd를 바인드시킨다.
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &AABCharacterBase::ComboActionEnd);
// 종료가 안되면 호출이 안된다? -> 결국 마지막 콤보에서는 종료가 되게 되있다.
// ComboAction Montage가 종료가 될 때 EndDelegate호출 -> ComboActionEnd가 호출된다.
AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);
// 시간 초기화
ComboTimerHandle.Invalidate();
SetComboCheckTimer();
}
void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
ensure(CurrentCombo != 0);
CurrentCombo = 0;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}
void AABCharacterBase::SetComboCheckTimer()
{
int32 ComboIndex = CurrentCombo - 1;
ensure(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));
const float AttackSpeedRate = 1.0f;
float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] / ComboActionData->FrameRate) / AttackSpeedRate;
// 마지막 Combo의 EffectiveFrameCount가 -1 이기 때문에 마지막 콤보가 실행되고 나서는 넘어가지 않는다
if (ComboEffectiveTime > 0.0f)
{
// ComboTimerhandle을 활성화하고 ComboEffectiveTime이 지나면 ComboCheck()함수를 호출하지만 한번만 호출
GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle,this,&AABCharacterBase::ComboCheck,ComboEffectiveTime,false);
}
}
void AABCharacterBase::ComboCheck()
{
// Timer 초기화
ComboTimerHandle.Invalidate();
// 다음 콤보 커맨드가 들어오면
if (HasNextComboCommand)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
// ComboCount를 더해주지만 MaxComboCount를 넘지않도록 Clamp시켜준다
CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, ComboActionData->MaxComboCount);
// 섹션이름 가져오기 prefix인 ComboAction + CurrentCombo
FName NextSection = *FString::Printf(TEXT("%s%d"), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
// 입력한 지점으로 넘어가서 재생한다.
AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
SetComboCheckTimer();
HasNextComboCommand = false;
}
}
Delegate를 통해 몽타주 재생이 끝났을 때 호출할 함수인 ComboActionEnd()는 요구하는 인자를 맞춰주어야 한다.
ABACharacterPlayer 내부의 Attack()함수에 ProcessComboCommand()를 넣어준다.

캐릭터에 애니메이션 몽타주를 Character에 지정해주고 DataAsset까지 넣어준다.

OutPut으로 가는 중간에 Montage 슬롯을 넣어주면된다.
몽타주 슬롯을 두게되면 모든 애니메이션에 몽타주 애니메이션이 덧씌워져서 나가게 된다.
몽타주가 재생이 안되면 기존 애니메이션이 재생된다.

문제없이 잘 공격이 나가는 것을 볼 수 있다.