게임에 있어서 가장 중요한 부분을 다뤄보고자 한다.
캐릭터 컨트롤 요소
캐릭터를 컨트롤하기 위한 용어 및 요소를 알아보자.
1. 컨트롤러 : 입력자의 의지를 지정할 때 사용한다. 목표 지점, 방향, 위치를 지정하는 것, ControlRotation이라는 속성을 가지고 이를 주로 사용한다.
2. 폰 : 폰의 트랜스폼을 지정하는 용도로 사용된다.
3. 카메라 : 화면 구도를 설정하기 위해서 사용된다. 카메라만 단독 사용할 경우 1인칭의 경우에 사용된다.
4. 스프링 암: 카메라의 지지대, 화면 구도를 설정하는데 3인칭에 사용된다. 셀카봉이라 생각하면 된다.
5. 캐릭터 무브먼트 : 캐릭터의 이동과 회전을 조정하는 용도로 사용된다.
우리가 회전하고 싶은 값은 컨트롤러에서 Desired Rotation이다. 그리고 그냥 Rotation은 지금 나의 회전 값을 의미한다.
따라서 우리가 자연스러운 회전을 하려면 Rotataion에서 Desired Rotation으로 한번에 변하는 것이 아닌 각속도로 회전해야한다. 이는 설정을 통해 지정할 수 있다.
폰의 이동 함수
이전 포스팅에 작성한 움직임과 관련된 함수를 직접 확인해보자.
1. Look 함수 : 마우스 입력으로부터 컨트롤러의 ControlRotation을 설정한다.
2. Move 함수 : 컨트롤러의 ControlRotation으로부터 Yaw값을 참고해 이동 방향을 설정한다.
Look 함수를 봐보자.
Look
void AABCharacterPlayer::Look(const FInputActionValue& Value)
{
// input is a Vector2D
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
// add yaw and pitch input to controller
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
해당 코드는 마우스의 입력을 Vector로 값으로 받아와 yaw와 Pitch 값에 관해서 Input을 바꿔주는 구조로 진행이된다.
그렇다면 Character의 AddControllerYawInput 함수는 어떨까?
void APawn::AddControllerYawInput(float Val)
{
if (Val != 0.f && Controller && Controller->IsLocalPlayerController())
{
APlayerController* const PC = CastChecked<APlayerController>(Controller);
PC->AddYawInput(Val);
}
}
대충 보자면 Z축을 기준으로 회전을 하는 Yaw 값을 우리가 설정한 값으로 저장하는 역할을 한다.
void APlayerController::AddYawInput(float Val)
{
RotationInput.Yaw += !IsLookInputIgnored() ?
Val * (GetDefault<UInputSettings>()->bEnableLegacyInputScales ? InputYawScale_DEPRECATED : 1.0f)
: 0.0f;
}
결국 RotationInput.Yaw에다가 우리가 넣어준 값을 저장해주는 구조로 동작한다.
RotationInput은 매틱 마다 UpdateRotation 함수에서 사용되는데
void APlayerController::UpdateRotation( float DeltaTime )
{
// Calculate Delta to be applied on ViewRotation
// Rotation Input을 기반으로 해서 DeltaRotation을 구성한다.
FRotator DeltaRot(RotationInput);
// ViewRotation 자체가 DeltaRotation에 기여한 다음에,
// 이제 이를 기반으로 해서 ViewRotation이 만들어지면..
FRotator ViewRotation = GetControlRotation();
if (PlayerCameraManager)
{
PlayerCameraManager->ProcessViewRotation(DeltaTime, ViewRotation, DeltaRot);
}
AActor* ViewTarget = GetViewTarget();
if (!PlayerCameraManager || !ViewTarget || !ViewTarget->HasActiveCameraComponent() || ViewTarget->HasActivePawnControlCameraComponent())
{
if (IsLocalPlayerController() && GEngine->XRSystem.IsValid() && GetWorld() != nullptr && GEngine->XRSystem->IsHeadTrackingAllowedForWorld(*GetWorld()))
{
auto XRCamera = GEngine->XRSystem->GetXRCamera();
if (XRCamera.IsValid())
{
XRCamera->ApplyHMDRotation(this, ViewRotation);
}
}
}
//만들어진 ViewRotation을 ControlRotation으로 지정한다.
SetControlRotation(ViewRotation);
APawn* const P = GetPawnOrSpectator();
if (P)
{
P->FaceRotation(ViewRotation, DeltaTime);
}
}
void AController::SetControlRotation(const FRotator& NewRotation)
{
if (!IsValidControlRotation(NewRotation))
{
logOrEnsureNanError(TEXT("AController::SetControlRotation attempted to apply NaN-containing or NaN-causing rotation! (%s)"), *NewRotation.ToString());
return;
}
if (!ControlRotation.Equals(NewRotation, 1e-3f))
{
//이제 지정한 Rotation의 값을 ControlRotation으로 바꿔준다는 것.
ControlRotation = NewRotation;
if (RootComponent && RootComponent->IsUsingAbsoluteRotation())
{
RootComponent->SetWorldRotation(GetControlRotation());
}
}
else
{
//UE_LOG(LogPlayerController, Log, TEXT("Skipping SetControlRotation %s for %s (Pawn %s)"), *NewRotation.ToString(), *GetNameSafe(this), *GetNameSafe(GetPawn()));
}
}
이렇게 우리가 받은 값 자체는 사용자의 Control Rotation을 변경하는데 사용하겠다는 의미이다.
Move
void AABCharacterPlayer::Move(const FInputActionValue& Value)
{
// input is a Vector2D
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
// find out which way is forward
// 회전값을 가지고 온다. => 단 가장 최근에 업데이트가 완료된 회전.
const FRotator Rotation = Controller->GetControlRotation();
//이제 Yaw Rotation도 가져옴.
const FRotator YawRotation(0, Rotation.Yaw, 0);
//그리고 각각 앞쪽과 오른쪽에 기반으로 하는 방향 벡터를 가져와서,
// get forward vector
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
// get right vector
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// 그 방향대로 움직임을 전진 시키는 것임.
// add movement
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
Update가 완료된 ControlRotation을 가져와서 그 값을 기반으로 방향벡터를 정해 Move하는 것이다.
그렇다면 생기는 의문은 꼭 Update된 Rotation을 가져와야할까? Move를 먼저하고 Look을 하면 안될까? 싶지만
우리는 DX를 공부할 때 배웠지만, SRT를 꼭 지켜야 올바른 결과가 나온다는 사실을 알고있다. 그러한 이유 때문에와 같다.
캐릭터 컨트롤을 제어하는 설정
https://usingsystem.tistory.com/524
이 것을 참고하면 여기서 나오는 옵션들이 무슨 의미인지 알 수 있다.
폰의 경우에 있어서는
지금 컨트롤러 로테이션이 따로 지정이 되어있고, 지정된 로테이션 값을 폰의 로테이션과 맞출 것이냐를 의미하는 것이다.
저거를 하나씩 키게 된다면, 이제 동기화가 이루어진다.
즉, 이제 Desired Rotation = Control Rotation으로 만들어 주겠다는 것을 의미한다.
스프링 암도 비슷한 옵션이 존재하는데
Use Pawn Control Rotation 이 옵션을 클릭하여 활성화 하면 스프링 암의 회전이 ControlRotation과 동기화 되는 것이다.
우리가 마우스를 움직으면 그 값에 따라 ControlRotation이 변하게 되고 그 값과 스프링 암이 동기화 된다면
결론적으로 우리가 마우스를 움직이면 카메라도 같이 돈다는 것을 의미한다.
그렇다면 스프링 암이 아닌 Camera에서의 Use Pawn Control Rotation을 켜주게 되면 카메라가 내 마우스와 동일하게 움직이기 때문에 결국 1인칭 시점이 되는 것이다.
DataAsset
지금 캐릭터의 움직임을 제어하는데 옵션이 Pawn, Movement, Spring Arm, Camera... 등등 여러 컴포넌트에 분산되어 있다. 이를 한번에 관리하는 아주 효과적인 방법이 있는데 바로 DataAsset이다.
DataAsset은 UDataAsset을 상속받은 언리올 오브젝트 클래스이다.
에디터에서 에셋 형태로 편리하게 데이터들을 관리할 수 있다는 점이 장점이다.
생성법은 아주 간단하다.
- DataAsset / PrimaryDataAsset을 이용해서 클래스를 만들어 낸다.
- 내가 선언하고자 하는 값을 UPROPERTY로 만들어 낸다.
UCLASS()
class ARENABATTLE_API UABCharacterControlData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UABCharacterControlData();
//일반적으로 캐릭터의 경우 Yaw를 기반으로 폰을 이래저래 돌리면서 움직인다.
UPROPERTY(EditAnywhere, Category = Pawn)
uint32 bUseControllerRotationYaw : 1;
//다음에는 움직임에 관련한 것을 정의한다.
UPROPERTY(EditAnywhere, Category = CharacterMovement)
uint32 bOrientRotationToMovement : 1;
UPROPERTY(EditAnywhere, Category = CharacterMovement)
uint32 bUserControllerDesiredRotation : 1;
UPROPERTY(EditAnywhere, Category = CharacterMovement)
FRotator RotationRate;
//입력 매핑 컨텍스트를 지정. 이거는 Movement를 위함.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
TObjectPtr<class UInputMappingContext> InputMappingContext;
//스프링 암을 위한 변수들을 지정.
UPROPERTY(EditAnywhere, Category = SpringArm)
float TargetArmLength;
UPROPERTY(EditAnywhere, Category = SpringArm)
FRotator RelativeRotation;
UPROPERTY(EditAnywhere, Category = SpringArm)
uint32 bUsePawnControlRotation : 1;
UPROPERTY(EditAnywhere, Category = SpringArm)
uint32 bInheritPitch : 1;
UPROPERTY(EditAnywhere, Category = SpringArm)
uint32 bInteritRoll : 1;
UPROPERTY(EditAnywhere, Category = SpringArm)
uint32 bInteritYaw : 1;
UPROPERTY(EditAnywhere, Category = SpringArm)
uint32 bDoCollisionTest : 1;
};
이렇게 상속 받아서 BP로 에디터에서 수정이 가능하다.
시점 변경하기
이렇게 만들어둔 DataAsset을 두개로 만들어서 하나는 쿼터뷰 하나는 숄더뷰로 나눈다음, V키를 눌렀을 때 시점이 변경되도록 해보자.
먼저 CharacterBase 부터 수정해보자.
AABCharacterBase::AABCharacterBase() {
...
static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterSkeletalMesh(TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn.SKM_Quinn"));
if (CharacterSkeletalMesh.Object)
{
GetMesh()->SetSkeletalMesh(CharacterSkeletalMesh.Object);
}
static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceRef(TEXT("/Game/Characters/Mannequins/Animations/ABP_Quinn.ABP_Quinn_C"));
if (AnimInstanceRef.Class)
{
GetMesh()->SetAnimInstanceClass(AnimInstanceRef.Class);
}
static ConstructorHelpers::FObjectFinder<UABCharacterControlData> ShoulderDataRef(TEXT(""));
if (ShoulderDataRef.Object) {
CharacterControlManager.Add(ECharacterControlType::Shoulder, ShoulderDataRef.Object);
}
static ConstructorHelpers::FObjectFinder<UABCharacterControlData> QuaterDataRef(TEXT(""));
if (QuaterDataRef.Object) {
CharacterControlManager.Add(ECharacterControlType::Quater, QuaterDataRef.Object);
}
}
void AABCharacterBase::SetCharacterControlData(const class UABCharacterControlData* CharacterControlData) {
bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;
GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement; // Character moves in the direction of input...
GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUserControllerDesiredRotation;
GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate; // ...at this rotation rate
}
여기서는 TMap을 이용해서 enum을 키로 Value로는 UDataAsset으로 지정해서 나중에 찾기 쉽게 할 것이다.
여기서는 폰과 무브먼트에 관련한 데이터만 다룬다 카메라 같은 경우는 플레이어만 가지고 있고 다른 오브젝트는 가지고 있지 않기 때문이다.
void AABCharacterPlayer::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
Super::SetCharacterControlData(CharacterControlData);
CameraBoom->TargetArmLength = CharacterControlData->TargetArmLength; // The camera follows at this distance behind the character
CameraBoom->SetRelativeRotation(CharacterControlData->RelativeRotation);
CameraBoom->bUsePawnControlRotation = CharacterControlData->bUsePawnControlRotation;
CameraBoom->bInheritPitch = CharacterControlData->bInheritPitch;
CameraBoom->bInheritRoll = CharacterControlData->bInteritRoll;
CameraBoom->bInheritYaw = CharacterControlData->bInteritYaw;
CameraBoom->bDoCollisionTest = CharacterControlData->bDoCollisionTest;
}
이렇게 플레이어 쪽에서 세팅을 해줌 된다.
에디터로 돌아가 InputAction을 정의해주고 이를 Mapping 해주면 된다.
이제 마지막으로 v키를 눌렀을 때 지정된 값으로 변경되기만 하면 된다.
static ConstructorHelpers::FObjectFinder<UInputAction> JUMPACTION(TEXT("/Game/ThirdPerson/Input/Actions/IA_Jump.IA_Jump"));
if (JUMPACTION.Object) {
JumpAction = JUMPACTION.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> CHANGE_ACTION(TEXT("'/Game/ThirdPerson/Input/Actions/IA_CharacterControl.IA_CharacterControl'"));
if (CHANGE_ACTION.Object) {
ChangeControlAction = CHANGE_ACTION.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> INPUT_SHOULDER_MOVE_ACTION(TEXT("'/Game/ThirdPerson/Input/Actions/IA_ShoulderMove.IA_ShoulderMove'"));
if (INPUT_SHOULDER_MOVE_ACTION.Object) {
ShoulderMoveAction = INPUT_SHOULDER_MOVE_ACTION.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> INPUT_SHOULDER_LOOK_ACTION(TEXT("'/Game/ThirdPerson/Input/Actions/IA_ShoulderLook.IA_ShoulderLook'"));
if (INPUT_SHOULDER_LOOK_ACTION.Object) {
ShoulderLookAction = INPUT_SHOULDER_LOOK_ACTION.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> INPUT_QUATER_MOVE_ACTION(TEXT("'/Game/ThirdPerson/Input/Actions/IA_QuaterMove.IA_QuaterMove'"));
if (INPUT_QUATER_MOVE_ACTION.Object) {
QuaterMoveAction = INPUT_QUATER_MOVE_ACTION.Object;
}
void AABCharacterPlayer::ChangeCharacterControl()
{
if (CurrentCharacterControlType == ECharacterControlType::Quater) {
SetCharacterControl(ECharacterControlType::Shoulder);
}
else if(CurrentCharacterControlType == ECharacterControlType::Shoulder) {
SetCharacterControl(ECharacterControlType::Quater);
}
}
void AABCharacterPlayer::SetCharacterControl(ECharacterControlType NewCharacterControlType)
{
UABCharacterControlData* NewCharacterControl = CharacterControlManager[NewCharacterControlType];
check(NewCharacterControl);
SetCharacterControlData(NewCharacterControl);
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->ClearAllMappings();
UInputMappingContext* NewMappingContext = NewCharacterControl->InputMappingContext;
if (NewMappingContext) {
Subsystem->AddMappingContext(NewMappingContext, 0);
}
}
}
CurrentCharacterControlType = NewCharacterControlType;
}
void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) {
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {
//Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
//Moving
EnhancedInputComponent->BindAction(ShoulderMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderMove);
//Looking
EnhancedInputComponent->BindAction(ShoulderLookAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderLook);
EnhancedInputComponent->BindAction(QuaterMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::QuaterMove);
}
}
'Unreal Engine 5 > 강의' 카테고리의 다른 글
[UE5] 애니메이션 - 캐릭터 공격 판정 (0) | 2024.12.04 |
---|---|
[UE5] 애니메이션 - 캐릭터 애니메이션과 몽타주를 이용한 연속 공격 (0) | 2024.12.03 |
[UE5] 언리얼 게임 제작 기초 - C++ 과 캐릭터 입력 시스템 (0) | 2024.11.28 |
[UE5] 언리얼 오브젝트 관리- 직렬화, 패키지 (0) | 2024.11.27 |
[UE5] 언리얼 컨테이너 라이브러리 - Struct와 TMap, 언리얼 메모리 관리 (0) | 2024.11.25 |