Ch4 팀 프로젝트(1) - SurviorCharacter 기본 세팅

yys·2026년 6월 26일

TIL

목록 보기
61/86

주제 선정


이번 Ch4의 주요 주제는 데드 바이 데이라이트(이하 데바데) 의 비대칭 구도에 리썰 컴퍼니의 수집·납품 메커니즘을 덧붙인 구조로 결정되었다.

  • 생존자는 맵 곳곳의 수집품을 모아 양쪽 납품소에 일정 가치 이상 납품한 뒤 탈출한다.
  • 살인마는 생존자를 추격·처치하며 수집과 탈출을 방해한다.

AI와 GAS 요소는 팀원 간 경험이 부족해 이번 범위에서는 배제했고, 실력에 맞춰 무리 없이 구현할 수 있는 선까지만 메커니즘을 잡는 것을 목표로 두었다.

오늘은 기획 논의 비중이 컸던 탓에, 구현은 이동 및 카메라 세팅까지만 진행했다.

Input Data Asset


입력은 상황에 따라 Input Action을 CharacterPlayerController에 다음과 같이 직접 할당하는 방식으로 시작할 수 있다.

private:
	UPROPERTY(EditDefaultsOnly, Category = "Input|Action")
	TObjectPtr<UInputAction> MoveAction;

	UPROPERTY(EditDefaultsOnly, Category = "Input|Action")
	TObjectPtr<UInputAction> LookAction;

하지만 이 방식은 Action이 늘어날 때마다 CharacterPlayerController에 변수를 계속 추가해야 한다. 즉, 입력 데이터가 클래스 본문에 불필요하게 누적되는 문제가 생긴다.

그래서 보통은 입력 데이터를 Data Asset으로 분리하고, CharacterPlayerController는 그 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단계 — 행렬로 변환해 축 추출

YawRotationFRotationMatrix로 변환하고, 거기서 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);
}
profile
게임 개발 지망생

0개의 댓글