들어가기 앞서
좋은 코드란?
- 긴 매개변수 목록
- 매개변수가 많으면 함수를 가독성이 떨어진다.
- 필요한 정보만 간결하게, 너무 길어지면 구조체로 정리한다.
- 전역 데이터의 남용
- 디버깅과 유지보수가 복잡해짐
- Subsystem을 이용하여 분리하자!
- Subsystem은 생명주기가 있는 게임 인스턴스
- Subsystem의 장점
- 접근 경로가 명확하다. 관련 함수 호출부를 찾으면 문제 부분을 확인 가능하다.
- 캡슐화가 강제된다.
- 맵이 바뀌거나 게임이 꺼지면 데이터를 초기화할 필요 없이, 새 인스턴스를 생성하면 된다.
- Subsytem 공식 문서: https://dev.epicgames.com/documentation/ko-kr/unreal-engine/programming-subsystems-in-unreal-engine
// 언리얼 Subsystem을 이용한 예
UCLASS()
class UScoreSystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
private:
int32 Score;
public:
void AddScore(int32 Amount)
{
Score += Amount;
// 점수가 변경됐음을 알리는 로직
}
int32 GetScore() const { return Score; }
};
// 사용은 이렇게 함.
void AEnemy::OnDefeated()
{
if (UGameInstance* GI = GetGameInstance()) // 게임 인스턴스를 이용
{
// GetSubsystem<UScoreSystem>() 쓰는 곳만 접근 가능
if (UScoreSystem* ScoreSys = GI->GetSubsystem<UScoreSystem>())
{
ScoreSys->AddScore(50);
}
}
}
- 가변 데이터
- 값이 자주 바뀌면 예기치 못한 오류나 복잡도가 증가
- 변경 가능한 범위를 최소화하고, 가급적 불변데이터를 활용
- 수정이 필요한 부분을 명확히 나누기
TObjectPtr
언리얼 5에서 도입된 템플릿 스마트 포인터이자, 언리얼 4의 원시 포인터 UObject*를 대체하기 위해 사용된다.
에디터 환경 전용으로 설계되어 두 가지 핵심 기능을 제공한다.
- 지연로딩(Lazy loading)
- 변수가 실제로 접근될 때만 리소스를 로드한다.
- 내부 멤버함수를 호출 할 때
- 다른 변수에 대입하려고 할 때
- 조건문에서 검사할 때
- 지역 변수나 잠깐 쓰이는 매개변수 등은 이미 메모리에 올라와 있는 객체들을 잠깐 가리키는 경우가 많아 원시포인터를 쓰는 게 더 효율적이다.
- 변수가 실제로 접근될 때만 리소스를 로드한다.
- 액세스 트래킹(Access tracking)
- 객체의 참조 경로를 기록하여 가비지 컬렉션 시스템과 간결하게 통합
- 어떤 리소스가 사용 중인지 정확히 식별
패키징 배포 후 TObjectPtr은 자동으로 변환되어 원시 포인터와 똑같이 동작하므로, 성능상의 차이는 없다.
참조로 가져오기와 포인터로 가져오기
참조는 포인터에 비해 연산이 적으므로, 참조를 사용하는 것이 좋다.
void ATestMyActor::BeginPlay()
{
Super::BeginPlay();
for (TObjectPtr<USceneComponent>& Component : Components) // TObjectPtr 참조를 쓰면 내부 로직이 돌아가지 않는다.
{
}
for (USceneComponent* Component : Components) // 원시 포인터 타입을 쓰면 포인터 관련 내부 로직이 돌아간다.
{
}
}
컨테이너 순회
TObjectPtr 컨테이너를 순회할 때 반복자 타입 선택
- auto*
- 비권장
- 매 반복마다 내부 로드 검사실행
- 원시 포인터 주소를 재계산하여 반환
- 대형 컨테이너에서 뚜렷한 성능 오버헤드 발생
- auto&
- 컨테이너 내 TObjectPtr 자체를 직접 참조
- 내부 로드 및 주소 계산 로직 생략
- 캐시 효율 우수, 최적의 성능
TSubclassOf
클래스 포인터 레퍼런스 컨테이너. 컨텐츠 브라우저에 있는 Class들을 담는다.



- UClass와의 차이점
- UClass는 모든 클래스, TSubclassOf<>는 타입을 지정해서 받을 수 있다.
- 빌드 시점에 잘못된 것을 넣으면 크래시가 난다.
- 런타임에 잘못된 것을 넣으면 nullptr이 되어 잘못된 점을 바로 알 수 있다.

// 이 둘의 차이는?
UPROPERTY(EditAnywhere, Category = "Test")
UClass* MyClass;
UPROPERTY(EditAnywhere, Category = "Test")
TSubclassOf<UClass> MyClass2;
// TSubclassOf<UClass>는 TSubclassOf<UActorComponent> MyClass2;로 선언하여 필터링이 가능하다!
TArray
연속 메모리 레이아웃
- index 접근이 가능하다
- 삽입/삭제/탐색이 느리다.
TArray의 메서드
TArray<int32> IntArray;
// 삽입
IntArray.Add(5); // 새로운 객체를 생성하여 배열에 복사해넣음. 약간의 성능 저하.
IntArray.Emplace(6); // 복사 없이 바로 배열에 생성. 의도치 않은 암시적 형변환이 생길 수 있음. 성능상 중요한 부분만 넣음.
IntArray.AddUnique(6); // 성능이 안 좋아서 대신 TSet을 쓴다. 즉, 사용 안 한다.
IntArray.Insert(600, 2); // 600을 2번 인덱스에 넣겠다는 뜻
// 길이
IntArray.Num(); // 배열 안의 원소 수를 반환.
// 검색
TArray<int32> ReturnIntArray = IntArray.FilterByPredicate([](const int32 FindValue) {return FindValue < 9;}); // 9보다 작은 값만 빼서 배열을 재생성
int32 index = IntArray.Find(5); // 5를 찾아 해당 인덱스를 반환
bool b = IntArray.Contains(5); // 배열에 5가 포함되어 있는지 아닌지를 반환
// 삭제
IntArray.Remove(10); // 안에 10이 있으면 10을 전부 없앤다.
IntArray.RemoveSingle(10); // 안에서 첫번째 10 값을 없앤다.
IntArray.RemoveAt(2); // 해당 인덱스를 지워준다. 인덱스가 존재하지 않으면 크래시가 난다. 따라서 먼저 존재하는지 체크해준다.
if (IntArray.IsValidIndex(2)) // 해당 인덱스가 유효한지 확인한다.
{
IntArray.RemoveAt(2);
}
IntArray.RemoveAll([](int32 Value) {return Value == 5;}); // 조건에 맞는 값을 전부 삭제한다.
IntArray.Empty(); // 배열 안의 값을 전부 삭제한다.
실습 예시
while (IntArray.Num() < 30)
{
int32 n = FMath::RandRange(0, 1000);
if (n <= 100)
{
IntArray.Add(n);
}
}
IntArray.RemoveAll([](int32 Value) {return Value < 50;});
IntArray.Sort();
TSet
해시 분산 레이아웃
- index 접근 지원하지 않는다.
- 탐색/삽입/삭제가 굉장히 빠르다.
- 중복방지가 되어 같은 값이 들어오면 무시한다.
TSet<int32> IntArraySet;
IntArraySet.Add(100); // Set에 값을 추가한다.
if (IntArraySet.Contains(100)) // Set에 값이 있는지 확인한다.
{
}
// 검색과 순회
int32* FoundPtr = IntArraySet.Find(20); // Array에서는 인덱스 반환이 되지만, Set에서는 포인터 반환이 된다. 순서대로 저장되지 않아 순회하려면 Iterator가 필요하다.
for (TSet<int32>::TIterator It = IntArraySet.CreateIterator(); It; ++It)
{
if (*It < 60)
{
It.RemoveCurrent(); // 현재 원소를 없애버린다.
}
}
// 값을 변경할 수 없는 const버전
for (TSet<int32>::TConstIterator It = IntArraySet.CreateConstIterator(); It; ++It)
{
}
// auto 버전 - 타입이 확실한 경우가 아니면 지양한다.
for (auto It = IntArraySet.CreateConstIterator(); It; ++It)
{
}
TSet<int32> SetA = { 1, 2, 3 };
TSet<int32> SetB = { 3, 5, 6 };
TSet<int32> SetC = SetA.Union(SetB); // 두 개의 Set을 합친다. 중복값은 하나로 유지된다.
TSet<int32> SetD = SetA.Intersect(SetB); // 두 개의 Set에서 공통된 부분만 추출한다.
SetC.Compact(); // 해시테이블에서 빈 슬롯을 정리하여 재배치
SetC.Shrink(); // 빈 슬롯을 없애고 차지하는 메모리 크기를 줄임
TArray<int32> MyArray = SetC.Array(); // Set을 Array로 변환
공격 궤적을 따라 데미지를 입는 몬스터 리스트 등에 쓴다.
TMap
해시 분산 레이아웃
- index 접근 지원하지 않는다.
- 탐색/삽입/삭제가 TSet과 같이 빠르다.
- Key와 Value 데이터를 저장한다.
TMap<int32, FString> ItemMap;
// FString 쓸 때는 TEXT로 감싸야한다.
ItemMap.Add(101, TEXT("Sword"));
ItemMap.Add(102, TEXT("Shield"));
ItemMap.Emplace(103, TEXT("Potion"));
ItemMap.Add(102, TEXT("Shield")); // 중복값은 덮어쓰기된다.
if (ItemMap.Contains(101))
{
FString* FoundItem = ItemMap.Find(101); // 검색하면 second값으로 이루어진 포인터로 받아야한다.
}
FString& ItemRef = ItemMap.FindOrAdd(104); // 없으면 만들라는 함수. second값의 참조형을 받는다.
ItemRef = TEXT("Bow"); // 찾은 참조값을 변경해줄 수 있다.
// 위험한 코드
FString Name = ItemMap[105]; // 값이 없으면 생성하는 대신 크래시가 난다.
// 안전한 코드
if (ItemMap.Contains(105)) // 있는지 먼저 확인한다.
{
FString Name = ItemMap[105];
}
for (const TPair<int32, FString>& i : ItemMap) // TPair로 순회 가능. 하지만 삭제를 하면 위험할 수 있다.
{
}
for (TMap<int32, FString>::TIterator It = ItemMap.CreateIterator(); It; ++It) // It으로 순회할 때는 It의 함수인 RemoveCurrent를 사용 가능
{
if (It.Key() == 103)
{
It.RemoveCurrent(); // Iterator만의 함수
}
}
ItemMap.Remove(102); // 키 값으로 삭제할 수 있다.
ItemMap.Compact(); // Set과 똑같이 메모리를 정리하는 함수
ItemMap.Shrink();
람다 함수
PREDICATE_CLASS(조건 검사기)가 들어갈 자리에 넣을 수있다.


사용법
- [](){} -> 캡처, 매개변수, 함수 본문
'게임 개발 > 학습 일지' 카테고리의 다른 글
| 언리얼 C++ - 아이템 스폰 및 레벨 데이터 관리하기 (0) | 2026.04.22 |
|---|---|
| 언리얼 C++ - 인터페이스 기반 아이템 클래스 설계하기 (1) | 2026.04.21 |
| 언리얼 게임 개발 - 차량 에셋에 바퀴 굴러가는 애니메이션 추가하기 (0) | 2026.04.17 |
| 언리얼 C++ - Enhanced_input_System을 활용한 입력 매핑 구현하기 (0) | 2026.04.16 |
| 언리얼 C++ - Character 클래스를 이용한 캐릭터 구현하기 (0) | 2026.04.15 |