개요
본 글에서는 ALS-Refactor 플러그인을 사용해 TPS게임에서 사용하는 정조준 시스템((Aimimg Down Sight, ADS라고도 한다.)을 구현하는 방법에 대해서 살펴볼 것이다. ALS-Refactor 플러그인을 사용했지만, Enhanced Input System을 제외하곤 플러그인에서 제공하는 기능은 거의 사용하지 않았다. 따라서 플러그인 사용자는 물론 미사용자도 비슷하게 구현할 수 있을 것이라 생각한다.
자료 조사
ALS가 기본적으로 1인칭 조준 카메라를 지원하긴 했지만 별로 마음에 들지 않았다. GTA 5의 1인칭 시스템과 비슷한 수준으로, 3인칭 캐릭터에다 단순히 카메라만 달아놓고 움직이는 격이었다.
그래서 우선 레퍼런스를 배틀그라운드, 베일드 엑스퍼트로 잡고 구글로 자료조사를 시작했다.
정조준을 해외에서는 Aiming Down Sight, ADS라고 부르기 때문에 이것으로 검색을 했다.
- Aiming Down Sights -The CORRECT Way | Black Ops Zombies in UE4 #2
- UE4 TRUE FIRST PERSON ADS TUTORIAL
- Weapon System Series Part-3 Basic ADS System In Alsv4 / IronSights
- Procedural ADS / Aiming with IK-bones ( no animations needed ) - FPS Animations Full Tutorial - # 9
레퍼런스와 완전히 똑같은 영상은 찾을 수 없었지만 FPS게임에서 구현하는 방법은 자료가 많이 있었다.
이를 토대로 3인칭 게임에서의 정조준을 구현해야할 방법을 대충 추려보았다.
- 정조준용 카메라 컴포넌트를 따로 캐릭터에 생성
- 정조준을 했을때 정조준용 카메라로 전환
- 원상복구시에는 다시 3인칭 카메라로 전환
알고리즘
정조준을 했을 때, 3인칭 카메라가 1인칭 카메라의 위치로 부드럽게 이동하며 이동이 완료되었을 때 1인칭 카메라로 변경된다.
정조준을 풀었을 때, 1인칭 카메라가 3인칭 카메라로 변경되며, 3인칭 카메라는 원래 위치로 부드럽게 이동한다.
다만 카메라 이동속도보다 플레이어가 카메라를 더 빠르게 움직일 경우 이동이 완료되지 않는다. 따라서 제한 시간을 두고, 제한시간을 넘기면 이동을 끝내고 카메라를 강제로 변경시킨다.
또한, 1인칭 시점에서는 3인칭에서 보이는 캐릭터가 보이지 않게 해준다.
3인칭 시점에서는 1인칭에서 보이는 총기가 보이지 않게 해준다.
쉽게 말해서 눈속임인 것이다.
쓰다보니 용어가 좀 헷갈릴 것 같아 정리해보자면
1인칭 카메라는 ADS 카메라.
3인칭 카메라는 Third Person 카메라.
ADS는 Aiming Down Sight, 정조준을 뜻한다.
실제로 다른 게임들에서는 어떤 알고리즘을 사용하는지는 모르겠다. 하지만 플레이해본 것으로 추측해보자면, 위의 알고리즘이 아니라 즉시 1인칭 카메라로 변경한 후에 Field Of View값을 부드럽게 줄이고(확대하는 효과) 총기를 조준하는 애니메이션을 재생하는 식으로도 하는 것 같다.
캐릭터 클래스
AAlsCharacter를 상속받은 C++ 클래스를 새로 만든다. 그 후 이 클래스를 상속받은 블루프린트 클래스를 만든다. 컴포넌트를 사진과 같이 C++에서 구성해준다. 완전히 똑같을 필요는 없다. 1인칭 정조준용 카메라와 3인칭용 카메라를 따로 만든다는 것이 중요하다.
각각의 컴포넌트를 설명하자면 다음과 같다.
- Spring Arm: 1인칭 정조준 카메라용 스프링 암이다. 카메라(ADSCamera)와 정조준용 총 메쉬(ADS Skeletal Mesh)의 거리가 0이기 때문에 필요 없을 수도 있다. 하지만 카메라와 메쉬를 같은 부모 컴포넌트 아래에 두고 싶었고, 카메라를 메쉬의 자식으로 놓거나 역으로 하면 둘 중 하나의 트랜스폼을 변경했을 때 같이 변경되기 때문에 이렇게 했다.
- ADS Camera: 정조준했을 때 사용자가 보게 되는 카메라.
- ADS Skeletal Mesh: 정조준했을 때 사용자에게 보이는 총기 메쉬..
- Mesh: 3인칭에서 보이는 캐릭터의 메쉬.
- Overlay Static Mesh: ALS에서 사용하는 Overlay용 스태틱 메쉬.
- Overlay Skeletal Mesh: ALS에서 사용하는 Overlay용 스켈레탈 메쉬. 단순히 3인칭에서 보이는 무기의 메쉬라고 생각하면 된다. ALS에서 정의한 Overlay는 M4, PistolTwoHanded, Bow 등등이 있다. 기존 캐릭터 애니메이션에 메쉬와 애니메이션이 추가되어 각각 M4를 든 애니메이션, 양손으로 권총을 든 애니메이션, 활을 든 애니메이션이 재생된다. 이때 필요한 M4, 권총, 활 등의 메쉬가 스켈레탈 메쉬라면 이 메쉬에 할당해 사용한다. 스태틱 메쉬라면 Overlay Static Mesh에 할당해 사용한다.
- Third Person Spring Arm: 3인칭 카메라용 스프링 암.
- Third Person Arrow: 정조준을 풀었을 때 카메라를 다시 3인칭 카메라의 위치로 다시 이동하기 위한 트랜스폼을 저장하는 컴포넌트.
- Third Person Camera: 3인칭에서 사용자가 보게되는 카메라.
- Arrow Component: 캐릭터의 정면을 가리키는 애로우 컴포넌트. ALS에서 사용하는 것 같은데, 이 글에서는 중요하진 않다.
우선 컴포넌트를 정의하는 헤더파일 코드부터 살펴보자.
// 캐릭터 클래스의 헤더 파일 (ex: AMyCharacter.h)
// GENERATED_BODY() 아래 부분에 정의한다.
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<USpringArmComponent> SpringArm;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UCameraComponent> ADSCamera;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<USpringArmComponent> ThirdPersonSpringArm;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UCameraComponent> ThirdPersonCamera;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UArrowComponent> ThirdPersonArrow;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<USkeletalMeshComponent> OverlaySkeletalMesh;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<USkeletalMeshComponent> ADSSkeletalMesh;
float FieldOfView = 90.f; // 추후에 설명
다음은 정의한 컴포넌트를 생성하는 .cpp파일의 생성자 부분이다.
// 캐릭터 클래스의 cpp파일(ex: AMyCharacter.cpp)
AMyCharacter::AMyCharacter()
{
OverlaySkeletalMesh = CreateDefaultSubobject<USkeletalMeshComponent>("OverlaySkeletalMesh");
OverlaySkeletalMesh->SetupAttachment(GetMesh());
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(Cast<USceneComponent>(GetCapsuleComponent()));
SpringArm->bUsePawnControlRotation = true;
SpringArm->TargetArmLength = 0.f;
ThirdPersonSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("ThirdPersonSpringArm"));
ThirdPersonSpringArm->SetupAttachment(GetMesh());
ThirdPersonSpringArm->bUsePawnControlRotation = true;
ThirdPersonSpringArm->SetRelativeLocation({ -30,0,175 });
ThirdPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ThirdPersonCamera"));
ThirdPersonCamera->SetupAttachment(ThirdPersonSpringArm);
FieldOfView = ThirdPersonCamera->FieldOfView; // 추후에 설명
ThirdPersonArrow = CreateDefaultSubobject<UArrowComponent>(TEXT("ThirdPersonArrow"));
ThirdPersonArrow->SetupAttachment(ThirdPersonSpringArm);
ADSSkeletalMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("ADSSkeletalMesh"));
ADSSkeletalMesh->SetupAttachment(SpringArm);
ADSSkeletalMesh->SetOnlyOwnerSee(true);
ADSSkeletalMesh->SetVisibility(false);
ADSCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ADSCamera"));
ADSCamera->SetupAttachment(SpringArm);
}
이렇게 하고 컴파일한다. 에디터에서 캐릭터의 블루프린트를 열어보았을 때 위의 사진과 비슷하게 나온다면 된 것이다. 가끔 컴포넌트가 제대로 생성되지 않는 경우가 있다. 그럴땐 UPROPERTY
의 Visible, Edit 관련 설정이 잘못되었을 가능성이 높으므로 바꿔주고 다시 컴파일하면 된다. 그래도 안된다면 이 글을 참고하자.
입력 설정
본 글에서는 배틀그라운드 게임과 비슷하게 마우스 오른쪽 버튼을 꾹 눌렀을 때는 3인칭 조준, 한번 눌렀을때는 1인칭 정조준을 하게 만들 것이다. 하지만 이건 어디까지나 예시이며, 개발자가 원하는대로 바꿔도 상관없다.
ALS-Refactor의 Enhanced Input System
ALS은 Enhanced Input System을 사용한다. 이 시스템이 작동하는 방식은 비교적 간단하다. 우선 Input Action
을 정의한다. Input Action
에는 여러가지 값들이 있다. 액션의 값이 무엇인지(Value Type
), 언제 발동하는지 (Triggers
) 등등. 그러고나선 Input Mapping Context
를 만든다. 여기에는 앞서 정의한 액션들을 추가하고, 그 액션에 해당하는 키(키보드나 마우스 입력)이 무엇인지 정의한다. 요약하자면, Input Mapping Context
에 추가한 특정 Input Action
이 있고, 이 액션에 할당한 키를 정의한대로 조작하면 트리거가 되는 방식이다.
ALS-Refactor의 조준 관련 입력을 살펴보면 마우스 오른쪽 버튼을 눌렀을 때와 뗐을 때 IA_Aim
이 트리거된다. 이 액션은 AlsCharacterExample.cpp
파일을 보면 단순히 DesiredAiming
값을 바꿔주고 있다.
void AAlsCharacterExample::InputAim(const FInputActionValue& ActionValue)
{
SetDesiredAiming(ActionValue.Get<bool>());
}
이것들을 활용해서 아래에서 InputADS
함수와 InputAim
함수를 만들 것이다.
IA_Aim 바꾸기
우선 IA_Aim
을 마우스 오른쪽 버튼을 꾹 눌렀을 때만 트리거되도록 바꿀 것이다. 기존의 IA_Aim
은 아래와 같이 Pressed
, Released
했을 때 트리거 된다. 이를 아래와 같이 Hold
, Hold And Release
로 바꿔준다.
이렇게 해서 3인칭 조준 입력은 완성되었다. 이제 1인칭 정조준을 위한 액션을 만들 것이다.
IA_ADS 만들기
앞서 말했듯이, 오른쪽 마우스 버튼을 한번 눌렀을 때 1인칭 정조준(Aiming Down Sight, ADS)가 되도록 만들 것이다.
아래와 같이 만들어준다.
주의할 점은, Tap Release Time Threshold
를 IA_Aim
의 Hold Time Threshold
보다 작게 해줘야한다. 이 Time Threshold들로 마우스 오른쪽 버튼을 눌렀다 뗐을 동안의 시간에 따라 Hold냐 Tap이냐를 판단하기 때문이다. 사진과 같은 경우는 0.1초내로 눌렀다 뗐을 경우 Tap, 0.15초 이상 눌렀을때는 Hold로 구분해 처리한다.
Input Mapping Context에 등록
사용할 Input Mapping Context
에 IA_ADS
를 추가하고 Right Mouse Button
을 추가한다.
이렇게 하면 에디터에서 할 일은 끝났고, C++에서 작업을 계속한다.
C++에서 액션 바인딩
캐릭터 클래스에서 SetupPlayerInputComponent
를 오버라이드해 액션을 바인딩해준다.
사용할 Input Action
과 Input Mapping Context
를 헤더파일에 선언해주고 UPROPERTY
로 블루프린트 에디터에서 지정할 수 있게 해준다.
// 캐릭터 클래스의 헤더파일 (ex: AMyCharacter.h)에 선언한다.
protected:
virtual void SetupPlayerInputComponent(UInputComponent* Input) override;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputMappingContext> InputMappingContext;
// Locomotion 관련 Action
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> LookMouseAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> RunAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> CrouchAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> JumpAction;
// 조준 관련 Action
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> AimAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr<UInputAction> ADSAction;
이렇게 하고 컴파일하면 캐릭터의 블루프린트 클래스 에디터에서 아래와 같은 화면을 볼 수 있다. 알맞게 넣어주면 된다.
이제 .cpp파일의 오버라이드된 SetupPlayerInputComponent
함수에서 이 액션들을 바인딩해준다.
// 캐릭터 클래스의 cpp파일(ex: AMyCharacter.cpp)에 구현한다.
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* Input)
{
Super::SetupPlayerInputComponent(Input);
auto* EnhancedInput{ Cast<UEnhancedInputComponent>(Input) };
if (IsValid(EnhancedInput))
{
// Locomotion 관련 Action. 이 글에서는 이 함수들을 다루지 않는다.
// AAlsCharacterExample을 참고하자.
EnhancedInput->BindAction(LookMouseAction, ETriggerEvent::Triggered, this, &ThisClass::InputLookMouse);
EnhancedInput->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ThisClass::InputMove);
EnhancedInput->BindAction(RunAction, ETriggerEvent::Triggered, this, &ThisClass::InputRun);
EnhancedInput->BindAction(CrouchAction, ETriggerEvent::Triggered, this, &ThisClass::InputCrouch);
EnhancedInput->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ThisClass::InputJump);
// 조준 관련 Action
EnhancedInput->BindAction(AimAction, ETriggerEvent::Triggered, this, &ThisClass::InputAim);
EnhancedInput->BindAction(ADSAction, ETriggerEvent::Triggered, this, &ThisClass::InputADS);
}
}
그 후 InputAim과 InputADS함수를 구현한다.
C++ 기능 구현
InputAim 함수 구현
InputAim
함수에서 배틀그라운드처럼 3인칭 조준 시 카메라를 가까이 옮길수도 있겠지만, 이 코드에서는 구현하지 않았다. 각자 입맛에 맞게 원하는대로 구현하자. Third Person Spring Arm의 Target Arm Length를 FInterpTo
로 줄이고 늘리고 하면 비슷하게 될 것이다.
void AMyCharacter::InputAim(const FInputActionValue& ActionValue)
{
// 3인칭 조준 시 필요한 로직을 넣자.
// 꾹 누르고 있을때 한번 호출되며, ActionValue.Get<bool>()의 값이 1이다.
// 뗐을 때 한번 호출되며 ActionValue.Get<bool>()의 값이 0이다.
// SetDesiredAiming(ActionValue.Get<bool>());
}
InputADS 함수 구현
InputADS
가 하는 일은 개요에서 말했듯이 다음과 같다.
정조준을 했을 때, 3인칭 카메라가 1인칭 카메라로 부드럽게 이동하며 이동이 완료되었을 때 1인칭 카메라로 변경된다.
정조준을 풀었을 때, 1인칭 카메라가 3인칭 카메라로 변경되며, 원래 위치로 부드럽게 이동한다.
따라서 현재 정조준 상태인지 아닌지 구분이 필요하며 이에 사용할 bADS
변수를 헤더파일에 정의한다.
bool bADS = false; // 현재 정조준 상태인가?
우클릭을 했을 때 호출되는 InputADS함수는 아래와 같이 작성할 수 있을 것이다.
void AMyCharacter::InputADS(const FInputActionValue& ActionValue)
{
if (bADS)
{
// 정조준 상태(1인칭 카메라)이면 3인칭 카메라로 바꾼다.
SetDesiredAiming(false); // ALS 함수
ToADSCamera(false);
}
else
{
// 3인칭 카메라면 정조준 상태(1인칭 카메라)로 바꾼다.
SetDesiredAiming(true); // ALS 함수
ToADSCamera(true);
}
}
ToADSCamera(bool)
은 bool값에 따라 다른 카메라로의 부드러운 이동을 시작하라고 알리는 함수이다. 세부 구현은 아래에서 다룰 것이다. 코드를 보면 정조준 상태일때 우클릭을 하면 3인칭 카메라로, 정조준 상태가 아닐때 우클릭하면 1인칭 카메라로 변경함을 알 수 있다.
ToADSCamera 함수 구현
앞서 말했듯, ToADSCamera(bool)
은 bool값에 따라 다른 카메라로의 부드러운 이동을 시작하라고 알리는 함수이다. true면 3인칭 카메라에서 1인칭 카메라로, false면 1인칭 카메라에서 3인칭 카메라로 이동을 해야한다. 부드러운 이동은 DeltaTime을 활용해야하므로 Tick
에서 담당할 것이며 이 함수에서는 단지 부드러운 이동을 해야하는 지에 대한 bool 변수 설정을 해준다. 따라서 헤더파일에 변수를 추가로 정의한다.
bool bDesiredADS = false; // 정조준 상태로 전환해야하는가?
그리고 이 함수에서 할 일이 더 남아있는데, 좀 더 자연스럽게 보이기 위해 메쉬의 Visibility를 변경한다.
void AMyCharacter::ToADSCamera(bool bToADS)
{
SmoothCameraCurrentTime = 0.f;
if (bToADS)
{
// 카메라를 ThirdPerson에서 ADS로 변경
bDesiredADS = true;
SetViewMode(AlsViewModeTags::FirstPerson);
if (IsValid(OverlaySkeletalMesh))
{
OverlaySkeletalMesh->SetVisibility(false);
}
if (GetMesh())
{
GetMesh()->SetVisibility(false);
}
if (IsValid(ADSSkeletalMesh))
{
ADSSkeletalMesh->SetVisibility(true);
}
}
else
{
// 카메라를 ADS에서 ThirdPerson으로 변경
bDesiredADS = false;
ADSCamera->SetActive(false);
ThirdPersonCamera->SetActive(true);
SetViewMode(AlsViewModeTags::ThirdPerson);
if (IsValid(OverlaySkeletalMesh))
{
OverlaySkeletalMesh->SetVisibility(true);
}
if (GetMesh())
{
GetMesh()->SetVisibility(true);
}
if (IsValid(ADSSkeletalMesh))
{
ADSSkeletalMesh->SetVisibility(false);
}
}
}
이때 true와 false 케이스의 코드가 살짝 다르다. 부드럽게 이동하는 카메라는 3인칭 카메라(Third Person Camera)이기 때문이다.
정조준을 한 상태, 즉 이미 1인칭 카메라가 활성화된 경우 Tick함수에서 3인칭 카메라를 이동시켜도 플레이어가 보는 것은 1인칭 카메라기 때문에 변함이 없을 것이다. 따라서 정조준을 풀때(ToADSCamera(false)
를 호출했을때)는 먼저 SetActive를 통해 바로 1인칭 카메라를 비활성화시키고 3인칭 카메라를 활성화시켜준다.
정조준을 하지 않은 상태는 이미 활성화된 카메라가 3인칭 카메라이기 때문에, 다음에 구현할 함수에서 그대로 이동만 시켜주면 된다. 따라서 별 다른 처리없이 bDesiredADS = true
를 해주면 된다.
그 다음 부드러운 이동을 하기 위한 함수를 만든다.
SmoothADSCamera 함수 구현
SmoothADSCamera
는 DeltaTime
과 bDesiredADS
에 따라 카메라를 부드럽게 이동시키는 함수이다.
개요에서 다뤘던 알고리즘을 다시 생각해보자.
정조준을 했을 때, 3인칭 카메라가 1인칭 카메라의 위치로 부드럽게 이동하며 이동이 완료되었을 때 1인칭 카메라로 변경된다.
정조준을 풀었을 때, 1인칭 카메라가 3인칭 카메라로 변경되며, 3인칭 카메라는 원래 위치로 부드럽게 이동한다.
다만 카메라 이동속도보다 플레이어가 카메라를 더 빠르게 움직일 경우 이동이 완료되지 않는다. 따라서 제한 시간을 두고, 제한시간을 넘기면 이동을 끝내고 카메라를 강제로 변경시킨다.
따라서 매 프레임 호출되는 Tick
에서 호출해 DeltaSeconds
를 매개변수로 넘겨준다.
void AMyCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
SmoothADSCamera(DeltaSeconds);
}
제한시간을 위한 변수와 카메라 이동속도 변수도 헤더파일에 추가해준다. 수치 바꿀때마다 컴파일해서 확인하면 너무 느리니 블루프린트 에디터에서 수정할 수 있게 만들어 빨리빨리 확인할 수 있게 한다.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SmoothCameraSpeed = 5.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SmoothCameraTimeThreshold = 0.5f;
float SmoothCameraCurrentTime = 0.f;
여기에 추가로 혼자 테스트하다 알아낸 것이 있는데, 1인칭 카메라의 Field Of View값을 3인칭 카메라의 값보다 좀 더 작게 바꾸면 자연스럽다. 여타 게임에서도 테스트해보니 이렇게 하는 것처럼 보인다.
예를 들면 ADSCamera
의 FOV값을 70, ThirdPersonCamera
의 FOV값을 90으로 해놓고 SmoothADSCamera
함수에서 ThirdPersonCamera
의 FOV값을 70-90간 변경시킨다. 따라서 FOV값을 원래대로 다시 되돌릴 수 있게 하기 위해 3인칭 카메라의 FOV값을 변수에 저장해놓는다.
// 캐릭터 클래스의 헤더파일
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float FieldOfView = 90.f;
생성자에서 이 값을 초기화해준다.
// 캐릭터 클래스의 생성자 함수 내부
FieldOfView = ThirdPersonCamera->FieldOfView;
이렇게 앞서 말한 구현내용에 필요한 변수 선언은 모두 마쳤다.
SmoothADSCamera
함수 구현은 다음과 같다.
void AMyCharacter::SmoothADSCamera(float DeltaTime)
{
if(bDesiredADS && !bADS)
{
SmoothCameraCurrentTime += DeltaTime;
// 카메라를 ThirdPerson에서 ADS로 변경
if (IsValid(ThirdPersonCamera) && IsValid(ADSCamera))
{
FVector NewLocation = UKismetMathLibrary::VInterpTo(ThirdPersonCamera->GetComponentLocation(), ADSCamera->GetComponentLocation(), DeltaTime, SmoothCameraSpeed);
ThirdPersonCamera->SetWorldLocation(NewLocation);
float NewFOV = UKismetMathLibrary::FInterpTo(ThirdPersonCamera->FieldOfView, ADSCamera->FieldOfView, DeltaTime, SmoothCameraSpeed);
ThirdPersonCamera->SetFieldOfView(NewFOV);
if (UKismetMathLibrary::VSize(ThirdPersonCamera->GetComponentLocation() - ADSCamera->GetComponentLocation()) < 0.5f ||
SmoothCameraCurrentTime >= SmoothCameraTimeThreshold)
{
// 변경 완료
SmoothCameraCurrentTime = 0.f;
bADS = true;
ThirdPersonCamera->SetActive(false);
ADSCamera->SetActive(true);
if (IsValid(ADSSkeletalMesh))
{
ADSSkeletalMesh->SetVisibility(true);
}
}
}
}
else if(!bDesiredADS && bADS)
{
SmoothCameraCurrentTime += DeltaTime;
if (IsValid(ThirdPersonCamera) && IsValid(ADSCamera))
{
// 카메라를 ADS에서 ThirdPerson으로 변경
FVector NewLocation = UKismetMathLibrary::VInterpTo(ThirdPersonCamera->GetComponentLocation(), ThirdPersonArrow->GetComponentLocation(), DeltaTime, SmoothCameraSpeed);
ThirdPersonCamera->SetWorldLocation(NewLocation);
float NewFOV = UKismetMathLibrary::FInterpTo(ThirdPersonCamera->FieldOfView, FieldOfView, DeltaTime, SmoothCameraSpeed);
ThirdPersonCamera->SetFieldOfView(NewFOV);
if (UKismetMathLibrary::VSize(ThirdPersonCamera->GetComponentLocation() - ThirdPersonArrow->GetComponentLocation()) < 0.5f ||
SmoothCameraCurrentTime >= SmoothCameraTimeThreshold)
{
// 변경 완료
ThirdPersonCamera->SetFieldOfView(FieldOfView);
SmoothCameraCurrentTime = 0.f;
bADS = false;
}
}
}
}
ThirdPersonCamera
의 위치와 타겟의 위치를 VInterpTo
함수로 값을 보간하고, 보간한 값으로 위치를 갱신해준다. 함수가 호출될 때마다 DeltaTime * SmoothCameraSpeed
만큼 타겟에 가까워진다. 이 값은 0~1사이의 값이 될 것이며 0은 제자리, 1은 타겟 위치를 뜻한다. 예를 들어 이 값이 매 호출될 때마다 0.16이라면, 0.16의 비율만큼 계속해서 타겟 위치에 가까워지게 된다. 따라서 한 번에 위치를 이동하는게 아니라 호출될 때마다 (보통 프레임마다) 조금씩 이동하므로 결국 부드럽게 이동하는 것처럼 보이게 된다. 잘 이해가 안된다면 선형 보간에 대해서 찾아보자.
FOV값도 마찬가지로 FInterpTo
함수로 값을 보간해준다.
또 보간의 문제는 타겟에 가까워질지언정 완전히 그 타겟이 되지는 않는다는 것이다.
따라서 VSize
함수로 각 위치 간 거리를 계산해 이 값이 충분히 작다고 판단하면 이동이 완료되었다고 판단한다.
완료된 뒤에는 카메라 전환이나 값 변경을 해주면 된다.
이때 bADS
와 bDesiredADS
가 헷갈릴 수 있는데, 차근차근 따져보자.
- bDesiredADS == true && bADS == false: 정조준 상태가 아니지만 정조준을 하려고 하는 상태이다. 즉 카메라를 1인칭 카메라 위치로 이동시키면 된다. 이동이 완료된 후에는
bDesiredADS = true
,bADS = true
가 된다. - bDesiredADS == true && bADS == true: 정조준을 하려고 하고 이미 정조준한 상태이다. 정조준을 풀려면
bDesiredADS = false
로 바꿔주면 된다. - bDesiredADS == false && bADS == true: 정조준 상태이지만 정조준을 풀려고 하는 상태이다. 즉 카메라를 3인칭 카메라 위치로 이동시키면 된다. 이동이 완료된 후에는
bDesiredADS = false
,bADS = false
가 된다. - bDesiredADS == false && bADS == false: 정조준 상태가 아니며 하려고 하지도 않는 상태이다. 정조준하려면
bDesiredADS = true
로 바꿔주면 된다.
여기서 InputADS
를 통해 bDesiredADS
값이 바뀌는 것을 생각해보면 올바르게 작동함을 알 수 있다.
결론
이렇게 해서 ALS-Refactor를 사용했을 때 정조준 시스템을 구현하는 방법에 대해 알아보았다.
플러그인을 사용하지 않더라도 앞서 말한 핵심 알고리즘을 사용하면 비슷하게 구현이 될 것이라 생각한다.
다음에는 이렇게 정조준한 총기를 캐릭터가 이동할때나 마우스를 움직일 때 흔들어줘서 자연스러운 연출을 하는 법에 대해서 알아보겠다.
발생할 만한 문제들
캐릭터 클래스 컴파일 시 ALS 헤더 파일을 찾지 못한다.
게임이름.Build.cs
의 PublicDependencyModuleNames
에 아래와 같이 추가한다.
PublicDependencyModuleNames.AddRange(new string[]
{
"다른 모듈들...",
"ALS",
"ALSCamera",
"ALSEditor",
"ALSExtras",
});
1인칭 카메라가 총을 관통한다.
카메라가 총기 메쉬에 가까이 있기 때문에 발생하는 문제다.
Edit -> Project Settings -> Engine -> General Settings -> Settings -> Near Clip Plane의 값을 1.0정도로 작게 바꾼다.
이후 언리얼 엔진을 껐다 켜야 제대로 적용된다.
카메라 이동속도보다 플레이어가 카메라를 움직이는 속도가 더 빠르다.
이 글에서는 제한 시간을 두어 해결했고 플레이하기에 문제가 없어보였다.
하지만 다른 게임들을 해본 결과 비슷한 방식을 사용한 것 같지는 않았다. 플레이어가 카메라를 움직이는 속도에 비례해서 추가적인 처리를 해줘도 괜찮을 것 같다.
혹시 더 좋은 방법이나 알고리즘이 있다면 댓글로 남겨주시면 감사하겠습니다!
1인칭 총기 메쉬가 벽이나 다른 물체를 관통해서 들어간다.
본 글에서는 이러한 문제를 겪진 않았지만, 구현에 따라 겪을 수도 있다고 생각해 찾아본 방법들을 남긴다.
배틀그라운드에서는 1인칭 시점으로 벽에 가까이 붙었을 때 총을 위로 드는 행동을 취한다.
매테리얼을 수정해서 관통하지 않게 하는 방법도 있다.
[Youtube] Unreal Engine 4 Tutorial - Fix Weapon Clipping and Object-specific FOV
아니면 이런 방법도 있는데, 언리얼에서 어떻게 구현하는 것인지는 아직 잘 모르겠다.
[Youtube] A Sneaky Trick Most FPS Games Use
ALS 디스코드에서 찾은 짤이다. FPS 게임에서 총기 메쉬의 스케일을 줄여서 눈속임하는 법이다.