Skip to content

UE5 C++ 컴포넌트 기반 설계

언리얼 엔진에서 **컴포넌트(Component)**는 Actor에 기능을 추가하는 조립 블록입니다. 하나의 Actor가 메시, 충돌, 파티클, 오디오, AI 인식 등의 기능을 각각의 컴포넌트로 조합해 갖추는 것이 컴포넌트 기반 설계(Component-Based Design)입니다.

상속(Inheritance)으로 기능을 추가하면 클래스 계층이 폭발적으로 증가합니다. 반면 컴포넌트(Composition)를 사용하면 기능을 독립적으로 재사용하고 조합할 수 있습니다.

비교상속 방식컴포넌트 방식
기능 재사용서브클래싱 필요컴포넌트를 다른 Actor에 붙이면 됨
유연성컴파일 타임 고정런타임 추가/제거 가능
복잡도다중 상속 문제 발생 가능명확한 단일 책임 분리
UE 권장최소화핵심 설계 패턴

UActorComponent트랜스폼(위치/회전/크기)이 없는 순수 기능 컴포넌트입니다. 월드 내 물리적 위치와 무관한 로직, 데이터 관리에 사용합니다.

  • 스탯 관리 (체력, 마나, 경험치)
  • 인벤토리 관리
  • 쿨다운 시스템
  • 상태 머신(State Machine)
UObject
└─ UActorComponent ← 트랜스폼 없음, 순수 기능
└─ USceneComponent ← 트랜스폼 있음, 계층 구조 지원
└─ UPrimitiveComponent ← 렌더링 + 충돌
└─ UMeshComponent
└─ UShapeComponent (캡슐, 구, 박스)

USceneComponent트랜스폼을 갖는 컴포넌트입니다. 부모-자식 계층 구조(Attachment Hierarchy)를 구성할 수 있으며, 루트 컴포넌트로 사용됩니다.

  • 메시 컴포넌트 (시각적 표현)
  • 충돌 컴포넌트 (SphereComponent, CapsuleComponent)
  • 소켓 기반 부착점 (WeaponSocket, HandSocket)
  • 카메라, 스프링암
// 위치/회전/부착이 필요 없는 기능 → UActorComponent
// 예: 스탯, 인벤토리, AI 로직 컴포넌트
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYGAME_API UStatComponent : public UActorComponent { ... };
// 위치/회전/부착이 필요한 기능 → USceneComponent
// 예: 무기 비주얼, 히트박스, 이펙트 발생 위치
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYGAME_API UWeaponMeshComponent : public USceneComponent { ... };

2. CreateDefaultSubobject로 컴포넌트 추가

Section titled “2. CreateDefaultSubobject로 컴포넌트 추가”

컴포넌트를 Actor의 기본 구조로 포함하려면 **생성자(Constructor)**에서 CreateDefaultSubobject<T>()를 사용합니다. 이 함수는 생성자 밖에서 호출하면 안 됩니다.

MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
protected:
// SceneComponent — 카메라 시스템
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera",
meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USpringArmComponent> CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera",
meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UCameraComponent> FollowCamera;
// ActorComponent — 기능 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components",
meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UStatComponent> StatComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components",
meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UWeaponComponent> WeaponComponent;
};
MyCharacter.cpp
#include "MyCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Components/StatComponent.h"
#include "Components/WeaponComponent.h"
AMyCharacter::AMyCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// 1. 스프링 암 — 캐릭터 메시(루트)에 부착
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(GetMesh()); // 메시 소켓에 부착
CameraBoom->TargetArmLength = 300.f;
CameraBoom->bUsePawnControlRotation = true;
// 2. 카메라 — 스프링 암 끝에 부착
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// 3. 순수 기능 컴포넌트 — SetupAttachment 불필요
StatComponent = CreateDefaultSubobject<UStatComponent>(TEXT("StatComponent"));
WeaponComponent = CreateDefaultSubobject<UWeaponComponent>(TEXT("WeaponComponent"));
}

2.2 SetupAttachment로 계층 구조 구성

Section titled “2.2 SetupAttachment로 계층 구조 구성”

USceneComponentSetupAttachment()로 부모 컴포넌트에 부착합니다. 부착 계층은 부모의 트랜스폼 변경 시 자식이 함께 이동하게 만듭니다.

// 무기 Actor의 컴포넌트 계층 예시
AWeaponActor::AWeaponActor()
{
// 루트 컴포넌트: SceneComponent (빈 트랜스폼 기준점)
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(Root);
// 메시 — 루트에 부착
WeaponMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WeaponMesh"));
WeaponMesh->SetupAttachment(Root);
// 총구 이펙트 위치 — 메시의 MuzzleSocket 소켓에 부착
MuzzleLocation = CreateDefaultSubobject<USceneComponent>(TEXT("MuzzleLocation"));
MuzzleLocation->SetupAttachment(WeaponMesh, FName("MuzzleSocket"));
// 탄피 배출 위치 — 메시의 EjectSocket 소켓에 부착
EjectLocation = CreateDefaultSubobject<USceneComponent>(TEXT("EjectLocation"));
EjectLocation->SetupAttachment(WeaponMesh, FName("EjectSocket"));
}

// 타입으로 컴포넌트 검색 (단일 결과)
UStatComponent* Stat = GetOwner()->GetComponentByClass<UStatComponent>();
if (Stat)
{
Stat->ApplyDamage(10.f);
}
// 여러 컴포넌트 검색
TArray<UActorComponent*> Comps;
GetOwner()->GetComponents(UStatComponent::StaticClass(), Comps);

3.2 FindComponentByClass (템플릿 버전)

Section titled “3.2 FindComponentByClass (템플릿 버전)”
// 템플릿 버전 — 캐스팅 불필요
UWeaponComponent* Weapon = FindComponentByClass<UWeaponComponent>();
// 다른 Actor의 컴포넌트에 접근
void AMyCharacter::OnHit(AActor* OtherActor)
{
if (!OtherActor)
{
return;
}
// 피격 대상의 HealthComponent를 가져와 데미지 적용
if (UHealthComponent* HealthComp = OtherActor->FindComponentByClass<UHealthComponent>())
{
HealthComp->ApplyDamage(WeaponDamage);
}
}

3.4 UPROPERTY 멤버 포인터 직접 참조

Section titled “3.4 UPROPERTY 멤버 포인터 직접 참조”

같은 Actor 내 컴포넌트는 UPROPERTY 멤버로 미리 저장해두는 것이 가장 효율적입니다.

// 검색 없이 직접 접근 — 가장 빠름
void AMyCharacter::OnDamageReceived(float Damage)
{
// StatComponent는 생성자에서 CreateDefaultSubobject로 생성한 멤버
if (StatComponent)
{
StatComponent->ApplyDamage(Damage);
}
}

4. 런타임에 컴포넌트 동적 추가/제거

Section titled “4. 런타임에 컴포넌트 동적 추가/제거”
// 런타임에 컴포넌트 추가 (생성자 외부)
void AMyCharacter::EquipShield()
{
// NewObject로 생성 후 RegisterComponent 호출
UShieldComponent* Shield = NewObject<UShieldComponent>(this, UShieldComponent::StaticClass());
if (Shield)
{
Shield->SetupAttachment(GetMesh(), FName("ShieldSocket"));
Shield->RegisterComponent(); // 월드에 등록
// 필요 시 멤버 포인터에 저장
ActiveShieldComponent = Shield;
}
}
// 런타임에 컴포넌트 제거
void AMyCharacter::UnequipShield()
{
if (ActiveShieldComponent)
{
ActiveShieldComponent->DestroyComponent();
ActiveShieldComponent = nullptr;
}
}

StatComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "StatComponent.generated.h"
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnHealthChangedSignature, float /*NewHP*/, float /*MaxHP*/);
DECLARE_MULTICAST_DELEGATE(FOnDeathSignature);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYGAME_API UStatComponent : public UActorComponent
{
GENERATED_BODY()
public:
UStatComponent();
// C++ 구독용 이벤트
FOnHealthChangedSignature OnHealthChanged;
FOnDeathSignature OnDeath;
// Blueprint 노출 이벤트
UPROPERTY(BlueprintAssignable, Category = "Stat|Events")
FOnHealthChangedBP OnHealthChangedBP; // Dynamic 버전
UFUNCTION(BlueprintCallable, Category = "Stat")
void ApplyDamage(float DamageAmount);
UFUNCTION(BlueprintCallable, Category = "Stat")
void RestoreHealth(float Amount);
UFUNCTION(BlueprintPure, Category = "Stat")
float GetCurrentHealth() const { return CurrentHealth; }
UFUNCTION(BlueprintPure, Category = "Stat")
float GetMaxHealth() const { return MaxHealth; }
UFUNCTION(BlueprintPure, Category = "Stat")
float GetHealthPercent() const;
UFUNCTION(BlueprintPure, Category = "Stat")
bool IsAlive() const { return !bIsDead; }
protected:
virtual void BeginPlay() override;
private:
UPROPERTY(EditDefaultsOnly, Category = "Stat|Config", meta = (ClampMin = "1.0"))
float MaxHealth = 100.f;
UPROPERTY(VisibleInstanceOnly, Category = "Stat|Debug")
float CurrentHealth = 0.f;
UPROPERTY(VisibleInstanceOnly, Category = "Stat|Debug")
bool bIsDead = false;
};
StatComponent.cpp
#include "StatComponent.h"
UStatComponent::UStatComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UStatComponent::BeginPlay()
{
Super::BeginPlay();
CurrentHealth = MaxHealth;
}
void UStatComponent::ApplyDamage(float DamageAmount)
{
if (bIsDead || DamageAmount <= 0.f)
{
return;
}
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
if (CurrentHealth <= 0.f)
{
bIsDead = true;
OnDeath.Broadcast();
}
}
void UStatComponent::RestoreHealth(float Amount)
{
if (bIsDead || Amount <= 0.f)
{
return;
}
CurrentHealth = FMath::Clamp(CurrentHealth + Amount, 0.f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
}
float UStatComponent::GetHealthPercent() const
{
return MaxHealth > 0.f ? CurrentHealth / MaxHealth : 0.f;
}

5.2 UWeaponComponent — SceneComponent 파생 실전 예시

Section titled “5.2 UWeaponComponent — SceneComponent 파생 실전 예시”
WeaponComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "WeaponComponent.generated.h"
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYGAME_API UWeaponComponent : public UActorComponent
{
GENERATED_BODY()
public:
UWeaponComponent();
// 장착된 무기 Actor 클래스 (에디터에서 설정)
UPROPERTY(EditDefaultsOnly, Category = "Weapon")
TSubclassOf<class AWeaponActor> DefaultWeaponClass;
UFUNCTION(BlueprintCallable, Category = "Weapon")
void EquipWeapon(TSubclassOf<AWeaponActor> WeaponClass);
UFUNCTION(BlueprintCallable, Category = "Weapon")
void UnequipWeapon();
UFUNCTION(BlueprintCallable, Category = "Weapon")
void FireWeapon();
UFUNCTION(BlueprintPure, Category = "Weapon")
AWeaponActor* GetCurrentWeapon() const { return CurrentWeapon.Get(); }
protected:
virtual void BeginPlay() override;
private:
// TWeakObjectPtr: 소유하지 않는 참조 — 무기 Actor는 월드가 소유
UPROPERTY(VisibleInstanceOnly, Category = "Weapon|Debug")
TObjectPtr<AWeaponActor> CurrentWeapon;
// 소켓 이름 — 캐릭터 메시에서 무기가 부착될 위치
UPROPERTY(EditDefaultsOnly, Category = "Weapon")
FName WeaponSocketName = FName("WeaponSocket_R");
};
WeaponComponent.cpp
#include "WeaponComponent.h"
#include "WeaponActor.h"
#include "GameFramework/Character.h"
UWeaponComponent::UWeaponComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UWeaponComponent::BeginPlay()
{
Super::BeginPlay();
// 기본 무기 자동 장착
if (DefaultWeaponClass)
{
EquipWeapon(DefaultWeaponClass);
}
}
void UWeaponComponent::EquipWeapon(TSubclassOf<AWeaponActor> WeaponClass)
{
if (!WeaponClass || !GetWorld())
{
return;
}
// 기존 무기 해제
UnequipWeapon();
// 새 무기 스폰
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwner();
SpawnParams.Instigator = Cast<APawn>(GetOwner());
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
CurrentWeapon = GetWorld()->SpawnActor<AWeaponActor>(WeaponClass, SpawnParams);
if (CurrentWeapon)
{
// 캐릭터 메시 소켓에 부착
if (ACharacter* OwnerChar = Cast<ACharacter>(GetOwner()))
{
CurrentWeapon->AttachToComponent(
OwnerChar->GetMesh(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
WeaponSocketName
);
}
}
}
void UWeaponComponent::UnequipWeapon()
{
if (CurrentWeapon)
{
CurrentWeapon->Destroy();
CurrentWeapon = nullptr;
}
}
void UWeaponComponent::FireWeapon()
{
if (CurrentWeapon && CurrentWeapon->CanFire())
{
CurrentWeapon->Fire();
}
}

상황권장 방식
IS-A 관계 (예: AEnemy는 ACharacter이다)상속
HAS-A 관계 (예: ACharacter는 무기를 가진다)컴포넌트
기능을 여러 다른 Actor 클래스에서 재사용컴포넌트
런타임에 기능 추가/제거 필요컴포넌트
엔진 기본 기능 확장 (이동, 충돌 등)상속
// 나쁜 예 — 상속으로 기능 조합 시 다이아몬드 문제
class AArmedCharacter : public ACharacter { ... }; // 무기 있음
class AArmoredCharacter : public ACharacter { ... }; // 방어구 있음
// class AArmedArmoredCharacter : public AArmedCharacter, public AArmoredCharacter ???
// 좋은 예 — 컴포넌트로 자유로운 조합
class AMyCharacter : public ACharacter
{
// 필요한 기능을 컴포넌트로 조합
TObjectPtr<UWeaponComponent> WeaponComp; // 무기 기능
TObjectPtr<UArmorComponent> ArmorComp; // 방어구 기능
TObjectPtr<UStatComponent> StatComp; // 스탯 기능
TObjectPtr<UInventoryComponent> InventoryComp; // 인벤토리 기능
};

개념핵심 요약
UActorComponent트랜스폼 없는 순수 기능 컴포넌트 (스탯, 인벤토리)
USceneComponent트랜스폼 있는 컴포넌트, 부착 계층 구성 가능
CreateDefaultSubobject생성자에서만 사용, 컴포넌트를 Actor 기본 구조로 등록
SetupAttachmentSceneComponent 부모-자식 계층 구성
FindComponentByClass런타임 컴포넌트 검색
Composition 패턴상속보다 유연하고 재사용성 높은 기능 분리