안동민 개발노트

본문 시작

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

액터 기능을 컴포넌트로 조합하는 원리를 이해하고 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를 위한 포인터 변수를 선언합니다.

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; // 실제 카메라 렌더링을 담당하는 컴포넌트

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

#include 지시문은 USpringArmComponent.h와 UCameraComponent.h를 직접 추가하지 않고, 클래스 정의만 미리 알려주는 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)로 설정해야 합니다.

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

카메라와 몸체가 따라가는 회전

같은 ControlRotation을 사용해도 캐릭터 몸체와 카메라의 적용 방식은 다릅니다.

카메라와 몸체가 따라가는 회전
대상원문의 최종 설정결과를 해석할 기준
컨트롤러Look 입력으로 ControlRotation 갱신몸체 트랜스폼과 별도의 시점·조준 회전
캐릭터 이동DesiredRotation=true, OrientToMovement=falseRotationRate로 목표 방향을 향해 몸체 회전
스프링 암bUsePawnControlRotation=truePawn의 시점 회전을 사용하고 상속 축 설정 반영
카메라스프링 암 소켓에 부착; ControlRotation 직접 사용=false부모가 계산한 소켓 트랜스폼으로 시점 제공
컨트롤러
원문의 최종 설정: Look 입력으로 ControlRotation 갱신
결과를 해석할 기준: 몸체 트랜스폼과 별도의 시점·조준 회전
캐릭터 이동
원문의 최종 설정: DesiredRotation=true, OrientToMovement=false
결과를 해석할 기준: RotationRate로 목표 방향을 향해 몸체 회전
스프링 암
원문의 최종 설정: bUsePawnControlRotation=true
결과를 해석할 기준: Pawn의 시점 회전을 사용하고 상속 축 설정 반영
카메라
원문의 최종 설정: 스프링 암 소켓에 부착; ControlRotation 직접 사용=false
결과를 해석할 기준: 부모가 계산한 소켓 트랜스폼으로 시점 제공

TargetArmLength는 목표 거리입니다. 충돌 검사를 켜면 장애물 때문에 실제 카메라 거리가 짧아질 수 있습니다.

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

컴파일 및 테스트

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

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


컴포넌트 시스템의 장점

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

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

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