오늘의 코드카타
오늘의 공부
팀 프로젝트 진행 내용
1. Traversal을 위한 PSC, PSS, PSD 생성
- 어제 진행했던 내용 중에서 모션 매칭을 사용하기 위해서는 몽타주에
UAnimNotifyState_PoseSearchBranchIn 노티파이 스테이트가 추가하고 PSD를 설정해야 된다는 것을 알게 되었다.
- 파쿠르 애니메이션을 가지고 있는 PSD를 생성해야 하는데, 이를 위해 PSC(Pose Search Channel)과 PSS(Pose Search Schema)가 필요하다. PSC의 경우에는 일반적으로는 필요하지 않을 수도 있지만, 파쿠르의 동작 과정이 매우 특이하게 때문에 PSS에서 사용할 궤적 정보를 별도로 받아서 사용하고자 했다.
- 이를 위해서 별도의 Interface를 만들어서 파쿠르를 실행할 정보 데이터를 받고 이를 사용하고자 했다.
// AO_TraversalTransform.h
UINTERFACE(MinimalAPI, BlueprintType)
class UAO_TraversalTransform : public UInterface
{
GENERATED_BODY()
};
class AO_API IAO_TraversalTransform
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Traversal")
void SetTraversalTransform(const FTransform& TraversalTransform);
virtual void SetTraversalTransform_Implementation(const FTransform& TraversalTransform) = 0;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Traversal", meta = (BlueprintThreadSafe))
FTransform GetTraversalTransform();
virtual FTransform GetTraversalTransform_Implementation() = 0;
};
- 이후에 AnimInstance에 해당 인터페이스를 상속받아서 Transform을 저장하게끔 했다.
// AO_PlayerCharacter_AnimInstance.h
UCLASS()
class AO_API UAO_PlayerCharacter_AnimInstance : public UAnimInstance, public IAO_TraversalTransform
{
GENERATED_BODY()
...
virtual void SetTraversalTransform_Implementation(const FTransform& InTraversalTransform) override;
virtual FTransform GetTraversalTransform_Implementation() override;
UPROPERTY(BlueprintReadWrite, Category = "PlayerCharacter|Essential Values")
FTransform TraversalTransform;
...
}
- 다음으로는
PSC_Traversal_Head에서는 Heading Channel을 상속받아서, 아래와 같이 머리의 방향을 지정해줬다.

PSC_Traversal_Pos에서는 Position Channel을 상속받아서, 아래와 같이 위치 정보를 지정해줬다.

PSS_Traversal에서 위의 채널들과 기존의 채널들을 활용해서 가중치를 할당해서 저장했다.
- 가중치, 채널 등의 정보는 GASP를 기반으로 작성했다.

2. Traversal AnimMontage들한테 Notify State, Curve 추가
- 모션 매칭을 사용하기 위해 파쿠르 AnimMontage 모두에게 아래의
UAnimNotifyState_PoseSearchBranchIn 노티파이 스테이트를 추가했다.
- 그리고 Traversal 직후에 자연스러운 애니메이션을 재생하기 위해서 방금 Traversal이 재생되었다는 정보를 Curve data를 추가함으로써 파악하고자 했다.
- Curve Data는 필요한 Montage에만 넣었고, 실질적으로 여부만 중요하기 때문에 메타데이터로 생성을 했다.

PoseSearchBranchIn라는 노티파이를 선택하게 되면 아래의 Details 창에 PSD를 선택할 수 있게 되고, 이를 내가 생성한 PSD_Traversal라는 파일을 할당했다.

- 노티파이를 생성하고, PSD를 선택하면 해당 PSD 내부에는 자동으로 애니메이션이 추가된다.
- 자동으로 추가가 되면 왼쪽에 하늘색의 화살표시가 나오게 된다.

- 위의 모든 과정을 거치니 어제 되지 않았던 모션 매칭 과정이 정상적으로 되는 것을 확인할 수 있었다.
3. AnimMontage를 실행하고 끝날 시에 실행할 함수 바인딩
void UAO_TraversalComponent::PerformTraversalAnimation()
{
// 서버 권위를 일시적으로 완화하고, 클라이언트 측 예측을 극대화하기 위해 사용
CharacterMovement->bIgnoreClientMovementErrorChecksAndCorrection = true;
CharacterMovement->bServerAcceptClientAuthoritativePosition = true;
// 몽타주 재생
TObjectPtr<UAnimInstance> AnimInstance = Character->GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(
TraversalResult.ChosenMontage,
TraversalResult.PlayRate,
EMontagePlayReturnType::MontageLength,
TraversalResult.StartTime);
// 몽타주 종료 시에 실행할 함수 바인딩
FOnMontageEnded MontageEndDelegate;
MontageEndDelegate.BindUFunction(this, FName("OnMontageEnded"));
AnimInstance->Montage_SetEndDelegate(MontageEndDelegate, TraversalResult.ChosenMontage);
// 각종 변수 설정
bDoingTraversal = true;
TObjectPtr<UCapsuleComponent> CapsuleComponent = Cast<UCapsuleComponent>(
GetOwner()->GetComponentByClass(UCapsuleComponent::StaticClass()));
CapsuleComponent->IgnoreComponentWhenMoving(TraversalResult.HitComponent, true);
CharacterMovement->SetMovementMode(MOVE_Flying);
}
void UAO_TraversalComponent::OnMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
bDoingTraversal = false;
TObjectPtr<UCapsuleComponent> CapsuleComponent = Cast<UCapsuleComponent>(
GetOwner()->GetComponentByClass(UCapsuleComponent::StaticClass()));
CapsuleComponent->IgnoreComponentWhenMoving(TraversalResult.HitComponent, false);
if (TraversalResult.ActionType == ETraversalActionType::Vault)
{
CharacterMovement->SetMovementMode(MOVE_Falling);
}
else
{
CharacterMovement->SetMovementMode(MOVE_Walking);
}
// 서버 권위를 다시 활성화
CharacterMovement->bIgnoreClientMovementErrorChecksAndCorrection = false;
CharacterMovement->bServerAcceptClientAuthoritativePosition = false;
}
- 해당 코드처럼 완전히 몽타주가 종료되었을 때, 다른 동작이 가능하게끔 구현하니 몽타주들이 다소 뒷동작이 길어서 아래처럼 자연스럽게 Blending이 되지 않는다.

4. 파쿠르 동작 후에 자연스러운 Blending 되도록 수정
AnimNofifyState를 상속받는 UAO_NotifyState_MontageBlendOut를 선언해서 자연스러운 블렌딩을 하고자 했다.
- Traversal 동작에 따라 자연스러운 전환을 위해 조건에 따라 블렌딩하고자 했다.
// AO_NotifyState_MontageBlendOut.h
UENUM(BlueprintType)
enum class EAO_TraversalBlendOutCondition : uint8
{
ForceBlendOut UMETA(DisplayName = "ForceBlendOut"),
WithMovementInput UMETA(DisplayName = "WithMovementInput"),
IfFalling UMETA(DisplayName = "IfFalling")
};
UCLASS()
class AO_API UAO_NotifyState_MontageBlendOut : public UAnimNotifyState
{
GENERATED_BODY()
protected:
virtual void NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference) override;
virtual FString GetNotifyName_Implementation() const override;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MontageBlendOut")
EAO_TraversalBlendOutCondition BlendOutCondition = EAO_TraversalBlendOutCondition::ForceBlendOut;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MontageBlendOut")
float BlendOutTime = 0.2f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MontageBlendOut")
FName BlendProfile = FName(TEXT("FastFeet_InstantRoot"));
};
- Blend Profile의 경우, 기본값으로
FastFeet_InstantRoot라는 Blend Profile이 설정되는데, 발과 다리에 가중치가 살짝 낮게 되어있다.
- Blend Profile은
Skeleton 에셋에서 확인을 할 수 있다.

NotifyTick을 통해 조건에 부합하는지 확인한 후에, 조건에 부합하게 되면 블렌드 세팅을 지정한 후에 Blend Out을 실행함
void UAO_NotifyState_MontageBlendOut::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
float FrameDeltaTime, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyTick(MeshComp, Animation, FrameDeltaTime, EventReference);
TObjectPtr<ACharacter> Character = Cast<ACharacter>(MeshComp->GetOwner());
TObjectPtr<UAnimInstance> AnimInstance = MeshComp->GetAnimInstance();
TObjectPtr<UAnimMontage> Montage = Cast<UAnimMontage>(Animation);
bool bShouldBlendOut = false;
if (BlendOutCondition == EAO_TraversalBlendOutCondition::ForceBlendOut)
{
bShouldBlendOut = true;
}
else if (BlendOutCondition == EAO_TraversalBlendOutCondition::WithMovementInput)
{
if (TObjectPtr<UCharacterMovementComponent> CharacterMovement = Character->GetCharacterMovement())
{
if (CharacterMovement->GetCurrentAcceleration().IsNearlyZero(0.1f))
{
bShouldBlendOut = true;
}
}
}
else if (BlendOutCondition == EAO_TraversalBlendOutCondition::IfFalling)
{
if (TObjectPtr<UCharacterMovementComponent> CharacterMovement = Character->GetCharacterMovement())
{
if (CharacterMovement->IsFalling())
{
bShouldBlendOut = true;
}
}
}
if (!bShouldBlendOut)
{
return;
}
// 블렌딩 시간과 커브를 정의
FAlphaBlendArgs AlphaBlendArgs;
AlphaBlendArgs.BlendTime = BlendOutTime;
AlphaBlendArgs.BlendOption = EAlphaBlendOption::HermiteCubic;
// BlendProfile : 몽타주 종료 시 특정 관절별로 블렌딩 가중치를 다르게 적용하는 블렌드 프로파일을 저장하고 있음
FMontageBlendSettings BlendSettings;
BlendSettings.BlendProfile = const_cast<UBlendProfile*>(AnimInstance->GetBlendProfileByName(BlendProfile));
BlendSettings.Blend = AlphaBlendArgs;
BlendSettings.BlendMode = EMontageBlendMode::Standard;
// BlendSettings를 사용하고 현재 재생 중인 Montage를 즉시 멈추고 블렌드 아웃을 진행함
AnimInstance->Montage_StopWithBlendSettings(BlendSettings, Montage);
}
FOnMontageEnded 델리게이트에 할당했던 함수를 FOnMontageBlendingOutStarted 델리게이트에 할당
- 호출할 함수 이름 또한
OnMontageBlendingOut으로 수정함
// AO_TraversalComponent.cpp
void UAO_TraversalComponent::PerformTraversalAnimation()
{
...
FOnMontageBlendingOutStarted BlendOutingDelegate;
BlendOutingDelegate.BindUFunction(this, FName("OnMontageBlendingOut"));
AnimInstance->Montage_SetBlendingOutDelegate(BlendOutingDelegate, TraversalResult.ChosenMontage);
...
}
- 애님 몽타주에
UAO_NotifyState_MontageBlendOut 노티파이 스테이트 추가


오늘의 CS
실수 자료형 주의사항
1. 정밀도
- 실수 자료형은 유한한 비트로 수를 표현하게 됨
- 따라서 수학적으로 완벽한 실수와 다르게 정밀도 문제가 발생할 수 있음
- 특정 자리수를 초과한 복잡한 계산을 수행하면, 이후의 숫자는 반올림되어 부정확해짐
2. 등가 비교
- 0.1과 같은 간단한 실수조차 이진수로 정확하게 표현할 수 없어서 항상 미세한 오차가 발생함
- 두 실수를 비교할 때, 앱실론(허용하는 오차) 값을 정의하여, 두 수의 차이가 아주 작은 오차 범위 내에 있는지 확인해야 함