futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

3강: 캐릭터 컨트롤 설정 #28

Closed fkdl0048 closed 4 months ago

fkdl0048 commented 4 months ago

3강: 캐릭터 컨트롤 설정

캐릭터 컨트롤 요소

캐릭터 컨트롤

일반적으로 컨트롤러와 폰, 카메라, 스프링암, 캐릭터 무브먼트의 다섯 가지 요소를 사용해 설정한다.

DesiredRotation은 캐릭터가 원하는 방향을 나타내고 Rotation은 현재 상태를 나타낸다. 부드러움 움직임을 위해 선형보간과 같이 각속도로 회전할 수 있도록 구현한다. (일반적으로)

폰 이동 함수

전 강의에서 사용한 Move함수와 Look함수를 통해 직접 캐릭터가 어떻게 이동하는지 알아보자.

폰의 컨트롤 옵션

블루프린트로 접근하여 Pawn의 속성을 보면 Use Controller Ratation항목이 있는데 Yaw, Pitch, Roll의 값을 컨트롤러에 지정된 Control Rotation값에 폰의 Rotation을 맞출 것인지 설정하는 항목이다.

2강에서 보면 코드에서 각 설정들에 대해서 false를 한 것을 볼 수 있다. (마우스와 키보드의 입력으로 캐릭터를 제어하기 위해)

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;

깨알이지만, Boolean이라 b접두사를 사용한 모습..

해당 값을 true하고 실행해보면 캐릭터가 마우스 방향으로 이동은 하지만, 캐릭터 매쉬자체의 회전은 없어진다. 매우 어색한 느낌

스프링암의 컨트롤 옵션

마찬가지로 실제 값을 보고 싶다면 블루프린트로 접근하여 값을 확인할 수 있다. 마찬가지로 이 값을 블루프린트에서 볼 수 있다면 코드에서도 설정할 수 있다는 뜻이다.

3인칭 카메라의 카메라 앞으로 당기기 기능이 언리얼에서는 주어진다니.. 유니티에선 매우 힘들게 구현해봤던 기억이..

카메라의 컨트롤 옵션

카메라의 컨트롤 옵션으로 동일하게 폰의 컨트롤 회전을 사용할 것인가를 설정할 수 있다. (1인칭에 주로 사용)

캐릭터 무브먼트의 이동 옵션

다양한 무브먼트 모드들을 제공해준다. (Walking, Falling, Swimming, Flying, Custom..)

각 모드에 관한 속성은 코드에서 다룬 MaxWalkSpeed등으로 조절할 수 있다.

캐릭터 무브먼트의 회전 옵션

데이터 에셋

위에서 다룬 데이터들은 매우 분산되어 있고, 이를 관리하기 위해 데이터 에셋을 사용한다.

데이터 에셋이란, UdataAsset클래스를 상속받은 언리얼 오브젝트 클래스이다. 에디터에서 쉽게 에셋 형태로 데이터를 관리할 수 있으며, 캐릭터 컨트롤에 관련된 주요 옵션을 모아 에셋으로 관리한다.

Unity의 ScriptableObject와 비슷한 개념인 것 같다.

실습

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

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ABCharacterControlData.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABCharacterControlData : public UPrimaryDataAsset
{
 GENERATED_BODY()

public:
 UABCharacterControlData();

 // 캐릭터 컨트롤러에서 사용하는 회전 Yaw값
 UPROPERTY(EditAnywhere, Category = Pawn)
 uint32 bUseControllerRotationYaw : 1;

 // Movement에서 사용되는 변수들
 UPROPERTY(EditAnywhere, Category = CharacterMovement)
 uint32 bOrientRotationToMovement : 1;

 UPROPERTY(EditAnywhere, Category = CharacterMovement)
 uint32 bUseControllerDesiredRotation : 1;

 UPROPERTY(EditAnywhere, Category = CharacterMovement)
 FRotator RotationRate;

 // 입력 매핑 컨텍스트를 담을 변수
 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 bInheritYaw : 1;

 UPROPERTY(EditAnywhere, Category = SpringArm)
 uint32 bInheritRoll : 1;

 UPROPERTY(EditAnywhere, Category = SpringArm)
 uint32 bDoCollisionTest : 1;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "Character/ABCharacterControlData.h"

UABCharacterControlData::UABCharacterControlData()
{
 TargetArmLength = 400.0f;
}

데이터 에셋의 관리

현재 두 가지 데이터 에셋을 만들고 이를 통해 컨트롤 모드를 자율적으로 만들 수 있다. (3인칭 숄더뷰, 3인칭 쿼터뷰) 런타임에 데이터 에셋을 변경이 가능하다. (ENUM을 통해 상태를 관리.)

데이터 에셋의 구성과 적용

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "ABCharacterBase.generated.h"

UENUM()
enum class ECharacterControlType : uint8
{
 Shoulder,
 Quater
};

UCLASS()
class ARENABATTLE_API AABCharacterBase : public ACharacter
{
 GENERATED_BODY()

public:
 // Sets default values for this character's properties
 AABCharacterBase();

protected:
 virtual void SetCharacterControlData(const class UABCharacterControlData* CharacterControlData);

 UPROPERTY(EditAnywhere, Category = CharacterControl, Meta = (AllowPrivateAccess = "true"))
 TMap<ECharacterControlType, class UABCharacterControlData*> CharacterControlManager;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "Character/ABCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ABCharacterControlData.h"

// Sets default values
AABCharacterBase::AABCharacterBase()
{
    ... // 생략

 static ConstructorHelpers::FObjectFinder<UABCharacterControlData> ShoulderDataRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharacterControl/ABC_Shoulder.ABC_Shoulder'"));
 if (ShoulderDataRef.Object)
 {
  CharacterControlManager.Add(ECharacterControlType::Shoulder, ShoulderDataRef.Object);
 }

 static ConstructorHelpers::FObjectFinder<UABCharacterControlData> QuaterDataRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharacterControl/ABC_Quater.ABC_Quater'"));
 if (QuaterDataRef.Object)
 {
  CharacterControlManager.Add(ECharacterControlType::Quater, QuaterDataRef.Object);
 }
}

void AABCharacterBase::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
 // Pawn
 bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;

 // CharacterMovement
 GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement;
 GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUseControllerDesiredRotation;
 GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate;
}

뷰의 전환

컨트롤을 변경할 때 서로 다른 입력 매핑 콘텍스트가 지정되어야 한다. 예를 들어 숄더뷰의 경우엔 마우스를 통해 회전하고 이동하지만, 쿼터뷰의 경우 마우스를 사용하지 않기에 다른 입력 매핑 콘텍스트를 사용해야 한다. (입력 액션을 통해 변경 V)

가장 먼저 추가로 사용할 액션과 입력 매핑 콘텍스트를 만들어야 한다. 입력 매핑 콘텍스트는 QuaterShoulder로 나누어서 만들고 각 입력 매핑 콘텍스트에 액션을 추가한다. (에디터로 진행) 추가적으로 뷰를 전환하기 위한 액션을 추가한다.

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

#pragma once

#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "InputActionValue.h"
#include "ABCharacterPlayer.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API AABCharacterPlayer : public AABCharacterBase
{
 GENERATED_BODY()
public:
 AABCharacterPlayer();

protected:
 virtual void BeginPlay() override;

public:
 virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

// Character Control Setion
protected:
 void ChangeCharacterControl();
 void SetCharacterControl(ECharacterControlType NewCharacterControlType);
 virtual void SetCharacterControlData(const class UABCharacterControlData* CharacterControlData) override;

// Input
protected:
 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> JumpAction;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> ChangeControlAction;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> ShoulderMoveAction;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> ShoulderLookAction;

 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
 TObjectPtr<class UInputAction> QuaterMoveAction;

 void ShoulderMove(const FInputActionValue& Value);
 void ShoulderLook(const FInputActionValue& Value);

 void QuaterMove(const FInputActionValue& Value);

 ECharacterControlType CurrentCharacterControlType;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "Character/ABCharacterPlayer.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputMappingContext.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "ABCharacterControlData.h"

AABCharacterPlayer::AABCharacterPlayer()
{
 // Input
 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionJumpRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Jump.IA_Jump'"));
 if (nullptr != InputActionJumpRef.Object)
 {
  JumpAction = InputActionJumpRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputChangeActionControlRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_ChangeControl.IA_ChangeControl'"));
 if (nullptr != InputChangeActionControlRef.Object)
 {
  ChangeControlAction = InputChangeActionControlRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionShoulderMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_ShoulderMove.IA_ShoulderMove'"));
 if (nullptr != InputActionShoulderMoveRef.Object)
 {
  ShoulderMoveAction = InputActionShoulderMoveRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionShoulderLookRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_ShoulderLook.IA_ShoulderLook'"));
 if (nullptr != InputActionShoulderLookRef.Object)
 {
  ShoulderLookAction = InputActionShoulderLookRef.Object;
 }

 static ConstructorHelpers::FObjectFinder<UInputAction> InputActionQuaterMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_QuaterMove.IA_QuaterMove'"));
 if (nullptr != InputActionQuaterMoveRef.Object)
 {
  QuaterMoveAction = InputActionQuaterMoveRef.Object;
 }

 // 현재 상태 기본값으로 초기화 (Quater로 시작)
 CurrentCharacterControlType = ECharacterControlType::Quater;
}

void AABCharacterPlayer::BeginPlay()
{
 Super::BeginPlay();

 SetCharacterControl(CurrentCharacterControlType);
}

void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
 Super::SetupPlayerInputComponent(PlayerInputComponent);

 UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);// 반드시 EnhancedInputComponent로 캐스팅해야 합니다.

 EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
 EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
 EnhancedInputComponent->BindAction(ShoulderMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderMove);
 EnhancedInputComponent->BindAction(ShoulderLookAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderLook);
 EnhancedInputComponent->BindAction(QuaterMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::QuaterMove);
 EnhancedInputComponent->BindAction(ChangeControlAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ChangeCharacterControl);
}

void AABCharacterPlayer::ChangeCharacterControl()
{
 if (CurrentCharacterControlType == ECharacterControlType::Quater)
 {
  SetCharacterControl(ECharacterControlType::Shoulder);
 }
 else if (CurrentCharacterControlType == ECharacterControlType::Shoulder)
 {
  SetCharacterControl(ECharacterControlType::Quater);
 }
}

void AABCharacterPlayer::SetCharacterControl(ECharacterControlType NewCharacterControlType)
{
 // 상위 클래스에 정의된 딕셔너리에서 key값을 넣어서 컨텍스트를 가져온다.
 UABCharacterControlData* NewCharacterControl = CharacterControlManager[NewCharacterControlType];
 check(NewCharacterControl); // nullptr이 아닌지 체크 (어서션)

 SetCharacterControlData(NewCharacterControl); // 상위 클래스에 정의된 함수를 호출하여 캐릭터 컨트롤 데이터를 설정한다.
 // super을 통해 base에 pawn과 character movement를 설정하고
 // 현재 이 클래스에선 플레이어 입력과 스프링암 데이터를 설정한다.

 APlayerController* PlayerController = CastChecked<APlayerController>(GetController()); // 플레이어 컨트롤러를 가져온다. 반드시 가져와야 하는 경우 CastChecked를 사용한다.
 if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
 {
  // EnhancedInput 시스템에서 실제로 Input Mapping 컨텍스트를 운영하는 Subsystem을 가져온다.
  // 그리고 이전에 설정된 모든 매핑을 지우고 새로운 컨텍스트를 추가한다.
  Subsystem->ClearAllMappings();
  UInputMappingContext* NewMappingContext = NewCharacterControl->InputMappingContext;
  if (NewMappingContext)
  {
   Subsystem->AddMappingContext(NewMappingContext, 0);
   // 우선순위가 있는 것 같은데, 그렇다면 초기화에 이 작업을 컨택스트 만큼 수행하고 변경을 우선순위만 바꾸면 되지 않을까?
  }
 }

 CurrentCharacterControlType = NewCharacterControlType;
}

void AABCharacterPlayer::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
 Super::SetCharacterControlData(CharacterControlData); // 전에 구현한 Base의 클래스 함수를 호출 (필수)

 CameraBoom->TargetArmLength = CharacterControlData->TargetArmLength;
 CameraBoom->SetRelativeRotation(CharacterControlData->RelativeRotation);
 CameraBoom->bUsePawnControlRotation = CharacterControlData->bUsePawnControlRotation;
 CameraBoom->bInheritPitch = CharacterControlData->bInheritPitch;
 CameraBoom->bInheritYaw = CharacterControlData->bInheritYaw;
 CameraBoom->bInheritRoll = CharacterControlData->bInheritRoll;
 CameraBoom->bDoCollisionTest = CharacterControlData->bDoCollisionTest;
}

void AABCharacterPlayer::ShoulderMove(const FInputActionValue& Value)
{
 FVector2D MovementVector = Value.Get<FVector2D>();

 const FRotator Rotation = Controller->GetControlRotation();
 const FRotator YawRotation(0, Rotation.Yaw, 0);

 const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
 const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

 AddMovementInput(ForwardDirection, MovementVector.X);
 AddMovementInput(RightDirection, MovementVector.Y);
}

void AABCharacterPlayer::ShoulderLook(const FInputActionValue& Value)
{
 FVector2D LookAxisVector = Value.Get<FVector2D>();

 AddControllerYawInput(LookAxisVector.X);
 AddControllerPitchInput(LookAxisVector.Y);
}

void AABCharacterPlayer::QuaterMove(const FInputActionValue& Value)
{
 FVector2D MovementVector = Value.Get<FVector2D>();

 float InputSizeSquared = MovementVector.SquaredLength();
 float MovementVectorSize = 1.0f;
 float MovementVectorSizeSquared = MovementVector.SquaredLength();
 if (MovementVectorSizeSquared > 1.0f)
 {
  MovementVector.Normalize();
  MovementVectorSizeSquared = 1.0f;
 }
 else
 {
  MovementVectorSize = FMath::Sqrt(MovementVectorSizeSquared);
 }

 FVector MoveDirection = FVector(MovementVector.X, MovementVector.Y, 0.0f);
 GetController()->SetControlRotation(FRotationMatrix::MakeFromX(MoveDirection).Rotator());
 AddMovementInput(MoveDirection, MovementVectorSize);
}

정리