Data Asset
으로 체계적인 데이터 관리Input Mapping Context
바꾸기Controller
: 입력자의 의지(목표 지점)을 지정할 때 사용. Control Rotation 속성Pawn
: Pawn의 transform을 지정Camera
: 화면 구도를 설정하기 사용. 주로 1인칭 시점에서 사용.Spring Arm
: 화면 구도를 설정하기 위해 사용. 주로 3인칭 시점에서 사용.Character Movement
: Character의 이동과 회전을 조정하는 용도로 사용. Desired Rotation
은 내가 회전하고 싶은 회전값. Rotation
은 현재 회전 상태. 하지만 바로 Rotation 값을 Desired Rotation값으로 덮어씌우면 매우 부자연스러움으로, 일정한 각속도(Rotation Rate)를 Rotation에 계속 더해서 Desired Rotation값으로 만들게 된다.
화면에서 마우스의 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
이 짱짱 중요하다는 소리!
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 BP를 만들고 Detail panel에 pawn을 검색하면, Use Controller Rotation Pitch
, Use Controller Rotation Yaw
, Use Controller Rotation Roll
이 체크 해제되어있는 것을 알 수 있는데, 이를 체크하면 Character와 Controller의 회전이 동기화가 된다. 이렇게 되면 Controller가 돌때 캐릭터가 따라 회전함으로 화면상에서 이 캐릭터의 얼굴을 볼 수 있는 방법이 없어진다!
반대로 Camera Arm은 Controller의 방향에 따라서 회전하여야 함으로 이렇게 default 값이 setting 되어있음을 알 수 있다.
Detail panel에서 Character Movement
section에는 캐릭터 이동에 관한 다양한 설정을 정할 수 있다. Character Movement
는 다양한 곳에 활용될 수 있는데, 캐릭터가 움직이지 않게 만들고 싶을때는 Movement를 None
으로 설정할 수 있고, 캐릭터가 땅에 붙어있지 않는 경우를 체크해보고 싶을때는 Movement가 Falling
인지 확인해보면 된다.
여기까지만 봐도 설정들이 굉장히 분산되어 있다는 사실을 알 수 있다. 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을 사용할 것임.
우리는 이제 런타임에서 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;
void ARyanCharacterBase::SetCharacterControlData(const URyanCharacterControlData* CharacterControlData)
{
// Pawn
bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;
// CharacterMovement
GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement;
GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUseControllerDesiredRotation;
GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate;
}
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;
}
// 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;
}
// 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 함수 로직간의 차이를 잘 살펴보자.