UE5 C++ 핵심 개념 입문
개요 — 언리얼 C++이 일반 C++과 다른 이유
섹션 제목: “개요 — 언리얼 C++이 일반 C++과 다른 이유”언리얼 엔진 C++(이하 UE C++)은 표준 C++을 기반으로 하지만, 엔진 고유의 리플렉션 시스템, 가비지 컬렉션, 직렬화, 에디터 통합 레이어가 얹혀 있습니다. 이 때문에 일반 C++ 경험만으로 처음 UE C++ 코드를 보면 낯선 매크로와 접두사들로 인해 당황하기 쉽습니다.
핵심 차이점을 한 줄로 요약하면 이렇습니다.
일반 C++에서는 객체의 생성과 소멸을 개발자가 직접 관리하지만, UE C++에서는
UObject를 상속한 객체는 엔진의 가비지 컬렉터가 관리합니다. 매크로는 엔진에게 객체 정보를 알려주는 “신고서” 역할을 합니다.
이 가이드에서는 UE C++ 입문자가 가장 먼저 이해해야 할 네 가지 축을 다룹니다.
| 축 | 핵심 내용 |
|---|---|
| UObject 시스템 | 모든 UE 객체의 뿌리, 계층 구조 |
| 매크로 시스템 | UCLASS, UPROPERTY, UFUNCTION의 역할 |
| 가비지 컬렉션 | 메모리 안전을 위한 포인터 규칙 |
| AActor 생명주기 | 스폰부터 소멸까지 이벤트 흐름 |
1. UObject 시스템
섹션 제목: “1. UObject 시스템”1.1 UObject란 무엇인가
섹션 제목: “1.1 UObject란 무엇인가”UObject는 언리얼 엔진의 모든 엔진 관리 객체의 기반 클래스입니다. UObject를 상속하면 다음 기능을 자동으로 얻습니다.
- 리플렉션: 런타임에 클래스·프로퍼티·함수 정보 조회 가능
- 직렬화: 에디터 저장/로드, 네트워크 복제에 활용
- 가비지 컬렉션: 참조가 없어지면 엔진이 자동으로 메모리 해제
- 에디터 통합: Blueprint에서 접근, 디테일 패널 노출
UObject를 상속하지 않는 일반 C++ 구조체(예: FVector, FTransform)는 이 기능을 갖지 않으며, GC의 관리 대상도 아닙니다.
1.2 UObject 계층 구조
섹션 제목: “1.2 UObject 계층 구조”언리얼의 주요 클래스들은 모두 UObject에서 파생됩니다. 게임플레이 프로그래밍에서 자주 만나는 계층 구조는 다음과 같습니다.
UObject └─ UActorComponent (컴포넌트 기반 클래스) └─ USceneComponent (트랜스폼을 가진 컴포넌트) └─ AActor (월드에 배치 가능한 객체) └─ APawn (빙의 가능한 Actor) └─ ACharacter (이동 컴포넌트 포함 캐릭터) └─ UGameInstance (세션 전체 지속) └─ USubsystem (엔진 서브시스템)접두사 규칙이 계층을 바로 알려줍니다.
| 접두사 | 의미 | 예시 |
|---|---|---|
U | UObject 파생 클래스 | UActorComponent, UTexture |
A | AActor 파생 클래스 | ACharacter, APlayerController |
F | 일반 C++ 구조체 (UObject 아님) | FVector, FHitResult |
T | 템플릿 클래스 | TArray<T>, TObjectPtr<T> |
I | 인터페이스 클래스 | IAbilitySystemInterface |
E | 열거형 | ECollisionChannel |
1.3 UObject 생성과 파괴
섹션 제목: “1.3 UObject 생성과 파괴”UObject 파생 클래스는 new 연산자 대신 엔진 전용 생성 함수를 사용해야 합니다. new로 생성하면 GC가 해당 객체를 추적하지 않아 메모리 문제가 발생합니다.
// ---- 생성 방법 세 가지 ----
// 1. NewObject<T>() — 런타임에 UObject 생성 (Actor가 아닌 컴포넌트, 에셋 등)UMyComponent* Comp = NewObject<UMyComponent>(this, UMyComponent::StaticClass());
// 2. CreateDefaultSubobject<T>() — 생성자(Constructor) 안에서만 사용// 컴포넌트를 Actor의 기본 서브오브젝트로 등록할 때 사용USkeletalMeshComponent* MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("MeshComp"));
// 3. SpawnActor<T>() — 월드에 AActor를 스폰FActorSpawnParameters SpawnParams;AMyActor* NewActor = GetWorld()->SpawnActor<AMyActor>(AMyActor::StaticClass(), Location, Rotation, SpawnParams);파괴는 직접 호출하지 않습니다. Actor의 경우 Destroy()를 호출하면 다음 GC 사이클에서 메모리가 해제됩니다. 일반 UObject는 참조가 없어지면 GC가 자동 처리합니다.
// Actor 제거 요청 (실제 메모리 해제는 GC가 수행)MyActor->Destroy();
// 절대 하지 말아야 할 것// delete MyActor; // UObject에 delete는 크래시 또는 undefined behavior2. 핵심 매크로 시스템
섹션 제목: “2. 핵심 매크로 시스템”언리얼 헤더 툴(UHT, Unreal Header Tool)은 빌드 시 .h 파일의 매크로를 분석해 리플렉션 코드를 자동 생성합니다. 매크로는 “엔진에게 이 클래스/프로퍼티/함수를 등록해달라”는 선언입니다.
2.1 UCLASS — 클래스 등록
섹션 제목: “2.1 UCLASS — 클래스 등록”UCLASS 매크로는 해당 C++ 클래스를 언리얼 리플렉션 시스템에 등록합니다. 매크로 없이는 엔진이 이 클래스의 존재를 알 수 없습니다.
#pragma once
#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "MyActor.generated.h" // 반드시 마지막 include — UHT 생성 코드 포함
UCLASS(BlueprintType, Blueprintable) // 지정자(Specifier)로 동작 제어class MYGAME_API AMyActor : public AActor{ GENERATED_BODY() // UHT가 채워넣을 자동 생성 코드 위치
public: AMyActor();};주요 UCLASS 지정자:
| 지정자 | 의미 |
|---|---|
BlueprintType | 이 클래스를 Blueprint 변수로 사용 가능 |
Blueprintable | 이 클래스를 Blueprint에서 상속 가능 |
Abstract | 인스턴스 직접 생성 불가 (순수 기반 클래스) |
NotBlueprintable | Blueprint 상속 불가 |
MinimalAPI | 다른 모듈에 최소한의 API만 노출 |
2.2 UPROPERTY — 프로퍼티 리플렉션 및 GC 연동
섹션 제목: “2.2 UPROPERTY — 프로퍼티 리플렉션 및 GC 연동”UPROPERTY는 멤버 변수를 엔진에 등록합니다. 이를 통해 에디터 노출, 직렬화, 네트워크 복제, 그리고 GC 추적이 가능해집니다.
UCLASS()class MYGAME_API AMyActor : public AActor{ GENERATED_BODY()
public: // EditAnywhere: 에디터 모든 위치에서 편집 가능 // BlueprintReadWrite: Blueprint에서 읽기/쓰기 가능 // Category: 디테일 패널 카테고리 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats") float Health = 100.f;
// VisibleAnywhere: 에디터에서 보기만 가능 (수정 불가) // BlueprintReadOnly: Blueprint에서 읽기만 가능 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") TObjectPtr<UStaticMeshComponent> MeshComponent;
// Replicated: 네트워크 복제 대상 UPROPERTY(Replicated, BlueprintReadOnly, Category = "Stats") int32 Score = 0;
// EditDefaultsOnly: Blueprint 클래스 기본값에서만 편집 (인스턴스 편집 불가) // TSubclassOf<T>: 특정 클래스 또는 그 서브클래스만 지정 가능 UPROPERTY(EditDefaultsOnly, Category = "Config") TSubclassOf<AActor> ProjectileClass;};주요 UPROPERTY 접근 지정자:
| 지정자 | 에디터 접근 | Blueprint 접근 |
|---|---|---|
EditAnywhere | 기본값 + 인스턴스 모두 편집 | — |
EditDefaultsOnly | 기본값만 편집 | — |
EditInstanceOnly | 인스턴스만 편집 | — |
VisibleAnywhere | 보기만 | — |
BlueprintReadWrite | — | 읽기 + 쓰기 |
BlueprintReadOnly | — | 읽기만 |
중요:
UPROPERTY가 없는UObject*원시 포인터는 GC가 추적하지 않습니다. 해당 포인터가 참조하는 객체가 GC에 의해 수집되면 포인터는 **댕글링 포인터(dangling pointer)**가 됩니다.
2.3 UFUNCTION — 함수 리플렉션 및 Blueprint 노출
섹션 제목: “2.3 UFUNCTION — 함수 리플렉션 및 Blueprint 노출”UFUNCTION은 멤버 함수를 엔진에 등록합니다. Blueprint 호출, 네트워크 RPC, 이벤트 바인딩 등에 필요합니다.
UCLASS()class MYGAME_API AMyActor : public AActor{ GENERATED_BODY()
public: // BlueprintCallable: Blueprint에서 호출 가능한 노드로 노출 UFUNCTION(BlueprintCallable, Category = "Combat") void TakeDamage_Custom(float DamageAmount);
// BlueprintPure: 실행 핀 없는 순수 함수 (값 반환만) UFUNCTION(BlueprintPure, Category = "Stats") float GetHealthPercent() const;
// BlueprintImplementableEvent: C++에서 선언, Blueprint에서 구현 UFUNCTION(BlueprintImplementableEvent, Category = "Events") void OnHealthChanged(float NewHealth);
// BlueprintNativeEvent: C++에 기본 구현 있음, Blueprint에서 오버라이드 가능 // 구현부는 함수명_Implementation으로 정의 UFUNCTION(BlueprintNativeEvent, Category = "Events") void OnDeath(); virtual void OnDeath_Implementation();
// Server: 클라이언트에서 호출 → 서버에서 실행되는 RPC // Reliable: 패킷 유실 없이 보장 UFUNCTION(Server, Reliable, WithValidation, Category = "Network") void ServerRequestJump();
// NetMulticast: 서버에서 호출 → 모든 클라이언트에서 실행 UFUNCTION(NetMulticast, Unreliable, Category = "Network") void MulticastPlayEffect();};2.4 GENERATED_BODY — 자동 생성 코드 자리표시자
섹션 제목: “2.4 GENERATED_BODY — 자동 생성 코드 자리표시자”GENERATED_BODY()는 UHT가 리플렉션 코드를 삽입할 위치를 지정합니다. UCLASS, USTRUCT, UENUM 매크로를 사용하는 모든 타입의 선언부에 반드시 포함해야 합니다.
UCLASS()class AMyActor : public AActor{ GENERATED_BODY() // 이 자리에 UHT가 생성자, 리플렉션 테이블 등을 자동 삽입 // ...};
USTRUCT(BlueprintType)struct FMyData{ GENERATED_BODY() // 구조체도 동일하게 필요
UPROPERTY(EditAnywhere) FString Name;
UPROPERTY(EditAnywhere) int32 Value = 0;};3. 가비지 컬렉션 (Garbage Collection)
섹션 제목: “3. 가비지 컬렉션 (Garbage Collection)”3.1 UE5 GC 동작 원리
섹션 제목: “3.1 UE5 GC 동작 원리”언리얼의 GC는 표시-정리(Mark-and-Sweep) 방식으로 동작합니다.
- 루트셋(Root Set) 탐색:
GUObjectArray에 등록된 모든UObject를 순회합니다. - 도달성 분석: 루트셋에서 참조를 따라가며 도달 가능한 객체에 마킹합니다.
- 정리: 마킹되지 않은(참조 없는) 객체의 메모리를 해제합니다.
GC는 기본적으로 60초마다 실행되며, 프레임 사이에 분산 처리(Incremental GC)됩니다. 개발자는 ForceGarbageCollection(true)로 즉시 실행을 요청할 수 있지만 일반 게임플레이 코드에서는 불필요합니다.
3.2 UPROPERTY가 GC에서 중요한 이유
섹션 제목: “3.2 UPROPERTY가 GC에서 중요한 이유”GC가 참조를 추적하는 유일한 방법은 UPROPERTY로 등록된 포인터를 통해서입니다.
UCLASS()class AMyActor : public AActor{ GENERATED_BODY()
// 올바른 방식: UPROPERTY로 등록 → GC가 추적 → 안전 UPROPERTY() TObjectPtr<UStaticMeshComponent> SafeComponent;
// 위험한 방식: UPROPERTY 없음 → GC가 추적 불가 → 댕글링 포인터 위험 UStaticMeshComponent* DangerousComponent; // 하지 마세요};UPROPERTY가 없는 원시 포인터에 할당된 UObject가 다른 곳에서 참조가 끊기면, GC가 해당 객체를 수집할 수 있습니다. 이후 해당 포인터를 역참조하면 크래시가 발생합니다.
3.3 안전한 포인터 타입
섹션 제목: “3.3 안전한 포인터 타입”UE5는 UObject 참조를 위한 전용 포인터 타입을 제공합니다.
// TObjectPtr<T> — UE5에서 TObjectPtr<T>이 UObject 멤버 포인터의 표준// UPROPERTY와 함께 사용하면 GC 추적 + 에디터 직렬화 모두 지원UPROPERTY(VisibleAnywhere)TObjectPtr<UStaticMeshComponent> MeshComp;
// TWeakObjectPtr<T> — 약한 참조 (GC 수집을 막지 않음)// 참조 대상이 삭제될 수 있는 경우 사용 (캐시, 관찰자 패턴)TWeakObjectPtr<AActor> TargetActor;
void CheckTarget(){ // IsValid() / Get()으로 안전하게 접근 if (TargetActor.IsValid()) { TargetActor->DoSomething(); }}
// TStrongObjectPtr<T> — 명시적으로 GC 수집 방지// 테스트 코드나 특수 케이스에서만 사용TStrongObjectPtr<UMyObject> PinnedObject;비 UObject 타입(일반 C++ 클래스)을 힙에 할당해야 한다면 표준 스마트 포인터를 사용합니다.
// TSharedPtr<T> — 공유 소유권 (레퍼런스 카운팅)TSharedPtr<FMyStruct> SharedData = MakeShared<FMyStruct>();
// TUniquePtr<T> — 단독 소유권TUniquePtr<FMyStruct> UniqueData = MakeUnique<FMyStruct>();
// TSharedRef<T> — null이 될 수 없는 TSharedPtrTSharedRef<FMyStruct> RefData = MakeShared<FMyStruct>();3.4 흔한 실수와 메모리 안전 패턴
섹션 제목: “3.4 흔한 실수와 메모리 안전 패턴”// 실수 1: UPROPERTY 없는 원시 포인터 멤버// 해결: 항상 UPROPERTY + TObjectPtr 사용UPROPERTY()TObjectPtr<UMyComponent> MyComp; // 올바름
// 실수 2: 지역 변수에 NewObject로 생성 후 저장하지 않음void BadFunction(){ UMyObject* TempObj = NewObject<UMyObject>(this); // TempObj를 UPROPERTY 멤버에 저장하지 않으면 다음 GC에서 수집됨}
// 해결: UPROPERTY 멤버에 저장void GoodFunction(){ MyStoredObject = NewObject<UMyObject>(this); // UPROPERTY 멤버에 저장}
// 실수 3: IsValid() 없이 포인터 역참조void BadDereference(){ TargetActor->DoSomething(); // TargetActor가 Destroy() 호출 후면 크래시}
// 해결: IsValid() 또는 IsValidLowLevel() 체크void SafeDereference(){ if (IsValid(TargetActor)) { TargetActor->DoSomething(); }}팁:
IsValid(Obj)는 포인터가 null인지, Pending Kill(소멸 대기)인지 모두 체크합니다. 단순Obj != nullptr는 Pending Kill 상태를 잡지 못합니다.
4. AActor 생명주기
섹션 제목: “4. AActor 생명주기”4.1 생명주기 단계 전체 흐름
섹션 제목: “4.1 생명주기 단계 전체 흐름”AActor는 월드에 스폰되는 순간부터 소멸될 때까지 정해진 순서로 이벤트 함수가 호출됩니다. 이 순서를 모르면 초기화 코드를 잘못된 타이밍에 넣어 버그를 만들기 쉽습니다.
[스폰/로드] 1. 생성자(Constructor) — 기본값 설정, 서브오브젝트 생성 2. PostInitProperties() — 생성자 이후 프로퍼티 초기화 완료 3. PostLoad() (로드된 경우) — 에셋/레벨에서 로드 시 호출
[초기화] 4. PostActorCreated() — 에디터/게임에서 새로 생성 시 5. PostInitializeComponents() — 모든 컴포넌트 InitializeComponent() 완료 후
[플레이 시작] 6. BeginPlay() — 게임플레이 로직 시작 (가장 많이 사용) 7. Tick(float DeltaSeconds) — 매 프레임 호출 (활성화된 경우)
[소멸] 8. EndPlay(EEndPlayReason) — BeginPlay의 쌍대, 정리 코드 작성 9. BeginDestroy() — GC 수집 직전 (직접 오버라이드는 드묾) 10. IsReadyForFinishDestroy() — 비동기 소멸 대기 여부 11. FinishDestroy() — 메모리 해제 직전 마지막 콜백4.2 핵심 이벤트 구현 예시
섹션 제목: “4.2 핵심 이벤트 구현 예시”#pragma once
#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "MyActor.generated.h"
UCLASS()class MYGAME_API AMyActor : public AActor{ GENERATED_BODY()
public: AMyActor();
protected: // 컴포넌트 초기화 완료 후 — 컴포넌트 간 의존성 설정에 적합 virtual void PostInitializeComponents() override;
// 게임플레이 로직 시작 지점 — 대부분의 초기화 코드는 여기에 virtual void BeginPlay() override;
// 매 프레임 호출 virtual void Tick(float DeltaTime) override;
// 소멸 또는 레벨 언로드 시 — 리소스 정리, 이벤트 언바인드 virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) TObjectPtr<UStaticMeshComponent> MeshComponent;
UPROPERTY(EditDefaultsOnly, Category = "Config") float RotationSpeed = 45.f;
// 타이머 핸들 — 타이머 취소에 필요 FTimerHandle EffectTimerHandle;};#include "MyActor.h"#include "Components/StaticMeshComponent.h"
AMyActor::AMyActor(){ // Tick 활성화 여부 — 불필요하면 false로 성능 절약 PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.TickInterval = 0.f; // 0 = 매 프레임, 0.1 = 초당 10회
// 생성자에서 컴포넌트 생성 — CreateDefaultSubobject는 생성자에서만 유효 MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent")); SetRootComponent(MeshComponent);}
void AMyActor::PostInitializeComponents(){ Super::PostInitializeComponents();
// 컴포넌트가 모두 초기화된 이후 시점 // 예: 컴포넌트의 이벤트에 델리게이트 바인딩 if (MeshComponent) { MeshComponent->OnComponentHit.AddDynamic(this, &AMyActor::OnHit); }}
void AMyActor::BeginPlay(){ Super::BeginPlay(); // 부모 구현 호출 필수
// 게임 시작 시 1회 초기화 로직 UE_LOG(LogTemp, Log, TEXT("AMyActor BeginPlay: %s"), *GetName());
// 타이머 예시: 3초 후 Effect 호출 GetWorldTimerManager().SetTimer( EffectTimerHandle, this, &AMyActor::PlayEffect, 3.f, false // 반복 여부 );}
void AMyActor::Tick(float DeltaTime){ Super::Tick(DeltaTime);
// 매 프레임 회전 AddActorLocalRotation(FRotator(0.f, RotationSpeed * DeltaTime, 0.f));}
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason){ // EndPlayReason으로 소멸 원인 파악 가능 switch (EndPlayReason) { case EEndPlayReason::Destroyed: UE_LOG(LogTemp, Log, TEXT("Actor destroyed: %s"), *GetName()); break; case EEndPlayReason::LevelTransition: UE_LOG(LogTemp, Log, TEXT("Level transition: %s"), *GetName()); break; default: break; }
// 타이머 정리 GetWorldTimerManager().ClearTimer(EffectTimerHandle);
// 부모 구현 호출 — 반드시 마지막에 Super::EndPlay(EndPlayReason);}
void AMyActor::PlayEffect(){ // 타이머 콜백 예시 UE_LOG(LogTemp, Log, TEXT("Effect played!"));}
void AMyActor::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit){ if (OtherActor && OtherActor != this) { UE_LOG(LogTemp, Log, TEXT("Hit by: %s"), *OtherActor->GetName()); }}4.3 PrimaryActorTick 설정과 성능 고려
섹션 제목: “4.3 PrimaryActorTick 설정과 성능 고려”Tick은 강력하지만 비용이 높습니다. 불필요한 Tick은 성능에 악영향을 줍니다.
AMyActor::AMyActor(){ // 기본: Tick 비활성화 (필요할 때만 켜는 것이 권장) PrimaryActorTick.bCanEverTick = false;}
// 런타임에 Tick 동적 활성화/비활성화void AMyActor::EnableTick(bool bEnable){ SetActorTickEnabled(bEnable);}
// Tick 간격 조정 — 모든 오브젝트가 매 프레임 업데이트될 필요는 없음// 예: AI 판단 로직은 0.1~0.5초 간격으로도 충분PrimaryActorTick.TickInterval = 0.1f; // 초당 10회Tick 대신 타이머를 쓸 수 있는 경우: 일정 주기로 한 번씩 실행되는 로직은 SetTimer가 Tick + 카운터보다 효율적입니다.
5. 실습 예시 — 미니 아이템 컴포넌트 구현
섹션 제목: “5. 실습 예시 — 미니 아이템 컴포넌트 구현”앞서 배운 내용을 종합한 간단한 아이템 Actor를 만들어 봅니다. 플레이어가 가까이 오면 자동으로 획득되고, 일정 시간 후 리스폰되는 아이템입니다.
#pragma once
#include "CoreMinimal.h"#include "GameFramework/Actor.h"#include "PickupItem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnItemPickedUp, APickupItem*, Item);
UCLASS(BlueprintType, Blueprintable)class MYGAME_API APickupItem : public AActor{ GENERATED_BODY()
public: APickupItem();
// Blueprint에서 구독 가능한 이벤트 UPROPERTY(BlueprintAssignable, Category = "Pickup") FOnItemPickedUp OnItemPickedUp;
// Blueprint에서 호출 가능 UFUNCTION(BlueprintCallable, Category = "Pickup") void PickUp(AActor* Picker);
protected: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) TObjectPtr<UStaticMeshComponent> MeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) TObjectPtr<class USphereComponent> OverlapComp;
// 아이템 이름 — 에디터에서 설정 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item", meta = (AllowPrivateAccess = "true")) FText ItemName;
// 리스폰 대기 시간 (초) UPROPERTY(EditDefaultsOnly, Category = "Item") float RespawnDelay = 5.f;
// 이미 획득된 상태인지 bool bIsPickedUp = false;
FTimerHandle RespawnTimerHandle;
UFUNCTION() void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
void Respawn();};#include "PickupItem.h"#include "Components/SphereComponent.h"#include "Components/StaticMeshComponent.h"
APickupItem::APickupItem(){ PrimaryActorTick.bCanEverTick = false; // Tick 불필요
// 루트: 충돌 구체 OverlapComp = CreateDefaultSubobject<USphereComponent>(TEXT("OverlapComp")); OverlapComp->SetSphereRadius(100.f); OverlapComp->SetCollisionProfileName(TEXT("Trigger")); SetRootComponent(OverlapComp);
// 비주얼 메시 (루트에 부착) MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp")); MeshComp->SetupAttachment(OverlapComp); MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);}
void APickupItem::BeginPlay(){ Super::BeginPlay();
// 오버랩 이벤트 바인딩 OverlapComp->OnComponentBeginOverlap.AddDynamic(this, &APickupItem::OnOverlapBegin);}
void APickupItem::EndPlay(const EEndPlayReason::Type EndPlayReason){ GetWorldTimerManager().ClearTimer(RespawnTimerHandle); Super::EndPlay(EndPlayReason);}
void APickupItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult){ if (bIsPickedUp || !OtherActor) { return; }
// 플레이어인지 확인 (프로젝트에 맞게 캐스트 타입 변경) if (OtherActor->ActorHasTag(FName("Player"))) { PickUp(OtherActor); }}
void APickupItem::PickUp(AActor* Picker){ if (bIsPickedUp) { return; }
bIsPickedUp = true;
UE_LOG(LogTemp, Log, TEXT("%s picked up %s"), *Picker->GetName(), *ItemName.ToString());
// Blueprint 및 C++ 구독자에게 이벤트 전파 OnItemPickedUp.Broadcast(this);
// 비주얼 숨기기, 충돌 비활성화 SetActorHiddenInGame(true); SetActorEnableCollision(false);
// 리스폰 타이머 GetWorldTimerManager().SetTimer( RespawnTimerHandle, this, &APickupItem::Respawn, RespawnDelay, false );}
void APickupItem::Respawn(){ bIsPickedUp = false; SetActorHiddenInGame(false); SetActorEnableCollision(true);
UE_LOG(LogTemp, Log, TEXT("%s respawned"), *ItemName.ToString());}6. 정리 및 다음 단계
섹션 제목: “6. 정리 및 다음 단계”이 가이드에서 다룬 UE C++ 입문 핵심 4가지를 정리합니다.
| 개념 | 핵심 요약 |
|---|---|
| UObject 시스템 | 모든 UE 관리 객체의 기반. NewObject, CreateDefaultSubobject, SpawnActor로 생성 |
| 매크로 시스템 | UCLASS·UPROPERTY·UFUNCTION은 엔진 리플렉션 등록용 “신고서” |
| 가비지 컬렉션 | UPROPERTY 없는 원시 포인터는 위험. TObjectPtr/TWeakObjectPtr 사용 |
| AActor 생명주기 | 생성자 → BeginPlay → Tick → EndPlay 순서를 지켜 코드를 배치 |
다음 단계로 학습할 주제
섹션 제목: “다음 단계로 학습할 주제”- UActorComponent vs USceneComponent: 컴포넌트 설계 패턴과 부착(Attach) 계층 구조
- 델리게이트(Delegate):
DECLARE_DYNAMIC_MULTICAST_DELEGATE를 활용한 이벤트 시스템 - 네트워크 복제(Replication):
Replicated,RepNotify, RPC 기초 - GameplayAbilitySystem(GAS): 스킬·버프 시스템 구현 프레임워크
- Enhanced Input System: 입력 액션과 매핑 컨텍스트 C++ 연동
6.5 런타임 리플렉션 — TFieldIterator & ProcessEvent
섹션 제목: “6.5 런타임 리플렉션 — TFieldIterator & ProcessEvent”리플렉션 시스템을 활용하면 컴파일 타임에 알 수 없는 프로퍼티나 함수를 런타임에 동적으로 조회하고 조작할 수 있습니다. 에디터 도구, 자동화 테스트, 범용 직렬화 레이어에서 주로 사용합니다.
// 클래스의 모든 float UPROPERTY를 순회하며 출력void PrintFloatProperties(UObject* Obj){ if (!IsValid(Obj)) return;
for (TFieldIterator<FFloatProperty> It(Obj->GetClass()); It; ++It) { FFloatProperty* Prop = *It; float Value = Prop->GetPropertyValue_InContainer(Obj); UE_LOG(LogTemp, Log, TEXT("%s = %.2f"), *Prop->GetName(), Value); }}
// 이름으로 특정 float 프로퍼티를 찾아 값 변경void SetFloatPropertyByName(UObject* Obj, FName PropName, float NewValue){ FProperty* Prop = Obj->GetClass()->FindPropertyByName(PropName); if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop)) { FloatProp->SetPropertyValue_InContainer(Obj, NewValue); }}ProcessEvent — 함수 동적 호출
섹션 제목: “ProcessEvent — 함수 동적 호출”UFunction을 이름으로 찾아 ProcessEvent로 호출하면 파생 클래스 여부나 Blueprint 오버라이드 여부와 무관하게 함수를 실행할 수 있습니다.
// 파라미터 없는 함수 동적 호출void CallFunctionByName(UObject* Obj, FName FuncName){ UFunction* Func = Obj->FindFunction(FuncName); if (Func) { Obj->ProcessEvent(Func, nullptr); }}
// 파라미터가 있는 경우 — 파라미터 구조체로 패킹struct FDamageParams { float Amount = 0.f; };
void ApplyDamageByReflection(UObject* Target, float Damage){ UFunction* Func = Target->FindFunction(TEXT("TakeDamage_Custom")); if (Func) { FDamageParams Params; Params.Amount = Damage; Target->ProcessEvent(Func, &Params); }}주의: 리플렉션 기반 코드는 타입 안전성이 낮고 이름 오타가 런타임 오류로 이어집니다. 일반 게임플레이 코드에서는 직접 호출을 사용하고, 리플렉션은 에디터 유틸리티·범용 프레임워크에 한정하세요.