포스트

TIL 2026-04-17

TIL 2026-04-17

2026-04-17 언리얼 컨테이너와 래핑 포인터

목차


좋은 코드에 대한 이야기

긴 매개변수 목록 (Long Parameter List)

  • 매개변수가 많으면 함수를 이해하고 쓰기가 너무 불편함
  • 필요한 정보만 간결하게 전달할 수 있도록 묶거나 축소하자
  • 중복된 정보가 있는지 확인하고, 불필요한 인수는 제거

나쁜 예시

1
2
3
4
5
6
7
void InitWeapon(FString Name, float Damage, float FireRate, int32 AmmoCount,
                float ReloadTime, USkeletalMesh* Mesh, USoundBase* Sound)
{
    // 와, 많다...
}

InitWeapon("AK47", 42.0f, 0.25f, 30, 2.5f, MeshAsset, FireSound);

좋은 예시 — 구조체로 묶기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct FWeaponData
{
    FString Name;
    float Damage;
    float FireRate;
    int32 AmmoCount;
};

struct FWeaponAssets
{
    USkeletalMesh* Mesh;
    USoundBase* Sound;
};

void InitWeapon(const FWeaponData& InData, const FWeaponAssets& InAssets)
{
    // 훨씬 깔끔!
}

전역 데이터의 남용 (Global Data)

  • 프로그램의 악취 중 가장 독한 악취 중 하나
  • 어디서든 접근 가능해 디버깅과 유지보수가 복잡해짐
  • 값이 바뀔 때 추적이 어려워 에러가 숨어듦
  • 데이터 범위를 최소화하고, 꼭 필요한 곳에서만 사용하도록 통제

나쁜 예시

1
2
3
4
5
6
UGameManager* GGameManager;  // 전역 변수!

void IncreaseScore()
{
    GGameManager->Score += 10;  // 아무데서나 수정 가능
}

좋은 예시 — 언리얼 Subsystem 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
UCLASS()
class UScoreSystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

private:
    int32 Score;

public:
    void AddScore(int32 Amount)
    {
        Score += Amount;
        OnScoreChanged.Broadcast(Score);  // 변경 이벤트 브로드캐스트
    }
    int32 GetScore() const { return Score; }
};

// 사용
if (UGameInstance* GI = GetGameInstance())
{
    if (UScoreSystem* ScoreSys = GI->GetSubsystem<UScoreSystem>())
    {
        ScoreSys->AddScore(50);
    }
}

Subsystem의 장점

구분전역 변수Subsystem
접근 경로 추적어디서든 수정 가능 — 범인 찾기 어려움GetSubsystem<T>() 호출부만 검색하면 됨
캡슐화데이터 그대로 노출Setter/Getter/Delegate로 변화 감시 가능
생명 주기수동 초기화 안 하면 이전 판 데이터 남음맵/게임 종료 시 엔진이 파괴·재생성 → 데이터 오염 원천 차단

언리얼 Subsystem 공식 문서


가변 데이터 (Mutable Data)

  • 값이 자주 바뀌면 예기치 못한 오류나 복잡도 증가
  • 변경 가능한 범위를 최소화하고, 가급적 불변 데이터를 활용
  • 수정이 필요한 부분을 명확히 나누는 습관

나쁜 예시 — public 멤버로 공공재화

1
2
3
4
5
6
7
8
9
10
11
12
class APlayerCharacter : public ACharacter
{
public:
    float Health;  // 마음대로 바꿀 수 있는 공공재(!)
    int32 Level;
};

void SomeRandomFunc(APlayerCharacter* Player)
{
    Player->Health = 99999.f;  // 이걸 발견하면 팀원들 열받음
    Player->Level = 999;
}

좋은 예시 — 캡슐화 + 수정 API 제한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class APlayerCharacter : public ACharacter
{
private:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Stats")
    float Health;
    int32 Level;

public:
    float GetHealth() const { return Health; }
    int32 GetLevel() const { return Level; }

    void TakeDamage(float Amount)
    {
        Health = FMath::Max(0.0f, Health - Amount);
    }

    void LevelUp()
    {
        Level++;
        Health = 100.f * Level;
    }
};





TObjectPtr

UE4까지 쓰던 UObject* (원시 포인터)를 대체하기 위해 UE5에서 도입된 템플릿 기반 포인터. 지연 로딩(Lazy Loading)액세스 트래킹 기능을 제공.

지연 로딩 (Lazy Loading)

에디터에서 에셋을 불러올 때 모든 데이터를 한꺼번에 메모리에 올리면 무거워짐. → TObjectPtr변수에 담긴 에셋을 진짜 필요할 때 로드하게 돕는 스마트 핸들.

“필요할 때 로드된다” = 실제 코드가 접근하는 순간 로드된다는 뜻.

1
2
3
4
5
6
7
8
// 1. 내부 멤버 함수를 호출할 때 (가장 흔한 케이스)
MyMesh->GetName();

// 2. 다른 변수에 대입하려고 할 때
UStaticMesh* RawMesh = MyMesh;

// 3. 조건문에서 검사할 때
if (MyMesh != nullptr) { ... }

→ 지역 변수나 잠깐 쓰이는 매개변수는 이미 메모리에 올라온 객체를 잠깐 가리키는 경우가 대부분이라 지연 로딩이 필요 없음 → 원시 포인터가 더 효율적.

액세스 트래킹

  • 언리얼 GC는 UPROPERTY로 선언된 변수들을 추적해 사용 중인 객체를 판단
  • TObjectPtr은 여기에 더해 “이 객체가 어디서 참조되고 있는지” 를 더 정밀하게 추적
  • 지역 변수는 GC 추적 대상이 아니므로 (함수 끝나면 사라짐) 이 기능이 불필요

정리

상황권장
헤더에서 UPROPERTY() 붙인 멤버 변수TObjectPtr<T>
지역 변수, 매개변수원시 포인터 T*

주의할 점

  • 성능 향상 도구가 아니다. 관리 효율을 높이기 위한 툴
  • 최종 패키징 시 자동으로 원시 포인터로 변환 → 런타임 성능 저하 없음
  • 단, 패키징 후에는 지연 로딩·액세스 트래킹도 사라짐 (에디터/개발자용 도구)
  • 실제 최적화는 레벨 스트리밍, 소프트 포인터로 진행
1
2
3
4
5
6
7
// 기존 UE4 방식
UPROPERTY(EditAnywhere)
USceneComponent* RootComponent;

// 권장되는 UE5 방식
UPROPERTY(EditAnywhere)
TObjectPtr<USceneComponent> RootComponent;

반복문 주의 — auto* 대신 auto&

1
2
3
4
5
6
7
8
UPROPERTY(EditAnywhere, Category = "Test")
TArray<TObjectPtr<USceneComponent>> Components;

// ❌ 기존 (비효율적)
for (auto* Component : Components) { ... }

// ✅ 변경 권장 (캐싱 효율성)
for (auto& Component : Components) { ... }

auto&를 써야 하나?

  • 포인터 vs 레퍼런스: 포인터는 주소를 복사해오는 것, 레퍼런스는 원본을 그대로 가져다 씀
  • auto*로 꺼내면 TObjectPtr이 매번 “로드됐나 확인 → 주소 계산 → 반환”이라는 내부 로직을 실행
  • auto&로 꺼내면 배열 안에 들어있는 TObjectPtr 자체를 가리키므로 내부 로직이 한 번 더 돌지 않음





TSubclassOf

클래스를 담는 바구니. 원래 UClass*로 쓰이던 것을 대체.

  • TObjectPtr → 레벨에 배치된 인스턴스를 담는 용도
  • TSubclassOf → 컨텐츠 브라우저에 있는 Class를 담는 용도 (SpawnActor 등)

장점 — 필터링

1
2
3
4
5
6
7
// ❌ 필터링 안 됨 — 에디터에서 모든 클래스 노출, 잘못된 클래스 고를 위험
UPROPERTY(EditAnywhere, Category = "Test")
UClass* MyClass;

// ✅ 필터링 됨 — UActorComponent 파생 클래스만 선택 가능
UPROPERTY(EditAnywhere, Category = "Test")
TSubclassOf<UActorComponent> MyClass2;

성능 / 안전성

  • 빌드 타임: 말도 안 되는 클래스를 넣으면 빌드 자체가 실패
  • 런타임: 잘못된 클래스가 들어오면 nullptr 처리
  • 코드에서 직접 클래스를 꽂을 때는 AActor::StaticClass() 형태로 사용





컨테이너 비교 — TArray / TSet / TMap

구분TArray (Vector 기반)TSet / TMap (Hash 기반)
접근 (Access)O(1) — 인덱스로 즉시 이동 (최강)O(1) — 해시 계산 후 즉시 이동
탐색 (Search)O(n) — 처음부터 다 뒤짐O(1) — 키/값으로 바로 점프 (최강)
삽입 (Insert)O(n) — 뒤에 붙이기는 빠름, 중간은 밀어야 함O(1) — 빈 칸 찾아 바로 넣음
삭제 (Remove)O(n) — 지우고 빈틈 메워야 함O(1) — 해당 칸만 딱 지움
특징순회·접근 빠름메모리에 빈틈 있음 / 중복 방지

핵심 개념

TArray: 연속적인 메모리에 따닥따닥 붙어있음 → 인덱스 접근 빠름. 단, 중간 삭제/삽입 시 뒤에 있는 메모리를 밀거나 당겨야 함.

TSet: 연속적이지 않음. 중간 삭제 시 구멍이 뚫린 채로 유지되고, 새 값을 추가하면 그 구멍에 들어감 → 순서 보장 안 됨 → 인덱스 접근 불가.

해시 테이블 원리: 사물함 [0][1][2][3][4][5]가 있을 때, 그냥 찾으면 전부 열어봐야 함. 해시는 Key를 가지고 특정 사물함 번호로 바로 이동. 중간에 보안 정책(해시 함수)을 거쳐 Key로만 해당 값을 찾을 수 있게 꼬아서 저장.






TArray

T는 템플릿 약자 — 어떤 유형이 들어가도 호환.

주요 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TArray<int32> IntArray;

IntArray.Init(10, 5);        // 10을 5개 채움
IntArray.Add(5);             // 임시 공간에 복사해 삽입
IntArray.Emplace(6);         // 복사 없이 내부에서 바로 생성
IntArray.AddUnique(6);       // 중복 없을 때만 추가 (내부 순회 → 느림)
IntArray.Insert(500, 3);     // 인덱스 3에 500 삽입
IntArray.Num();              // 요소 개수

IntArray.Find();           // 인덱스 반환 (없으면 INDEX_NONE)
IntArray.Contains();       // true/false
IntArray[5];                 // 인덱스 접근

IntArray.Remove(10);         // 값이 10인 요소 전부 삭제
IntArray.RemoveSingle(10);   // 첫 번째 10만 삭제
IntArray.RemoveAt(Index);    // 인덱스 기준 삭제
IntArray.IsValidIndex(Idx);  // 유효 인덱스 확인
IntArray.RemoveAll([](int32 v){ return v == 5; });  // 조건부 삭제
IntArray.Empty();            // 전부 삭제
비교AddEmplace
언제일반적인 경우성능을 쥐어짜야 할 때
특징복사 → 명확함내부 생성 → 암시적 변환 개입 여지

AddUnique는 내부에서 배열을 순회하며 중복을 확인하므로 성능이 나쁨. 이럴 땐 TSet을 써라.

정렬 — 람다

1
2
IntArray.Sort();                                        // 오름차순
IntArray.Sort([](int32 a, int32 b){ return a > b; });   // 내림차순

캡처 문법: [&] 참조 / [=] 복사 / [] 캡처 안 함

필터링

1
2
TArray<int32> Filtered =
    IntArray.FilterByPredicate([](const int32 v){ return v < 9; });





TSet

주요 메서드

1
2
3
4
5
6
7
8
9
10
TSet<int32> Numbers;

Numbers.Add(100);             // 추가 (중복 무시)
Numbers.Contains(20);         // true/false

int32* FoundPtr = Numbers.Find(20);  // 인덱스 없음 → 포인터 반환

Numbers.Remove(100);          // 삭제
Numbers.Compact();            // 구멍을 뒤로 몰아 빈 공간 모음
Numbers.Shrink();             // 빈 공간 OS에 반납

순회 — Iterator

메모리가 [10][구멍][30][40] 처럼 비어있을 수 있어 Iterator로 순회.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 읽기 전용
for (TSet<int32>::TConstIterator It = Numbers.CreateConstIterator(); It; ++It)
{
    int32 Value = *It;
}

// 순회 중 삭제
for (TSet<int32>::TIterator It = Numbers.CreateIterator(); It; ++It)
{
    if (*It < 60)
    {
        It.RemoveCurrent();  // 안전하게 제거
    }
}

타입이 길면 auto를 써도 됨. 단 확실한 경우 외엔 지양 (언리얼 공식 권장 사항).

집합 연산

1
2
3
4
5
TSet<int32> A = { 1, 2, 3 };
TSet<int32> B = { 3, 4, 5 };

A.Intersect(B);  // 교집합: {3}
A.Union(B);      // 합집합: {1,2,3,4,5}





TMap

Key-Value 쌍. 사물함 구조 — Key만 있으면 내용물을 매우 빠르게 찾음.

주요 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
TMap<int32, FString> ItemMap;

ItemMap.Add(101, TEXT("Sword"));       // 추가 (중복 키는 덮어쓰기)
ItemMap.Emplace(103, TEXT("Potion"));  // 복사 없이 생성

ItemMap.Contains(101);                 // true/false
ItemMap.Find(101);                     // FString* 반환 (없으면 nullptr)
ItemMap.FindOrAdd(104);                // 없으면 만들어서라도 반환 (FString&)
ItemMap[105];                          // ⚠️ 키 없으면 크래시 → Contains 체크 필수

ItemMap.Remove(102);                   // 키 기준 삭제
ItemMap.Compact();                     // 구멍 뒤로 몰기
ItemMap.Shrink();                      // 빈 공간 반납

순회 — TPair vs Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 일반 순회 (읽기)
for (const TPair<int32, FString>& Pair : ItemMap)
{
    int32 Key = Pair.Key;
    FString Value = Pair.Value;
}

// Iterator 순회 (삭제 가능)
for (TMap<int32, FString>::TIterator It = ItemMap.CreateIterator(); It; ++It)
{
    if (It.Key() == 103)
    {
        It.RemoveCurrent();
    }
}

Iterator를 쓰는 이유?삭제 시 안전성. 그냥 순회하면서 지우면 길을 잃을 수 있음. Iterator는 가이드라인이 있는 안전한 경로.






필수 과제 — 인벤토리 시스템

오늘 배운 컨테이너를 조합해 가방 시스템 구현.

요구사항사용 컨테이너
가방TArray
아이템 정보 조회 (Key → Info)TMap
칭호 획득TSet
칭호 보유 시에만 아이템 사용 가능TSet::Contains()

도전 과제

  • UI(UMG)를 통해 비주얼로 확인





7번 과제 — Pawn 3D 캐릭터 (도전 구현)

구현계획 재정리

기존 “필수 → 도전” 분리 구조를 도전 우선 통합 플랜으로 재작성.

  • 필수과제에서 버릴 것
    • SpringArm Pitch 적용
    • CurrentPitch + Clamp(-80, 80)
    • 평면 이동 제약
    • 중력 무시


C++ 구현

STEP메서드내용
1ANBC_AssignmentPawn()4 컴포넌트 + SM_Drone 로드 + AutoPossessPlayer = Player0
2BeginPlay()IMC 등록 (GetControllerGetLocalPlayer → Subsystem → AddMappingContext)
3SetupPlayerInputComponent()Move / MoveUp / Roll / Look 4종 BindAction
4Look()AddActorLocalRotation(FRotator(V.Y, V.X, 0)) — 6DOF, Clamp 없음
5Move()(Forward*V.X + Right*V.Y) * MoveSpeed * DeltaTime + AddActorLocalOffset(Δ, true)
6aMoveUp()GetActorUpVector() 기반 AddActorLocalOffset
6bRoll()AddActorLocalRotation(FRotator(0, 0, V * LookSensitivity))
7aCheckGround()LineTraceSingleByChannel + CapsuleComp->GetScaledCapsuleHalfHeight() + AddIgnoredActor(this)
7bTick()FallVelocity += GravityZ * DeltaTime · AddActorWorldOffset · 착지 스냅
8Move() 분기ControlScale = bIsGrounded ? 1.f : 0.4f (에어컨트롤)


오늘 학습·정리한 개념

  • 6DOF 메서드 개수 — 축은 6개지만 메서드는 4개 (Move·Look이 Axis2D라 한 함수가 2축 담당)

  • bSweep 파라미터 — 이동/회전 중 충돌 검사 여부. 이동엔 true, 회전엔 보통 생략

  • FCollisionQueryParams — 트레이스 필터용 구조체. CapsuleHalfHeight 조회 용도와 무관 (→ CapsuleComp->GetScaledCapsuleHalfHeight() 사용)

  • AddControllerYawInput/PitchInput 금지 이유 — 과제 명세가 강제. 로우레벨 원리 학습이 목표

  • 회전 주체Look/Roll은 Pawn 자체를 회전 → 카메라는 SpringArm으로 따라 돎 (bUsePawnControlRotation = false 설정)

  • 헤더 UPROPERTYGravityZ (EditAnywhere), FallVelocity/bIsGrounded (VisibleAnywhere) 추가로 에디터 디버깅 가능


남은 에디터 작업

  • Content/Input 폴더 생성

  • IA_Move (Axis2D) / IA_MoveUp (Axis1D) / IA_Roll (Axis1D) / IA_Look (Axis2D) 생성

  • IMC_Default 생성 후 키 + Modifier 매핑
    • IA_Move: WASD + Negate/Swizzle 조합
    • IA_Look: Mouse XY + Negate(Y)
    • IA_MoveUp: Space / Shift(Negate)
    • IA_Roll: Mouse Wheel Axis
  • BP_NBCPawn (ANBC_AssignmentPawn 상속) 생성 후 IA/IMC 슬롯 할당

  • BP_NBCGameMode 생성 후 DefaultPawnClass = BP_NBCPawn 지정

  • Project Settings → Default GameMode 적용

  • 플레이 테스트 (지상·공중 이동, 6축 회전, 착지 스냅, 에어컨트롤)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.