이번 Ch4의 주요 주제는 데드 바이 데이라이트(이하 데바데) 의 비대칭 구도에 리썰 컴퍼니의 수집·납품 메커니즘을 덧붙인 구조로 결정되었다.
AI와 GAS 요소는 팀원 간 경험이 부족해 이번 범위에서는 배제했고, 실력에 맞춰 무리 없이 구현할 수 있는 선까지만 메커니즘을 잡는 것을 목표로 두었다.
오늘은 기획 논의 비중이 컸던 탓에, 구현은 이동 및 카메라 세팅까지만 진행했다.
입력은 상황에 따라 Input Action을 Character나 PlayerController에 다음과 같이 직접 할당하는 방식으로 시작할 수 있다.
private:
UPROPERTY(EditDefaultsOnly, Category = "Input|Action")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditDefaultsOnly, Category = "Input|Action")
TObjectPtr<UInputAction> LookAction;
하지만 이 방식은 Action이 늘어날 때마다 Character나 PlayerController에 변수를 계속 추가해야 한다. 즉, 입력 데이터가 클래스 본문에 불필요하게 누적되는 문제가 생긴다.
그래서 보통은 입력 데이터를 Data Asset으로 분리하고, Character나 PlayerController는 그 Data Asset 하나만 참조하도록 구성한다.
// InputConfigData.h
USTRUCT(BlueprintType)
struct FInputMappingContextEntry
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> MappingContext = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
int32 Priority = 0;
};
UCLASS(BlueprintType)
class SPACH4_API UInputConfigData : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input|Context")
TArray<FInputMappingContextEntry> MappingContexts;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input|Action")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input|Action")
TObjectPtr<UInputAction> LookAction;
};
MappingContext를 단순 배열이 아니라 FInputMappingContextEntry 구조체로 감싼 이유는 Context별 우선순위(Priority)를 함께 묶어두기 위해서다. 이렇게 해두면 IMC를 등록할 때 별도의 우선순위 관리 없이 엔트리 하나만 보면 된다.
이후 IMC와 IA를 만들고 에셋에 값만 채워주면, 입력 데이터가 클래스에서 완전히 분리된다.

그리고 PlayerController에서는 Context 배열을 순회하며 Subsystem에 IMC를 등록해준다.
// ASPPlayerController.cpp
for (const FInputMappingContextEntry& Entry : InputConfig->MappingContexts)
{
AddInputMappingContext(Entry.MappingContext, Entry.Priority);
}
이동 처리는 다음 순서로 이루어진다. 각 단계가 실제로 어떤 벡터를 다루는지 월드 공간에 기즈모로 그려 확인했다.
void ASurvivorCharacter::Move(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get<FVector2D>();
const FRotator Rotation = GetActorRotation();
const FRotator YawRotation = FRotator(0, Rotation.Yaw, 0);
// Yaw 회전을 행렬로 만든 뒤, 각 축 벡터를 추출
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
1단계 — 입력 값과 회전 값 수집
입력으로 들어온 2D 벡터와 캐릭터의 현재 회전(GetActorRotation())을 가져온다. 이 시점의 회전은 Pitch·Roll까지 포함한 전체 좌표계다.
2단계 — Yaw 성분만 분리
회전 값에서 좌우 회전(Yaw)만 남긴다. Pitch·Roll이 섞이면 카메라가 위를 볼 때 캐릭터가 공중으로 전진하는 식의 이동이 생기므로, 지면을 따라 움직이도록 수평 성분만 추린다.
3단계 — 행렬로 변환해 축 추출
YawRotation을 FRotationMatrix로 변환하고, 거기서 X(Forward)·Y(Right) 단위축을 뽑는다. 코드상의 추상적인 축 벡터가, 화면에서는 캐릭터를 기준으로 뻗는 빨강·초록 화살표로 나타난다.
4단계 — 입력을 축에 매핑해 반영
각 축에 입력 값을 매핑해 MovementComponent에 반영한다. 두 축에 스틱 입력이 합성되어 실제 이동 방향(파랑) 이 결정되고, 이는 결과적으로 캐릭터의 속도(GetVelocity())로 이어진다.
시선 처리는 컨트롤러의 회전만 조정하면 되므로 훨씬 단순하다.
void ASurvivorCharacter::Look(const FInputActionValue& Value)
{
const FVector2D LookVector = Value.Get<FVector2D>();
AddControllerYawInput(LookVector.X);
AddControllerPitchInput(LookVector.Y);
}