컨텐츠 검색
[UE5 C++] C++와 블루프린트, 어디까지 나눠야 할까? (GameMode, Character, Pawn)

2026. 1. 13. 20:14Unreal Engine/개념

1. 오늘의 학습 목표

오늘은 언리얼 엔진의 기본 프레임워크인 GameMode, Character, Pawn을 C++ 기반으로 설계하고 구현해보았습니다. 단순히 기능을 구현하는 것을 넘어, "C++와 블루프린트 사이에서 역할을 어떻게 분담해야 유지보수에 좋을까?"를 중점적으로 고민했습니다.

 


2. GameMode와 레벨별 규칙의 이해

프로젝트를 설정하며 GameMode의 적용 우선순위에 대해 알게 되었습니다.

  • 프로젝트 세팅(Project Settings): 게임 전체의 기본 게임 모드 설정
  • 월드 세팅(World Settings): 현재 레벨에만 적용되는 게임 모드 설정

[확인 결과] 월드 세팅 > 프로젝트 세팅 순으로 우선순위가 적용됩니다. 이를 통해, 기본 규칙은 프로젝트 세팅에 두되, 특정 스테이지(예: 보스전, 퍼즐 맵)에서는 월드 세팅을 통해 별도의 게임 모드를 손쉽게 갈아끼울 수 있는 유연한 구조임을 이해했습니다.

 

[트러블 슈팅] C++로 DefaultPawnClass를 설정해두었으나, GameMode를 상속받은 블루프린트 쪽에서 설정이 덮어씌워져 동작하지 않는 현상을 겪었습니다. 블루프린트 에디터 내의 설정이 C++ 생성자 설정보다 우선된다는 점을 확인했습니다.

// CollectorsWoodsGameMode.cpp

// StaticClass를 사용하여 기본 폰 클래스를 설정한다.
DefaultPawnClass = ACollectorsWoodsCharacter::StaticClass();

 


3. Character 클래스 설계: "어디까지 C++로 짜야 할까?"

ACharacter를 상속받아 캐릭터 클래스를 만들었습니다.

ACharacter는 기본적으로 CapsuleComponent, ArrowComponent, Mesh를 포함하고 있었습니다.

저는 여기에 3인칭 시점을 위한 카메라 구성을 C++로 추가했습니다.

// CollectorsWoodsCharacter.cpp

// SpringArm 컴포넌트 설정
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
SpringArmComp->TargetArmLength = 300.0f; // 카메라와 캐릭터 간의 거리 설정
SpringArmComp->SocketOffset = FVector(0.0f, 45.0f, 45.0f); // 카메라를 캐릭터의 중심보다 약간 오른쪽 위로 이동
SpringArmComp->bUsePawnControlRotation = true; // 컨트롤러의 회전을 스프링 암에 적용

// Camera 컴포넌트 설정
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName); // 스프링 암의 끝에 카메라를 부착
CameraComp->bUsePawnControlRotation = false; // 카메라 자체는 컨트롤러의 회전을 사용하지 않음

 

구현 도중 가장 고민되었던 점은 "Skeletal Mesh와 같은 에셋 할당을 어디서 할 것인가?"였습니다.

처음에는 C++로 구현하는 게 맞다고 생각했었습니다.

그 이유는 두 가지인데 첫째는 디버깅 추적이 편하다는 것이었고, 둘째는 블루프린트 상에서 설정이 누락되었을 때 그에 대한 기본값 설정을 C++에서 해주면 되지 않을까? 하는 생각이었습니다.

 

하지만 에셋을 자주 수정해야 할 때와 협업을 생각하면 에셋 할당하는 건 블루프린트 상에서 처리하는 게 맞다는 생각이 들었습니다. 이를 토대로 다음 내용을 정리했습니다.

  1. C++ 하드 코딩 (ConstructorHelpers 사용):
    • 장점: 코드가 명시적이고, 에셋 경로를 한눈에 볼 수 있음.
    • 단점: 에셋의 경로나 이름이 바뀌면 코드를 수정하고 다시 컴파일해야 함. (Hard Reference 문제)
  2. 블루프린트 할당:
    • 장점: 컴파일 없이 에디터에서 바로 에셋을 교체 가능. 기획/아트와의 협업에 유리함.
    • 단점: C++ 코드만 봐서는 어떤 에셋이 쓰였는지 알 수 없음.

결론: "변하지 않는 로직(컴포넌트 구조)은 C++로, 자주 바뀌는 리소스(Mesh, Sound)는 블루프린트로 관리한다."

 


4. Pawn 구현과 이동 로직 (배 만들기)

다음으로 Character 클래스와 명확한 차이를 알기 위해 APawn을 상속받아 배(Boat)를 만들었습니다.

Character와 달리 Pawn은 DefaultSceneRoot만 존재하여, StaticMeshComponent부터 직접 C++로 생성해주었습니다.

이번에는 실험적으로 C++ 내부에서 에셋 경로를 직접 지정하여 모델을 띄워보았습니다.

// BoatPawn.cpp

SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);

StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);

static ConstructorHelpers::FObjectFinder<UStaticMesh> BoatMeshAsset(TEXT("/Game/Assets/Meshes/SM_Boat.SM_Boat"));

if (BoatMeshAsset.Succeeded())
{
    StaticMeshComp->SetStaticMesh(BoatMeshAsset.Object);
}

 

[이동 로직 구현]

AddActorLocalOffset(FVector(10.f * DeltaTime, 0.f, 0.f));

매 프레임마다 X축 방향으로 10 단위씩 이동합니다. 

Tick 함수 내에서 DeltaTime을 곱함으로써 프레임 속도에 관계없이 일정한 속도로 이동하게 됩니다.

 

[아쉬운 점 & 개선 계획] 현재 사용한 AddActorLocalOffset은 단순히 좌표를 이동시키는 함수라 물리적인 마찰이나 가속도가 적용되지 않습니다. 배(Boat)와 같은 탈것의 자연스러운 관성을 구현하기 위해서는:

  1. FloatingPawnMovement 컴포넌트를 추가하고,
  2. AddMovementInput 함수를 사용하여 입력을 처리하는 방식

으로 개선하는 것이 좋겠다는 인사이트를 얻었습니다. 다음 포스팅에서는 이 방식을 적용해 더 부드러운 움직임을 구현해 볼 예정입니다.

 


5. 오늘 배운 점 요약

  1. GameMode 우선순위: 월드 세팅이 프로젝트 세팅보다 우선한다.
  2. 설계의 분리: 컴포넌트의 부착 관계 등 구조(Structure)는 C++이 강력하고, 에셋 할당 등 데이터(Data)는 블루프린트가 강력하다. 이 둘을 적절히 섞어 쓰는 것이 중요하다.
  3. Hard Reference의 위험성: C++ 내에 에셋 경로를 하드코딩하는 것은 유연성을 해치므로, UPROPERTY(EditAnywhere) 등을 활용해 에디터에 권한을 위임하자.