유니티 팀 프로젝트를 하다가 오랜만에 언리얼으로 돌아왔는데 빠르게 진도를 나가야겠다
저번 개발일지에서 줌 인, 줌 아웃을 위한 입력 시스템까지 만들어서 로그까지 출력해봤던것으로 기억하는데 이제 실제로 구현해보자
줌 인/아웃을 구현하려면 어떻게 해야할까? 플레이어 캐릭터를 찍는 3인칭 카메라는 스프링 암에 붙어있고 우리는 스프링 암의 길이와 회전값을 조절해 멀리서 바라보는 것처럼 카메라를 구현해놨다
그럼 카메라의 위치를 변경하려면 스프링암의 길이와 회전값을 변경시키면 되지 않을까??
플레이어 캐릭터 클래스에 줌 인, 줌 아웃 함수를 만들어서 컨트롤러에서 줌 인, 줌 아웃 입력을 하면 캐릭터의 함수들이 실행이 되게 하면 값이 바뀌긴 하지만 예를 들어 줌 인 함수를 보자
CameraBoom->TargetArmLength = 100.f;
CameraBoom->SetRelativeRotation(FRotator(0.f, 0.f, 0.f));
이렇게 한다면 문제가 뭘까?
길이와 회전값이 바로 저 값으로 바뀌어서 뚝뚝 끊어보이는 부자연스러운 느낌이 들 것이다. 로스트아크에서는 휠을 돌리면 줌 인과 아웃이 부드럽게 바뀐다
부드럽게에 초점을 맞출 것이다. 언리얼에서 부드럽게라는 느낌을 주려면 값을 보간하는 느낌으로 가야함
구글링해보면 보간 함수를 다양하게 할 수 있는데 필자는 InterpTo 함수를 사용할 것임
값의 타입에 따라 InterpTo 함수가 다양하게 있는데 길이는 float이고 회전값은 FRotator라서 각각 해당하는 함수는 FInterpTo와 RInterpTo 함수이다
FMath 라이브러리에 있으니까 함수를 사용하려면 FMath::FInterpTo와 같이 사용해야 함
FMath::FInterpTo(float Current, float Target, float DeltaTime, float InterpSpeed)
자세히는 템플릿 함수이고 함수 내부에서 다양한 작업이 들어가는데 그건 직접 확인해보는 것으로
언리얼에서 값을 부드럽게 변화할 때 쓰는 함수이므로 알아두자
이제 플레이어 컨트롤러에서 ZoomIn, ZoomOut 이벤트가 발생하면 캐릭터 클래스에서 실행되는 함수를 정의할 것이다
플레이어 캐릭터 헤더에 bZoomIn이라는 현재 줌 인상태인지를 나타내는 부울 변수를 하나 두었다. 그리고 이 값은 ZoomIn, ZoomOut 함수에서 값을 변경한다(값만 바로 변경하는 짧은 함수이므로 인라인화 시킴)
// LKPlayerCharacter.h
public:
FORCEINLINE void ZoomIn() { bZoomIn = true; }
FORCEINLINE void ZoomOut() { bZoomIn = false; }
private:
uint8 bZoomIn : 1;
이 부울 변수의 값이 바뀌면 Tick 함수에서 이를 감지해 스프링암의 길이와 회전값을 InterpTo 함수를 이용해 보간할 것이다
그래서 줌 인/아웃 시 길이와 회전 값을 에디터에서 수정하면서 변경하기 위해 UPROPERTY를 사용했다
private:
UPROPERTY(EditAnywhere, Category = Camera, meta = (AllowPrivate))
float ZoomInArmLength;
UPROPERTY(EditAnywhere, Category = Camera, meta = (AllowPrivate))
float ZoomOutArmLength;
UPROPERTY(EditAnywhere, Category = Camera, meta = (AllowPrivate))
FRotator ZoomInRotation;
UPROPERTY(EditAnywhere, Category = Camera, meta = (AllowPrivate))
FRotator ZoomOutRotation;
이러면 이제 블루프린트로 만든 캐릭터 클래스에서 이를 확인하며 수정이 가능함

원래 캐릭터를 만들 때 Tick이 필요없을 것 같아 함수를 제거했었는데 다시 살려야겠지?
헤더 파일에 Tick 함수를 override 해주고 생성자에서 Tick을 활성화 시켜줘야함
virtual void Tick(float DeltaTime) override;
ALKPlayerCharacter::ALKPlayerCharacter()
{
PrimaryActorTick.bCanEverTick = true;
...
}
다음 Tick 함수를 구현해보면
void ALKPlayerCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bZoomIn)
{
CameraBoom->TargetArmLength = FMath::FInterpTo(CameraBoom->TargetArmLength, ZoomInArmLength, DeltaTime, 5.f);
CameraBoom->SetRelativeRotation(FMath::RInterpTo(CameraBoom->GetRelativeRotation(), ZoomInRotation, DeltaTime, 5.f));
}
else
{
CameraBoom->TargetArmLength = FMath::FInterpTo(CameraBoom->TargetArmLength, ZoomOutArmLength, DeltaTime, 5.f);
CameraBoom->SetRelativeRotation(FMath::RInterpTo(CameraBoom->GetRelativeRotation(), ZoomOutRotation, DeltaTime, 5.f));
}
}
자세한 설명은 안해도 될 것이라고 믿음! 근데 저 InterpSpeed를 따로 변수로 만들어야 할 것 같기도 한데 일단 이걸로 픽스

이제 어느정도 조작이 되니까 밋밋한 언리얼 기본 캐릭터에서 프로젝트에 맞는 플레이어 캐릭터 애셋을 받아올 것이다
에픽 스토어에서 언리얼 엔진 > 마켓 플레이스로 가서 무료 콘텐츠인 Paragon을 검색하면 많은 캐릭터들이 무료로 있다

이 애셋을 받아서 내 프로젝트에 추가해주자

Animation 폴더를 보면 애니메이션도 많으니까 무료로 이정도면 아주 좋다
이제 그럼 캐릭터를 바꿔야겠지?
현재는 LKCharacterBase에서 언리얼의 기본 마네킹을 가져와 스켈레탈 메시로 사용하는데 우리가 월드에서 쓰고 있는 플레이어 캐릭터는 LKCharacterBase를 상속받은 LKPlayerCharacter의 상속을 받은 블루프린트 클래스를 사용하고 있다
그럼 이 블루프린트 캐릭터로 들어가 스켈레탈 메시만 임포트한 메시로 바꾸면 되지 않을까??


예상대로 잘 된다! (늠름하네) 근데 좀 떠 있는 느낌이니까 알아서 조절해주자
이제 애니메이션을 적용해보자. Idle, Walking을 기본적으로 구현할 것임
강의에서 배운대로 하자면 먼저 C++ AnimInstance 클래스를 만들어서 애니메이션을 관리하게 할 것임
애님 인스턴스 클래스를 만들었다. 이제 캐릭터 클래스에서 이 애님 인스턴스 클래스를 사용하도록 해야 함
애니메이션 블루프린트 클래스를 만들어주면서 방금 만든 애님 인스턴스를 부모로 할 것임

LKCharacterBase와 LKPlayerCharacter 중 어느 클래스에서 초기화할 것인지는 개발자 선택임.
필자는 NPC들도 Idle 애니메이션이 필요하다 생각해 CharacterBase의 생성자에서 초기화시킬 것임
static ConstructorHelpers::FClassFinder<UAnimInstance> CharacterAnimRef(TEXT("/Game/LostKingdom/Animation/ABP_LKCharacter.ABP_LKCharacter_C"));
if (CharacterAnimRef.Class)
{
GetMesh()->SetAnimInstanceClass(CharacterAnimRef.Class);
}
생성자를 수정했으니 에디터 껐다 키기

키면 이렇게 된다.(근데 나는 테스트한다고 따로 애니메이션 블루프린트를 지정해줘서 상속이 적용안돼서 그냥 수동으로 넣었음. 새로 만들면 상속 잘 된다)
이제 캐릭터와 애님 인스턴스는 서로 GetAnimInstance, GetOwningActor 함수를 통해 서로에 대해 알 수 있다.
간략히 말하면 이제 애님 인스턴스에서 값이 변경되면 대충 애니메이션이 재생된다는 느낌
// LKAnimInstance.h
public:
ULKAnimInstance();
protected:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
TObjectPtr<class ACharacter> Owner;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Character")
TObjectPtr<class UCharacterMovementComponent> Movement;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character")
FVector Velocity;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character")
float GroundSpeed;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character")
uint8 bIsIdle : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character")
float MovingThreshold;
NativeInitializeAnimationNativeUpdateAnimationULKAnimInstance::ULKAnimInstance()
{
MovingThreshold = 3.f;
}
void ULKAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
Owner = Cast<ACharacter>(GetOwningActor());
if (Owner)
{
Movement = Owner->GetCharacterMovement();
}
}
void ULKAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
if (Movement)
{
Velocity = Owner->GetVelocity();
GroundSpeed = Velocity.Size2D(); // z축을 제외한 값만 얻어옴
bIsIdle = GroundSpeed < MovingThreshold;
}
}
그리고 변수들을 선언해줘서 초기화해주면 애니메이션 블루프린트의 애님 그래프에서 사용이 가능하다!
애니메이션 블루프린트로 가서 상속된 변수를 볼 수 있는지 체크하면 변수들이 보임

이제 애니메이션 블루프린트에서 변수들을 이용해 직접 애니메이션을 만들어보자

우리는 Locomotion을 따로 캐시에 저장시켜 다른 곳에서 해당 값을 받아올 것임

로코모션 스테이트 머신을 새로 만들어서 캐시에 저장


이거 다 스샷찍어서 할라면 너무 많아서 생략,,

Locomotion에서 idle과 walking 애니메이션을 캐릭터가 움직이는지에 따라 결과를 캐시하고
Main State Machine에 있는 Locomotion 상태에서 그 애니메이션을 사용하면 된다

이제 결과를 보면

잘 움직이는 것을 볼 수 있다.
오늘은 여기까지 하고 나중에 애니메이션을 더 발전시켜볼 것임!
Locomotion에서 Move 부분에 캐릭터의 GroundSpeed 값을 이용해 BlendSpace1D를 써서 원래는 Idle과 Walking을 섞었는데 생각대로 섞이지 않아 그냥 Walking만 애니메이션을 넣었는데 그러면 마우스 커서를 계속 누르고 있다가 캐릭터가 한 발만 들고 움직이는 상황이 발생했다
그래서 그냥 BlendSpace1D에서 Idle 애니메이션을 제외하고 Walking 애니메이션만 넣었는데 그러니까 정상적으로 움직이긴 했다..
아 애니메이션은 너무 어렵네 ㅠㅠ