게임개발일지/리그오브워치

[리그오브워치개발일지] #16 LyraStarterGame 기본 애니메이션 분석 및 이주

김진우 개발일지 2024. 1. 28. 05:31

Git

레포지토리 주소 : [https://github.com/kjinwoo12/UE5Game_LeagueOfWatch]
기준 태그 : 16_LyraCharacter

완성 영상

완성된 캐릭터의 관찰을 위해서 PlayableCharacter의 카메라를 3인칭으로 옮겼다.

요구사항 확인

  • Lyra Starter Game에서 제공하는 기본적인 캐릭터 애니메이션을 League Of Watch에 이식

Lyra Sample Game은 언리얼 엔진5가 출시된 후 개발자들의 적응을 돕기 위해 마켓 플레이스에서 무료로 배포하는 샘플 프로젝트다. 캐릭터, AI, 이펙트, 게임모드 등 하나의 게임을 완성할 때 어떤 방법으로 만들어야 효과적인지 알 수 있다.
Lyra Sample Game의 캐릭터 애니메이션 블루프린트에는 게임에서 일반적으로 만들어야 하는 캐릭터 애니메이션이 구현되어 있다. Lyra Starter Game에 있는 애니메이션 블루프린트를 내 프로젝트에 이주시켜서 League of watch 개발에 사용한다면 큰 노력 없이 적당한 퀄리티의 캐릭터를 만들 수 있을 것이라고 생각한다.
또, 애니메이션 블루프린트가 어떤 방식으로 만들어졌는지 확인하면서 내 프로젝트에 성공적으로 이주시킨다면 에픽 게임즈에서 권장하는 캐릭터 애니메이션을 만드는 노하우를 얻을 수 있을 것이다.

ABP_Mannequin_Base 분석

이벤트 그래프

이벤트 그래프를 살펴보면 간단한 튜토리얼과 함께 예제 코드와 설명이 있는 것을 확인할 수 있다.

요약하자면 성능 향상을 위해 애니메이션 블루프린트에서는 이벤트 그래프는 사용하지 않고, BlueprintThreadSafeUpdateAnimation 함수를 사용한다는 뜻이다. 이벤트 그래프를 사용하던 UE4와는 달라졌다.

BlueprintThreadSafeUpdateAnimation

BlueprintThreadSafeUpdateAnimation을 들어가면 다시 AnimBP Tour를 확인할 수 있다. 요야갛자면 BlueprintThreadSafeUpdateAnimation에서 게임 개체의 데이터를 가져오는 방법으로 Property access를 사용하는 방법을 알려준다.

코드를 살펴보면 Property Access를 사용해서 게임 개체의 데이터를 애니메이션 재생에 필요한 데이터로 처리하는 모습을 볼 수 있다. - Update Location Data : 개체의 위치 데이터 처리 - Update Rotation Data : 개체의 회전 데이터 처리 - Update Velocity Data : 개체의 속도 데이터 처리 - Update Acceleration Data : 개체의 속도 변화 데이터 처리 - Update Wall Detection Heuristic : 개체가 벽에 부딪치는 상태인지 확인 - Update Character State Data : 개체의 상태 데이터(앉아있는지, 공중에 떠있는지, 총으로 조준하는 중인지 등등) 처리 - Update Blend Weight Data : 애니메이션 블랜딩 비중 처리 - Update Root Yaw Offset : [Turn In Place](https://www.youtube.com/watch?v=3zVh1-4zmy4&ab\_channel=MattAspland)(제자리 회전)을 위한 Yaw 데이터 처리 - Update Aiming Data : 조준 방향 관련 데이터 처리 - Update Jump Fall Data : 점프/낙하 관련 데이터 처리

각각의 함수는 모두 애니메이션 그래프에서 데이터를 사용할 수 있도록 데이터 처리 후 변수에 값을 저장하는 형태로 되어있다. 어떻게 사용하는지는 애니메이션 그래프를 확인해볼 필요가 있다.

AnimGraph

애니메이션 그래프에 들어가면 확인할 수 있는 튜토리얼이다. 요약하면 이 애니메이션 그래프에서는 애니메이션을 직접 참조하지 않고, 재사용할 수 있도록 특점 지점에 애니메이션을 재생할 수 있는 진입점을 제공하며 동시에 여러 진입점에서 받아온 애니메이션을 블랜딩 한다는 뜻이다.

https://docs.unrealengine.com/5.0/ko/animation-blueprint-linking-in-unreal-engine/
애니메이션 블루프린트 링크하기
애니메이션 블루프린트 링크 및 템플릿을 사용하여 애니메이션 블루프린트 로직을 모듈화합니다.
docs.unrealengine.com

애니메이션 블루프린트를 모듈화 하기 위한 장치로 AnimLayerInterface 를 사용한다.

전체 모습
부분 확대

Locomotion

(좌)AnimGraph에서 Locomotion cache, (우)ABP_MannequinLefthandPose_Override
순서대로 LeftFingersMask, LeftHandPose_OverrideState의 Layered blend per bone 설정, MM_Rifle_Idle_Hipfire 애니메이션

캐릭터의 기본적인 이동 관련 움직임을 담당하는 부분이다. LeftHandPose_OverrideState의 경우 ABP_ItemAnimLayersBase에서 구현되었지만 자식 애니메이션 블루프린트에서 애님시퀀스 LeftHands_Override가 전부 값 할당이 되지 않았기 때문에 확인할 수 없었지만 총 굵기에 따라 달라지는 왼손 그립 애니메이션을 위한 것으로 보인다.

Start, Cycle, Pivot은 블렌드스페이스 적용, 나머지는 단순히 애님 레이어 재생

스테이트 머신에서 Start(뛰기시작),Cycle(뛰는중), Pivot(방향전환)는 블렌드스페이스가 Additive animation으로 적용되었지만 나머지는 전부 업데이트 시 함수를 바인딩하고 애니메이션 레이어 인터페이스의 레이어를 재생하도록 되어있다. 실제 애니메이션과 연결된 부분은 무기 애니메이션을 담당하는 ABP_ItemAnimLayerBase를 확인하면 된다. 애니메이션 시퀀스는 ABP_ItemAnimLayerBase의 자식 애니메이션 블루프린트에서 클래스 디폴드 값으로 변수에 들어간다. 애니메이션 그래프 로직이 있는 ABP_Mannequin_BaseABP_ItemAnimLayersBase는 다른 애니메이션 블루프린트임에도 ABP_Mannequin_Base의 애님 그래프 로직이 잘 작동하는 이유는 애님 레이어 재생 방법을 확인하면 알 수 있다. 아래 링크 참고.

https://docs.unrealengine.com/5.0/ko/animation-blueprint-linking-in-unreal-engine/

블루프린트 이주

ABP_Mannequin_Base

먼저 애니메이션 블루프린트를 이주시키기 전에 ABP_Mannequin_Base의 부모 클래스인 LyraAnimInstance를 확인할 필요가 있다. C++ 클래스이기 때문에 단순히 ABP_Mannequin_Base를 액션->이주로 LeagueOfWatch에 옮길 수 없기 때문이다. C++ 클래스를 블루프린트로 바꾸거나 C++ 코드를 가져와 빌드해야 한다.
ABP_Mannequin_BaseULyraAnimInstance에서 사용하는 것은 GroundDistance 변수다. ULyraCharcterMovementComponent에서 라인트레이스로 바닥과 액터 사이의 거리를 측정한다.

void ULyraAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
    Super::NativeUpdateAnimation(DeltaSeconds);

    const ALyraCharacter* Character = Cast<ALyraCharacter>(GetOwningActor());
    if (!Character)
    {
        return;
    }

    ULyraCharacterMovementComponent* CharMoveComp = CastChecked<ULyraCharacterMovementComponent>(Character->GetCharacterMovement());
    const FLyraCharacterGroundInfo& GroundInfo = CharMoveComp->GetGroundInfo();
    GroundDistance = GroundInfo.GroundDistance;
}

애니메이션 업데이트마다 호출되는 NativeUpdateAnimation(float)에서 오너 액터의 ULyraCharacterMovementComponent로 바닥과의 거리를 측정한다. 바닥과 거리를 측정하는 기능은 블루프린트로 빼두거나 UAdvanceCharacterMovementComponent 클래스를 만들어 기능을 추가하는 방법으로 LeagueOfWatch 프로젝트에 옮길 수 있다.

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "AdvCharacterMovementComponent.generated.h"

USTRUCT(BlueprintType)
struct FAdvCharacterGroundInfo
{
    GENERATED_BODY()

    FAdvCharacterGroundInfo() :
        LastUpdateFrame(0),
        GroundDistance(0.0f)
    {
    }

    uint64 LastUpdateFrame;

    UPROPERTY(BlueprintReadOnly)
    FHitResult GroundHitResult;

    UPROPERTY(BlueprintReadOnly)
    float GroundDistance;
};

/**
 * 
 */
UCLASS()
class LEAGUEOFWATCH_API UAdvCharacterMovementComponent : public UCharacterMovementComponent
{
    GENERATED_BODY()
public:
    const FAdvCharacterGroundInfo& GetGroundInfo();

protected:
    FAdvCharacterGroundInfo CachedGroundInfo;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "Characters/AdvCharacterMovementComponent.h"

#include "GameFramework/Character.h"
#include "Components/CapsuleComponent.h"

namespace AdvancedCharacter
{
    static float GroundTraceDistance = 100000.0f;
    FAutoConsoleVariableRef CVar_GroundTraceDistance(TEXT("LyraCharacter.GroundTraceDistance"), GroundTraceDistance, TEXT("Distance to trace down when generating ground information."), ECVF_Cheat);
};

const FAdvCharacterGroundInfo& UAdvCharacterMovementComponent::GetGroundInfo()
{
    if(!CharacterOwner || (GFrameCounter == CachedGroundInfo.LastUpdateFrame))
    {
        return CachedGroundInfo;
    }

    if(MovementMode == MOVE_Walking)
    {
        CachedGroundInfo.GroundHitResult = CurrentFloor.HitResult;
        CachedGroundInfo.GroundDistance = 0.0f;
    }
    else
    {
        const UCapsuleComponent* CapsuleComponent = CharacterOwner->GetCapsuleComponent();
        check(CapsuleComponent);

        const float CapsuleHalfHeight = CapsuleComponent->GetUnscaledCapsuleHalfHeight();
        const ECollisionChannel CollisionChannel = (UpdatedComponent ? UpdatedComponent->GetCollisionObjectType() : ECC_Pawn);
        const FVector TraceStart(GetActorLocation());
        const FVector TraceEnd(TraceStart.X, TraceStart.Y, (TraceStart.Z - AdvancedCharacter::GroundTraceDistance - CapsuleHalfHeight));

        FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(LyraCharacterMovementComponent_GetGroundInfo), false, CharacterOwner);
        FCollisionResponseParams ResponseParam;
        InitCollisionParams(QueryParams, ResponseParam);

        FHitResult HitResult;
        GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, CollisionChannel, QueryParams, ResponseParam);

        CachedGroundInfo.GroundHitResult = HitResult;
        CachedGroundInfo.GroundDistance = AdvancedCharacter::GroundTraceDistance;

        if(MovementMode == MOVE_NavWalking)
        {
            CachedGroundInfo.GroundDistance = 0.0f;
        }
        else if(HitResult.bBlockingHit)
        {
            CachedGroundInfo.GroundDistance = FMath::Max((HitResult.Distance - CapsuleHalfHeight), 0.0f);
        }
    }

    CachedGroundInfo.LastUpdateFrame = GFrameCounter;

    return CachedGroundInfo;
}

이후 ABP_Mannequin_Base의 부모 클래스를 AnimInstance로 변경한 다음에 BlueprintAnimationUpdate를 오버라이드하고 아래처럼 블루프린트 코드를 추가한다. ULyraAnimInstance::NativeUpdateAnimation를 대체하는 코드다.

ABP_UnarmedAnimLayers

실제 애니메이션 재생을 위해 빈 손으로 움직이는 애니메이션 블루프린트인 ABP_UnarmedAnimLayers를 이주한다. 이주 전 반드시 Animation Warping 플러그인과 Animation Locomotion Library 플러그인을 활성화하자.

실행하면 FootstepEffectTagModifier에서 많은 오류가 나타나는 것을 확인할 수 있다. 이주되지 못한 C++ 코드 때문으로 보인다.AnimNotify_LyraContextEffects를 추가하면 해결된다. AnimNotify_LyraContextEffect도 필요한 다른 C++ 코드가 많아 글에 다 적지 못하니 Git Repo에서 Source/*/Feedback/ContextEffects/* 를 확인하면 된다.