본문 바로가기

컴포넌트 시스템 이해와 활용

액터 기능을 컴포넌트로 조합하는 원리를 이해하고 C++에서 생성·부착·초기화하는 방법을 익힙니다.

이전 절에서 입력이 Pawn과 Character 동작으로 이어지는 흐름을 봤다면, 이제 누가 실제 동작을 수행하는가를 분해해야 합니다.

여러 기능을 재사용 가능한 컴포넌트(Component)로 나눌 수 있습니다.

언리얼 엔진은 기능을 컴포넌트 단위로 조립하는 구조를 중심으로 동작하며, 이 구조를 이해해야 확장성과 유지보수성이 높은 게임플레이 코드를 만들 수 있습니다.


컴포넌트 시스템이란 무엇인가요?

언리얼 엔진의 컴포넌트 시스템은 액터(Actor)의 기능을 모듈화하고 재사용성을 높이기 위한 설계 패턴입니다.

쉽게 말해, 하나의 액터가 가진 모든 복잡한 기능을 액터 자체에 직접 구현하는 대신, 각각의 기능을 담당하는 작은 부품들을 만들고, 필요에 따라 이 부품들을 액터에 붙였다 떼었다 하면서 기능을 조립하는 방식입니다.

예를 들어, 플레이어 캐릭터 액터를 생각해 보세요.

이 캐릭터는 3D 모델을 가지고, 움직이고, 점프하고, 충돌을 감지하며, 소리를 낼 수 있어야 합니다.

이 모든 기능을 캐릭터 클래스 하나에 모두 구현한다면 코드가 매우 복잡해지고, 나중에 차량이나 드론처럼 다른 형태의 액터를 만들 때 해당 기능을 재사용하기 어려워질 겁니다.

하지만 컴포넌트 시스템을 사용하면 다음과 같이 나눌 수 있습니다.

  • 시각적 표현: UStaticMeshComponent (정적 메시) 또는 USkeletalMeshComponent (스켈레탈 메시)
  • 이동: UCharacterMovementComponent (캐릭터 전용 이동) 또는 UFloatingPawnMovement (범용 이동)
  • 충돌 감지: UCapsuleComponent, UBoxComponent, USphereComponent
  • 사운드 재생: UAudioComponent
  • 파티클 효과: UParticleSystemComponent
  • 카메라 시점: UCameraComponent, USpringArmComponent

각 컴포넌트는 특정한 하나의 기능을 담당하며, 이 컴포넌트들을 원하는 액터에 부착함으로써 해당 액터가 필요한 기능을 가지게 됩니다.


컴포넌트 분류와 역할

모든 컴포넌트의 최상위 부모는 UActorComponent입니다.

UActorComponent 자체는 로직과 데이터를 가질 수 있지만, 3D 월드의 위치/회전/크기 같은 변환(Transform) 정보는 직접 가지지 않습니다.

예를 들어 체력 관리용 UHealthComponent는 공간 좌표가 필요 없으므로 UActorComponent로 충분합니다.

반대로 USceneComponent는 UActorComponent를 상속하면서 3D 변환 정보를 가질 수 있는 타입입니다.

따라서 USceneComponent 계열은 다른 컴포넌트나 액터에 붙어 특정 위치에 존재할 수 있습니다.

UStaticMeshComponent, UCapsuleComponent, USpringArmComponent가 대표적인 예입니다.


컴포넌트 활용: C++에서 추가하고 설정하기

이제 실제 C++ 코드를 통해 액터에 컴포넌트를 추가하고 설정하는 방법을 알아보겠습니다.

이전 절에서 만든 AMyCharacter 클래스를 확장하여 카메라를 추가해 봅시다.

컴포넌트 선언 (헤더 파일)

MyCharacter.h 파일에 USpringArmComponent와 UCameraComponent를 위한 포인터 변수를 선언합니다. 아래 헤더의 생략 주석은 앞 절 코드를 지우라는 뜻이 아닙니다. 기존 InputActionValue include·입력 속성·콜백 선언을 유지하면서 두 컴포넌트의 전방 선언과 멤버를 기존 클래스에 추가합니다. cpp에서도 생성자 하나에 카메라 설정을 병합하고 기존 BeginPlay의 IMC 등록과 입력 바인딩 함수들을 유지합니다.

MyCharacter.h
// MyCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputAction.h"
#include "MyCharacter.generated.h"

// 미리 선언 (Forward Declarations) - include보다 가볍습니다.
class UInputMappingContext;
class UInputAction;
class USpringArmComponent; // 스프링 암 컴포넌트 추가
class UCameraComponent;    // 카메라 컴포넌트 추가

UCLASS()
class MYFIRSTCPPPROJECT_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    AMyCharacter();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;

    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
    // ... 기존 입력 관련 UPROPERTY 변수들 ...

    // 새로운 컴포넌트들을 UPROPERTY로 선언
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
    USpringArmComponent* CameraBoom; // 카메라를 팔처럼 지지하는 컴포넌트

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
    UCameraComponent* FollowCamera; // 실제 카메라 렌더링을 담당하는 컴포넌트

    // ... 기존 입력 액션 바인딩 함수들 ...
};

GameFramework/SpringArmComponent.h와 Camera/CameraComponent.h를 #include 지시문으로 직접 추가하지 않고, 클래스를 미리 선언하는 class 키워드를 사용했습니다.

이는 컴파일 시간을 줄이는 데 도움이 되며, .cpp 파일에서 실제로 컴포넌트의 멤버 함수를 호출할 때만 해당 헤더를 포함하면 됩니다.

컴포넌트 생성 및 부착 (소스 파일 - 생성자)

MyCharacter.cpp 파일의 생성자에서 컴포넌트를 생성하고, 액터의 계층 구조에 맞게 부착(Attach)합니다.

MyCharacter.cpp
// MyCharacter.cpp
#include "MyCharacter.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "GameFramework/SpringArmComponent.h" // UPROPERTY로 선언한 컴포넌트의 헤더를 여기서 포함
#include "Camera/CameraComponent.h"        // UPROPERTY로 선언한 컴포넌트의 헤더를 여기서 포함
#include "GameFramework/CharacterMovementComponent.h"

AMyCharacter::AMyCharacter()
{
    PrimaryActorTick.bCanEverTick = true;

    // 몸체 회전은 CharacterMovement가 처리합니다.
    bUseControllerRotationYaw = false;

    // RotationRate에 따라 몸체를 컨트롤러의 목표 회전 쪽으로 보간합니다.
    GetCharacterMovement()->bUseControllerDesiredRotation = true;
    GetCharacterMovement()->bOrientRotationToMovement = false;

    // 1. CameraBoom (USpringArmComponent) 생성 및 부착
    CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    CameraBoom->SetupAttachment(RootComponent); // 캐릭터의 루트 컴포넌트 (CapsuleComponent)에 부착
    CameraBoom->TargetArmLength = 300.0f; // 카메라와 캐릭터 사이의 거리
    CameraBoom->bUsePawnControlRotation = true; // 컨트롤러의 회전(Yaw)을 따라가도록 설정
    CameraBoom->bInheritPitch = true; // 컨트롤러의 피치(Pitch) 회전을 상속받도록 설정
    CameraBoom->bInheritYaw = true;   // 컨트롤러의 야(Yaw) 회전을 상속받도록 설정
    CameraBoom->bInheritRoll = true;  // 컨트롤러의 롤(Roll) 회전을 상속받도록 설정
    CameraBoom->bDoCollisionTest = true; // 카메라와 장애물 충돌 시 카메라가 당겨지도록 설정

    // 2. FollowCamera (UCameraComponent) 생성 및 부착
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // CameraBoom에 부착
    FollowCamera->bUsePawnControlRotation = false; // 카메라 자체는 컨트롤러 회전을 따르지 않음 (스프링암이 따르므로)

    // ... 기존 변수 기본값 설정 ...
}

// ... BeginPlay(), Tick(), SetupPlayerInputComponent() 등 기존 함수들 ...
설명
  • CreateDefaultSubobject<T>(TEXT("Name")): 이 함수는 컴포넌트를 생성할 때 사용합니다. T는 생성할 컴포넌트의 타입이고, TEXT("Name")은 에디터에서 보일 컴포넌트의 이름입니다. 이 함수는 생성자에서만 호출되어야 합니다.
  • SetupAttachment(ParentComponent, SocketName): 아직 등록되지 않은 씬 컴포넌트의 부모·소켓을 설정합니다. 등록된 컴포넌트를 런타임에 붙일 때는 AttachToComponent와 변환 규칙을 확인합니다.
    • CameraBoom->SetupAttachment(RootComponent);: CameraBoom을 AMyCharacter의 RootComponent (기본적으로 UCapsuleComponent)에 부착합니다. 이렇게 하면 캐릭터가 움직이면 카메라 붐도 함께 움직입니다.
    • FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);: FollowCamera를 CameraBoom에 부착합니다. USpringArmComponent::SocketName은 스프링 암 컴포넌트가 카메라를 부착할 수 있는 기본 소켓 이름입니다.
  • 컴포넌트 속성 설정: 생성된 컴포넌트의 포인터를 통해 다양한 속성을 설정할 수 있습니다. 예를 들어 TargetArmLength는 카메라와 타겟 사이의 거리를 조절합니다.
  • bUseControllerRotationYaw 및 bOrientRotationToMovement: ACharacter의 기본 회전 설정입니다.
    • bUseControllerRotationYaw = false;로 설정하고 GetCharacterMovement()->bOrientRotationToMovement = false;로 설정하면 캐릭터 자체가 이동 방향으로 회전하는 것을 멈춥니다.
    • GetCharacterMovement()->bUseControllerDesiredRotation = true;는 캐릭터가 APlayerController의 회전 값을 따라가도록 합니다. RotationRate에 따라 목표 방향으로 회전하므로 순간적으로 카메라와 같은 각도여야 하는 것은 아닙니다.
    • bUsePawnControlRotation은 CameraBoom이 APlayerController의 ControlRotation (플레이어의 시점 회전)을 따르도록 합니다. 이는 마우스나 스틱으로 시점을 움직일 때 카메라가 함께 회전하게 만듭니다.

컨트롤러와 몸체 회전의 구분

bUseControllerRotationYaw·Pitch·Roll은 APawn의 속성입니다. PlayerController에 이 필드를 대입하지 않습니다. 위 구성은 CharacterMovement와 SpringArm이 ControlRotation을 각자의 방식으로 사용하므로, 카메라 회전만을 위해 별도 PlayerController 클래스를 만들 필요는 없습니다.

다음은 추가 컨트롤러 로직이 필요할 때 사용할 기본 클래스 골격입니다.

MyPlayerController.h (새로 만들거나 기존 PlayerController 클래스)
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"

UCLASS()
class MYFIRSTCPPPROJECT_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    AMyPlayerController();

protected:
    virtual void BeginPlay() override;
};
MyPlayerController.cpp
#include "MyPlayerController.h"

AMyPlayerController::AMyPlayerController()
{
    // Pawn의 bUseControllerRotation* 속성은 해당 Pawn에서 설정합니다.
}

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();
    // ... 추가 초기화 로직 ...
}

이 커스텀 클래스를 실제로 사용할 때만 GameMode 블루프린트 (BP_MyGameMode)에서 Player Controller Class를 방금 만든 BP_MyPlayerController (또는 여러분의 C++ AMyPlayerController)로 설정해야 합니다.

위 생성자의 최종 설정을 기준으로 회전 담당을 비교합니다.

UNREAL C++ · CH3-3

같은 ControlRotation을 몸체와 카메라가 다르게 사용합니다

본문 생성자의 최종 설정을 비교합니다. CharacterMovement는 목표 회전으로 몸체를 돌리고 SpringArm은 시점 회전과 소켓 트랜스폼을 계산합니다.

회전 값과 공간 부착의 서로 다른 관계5노드5관계:ControllerControlRotation→CharacterMovement→RootCapsule몸체회전;Controller시점→SpringArm;Capsule→SpringArm→FollowCamera점선은부착부모자식.카메라는부모소켓을따르며자체ControlRotation사용안함.실선은값사용/회전,점선은부착이지상속·권한전달아님.ControllerControlRotationCharacterMovement목표 회전 · RotationRateCapsule · Root몸체 트랜스폼값회전SpringArm시점 회전 · 충돌소켓 트랜스폼FollowCamera자체 회전 사용 안 함부모 소켓을 따름시점 회전부착부착실선: 값·회전 사용 · 점선: 부모→자식 부착
회전 값과 공간 부착의 서로 다른 관계5노드5관계:ControllerControlRotation→CharacterMovement→RootCapsule몸체회전;Controller시점→SpringArm;Capsule→SpringArm→FollowCamera점선은부착부모자식.카메라는부모소켓을따르며자체ControlRotation사용안함.실선은값사용/회전,점선은부착이지상속·권한전달아님.ControllerControlRotation회전 값CharacterMovement목표 몸체 회전RotationRate몸체 회전Capsule · Root몸체 트랜스폼부착SpringArm시점 회전 · 충돌소켓 트랜스폼소켓 부착FollowCamera자체 회전 사용 안 함부모 소켓을 따름실선: 값·회전 사용점선: 부모→자식 부착
카메라와 몸체가 따라가는 회전
대상원문의 최종 설정결과를 해석할 기준
컨트롤러Look 입력으로 ControlRotation 갱신몸체 트랜스폼과 별도의 시점·조준 회전
캐릭터 이동bUseControllerDesiredRotation=true; bOrientRotationToMovement=falseRotationRate로 목표 방향을 향해 몸체 회전
스프링 암bUsePawnControlRotation=truePawn의 시점 회전을 사용하고 상속 축 설정 반영
카메라스프링 암 소켓에 부착; bUsePawnControlRotation=false부모가 계산한 소켓 트랜스폼으로 시점 제공
컨트롤러
원문의 최종 설정: Look 입력으로 ControlRotation 갱신
결과를 해석할 기준: 몸체 트랜스폼과 별도의 시점·조준 회전
캐릭터 이동
원문의 최종 설정: bUseControllerDesiredRotation=true; bOrientRotationToMovement=false
결과를 해석할 기준: RotationRate로 목표 방향을 향해 몸체 회전
스프링 암
원문의 최종 설정: bUsePawnControlRotation=true
결과를 해석할 기준: Pawn의 시점 회전을 사용하고 상속 축 설정 반영
카메라
원문의 최종 설정: 스프링 암 소켓에 부착; bUsePawnControlRotation=false
결과를 해석할 기준: 부모가 계산한 소켓 트랜스폼으로 시점 제공

TargetArmLength=300은 목표 거리입니다. 충돌 검사를 켰으므로 장애물에 의해 실제 카메라 거리가 짧아질 수 있습니다. 몸체는 RotationRate로 회전하므로 순간적으로 카메라와 같은 각도일 필요는 없습니다.

예시: 목표 거리와 실제 배치를 구분하기

원 생성자의 TargetArmLength=300.0은 기본 길이 단위에서 300 cm, 즉 3 m의 목표 길이입니다. bDoCollisionTest가 켜져 있어 장애물에 따라 카메라 붐이 줄어들 수 있으므로 언제나 실제 거리가 3 m라고 단정하지 않습니다. 같은 시점 회전 값을 읽더라도 몸체는 CharacterMovement의 목표 회전 처리, 카메라는 SpringArm의 소켓 트랜스폼을 따릅니다. FollowCamera의 bUsePawnControlRotation=false를 보고 카메라가 부모와도 함께 회전하지 않는다고 해석해서는 안 됩니다.

책임을 나누어 판단하기

월드 위치가 필요 없는 체력 계산을 재사용할 컴포넌트와 카메라를 지지할 공간 부품을 만들려 합니다. UActorComponent와 USceneComponent 중 각각 어느 계열이 맞을까요? 원 설정의 FollowCamera가 자체 ControlRotation을 사용하지 않을 때 시점이 바뀌는 경로는 어디인지도 설명해 보세요.

공간과 회전 책임의 풀이

체력 계산에는 공간 트랜스폼이 필요 없으므로 UActorComponent로 충분합니다. 카메라 지지·공간 부착에는 USceneComponent 계열을 쓰며 원 예제의 SpringArm이 여기에 해당합니다. FollowCamera는 부모 SpringArm의 소켓에 부착되어 그 소켓이 계산한 트랜스폼을 따릅니다. 카메라 자체의 ControlRotation 옵션을 끈 것과 부모의 변화를 따르지 않는 것은 다른 조건입니다.

SpringArm API에서 소켓, 제어 회전, 목표 길이와 충돌 검사 조건을 확인할 수 있습니다.

생성자에서 만드는 기본 컴포넌트에는 CreateDefaultSubobject를 사용합니다. 런타임 추가 기능은 NewObject로 생성한 뒤 소유자·등록·필요한 공간 부착을 설정하고, 종료 시 DestroyComponent와 남은 참조·델리게이트 정리를 관리합니다.

컴파일 및 테스트

코드를 컴파일(Ctrl + Shift + B 또는 언리얼 에디터의 컴파일 버튼)하고, 언리얼 에디터에서 Play 버튼을 누릅니다.

이제 캐릭터 뒤에 카메라가 따라오며, 마우스를 움직이면 캐릭터의 시점(그리고 카메라의 시점)이 함께 움직이는 것을 확인할 수 있을 겁니다!


컴포넌트 시스템의 장점

  • 재사용성: 한 번 만든 컴포넌트는 여러 액터에 부착하여 재사용할 수 있습니다. 예를 들어, UHealthComponent는 플레이어, 적, 심지어 파괴 가능한 환경 오브젝트에도 붙일 수 있습니다.
  • 모듈성: 각 컴포넌트는 독립적인 기능을 수행하므로, 특정 기능을 수정하거나 추가할 때 해당 컴포넌트만 수정하면 됩니다. 이는 코드의 유지보수를 쉽게 만듭니다.
  • 유연성: 런타임에 액터에 컴포넌트를 동적으로 추가하거나 제거할 수 있습니다. 예를 들어, 파워업 아이템을 획득하면 UWeaponComponent를 추가하고, 특정 상태가 되면 제거하는 식으로 활용할 수 있습니다.
  • 협업 용이: 프로그래머는 컴포넌트를 C++로 구현하고, 디자이너는 블루프린트에서 이 컴포넌트를 조립하고 속성을 설정하여 게임플레이를 빠르게 만들 수 있습니다.

컴포넌트 시스템을 사용하면 액터 기능을 역할별로 분리하고 재사용할 수 있습니다.

반복되는 기능은 커스텀 컴포넌트로 분리하되, 공간 좌표가 필요한지와 소유 액터의 수명을 먼저 정합니다.