TIL 2026-04-17
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로 변화 감시 가능 |
| 생명 주기 | 수동 초기화 안 하면 이전 판 데이터 남음 | 맵/게임 종료 시 엔진이 파괴·재생성 → 데이터 오염 원천 차단 |
가변 데이터 (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(); // 전부 삭제
| 비교 | Add | Emplace |
|---|---|---|
| 언제 | 일반적인 경우 | 성능을 쥐어짜야 할 때 |
| 특징 | 복사 → 명확함 | 내부 생성 → 암시적 변환 개입 여지 |
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 | 메서드 | 내용 |
|---|---|---|
| 1 | ANBC_AssignmentPawn() | 4 컴포넌트 + SM_Drone 로드 + AutoPossessPlayer = Player0 |
| 2 | BeginPlay() | IMC 등록 (GetController → GetLocalPlayer → Subsystem → AddMappingContext) |
| 3 | SetupPlayerInputComponent() | Move / MoveUp / Roll / Look 4종 BindAction |
| 4 | Look() | AddActorLocalRotation(FRotator(V.Y, V.X, 0)) — 6DOF, Clamp 없음 |
| 5 | Move() | (Forward*V.X + Right*V.Y) * MoveSpeed * DeltaTime + AddActorLocalOffset(Δ, true) |
| 6a | MoveUp() | GetActorUpVector() 기반 AddActorLocalOffset |
| 6b | Roll() | AddActorLocalRotation(FRotator(0, 0, V * LookSensitivity)) |
| 7a | CheckGround() | LineTraceSingleByChannel + CapsuleComp->GetScaledCapsuleHalfHeight() + AddIgnoredActor(this) |
| 7b | Tick() | FallVelocity += GravityZ * DeltaTime · AddActorWorldOffset · 착지 스냅 |
| 8 | Move() 분기 | 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설정)헤더
UPROPERTY—GravityZ(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축 회전, 착지 스냅, 에어컨트롤)