언리얼C++ 기초, 알고리즘 1문제
CharacterBase와 PlayerCharacter를 나누는 이유가 무엇일까?
ACharacter는 Pawn보다 더 많은 기본 기능을 가진 클래스다. 기본적으로 CapsuleComponent, SkeletalMeshComponent, CharacterMovementComponent를 가지고 있다.
GetCharacterMovement()->함수 이런 식으로 사용 할 수 있는데 여기 함수 자리에는 들어갈 수 있는 것들은
MaxWalkSpeed -> 최대 이동속도 제한
MinAnlogWalkSpeed -> 게임패드 스틱 민감도 제한
JumpZVelocity -> 점프 높이 제한
AirControl -> 공중에서 방향전환 제한
BrakingDecelerationWalking -> 걷다가 입력을 멈췄을때 얼마나 빨리 감속하는지
같은 것들을 사용 할 수 있는데, 이렇게 나누어두면 나중에 NPCCharacter나 AIEnemy를 만들 때도 CharacterBase를 재사용할 수 있고, 플레이어 전용 입력/카메라 로직이 적 캐릭터에 섞이지 않는다.
APlayerController* PlayerController = Cast<APlayerController>(GetController());
// 왜 GetWorld()->GetFirstPlayerController()로 바로 가져오지않을까?
PlayerCharacter에서 EnhancedInput 시스템을 넣으려고 할때 왜 굳이 주석과 같이 처리하지않고 캐스팅하는지 궁금해졌다
이 Character에는 Player만 Possess할 수 있는게 아니라는 점이다. 그래서 굳이 캐스팅해서 "이PlayerController가 Player인가?" 확인을 해서 Player면 입력시스템을 주는 것.
GetController() -> 이 Pawn/Character를 현재 조종하는 Controller
GetFirstPlayerController() -> 월드에 있는 첫 번째 PlayerController
LocalPlayer는 게임 월드 안의 캐릭터나 컨트롤러가 아니라, 이 기기에서 입력을 하는 로컬 플레이어 자리에 가깝다. 혼자 플레이할 때는 보통 하나만 존재하고, 로컬 멀티플레이처럼 한 기기에서 1P/2P가 같이 플레이하는 경우에는 여러 개가 있을 수 있다. 만약 온라인 멀티플레이라면 각 클라이언트가 자기 LocalPlayer를 보통 하나씩 가진다.
PlayerController가 게임 안에서 실제로 캐릭터를 조종하는 객체라면, LocalPlayer는 그 플레이어의 입력과 플레이어 슬롯(자리)의 단위라고 이해하면 된다. 그래서 EnhancedInput처럼 "이 플레이어가 어떤 입력 방식을 쓰고있지?"를 관리해야 하는 시스템은 LocalPlayer쪽에 붙는 게 자연스럽다. 캐릭터는 바뀔 수 있지만, 입력 방식은 플레이어의 자리(LocalPlayer) 기준으로 관리하는 편이 더 유연하기 때문이다.
UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>
(PlayerController->GetLocalPlayer());
이건 캐스팅하는 것이 아니라, LocalPlayer에 붙어 있는 여러 관리자 중에서 EnhancedInput 전용 Subsystem을 가져오는 함수이다. 여기서 UEnhancedInputLocalPlayerSubsystem은 LocalPlayer의 가져오고 싶은 서브시스템의 타입을 지정하는 템플릿 인자이다.
이 코드는 "이 LocalPalyer에 소속된 EnhancedInput 관리자 객체를 꺼내라"는 뜻이고 (PlayerController->GetLocalPlayer());는 PlayerController가 소속된 LocalPlayer의 자리를 달라는 것이다.
즉 Subsystem은 PlayerController의LocalPlayer에 붙은 입력 관리자 객체를 꺼낸 것이다.
에디터에서 DataAsset을 만들고 InputAction을 관리하는 식으로 되어있는데 이게 없으면 왜 필요하고 이게 없으면 어떻게 될까?
이게 없다고 이론상 EnhancedInput이 불가능해지는 것은 아니다. 그냥 불편해진다.
예를들어 PlayerCharacterInputConfig(DataAsset)이 없으면 BindActioㅜ()할때 액션 참조를 다른 방식으로 들고 있어야한다. UInputAction* MoveAction이나 UInputAction* LookAction를 각각 두는 방식으로 이게 계속 늘어나면 지저분해진다. 그런데 여기서 InputMappingContext(IMC)랑 비슷한 성질을 띄는것을 보인다. 이러면 뭐가 다른걸까?
PlayerCharacterInputConfig(DataAsset) -> 코드에서 사용할 InputAciton 목록 보관
InputMappingContext(IMC) -> 실제 키/마우스/패드를 액션에 연결하는 곳
즉, 실질적인 Bind는
IMC에서 이루어지는게 맞고DataAsset은 그냥 코드에서 편하게 사용하기 위한 보관소이다.
Control Rotation은 플레이어 컨트롤러가 들고 있는 시점/의지 회전값이다. 이게 무슨말이냐면, 컨트롤러의 이동 방향이 아니라는 것이다. W/A/S/D를 눌렀다고 자동으로 방향이 바뀌는게 아니라 마우스 입력으로 바뀌는 회전값이다.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
일단 Pitch, Yaw, Roll이 무엇인지 확실하게 정의 하고 가겠다.
플레이어가 정면을 바라보고 있을 때의 축으로 설명하겠다.

Pitch -> 플레이어의 양/옆에서 오는 축이다. 돌렸을때 위 아래를 돌 수 있음.
Yaw -> 플레이어의 위/아래에서 오는 축이다. 돌렸을때 세로축으로 회전 할 수 있음.
Roll -> 플레이어의 정면/뒤에서 오는 축이다. 돌렸을때 가로축으로 회전 할 수 있음.
(Roll은 보통 캐릭터에 사용하기보다 비행물체 같은 곳에서 많이 사용된다.)
bUseControllerRotation은 결국 "Controller의 방향대로 돌 것 이냐?"를 물어보는 것이다. 그런데 여기서 착각한게 위에서 설명했듯이 Controller가 들고 있는 값은 방향벡터가 아닌 시점/의지 회전값이다.
즉, 밑에서 SpringArmComponent의 bInherit를 설정하지 않는다면(bUsePawnControlRotation을 true로 하지않는다면) 값이 true로 바꾸는 이유가 없다는 것이다. 왜냐하냐면 시점이 바뀌지않으면 Controller의 방향은 없고, 그저 앞을 바라보면서 옆으로 이동/앞을 바라보면서 뒤로 이동하는 이동 입력에 불과하기 때문이다.
결국 그럼 캐릭터가 방향에 따라 움직이고 시점에 영향을 받고싶지않으면 bOrientRotationToMovement를 사용해야한다. 여기서 Orient의 뜻은 방향을 맞추다, 정렬하다, 향하게 하다라는 뜻이다.
즉,
bOrientRotationToMovement = true는 캐릭터 몸의 회전을 현재 이동 방향에 맞춘다라는 뜻
플레이어가 실제로 어느 방향으로 움직이고 있는지를 보고, CharacterMovement가 몸을 그쪽으로 돌린다. 이때 캐릭터가 이동 방향으로 회전하는 건, AddMovementInput이 직접 몸을 돌리는 게 아니라 CharacterMovementComponent가 이동방향을 해석해서 회전을 맞춰 주는 것이다.
RotationRate는 얼마나 해당 방향으로 빨리 돌지를 정하는 값.
const FRotator ControlRotation = GetController()->GetControlRotation();
const FRotator ControlRotationYaw(0.f, ControlRotation.Yaw, 0.f);
const FVector ForwardVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::X);
const FVector RightVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardVector, MovementVector.X);
AddMovementInput(RightVector, MovementVector.Y);
FRotationMatrix는 회전 좌표계 정보를 저장하는 행렬이다. 쉽게 말하면, 이 회전값을 기준으로 앞/오른쪽/위 방향이 어디인지 계산해주는 도구이다. 현재 코드에서는 ControlRotationYaw를 FRotationMatrix에 넣어서, 카메라 Yaw 기준 앞/오른쪽 방향 벡터를 얻는 것이다.
GetUnitAxis(EAxis::X) / GetUnitAxis(EAxis::Y)는 해당 축 방향의 단위 벡터를 꺼내는 함수이다.
EAxis::X -> 앞 방향
EAxis::Y -> 오른쪽 방향
EAxis::Z -> 위 방향
그래서 현재 코드에서
const FVector ForwardVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::X);
const FVector RightVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::Y);
ForwardVector -> 카메라가 보는 방향 기준 앞(EAxis::X)
RightVector -> 카메라가 보는 방향 기준 오른쪽(EAxis::Y)
를 구하는 것이다. 여기서 카메라가 보는 기준은 ControlRotiatonYaw 기준 앞/오른쪽이다.
전체적인 Logic은 이렇게 된다.
1. 컨트롤러의 현재 시점 회전값을 가져온다
2. 그중 이동에 필요한 Yaw만 남긴다
3. 그 Yaw로 회전 행렬을 만든다
4. 그 회전 행렬에서 앞(X축), 오른쪽(Y축) 단위 벡터를 뽑는다.
5. 입력값 MovementVector.X/Y를 각각 앞/오른쪽 방향에 곱해 AddMovementInput을 한다
6. 카메라가 보는 방향 기준 이동 완성
void AUSPlayerCharacter::Tick(float DeltaSeconds){
Super::Tick(DeltaSeconds);
switch (CurrentViewMode)
{ case EViewMode::None:
case EViewMode::BackView:
break;
case EViewMode::QuarterView:
{
if (KINDA_SMALL_NUMBER < DirectionToMove.SizeSquared())
{ GetController()->SetControlRotation(FRotationMatrix::MakeFromX(DirectionToMove).Rotator());
AddMovementInput(DirectionToMove);
DirectionToMove = FVector::ZeroVector;
} break;
}
왜 이번엔 Tick을 이용해서 QuarterView를 구현했을까?
BackView처럼 Control Rotation을 기준축으로 쓸 이유가 약하기 때문이다. "그럼 굳이 Tick에서 해결하지 않아도 되지않냐?" 라고 생각할 수 있는데, 이동뿐만아니라
1. 입력 방향을 확인하고
2. 그 방향을 바라보게 회전시키고
3. 그 방향으로 이동시키는
이 세 가지를 한 프레임 안에서 같이 처리하기 위해서이다.
KINDA_SMALL_NUMBER -> 언리얼이 제공하는 매우 작은 실수값 상수. 거의 0에 가까운 값. 벡터의 길이가 진짜 0인지, 아니면 부동소수점 오차 수준의 아주 작은 값인지 구분할 때 사용.
SizeSquared() -> 벡터 길이의 제곱값을 구하는 함수이다. 보통 벡터가 0인지 아닌지 확인할 때는 실제 길이 Size()보다 SizeSquared()를 가벼워서 더 많이 사용. 이번 QuaterView Tick 코드에서는 입력 방향이 실제로 존재하는지를 빠르게 검사하기 위해 사용된다.
FRotationMatrix::MakeFromX(Vector) -> 주어진 Vector를 정면(Forward, X축)으로 삼는 회전 기준을 만드는 함수이다.
- W만 누르면 (1, 0, 0)방향이 정면
- D만 누르면 (0, 1, 0)방향이 정면
- W+D면 (1, 1, 0)대각선 방향이 정면
SpringArmComponent->bInheritPitch = false;
SpringArmComponent->bInheritYaw = false;
SpringArmComponent->bInheritRoll = false;
QuarterView에서 SpringArm의 bInheritPitch, bInheritYaw, bInheritRoll를 따로 설정하지 않았더니, 생각한 것과 다르게 카메라가 캐릭터/컨트롤러 회전을 따라 같이 움직였다.
QuarterView에서는 고정 시점을 원했기 때문에 아래처럼 회전 상속 옵션을 명시적으로 false로 설정해야 했다. 고정 시점처럼 회전 동작이 중요한 경우에는 "기본값은 이렇겠지"라고 추측하지 말고, 필요한 옵션은 반드시 코드나 블루프린트에서 명시적으로 지정하는 것이 안전하다.