이번엔 캐릭터의 점프와 애니메이션을 제작해보겠다.
점프 같은 경우 ACharacter 클래스에 Jump와 StopJumping이라는 함수로 전부 구현되어 있기 때문에 우리가 코드상에서 키 바인딩만 해주면 바로 사용이 가능하다.
점프 애니메이션
이전 포스팅에서 만든 Idle/Jog의 GroundLocomotion을 기본 베이스로 이 상태를 유지하다가 공중에 뜨게 된다면 Jump 모션을 진행하고 바닥에 닿을 경우 Landing을 진행해보자.
이렇게 LocoPose로 Save 할 것이다.
Locomotion의 State Machine은 다음과 같이 구상한다.
Idle/Jog는 GroundLocomotion을 Cahce한 값을 바로 건네주기만 하면 된다.
상태를 전이하는 것도 간단하다.
공중에 있는지 판단하기만 하면 된다.
Jumps의 경우 3가지 모션을 연속적으로 진행해줘야한다.
Start > Apex > PreLand 를 순서대로 진행해주면된다.
모든 상태 전이는 Auto로 넘어가게 된다. 각 애니메이션은 별다른 것 없이 넣어주기만 하면 된다.
Jump_Land를 하게 되는 경우는 단순히 IsInAir가 False가 될때 넘어가면 된다.
Land는 다음과 같이 구성된다.
땅에 자연스럽게 착지하기 위해 기본 베이스로 Ground_Loco를 설정하고 Jump후 땅에 도착했을 때 모션(살짝 앉았다 일어나는)을 Additive해서 섞어주면 된다.
이걸 최종 모션으로 지정해주면 이제 기본적인 움직임에 대한 모션은 제작이 완료되었다.
타이머
다만 이렇게 할 경우 점프 모션에 전부 진행되지 않았는데도 불구하고 계속 해서 점프를 하는 경우가 생긴다.
또한, 게임 내 기획 상으로 점프를 너무 많이 못하게 할 예정이기 때문에 점프를 시간 내에 다시 못하게 설정해야 한다.
따라서 Jump를 입력받는 곳에서 Timer 시스템을 이용해보자.
점프를 입력받는 곳에서 CheckJump 함수로 이어주자.
void APMPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* EnhancedInput = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Triggered, this, &APMPlayer::CheckJump);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Completed, this, &APMPlayer::JumpStop);
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APMPlayer::Move);
EnhancedInput->BindAction(LookAction, ETriggerEvent::Triggered, this, &APMPlayer::Look);
}
헤더에는 이렇게 선언해주고
// Jump 타이머
bool bIsPressJump;
FTimerHandle JumpHandle;
void CheckJump();
CheckJump에서 타이머를 발생한다.
void APMPlayer::CheckJump()
{
if (IsValid(this) == false) return;
TWeakObjectPtr<APMPlayer> WeakPlayer(this);
if (bIsPressJump == false)
{
Jump();
//UE_LOG(LogTemp, Log, TEXT("점프"));
bIsPressJump = true;
GetWorld()->GetTimerManager().SetTimer(JumpHandle, FTimerDelegate::CreateLambda(
[WeakPlayer]()
{
if (APMPlayer* WP = WeakPlayer.Get())
{
WP->bIsPressJump = false;
}
}), 2.0f, false);
}
}
람다 캡쳐를 이용해서 점프 후에 2초 동안 점프를 못하게 막은 것이다.
여기서 잘 알아봐야할 점은 유효성이다.
람다 캡처를 이용할 경우 문제는 타이머가 대기하고 있는 도중 객체가 사라지는 경우이다.
가령 예를 들어 5초뒤 스킬이 나가게 예약을 해두었는데 5초 안에 플레이어가 죽어서 플레이어가 Destory 되거나 레벨이 바뀌는 경우 객체를 참조하려고 하다 null 크래시가 날 수 있다.
물론 언리얼엔진에서는 객체가 파괴되거나 정리될때 자동으로 타이머를 수거하지만 멀티플레이 상황이거나 멀티쓰레드 환경에서는 정확히 동작할 수 없을 가능성도 있다.
따라서, 우리는 TWeakObjectPtr을 이용해서 지금 객체를 저장하고 있다가
2초뒤 타이머가 실행될 때, Get 함수를 이용해 WeakPtr로 부터 오브젝트가 유효한지 판단하고 그 뒤에 원하는 동작을 해주는 것이다.
이렇게 하면 비교적 안전하게 람다 캡처 문제를 해결할 수 있다.
다른 방법으로는 어느 객체가 사용하는 모든 TimerHandle을 TArray로 가지고 있다가,
EndPlay에서 TArray를 순회하며 ClearTimer를 해주는 방법이 있다.
두가지 방식을 모두 사용하면 더욱 좋을 것이다.
카메라 범위 제한
현재 3인칭 플레이로 카메라의 범위를 지정하지 않아 캐릭터의 머리부터 캐릭터의 발까지 회전이 가능하다.
따라서 되게 불쾌한 경험을 하거나 마우스의 감도가 제대로 설정되지 않아 플레이에 문제가 될것이다.
카메라의 상하 각도에 제한을 두어 이를 처리하자.
카메라는 현재 카메라 암에 붙어있는 상황이고 전부 ControlRotation의 모든 축을 그대로 사용하고 있다.
플레이어는 ControlRotation의 Yaw값만 사용하고 있다.
카메라의 상하를 담당하는 Rotation은 pitch 값이다. 따라서 ControlRotation의 Ptich 값을 강제로 수정해도 플레이어는 Yaw값만 사용하기 때문에 영향이 전혀 없을 것이다.
void APMPlayer::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
const float LookSensitivity = 0.3f; // 감도 조정
AddControllerYawInput(LookAxisVector.X * LookSensitivity);
// Pitch(상하) 회전
AddControllerPitchInput(LookAxisVector.Y * LookSensitivity);
// 현재 pitch 각도를 가져오고 -180에서 180 사이로 정규화
float CurrentPitch = FRotator::NormalizeAxis(GetControlRotation().Pitch);
// 정규화된 각도로 체크
if (CurrentPitch > MaxAngleCamera && CurrentPitch < 180.0f)
{
GetController()->SetControlRotation(FRotator(MaxAngleCamera, GetControlRotation().Yaw, 0.0f));
}
else if (CurrentPitch < MinAngleCamera || CurrentPitch > 180.0f)
{
GetController()->SetControlRotation(FRotator(MinAngleCamera, GetControlRotation().Yaw, 0.0f));
}
}
MaxAngle과 MinAngle을 지정해주기만 하면 카메라가 그 값을 벗어나지 않을 것이다.
결과
'Unreal Engine 5 > ProjectM' 카테고리의 다른 글
[UE5] ProjectM - 공격 애니메이션과 화살 발사 (0) | 2025.01.03 |
---|---|
[UE5] ProjectM - 캐릭터 움직임과 애니메이션 (1) | 2024.12.23 |