결국 모든 회전의 기준은 PlayerController가 쥐고 있는 ControlRotation(현재 바라보는 조준 방향)임. 이걸 누가, 어떤 방식으로 따라갈지가 핵심!
bUseControllerRotationYaw (APawn 옵션)bUseControllerDesiredRotation (CharacterMovement 옵션)RotationRate를 써서 부드럽게(점진적으로) 돎.bOrientRotationToMovement (CharacterMovement 옵션)RotationRateMoveComp->RotationRate = FRotator(0.0f, 240.0f, 0.0f); 이런 식으로 씀.💡 모아보기
| 옵션 | 어디 소속? | 무슨 역할? | 휙 도나? 서서히 도나? |
|---|---|---|---|
| ControlRotation | PlayerController | 컨트롤러가 바라보는 기준 방향 | - |
| bUseControllerRotationYaw | APawn | 컨트롤러 Yaw를 몸체 회전에 씀 | 즉시 확 돎 |
| bUseControllerDesiredRotation | CharacterMovement | 컨트롤러 방향 목표로 몸을 돎 | RotationRate 속도로 서서히 |
| bOrientRotationToMovement | CharacterMovement | 걷거나 뛰는 이동 방향으로 돎 | RotationRate 속도로 서서히 |
| bUsePawnControlRotation | SpringArm | 카메라가 Pawn의 컨트롤 회전을 쓸지 | - |
3인칭 기본 이동 세팅 예시
APlayerCharacter::APlayerCharacter()
{
// 캐릭터 몸체가 마우스 휙휙 돌릴 때마다 즉시 돌아가지 않게 막음
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
UCharacterMovementComponent* MoveComp = GetCharacterMovement();
// 포인터 체크 안 하면 나중에 무조건 크래시 터짐. 습관화할 것.
if (MoveComp)
{
// 걷는 방향으로 몸 돌리기 ON
MoveComp->bOrientRotationToMovement = true;
// 카메라 방향으로 몸 돌리기 OFF
MoveComp->bUseControllerDesiredRotation = false;
// 초당 360도로 휙휙 돌게 세팅
MoveComp->RotationRate = FRotator(0.0f, 360.0f, 0.0f);
}
}
참고: AI한테 코드 짜달라고 할 때도 "포인터 쓸 때 무조건 예외 처리해 줘"라고 규칙 박아두면 편함. 바이오하자드처럼 제자리 카메라 회전, AAA 게임의 상하/좌우 분리 회전 등 게임마다 감각이 다르니 이 옵션들을 완벽히 알고 있어야 기획 의도대로 뽑을 수 있음.
언리얼 C++ 클래스 앞에 붙는 U, A 접두사 제대로 짚고 넘어가기. C++이라고 냅다 new 썼다가는 큰일 남.
NewObject<T>(Outer)로 생성함. (Outer는 이 객체가 어디 소속인지 알려주는 용도)GetWorld()->SpawnActor<T>()로 생성.| 뭘 만들 건데? | 무슨 함수 쓸까? |
|---|---|
| UObject | NewObject<T>(Outer) |
| Actor | GetWorld()->SpawnActor<T>() |
| Component | CreateDefaultSubobject<T>(TEXT("이름")) |
생명주기와 생성자 함정
BeginPlay에 넣어야 됨.Destroy() 불렀다고 메모리 바로 안 날아감. 월드에서 빼내는 작업만 하고, 찐 메모리 청소는 GC(가비지 컬렉터)가 알아서 함. 그러니까 delete 절대 금지.IsValid() 폼 미쳤음: 포인터 살아있는지 체크할 때 if (Ptr)은 단순 널체크만 되지만, IsValid()는 GC가 지우려고 찜해둔(Pending Kill) 객체까지 다 걸러줌. 크래시 내기 싫으면 무조건 쓸 것.무기를 주웠을 때 Weapon->SetOwner(PlayerCharacter); 처럼 오너를 정해줄 수 있음.
근데 여기서 헷갈리면 안 되는 게, Owner ≠ 메모리 소유권 ≠ Attachment(부착) 임.
Owner는 "이 액터가 어느 플레이어/네트워크 커넥션 소속인가"를 따지는 게임플레이/네트워크 소유권임. (멀티 짤 때 RPC나 복제 기준이 됨)AttachToActor()로 붙였다고 해서 Owner가 자동으로 넘어가는 게 아님. 따로 세팅해 줘야 됨.V키 누를 때마다 3인칭 ➔ 1인칭 ➔ 탑다운 ➔ 숄더뷰로 휙휙 바뀌게 만드는 실습.
왜 Enum을 쓰나?
그냥 0, 1, 2 숫자로 쳐도 되지만 블루프린트 연동이랑 유지보수를 위해 uint8 기반 Enum을 씀. (uint8인 이유는 플랫폼 상관없이 1바이트 고정이라 네트워크 터질 일 없고 블플 노출 조건임)
UENUM(BlueprintType)
enum class EPlayerControlMode : uint8
{
ThirdPerson UMETA(DisplayName = "Third Person"),
FirstPerson UMETA(DisplayName = "First Person"),
TopDown UMETA(DisplayName = "Top Down"),
Shoulder UMETA(DisplayName = "Shoulder / Aim")
};
설계 철학: 책임 분리(SRP)
모드 바꾸는 함수 하나에 다 때려 넣지 말고, 다음 모드가 뭔지 고르는 놈(ChangeControlMode)이랑, 그 모드 스펙 덮어씌우는 놈(ApplyControlMode)을 쪼갬.
// V키 바인딩 (입력 액션 프로퍼티는 GC 안 당하게 TObjectPtr + UPROPERTY 달아둠)
if (ChangeControlModeAction)
{
EnhancedInputComponent->BindAction(
ChangeControlModeAction,
ETriggerEvent::Started,
this,
&AMyCharacter::ChangeControlMode
);
}
void AMyCharacter::ChangeControlMode()
{
switch (CurrentControlMode)
{
case EPlayerControlMode::ThirdPerson: ApplyControlMode(EPlayerControlMode::FirstPerson); break;
case EPlayerControlMode::FirstPerson: ApplyControlMode(EPlayerControlMode::TopDown); break;
case EPlayerControlMode::TopDown: ApplyControlMode(EPlayerControlMode::Shoulder); break;
case EPlayerControlMode::Shoulder: ApplyControlMode(EPlayerControlMode::ThirdPerson); break;
default: ApplyControlMode(EPlayerControlMode::ThirdPerson); break;
}
}
모드별 세팅값 (ApplyControlMode 내부 로직)
주의: 아래 값들은 절대적인 정답이 아니라 베이스라인임. 자기 기획서나 게임 장르에 맞게 FOV, Arm 길이 등 직접 만져봐야 됨.
if (!MovementComponent || !CameraBoom || !FollowCamera) return; 깔고 시작할 것.| 건드린 변수들 | 3인칭 (이동 위주) | 1인칭 (조준 위주) | 탑다운 (이동 위주) | 숄더 (조준 위주) |
|---|---|---|---|---|
| SetUsingAbsoluteRotation | false | false | true (캐릭터 돌아도 카메라는 월드 고정) | false |
| bUsePawnControlRotation | true | true | false | true |
| bDoCollisionTest | true | false | true | true |
| TargetArmLength | 400 | 0 | 900 | 220 |
| TargetOffset | (0,0,60) | (0,0,BaseEyeHeight) | Zero | (0,0,60) |
| SocketOffset | Zero | Zero | Zero | (0,70,0) (어깨너머) |
| SetWorldRotation | - | - | (-60,-45,0) (내려다보기) | - |
| SetFieldOfView(FOV) | 90 | 90 | 70 | 80 |
| bUseControllerRotationYaw | false | true | false | true |
| bOrientRotationToMovement | true | false | true | false |
| SetOwnerNoSee(Mesh) | false | true (내 몸 숨기기) | false | false |
꿀팁: 카메라 그냥 캐릭터에 갖다 붙이면 되지 왜 굳이 SpringArm 쓰냐고? 벽 등지고 섰을 때 카메라가 벽 뚫고 나가는 거나 반전되는 거 SpringArm이 알아서 보정해 줘서 씀.
NewObject/SpawnActor), 지울 땐 delete 대신 Destroy(). 널체크는 무조건 IsValid().