UE5 C++ 컴포넌트 기반 설계
개요 — 컴포넌트 기반 설계란
Section titled “개요 — 컴포넌트 기반 설계란”언리얼 엔진에서 **컴포넌트(Component)**는 Actor에 기능을 추가하는 조립 블록입니다. 하나의 Actor가 메시, 충돌, 파티클, 오디오, AI 인식 등의 기능을 각각의 컴포넌트로 조합해 갖추는 것이 컴포넌트 기반 설계(Component-Based Design)입니다.
상속(Inheritance)으로 기능을 추가하면 클래스 계층이 폭발적으로 증가합니다. 반면 컴포넌트(Composition)를 사용하면 기능을 독립적으로 재사용하고 조합할 수 있습니다.
| 비교 | 상속 방식 | 컴포넌트 방식 |
|---|---|---|
| 기능 재사용 | 서브클래싱 필요 | 컴포넌트를 다른 Actor에 붙이면 됨 |
| 유연성 | 컴파일 타임 고정 | 런타임 추가/제거 가능 |
| 복잡도 | 다중 상속 문제 발생 가능 | 명확한 단일 책임 분리 |
| UE 권장 | 최소화 | 핵심 설계 패턴 |
1. ActorComponent vs SceneComponent
Section titled “1. ActorComponent vs SceneComponent”1.1 UActorComponent
Section titled “1.1 UActorComponent”UActorComponent는 트랜스폼(위치/회전/크기)이 없는 순수 기능 컴포넌트입니다. 월드 내 물리적 위치와 무관한 로직, 데이터 관리에 사용합니다.
- 스탯 관리 (체력, 마나, 경험치)
- 인벤토리 관리
- 쿨다운 시스템
- 상태 머신(State Machine)
UObject └─ UActorComponent ← 트랜스폼 없음, 순수 기능 └─ USceneComponent ← 트랜스폼 있음, 계층 구조 지원 └─ UPrimitiveComponent ← 렌더링 + 충돌 └─ UMeshComponent └─ UShapeComponent (캡슐, 구, 박스)1.2 USceneComponent
Section titled “1.2 USceneComponent”USceneComponent는 트랜스폼을 갖는 컴포넌트입니다. 부모-자식 계층 구조(Attachment Hierarchy)를 구성할 수 있으며, 루트 컴포넌트로 사용됩니다.
- 메시 컴포넌트 (시각적 표현)
- 충돌 컴포넌트 (SphereComponent, CapsuleComponent)
- 소켓 기반 부착점 (WeaponSocket, HandSocket)
- 카메라, 스프링암
1.3 선택 기준
Section titled “1.3 선택 기준”// 위치/회전/부착이 필요 없는 기능 → 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로 컴포넌트 추가”2.1 생성자에서 컴포넌트 생성
Section titled “2.1 생성자에서 컴포넌트 생성”컴포넌트를 Actor의 기본 구조로 포함하려면 **생성자(Constructor)**에서 CreateDefaultSubobject<T>()를 사용합니다. 이 함수는 생성자 밖에서 호출하면 안 됩니다.
#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;};#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로 계층 구조 구성”USceneComponent는 SetupAttachment()로 부모 컴포넌트에 부착합니다. 부착 계층은 부모의 트랜스폼 변경 시 자식이 함께 이동하게 만듭니다.
// 무기 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"));}3. 런타임 컴포넌트 참조 방법
Section titled “3. 런타임 컴포넌트 참조 방법”3.1 GetComponentByClass
Section titled “3.1 GetComponentByClass”// 타입으로 컴포넌트 검색 (단일 결과)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>();3.3 Cast를 통한 참조
Section titled “3.3 Cast를 통한 참조”// 다른 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; }}5. 실전 — 스탯 컴포넌트 설계
Section titled “5. 실전 — 스탯 컴포넌트 설계”5.1 UStatComponent 전체 구현
Section titled “5.1 UStatComponent 전체 구현”#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;};#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 파생 실전 예시”#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");};#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(); }}6. 상속 vs Composition 선택 기준
Section titled “6. 상속 vs Composition 선택 기준”| 상황 | 권장 방식 |
|---|---|
| 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 기본 구조로 등록 |
SetupAttachment | SceneComponent 부모-자식 계층 구성 |
FindComponentByClass | 런타임 컴포넌트 검색 |
| Composition 패턴 | 상속보다 유연하고 재사용성 높은 기능 분리 |