2-3강 캐릭터 컨트롤 설정

Ryan Ham·2024년 6월 27일
0

이득우 Unreal

목록 보기
5/23
post-thumbnail

오늘 알아볼 것

  • Data Asset으로 체계적인 데이터 관리
  • 런타임에서 Input Mapping Context 바꾸기

캐릭터 컨트롤 요소

  • 일반적으로 Controller, Pawn, Camera, Spring arm, Character movement의 5가지 요소를 사용해 설정.
  • Controller : 입력자의 의지(목표 지점)을 지정할 때 사용. Control Rotation 속성
  • Pawn : Pawn의 transform을 지정
  • Camera : 화면 구도를 설정하기 사용. 주로 1인칭 시점에서 사용.
  • Spring Arm : 화면 구도를 설정하기 위해 사용. 주로 3인칭 시점에서 사용.
  • Character Movement : Character의 이동과 회전을 조정하는 용도로 사용.

Desired Rotation과 Rotation의 개념

Desired Rotation은 내가 회전하고 싶은 회전값. Rotation은 현재 회전 상태. 하지만 바로 Rotation 값을 Desired Rotation값으로 덮어씌우면 매우 부자연스러움으로, 일정한 각속도(Rotation Rate)를 Rotation에 계속 더해서 Desired Rotation값으로 만들게 된다.


Character Look 함수의 원리

화면에서 마우스의 X,Y의 변화가 어떻게 Controller의 방향을 회전하는지 알아보자.

// CharacterPlayer.cpp
void AMyRyanCharacter::Look(const FInputActionValue& Value)
{
	FVector2D LookAxisVector = Value.Get<FVector2D>();

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

// AddControllerYawInput 함수의 정의 
void APawn::AddControllerYawInput(float Val)
{
	if (Val != 0.f && Controller && Controller->IsLocalPlayerController())
	{
		APlayerController* const PC = CastChecked<APlayerController>(Controller);
		PC->AddYawInput(Val);
	}
}

// AddYawInput 함수의 정의
void APlayerController::AddYawInput(float Val)
{
	RotationInput.Yaw += !IsLookInputIgnored() ? Val * (GetDefault<UInputSettings>()->bEnableLegacyInputScales ? InputYawScale_DEPRECATED : 1.0f) : 0.0f;
}

void APlayerController::AddRollInput(float Val)
{
	RotationInput.Roll += !IsLookInputIgnored() ? Val * (GetDefault<UInputSettings>()->bEnableLegacyInputScales ? InputRollScale_DEPRECATED : 1.0f) : 0.0f;
}

최종적으로 RotationInput.Roll은 SetControllRotation을 통해 ControlRotation이라는 값을 수정하게 된다.

Look 함수에서 사용되는 AddControllerYawInput과 AddControllerPitchInput 함수가 결국 ControlRotation 값을 바꾸는 거라면, Move 함수에서는 ControlRotation에서 Yaw 회전값을 가져와 캐릭터가 움직일 방향의 기준점을 잡는다.

ControlRotation이 짱짱 중요하다는 소리!


Controller의 Control Rotation 값 확인하기

Level Play 버튼을 누르고, Console Command 창(~키)에 DisplayAll PlayerController ControlRotation을 누르면 다음 화면과 같이 Controller의 회전값을 viewport상에서 실시간으로 확인이 가능하다.

캐릭터 이동에서 중요한 점은 보통 캐릭터를 움직일때 캐릭터가 바로보고 있는 방향을 forward로 해서 움직이는 것이 아닌, controller가 바라보는 forward 방향을 기준으로 character의 forward movement를 정하는 것!

필자는 항상 왜 FRotator의 값이 (Y,Z,X) 순으로 되어있는지 궁금했는데 여기서 어렴풋이 궁금증을 해소했다. Unreal의 축 시스템상 사용자의 마우스가 화면의 X,Y에서 움직이면 캐릭터에 달린 카메라의 Y(pitch)와 Z(yaw)를 변경. 이를 순서대로 보기 쉽게 하기 위해서 그런게 아닐까? 보통, Roll 축을 이동에서 건들이는 일은 없기 때문.


Character와 Camera Arm의 controller rotation 상속

Character BP를 만들고 Detail panel에 pawn을 검색하면, Use Controller Rotation Pitch, Use Controller Rotation Yaw, Use Controller Rotation Roll이 체크 해제되어있는 것을 알 수 있는데, 이를 체크하면 Character와 Controller의 회전이 동기화가 된다. 이렇게 되면 Controller가 돌때 캐릭터가 따라 회전함으로 화면상에서 이 캐릭터의 얼굴을 볼 수 있는 방법이 없어진다!

반대로 Camera Arm은 Controller의 방향에 따라서 회전하여야 함으로 이렇게 default 값이 setting 되어있음을 알 수 있다.


Character Movement

Detail panel에서 Character Movement section에는 캐릭터 이동에 관한 다양한 설정을 정할 수 있다. Character Movement는 다양한 곳에 활용될 수 있는데, 캐릭터가 움직이지 않게 만들고 싶을때는 Movement를 None으로 설정할 수 있고, 캐릭터가 땅에 붙어있지 않는 경우를 체크해보고 싶을때는 Movement가 Falling인지 확인해보면 된다.


Data Asset

Data Asset이란?

  • UDataAsset을 상속받은 언리얼 오브젝트 클래스
  • Editor에서 Asset 형태로 편리하게 데이터를 관리
  • Character Control에 관련된 주요 옵션을 모아 asset으로 관리

여기까지만 봐도 설정들이 굉장히 분산되어 있다는 사실을 알 수 있다. DataAsset이라는 Class를 따로 만들어서 이를 체계적으로 관리해보자. 우리는 Shoulder와 Quater 총 2개의 View를 만들 것이기 때문에 이에 해당되는 DataAsset을 만들어보자. 먼저 Primary DataAsset을 상속하는 C++ 클래스를 만들고, 이 클래스를 다시 상속하는 BP를 만들자.

Quater View에서는 Use Constroller Desired Rotation만 check

Shoulder View에서는 Orient Rotation to Movement, Use Pawn Control Rotation, Inherit Pitch, Inherit Yaw, Inherit Roll을 check

DataAsset은 Miscellaneous(기타)->Data Asset을 통해 만들 수 있다. 우리는 좀더 기능이 확장된 Primary DataAsset을 사용할 것임.


Runtime에서 IMC switch

우리는 이제 런타임에서 Shoulder view와 Quater view를 전환하는 작업을 할 것이다. ENUM을 통해 2개의 control data를 관리하고, 입력키 'V'를 통해 control 설정을 변경한다.

ShoulderView는 Character의 움직임과 카메라의 시점 또한 마우스로 바꿀 수 있고,QuaterView는 Character의 움직임만 조작 가능하고 카메라의 시점은 고정되어 있다.

Control을 변경할때 IMC를 바꾸는데, 각각의 View에 IMC, IA를 각각 따로 설정해주어야한다.

총 2개의 IMC, Shoulder View는 Character Movement + Move와 Look에 해당되는 IA가 있으면 되고, Quater View는 Character Movement + Move에 해당되는 IA만 있으면 된다.

이 프로젝트는 CharacterBase에 Character Movement가 관리되고 있고 이 CharacterBase를 상속하는 MyCharacter에서 카메라를 관리하고 있다. MyCharacter.cpp의 SetCharacterControlData에서 Super를 사용하므로써 DataAsset에 Character Movement와 Camera 설정이 함께 되어 있는것을 부모와 자식 둘 다 바꿀 수 있게 한다.

// CharacterBase.h

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

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

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

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

	UPROPERTY(EditAnywhere, Category = CharacterControl, Meta = (AllowPrivateAccess = "true"))
	TMap<ECharacterControlType, class URyanCharacterControlData*> CharacterControlManager;

CharacterBase.cpp에서의 SetCharacterControlData 함수

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

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

MyCharacter.cpp에서의 SetCharacterControlData 함수

void AMyRyanCharacter::SetCharacterControlData(const URyanCharacterControlData* CharacterControlData)
{
	Super::SetCharacterControlData(CharacterControlData);

	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;
}

InputMappingContext를 runtime에서 switching 할 수 있게 만들어야 한다.

// Character 클래스에서 SetCharacterControl 함수
// 'V'키를 누르면 실행되는 함수
// 기존에 있던 IMC를 지우고, 새로운 IMC로 갈아치운다. 
void AMyRyanCharacter::SetCharacterControl(ECharacterControlType NewCharacterControlType)
{
	URyanCharacterControlData* NewCharacterControl = CharacterControlManager[NewCharacterControlType];
	check(NewCharacterControl);

	SetCharacterControlData(NewCharacterControl);

	// IMC는 Controller 관할이라는 사실 명심!
	APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
	{
		Subsystem->ClearAllMappings();
		UInputMappingContext* NewMappingContext = NewCharacterControl->InputMappingContext;
		if (NewMappingContext)
		{
			Subsystem->AddMappingContext(NewMappingContext, 0);
		}
	}

	CurrentCharacterControlType = NewCharacterControlType;
}

Top view와 Shoulder view에서 move 함수

// Character.cpp

void AMyRyanCharacter::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 AMyRyanCharacter::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);
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("X: %f, Y: % f"), MovementVector.X, MovementVector.Y));
	}
	GetController()->SetControlRotation(FRotationMatrix::MakeFromX(MoveDirection).Rotator());
	AddMovementInput(MoveDirection, MovementVectorSize);

}

둘의 move 함수 로직간의 차이를 잘 살펴보자.


최종화면

Quater View

Shoulder View

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글