icon
3장 : 게임플레이 프로그래밍

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


이전 절에서 플레이어 입력 처리에 대해 알아보면서, 궁극적으로 입력이 Pawn이나 Character의 특정 동작으로 이어지는 과정을 살펴보았습니다. 그런데 그 동작을 실제로 수행하는 주체는 무엇일까요? 바로 컴포넌트(Component) 입니다. 언리얼 엔진은 강력한 컴포넌트 시스템을 기반으로 설계되어 있습니다. 이 시스템을 이해하고 활용하는 것은 효율적이고 유연한 게임플레이 로직을 구축하는 데 핵심적인 부분입니다.


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

언리얼 엔진의 컴포넌트 시스템은 액터(Actor)의 기능을 모듈화하고 재사용성을 높이기 위한 설계 패턴입니다. 쉽게 말해, 하나의 액터가 가진 모든 복잡한 기능을 액터 자체에 직접 구현하는 대신, 각각의 기능을 담당하는 작은 부품들을 만들고, 필요에 따라 이 부품들을 액터에 '붙였다 떼었다' 하면서 기능을 조립하는 방식입니다.

예를 들어, 플레이어 캐릭터 액터를 생각해보세요. 이 캐릭터는 3D 모델을 가지고, 움직이고, 점프하고, 충돌을 감지하며, 소리를 낼 수 있어야 합니다. 이 모든 기능을 캐릭터 클래스 하나에 모두 구현한다면 코드가 매우 복잡해지고, 나중에 차량이나 드론처럼 다른 형태의 액터를 만들 때 해당 기능을 재사용하기 어려워질 겁니다.

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

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

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


컴포넌트의 종류

모든 컴포넌트의 최상위 부모 클래스는 UActorComponent 입니다. UActorComponent를 상속받는 컴포넌트들은 특정 로직이나 데이터를 가질 수 있지만, 3D 월드 내에서 위치, 회전, 크기와 같은 변환(Transform) 정보를 직접적으로 가지지는 않습니다. 예를 들어, 캐릭터의 체력을 관리하는 UHealthComponent는 3D 공간에 존재할 필요가 없으므로 UActorComponent를 상속받을 수 있습니다.

반면, USceneComponentUActorComponent를 상속받으면서 3D 월드 내에서 변환 정보를 가질 수 있는 컴포넌트입니다. 즉, USceneComponent를 상속받은 컴포넌트는 다른 컴포넌트나 액터에 '붙어' 특정 위치에 존재할 수 있습니다. 3D 모델을 나타내는 UStaticMeshComponent, 충돌 영역을 나타내는 UCapsuleComponent, 카메라를 연결하는 USpringArmComponent 등이 모두 USceneComponent를 상속받습니다.

  • UActorComponent

    • 3D 변환 정보를 가지지 않음.
    • 주로 순수 로직이나 데이터 관리에 사용.
    • 예: UHealthComponent, UInventoryComponent
  • USceneComponent

    • UActorComponent를 상속받고, 3D 변환 정보를 가짐.
    • 3D 월드 내에 시각적으로 존재하거나 특정 위치에 영향을 미칠 때 사용.
    • 계층 구조(Hierarchy)를 형성할 수 있음 (부모-자식 관계).
    • 예: UStaticMeshComponent, UCapsuleComponent, UCameraComponent, USpringArmComponent

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

이제 실제 C++ 코드를 통해 액터에 컴포넌트를 추가하고 설정하는 방법을 알아보겠습니다. 이전 절에서 만든 AMyCharacter 클래스를 확장하여 카메라를 추가해 봅시다.

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

AMyCharacter.h 파일에 USpringArmComponentUCameraComponent를 위한 포인터 변수를 선언합니다.

// AMyCharacter.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.hUCameraComponent.h를 직접 추가하지 않고, 클래스 정의만 미리 알려주는 class 키워드를 사용했습니다. 이는 컴파일 시간을 줄이는 데 도움이 되며, .cpp 파일에서 실제로 컴포넌트의 멤버 함수를 호출할 때만 해당 헤더를 포함하면 됩니다.

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

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

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

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

    // CharacterMovementComponent 설정 (ACharacter는 기본으로 가짐)
    GetCharacterMovement()->bOrientRotationToMovement = true; 
    bUseControllerRotationYaw = false; 
    
    // 캡슐 컴포넌트 (ACharacter의 RootComponent)가 YAW 회전을 따르도록 설정
    // 이렇게 해야 카메라 회전과 캐릭터 회전이 일치합니다.
    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): 생성된 컴포넌트를 다른 컴포넌트(부모)에 부착하여 계층 구조를 만듭니다.
    • CameraBoom->SetupAttachment(RootComponent);: CameraBoomAMyCharacterRootComponent (기본적으로 UCapsuleComponent)에 부착합니다. 이렇게 하면 캐릭터가 움직이면 카메라 붐도 함께 움직입니다.
    • FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);: FollowCameraCameraBoom에 부착합니다. USpringArmComponent::SocketName은 스프링 암 컴포넌트가 카메라를 부착할 수 있는 기본 소켓 이름입니다.
  • 컴포넌트 속성 설정: 생성된 컴포넌트의 포인터를 통해 다양한 속성을 설정할 수 있습니다. 예를 들어 TargetArmLength는 카메라와 타겟 사이의 거리를 조절합니다.
  • bUseControllerRotationYawbOrientRotationToMovement: ACharacter의 기본 회전 설정입니다.
    • bUseControllerRotationYaw = false;로 설정하고 GetCharacterMovement()->bOrientRotationToMovement = false;로 설정하면 캐릭터 자체가 이동 방향으로 회전하는 것을 멈춥니다.
    • GetCharacterMovement()->bUseControllerDesiredRotation = true;는 캐릭터가 APlayerController의 회전 값을 따라가도록 합니다. 이는 CameraBoom이 컨트롤러의 회전을 따를 때 캐릭터의 방향과 카메라의 방향을 일치시키는 데 중요합니다.
    • bUsePawnControlRotationCameraBoomAPlayerControllerControlRotation (플레이어의 시점 회전)을 따르도록 합니다. 이는 마우스나 스틱으로 시점을 움직일 때 카메라가 함께 회전하게 만듭니다.

플레이어 컨트롤러에서 회전 설정 (C++)

카메라가 컨트롤러의 시점 회전을 따르도록 하려면, APlayerController 에서 bUseControllerRotationYaw, bUseControllerRotationPitch, bUseControllerRotationRoll을 활성화해야 합니다. 만약 여러분의 게임 모드가 기본 APlayerController를 사용한다면 이 설정이 이미 되어 있을 가능성이 높습니다. 하지만 커스텀 APlayerController를 사용한다면 직접 설정해야 합니다.

// AMyPlayerController.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;
};
// AMyPlayerController.cpp
#include "MyPlayerController.h"

AMyPlayerController::AMyPlayerController()
{
    // 컨트롤러가 Pawn의 Yaw, Pitch, Roll 회전을 제어하도록 설정
    bUseControllerRotationPitch = true;
    bUseControllerRotationYaw = true;
    bUseControllerRotationRoll = true; // 일반적으로 Roll은 사용하지 않지만 필요에 따라
}

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

그리고 GameMode 블루프린트 (BP_MyGameMode) 에서 Player Controller Class를 방금 만든 BP_MyPlayerController (또는 여러분의 C++ AMyPlayerController)로 설정해야 합니다.

컴파일 및 테스트

코드를 컴파일(Ctrl + Shift + B 또는 언리얼 에디터의 '컴파일' 버튼)하고, 언리얼 에디터에서 Play 버튼을 누릅니다. 이제 캐릭터 뒤에 카메라가 따라오며, 마우스를 움직이면 캐릭터의 시점(그리고 카메라의 시점)이 함께 움직이는 것을 확인할 수 있을 겁니다!


컴포넌트 시스템의 장점

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

이제 언리얼 엔진의 강력한 컴포넌트 시스템에 대한 이해를 바탕으로 액터의 기능을 어떻게 모듈화하고 확장할 수 있는지 파악하셨을 것입니다. 컴포넌트는 언리얼 엔진 개발의 핵심적인 요소이므로, 다양한 컴포넌트들을 직접 사용해보고, 필요하다면 자신만의 커스텀 컴포넌트를 만들어보는 연습을 꾸준히 하셔야 합니다.