게임 개발/학습 일지

언리얼 엔진 심화 2주차 - 컨테이너와 포인터

이개 2026. 4. 19. 21:17

들어가기 앞서

좋은 코드란?

  • 긴 매개변수 목록
    • 매개변수가 많으면 함수를 가독성이 떨어진다.
    • 필요한 정보만 간결하게, 너무 길어지면 구조체로 정리한다.
  • 전역 데이터의 남용
    • 디버깅과 유지보수가 복잡해짐
    • 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들을 담는다.

SpawnAcotr 사용 시
이렇게 노드로 연결해주는 것이 코드에서 UClass*로 넘기는 방식. 이것을 TSubclassOf로 사용하면 된다.
이렇게 넣어주는 것은 StaticClass() 방식

 

  • 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(조건 검사기)가 들어갈 자리에 넣을 수있다.

Sort 함수를 쓸 때의 인자,
구조체로 이런 함수를 만들어서 쓰는 것도 유효하긴 하다.

사용법

  • [](){} -> 캡처, 매개변수, 함수 본문