2026-04-13(월) PublicIncludePaths, UKismetSystemLibrary, if , checkf, IsVaild, ensure

조범근·2026년 4월 13일

TIL

목록 보기
43/87

C++ Week 8

Study

언리얼C++ 기초, 게임기획, 알고리즘 1문제


Today I Learned


Unreal C++

Build.cs와 PublicIncludePaths

언리얼 엔진의 빌드 시스템(UBT)에게 헤더 파일을 찾을 때 명시한 폴더도 찾아봐라고 알려주는 경로 추가 작업이다.

Why?

게임 프로젝트 규모가 커지면 폴더가 많아진다.
만약 이 코드가 없으면 다른 폴더의 헤더를 가져올 때 #include "../../Public/Weapon/MyGun.h"처럼 복잡하게 적어야 한다.
경로를 추가해두면 간단하게 #include "MyGun.h"로 끝낼 수 있어 에러를 방지하고 가독성을 높여준다.





LifeCycle

  1. GameInstance (게임 전체의 삶)
    • Init() : 게임 앱이 켜질 때 딱 한 번
    • Shutdown() : 게임 앱이 완전히 꺼질 때 딱 한 번
  2. Actor (월드에 배치된 물체의 삶)
    • BeginPlay() : 월드에 생성되어 게임이 시작될 때.
    • Tick() : 매 프레임마다
    • EndPlay() : 어떤 이유든 월드에서 사라질 때 (레벨 전환, 파괴 등)
    • Destroyed() : 메모리에서 완전히 파괴되기 직전에




UKismetSystemLibrary

블루프린트에서 사용하는 노드들을 C++에서 사용할 수 있도록 모아놓은 클래스이다.(Kismet은 블루프린트의 옛 이름)

Why?

C++로만 기능을 구현하려면 코드가 매우 복잡해지는 경우가 있는데, 엔진 내부에서 블루프린트용으로 잘 만들어둔 기능을 가져다 쓰기 위해 사용한다.

함수목록

  1. LineTraceSingle / SphereTraceSingle : 총알이 맞았는지 검사할 때 사용

  2. PrintString : 디버깅 메시지 띄울 때

  3. QuitGame : 게임 종료 기능을 만들 때

  4. GetGameTimeInSeconds : 게임이 시작된 지 얼마나 지났는지 초 단위로 가져올 때

  5. DrawDebugSphere/DrawDebugLine : 충돌 영역이나 총알 궤적을 화면에 선으로 그려서 눈으로 확인하고 싶을 때





if vs check vs ensure 언제 뭘 써야 할까?

if(방어적) : 정상적인 예외 상황 -> 총알이 부족해서 발사가 안되는 경우

check(단언) : 절대 일어나면 안 되는 일 -> 캐릭터는 있는데 Mesh컴포넌트가 없는 경우

ensure(경고) : 에러지만 일단 살려둠 -> UI 아이콘 파일 경로가 잘못된 경우

모든 곳에 if를 남발하면, 에러가 발생했을 때 '어디서부터 잘못됐는지' 찾기가 매우 힘들다. 에러가 발생한 지점에서 즉시 게임을 터뜨려버리는(check) 것이 디버깅 시간을 단축해준다. 그래서 없으면 안 되는 것은 checkf()로 강하게 제약하는게 맞다.



IsValid() vs nullptr

IsValid()를 사용하는게 스테디다.
C++ nullptr 체크는 '주소 값이 0인지'만 본다. 하지만 언리얼의 UObject는 메모리에는 살아있지만 파괴중인 상태가 있다. if(MyActor != nullptr)는 통과했는데, 정작 그 액터는 가비지 컬렉터(GC)가 수거해 가기직전인 좀비상태라면 함수를 호출하는 순간 크래시가 남

**언리얼 오브젝트(UObject 상속 클래스)라면 무조건 IsValid() 사용





클래스 결합도

GameInstance / PlayerController등 계속 직접 참조를 해도 될까?

어느 클래스에서든 GetGameInstance라고 쓰는 순간, 그 클래스는 UMyGameInstance헤더를 포함(#include)를 해야한다. 즉 두 클래스는 결합도가 올라간다. 나중에 GameInstance의 구조를 바꾸면, 참조하던 수십 개의 파일이 전부 다시 컴파일되어야 한다.

어떻게 낮출까?

  1. 매개변수로 전달 : 매개인자로 받기
  2. 인터페이스 사용 : 같은 인터페이스를 쓰면 구체적인 클래스를 몰라도 함수를 호출할 수 있다.
  3. 델리게이트 : 신호를 주면 누가 점수를 올리든 말든 상관하지 않는 방식




LiveCoding

권장 상황 : .cpp파일 내부의 단순 로직 수정

비 권장 상황

  • .h 파일 수정 (변수 추가/삭제, 함수 선언 변경)
  • 생성자(Constructor) 코드 수정
  • 새로운 클래스 추가


Binaries 폴더

C++ 코드가 컴파일되어 나온 결과물이 저장되는 곳





CDO(Class Default Object)

함수를 부르지 않아도 엔진이 켜질 때 클래스당 딱 하나씩 생성자를 강제로 실행해서 만들어두는 '원본 객체'이다.
매번 생성자를 처음부터 끝까지 실행하면 CPU에 부하가 많이 걸리기 때문에, 언리얼은 미리 만들어둔 CDO(마스터 템플릿)를 메모리 상에서 통째로 복사 해버린다. 새로 계산하는 것보다 압도적으로 빠르기 때문이다.



GetDefault< T >()

실제로 액터를 월드에 스폰하지 않고도, "이 클래스의 기본 데미지가 얼마지?" 같은 정보가 궁금할 때 사용한다.

밑에는 GetDefault를 사용하는 예시이다.

void UShopWidget::DisplayWeaponInfo(TSubclassOf<AWeapon> SelectedWeaponClass)
{
    if (SelectedWeaponClass)
    {
        // 1. 해당 클래스의 CDO(마스터 템플릿)를 가져옵니다.
        // 월드에 총이 없어도 메모리에는 이미 이 원본(CDO)이 올라와 있습니다.
        const AWeapon* WeaponCDO = GetDefault<AWeapon>(SelectedWeaponClass);

        if (WeaponCDO)
        {
            // 2. CDO에 기록된 기본 데미지 값을 읽어옵니다.
            float Damage = WeaponCDO->DefaultDamage;
            
            UE_LOG(LogTemp, Warning, TEXT("Selected Weapon Damage: %f"), Damage);
        }
    }
}

플레이어가 상점에서 총을 고르고 있다고 하자. 총을 등 뒤에 메기 전까지는 월드에 총 액터가 존재하지 않는다. 하지만 UI에는 그 총의 데미지가 표시되어야 한다. 생성자에 적어둔 Damage나 종류 같은 것들을 객체로 만들지 않아도 미리 받아서 사용 가능하다.





Reflection

리플렉션은 런타임 중에 자기 자신을 조사하는 기능을 뜻한다.

그런데 왜 런타임 중에 자기 자신을 조사해야할까?

엔진이 코드를 자동으로 관리하기 위해서이다.
만약 리플렉션이 없다면, 변수 하나를 만들 때마다 에디터에 보여주는 코드, 메모리 해제하는 코드를 일일이 다 짜야한다. 계속 리플렉션 시스템으로 관리하면 런타임 중에 자기 자신을 조사하여 버려지는 메모리 같은 것들을 해제하거나, Details에서 변수값이 변한다 하면 변하게 해줄 수 있다.





StaticClass() vs GetClass()

StaticClass()
컴파일 타임에 실행 -> 클래스 그 자체의 정보를 가져온다.
특징 -> 특정 인스턴스(객체)가 없어도 클래스 이름만 알면 어디서든 부를 수 있다.
언제 사용할까? -> "이 녀석이 A 클래스가 맞아?"라고 물어볼 때의 'A 클래스'라는 기준점으로 사용한다.

GetClass()
런타임에 실행 -> 이 인스턴스가 실제로 어떤 클래스인지 가져온다.
특징 -> 반드시 살아있는 객체(인스턴스)가 있어야 부를 수 있다.
언제 사용할까? -> 지금 사용하는 이 액터의 Class를 알아볼때 사용한다.



Cast< T >() vs Class()로 비교하기

void ABullet::OnHit(AActor* OtherActor)
{
    if (OtherActor == nullptr) return;

    // 방법 A: StaticClass()와 GetClass()를 이용한 비교
    if (OtherActor->GetClass()->IsChildOf(AEnemy::StaticClass()))
    {
        UE_LOG(LogTemp, Log, TEXT("적을 맞췄습니다!"));
    }

    // 방법 B: Cast를 이용한 확인
    AEnemy* Enemy = Cast<AEnemy>(OtherActor);
    if (Enemy)
    {
        // Cast는 확인뿐만 아니라, Enemy->TakeDamage() 처럼 내부 함수를 직접 쓸 때 필수
        Enemy->TakeDamage(10.0f);
    }
}

단순히 같은 클래스인지 체크만 하고 싶을때는 IsChildOf(A...::StaticClass())를 사용.

체크한 뒤에 그 클래스의 함수나 변수를 사용해야 할 때 Cast<T>가 정석





UClass

클래스 위에 UCLASS()라고 적는 것은 이 클래스를 언리얼 엔진의 관리 대상으로 등록하는 것이다.

void USXGameInstance::Init()
{
    Super::Init();
	UClass* CompiletimeClassInfo = StaticClass();
	UClass* RuntimeClassInfo = GetClass();
	
	checkf(CompiletimeClassInfo != RuntimeClassInfo, TEXT("CompiletimeClassINfo != RuntimeClassInfo"));
}

UClass* CompiletimeClassInfo = StaticClass();는 뭘까?
엔진은 클래스를 설명하기 위한 설명서 객체를 내부적으로 하나 만든다. 그 설명서 객체의 타입이 바로 UClass이다.
UClass* CompiletimeClassInfo = USXGameInstance::StaticClass();
로 앞에 USXgameInstnace가 생략된 것

여기서 만약 C++로 USXGameInstance를 만들고, 이걸 상속받아서 블루프린트로 BP_MyGameInstnace를 만들었다고 치자. 여기서 StaticClass()는 여전히 C++ 클래스(USXGameInsatance) 의 정보를 가져온다. 하지만 실제 게임에서 돌아가는 this가 BP_MyGameInstance라면?

GetClass()는 블루프린트 클래스의 정보를 가져온다. 이때는 둘이 다르기 때문에 checkf를 통과하게 된다.

보통 BP를 사용하는데 어떻게 체크를 할까 그럼?


IsChildOf()

바로 IsChildOf()함수를 사용하면 된다.
if (OtherActor->GetClass()->IsChildOf(ABaseEnemy::StaticClass()))
GetClass가 AbaseEnemy거나 그걸 상속받은 BP든 어쨋든 ABaseEnemy 계열인지를 묻는 것



USXGameInstance vs UClass

USXGameInstance* GameInstance;로 만든 객체랑 UClass* MyClass = StaticClass()랑 똑같은 주소를 가리키고 있어야 하지않나? 라는 궁금증이 생겼다. 찾아보니 둘은 다른 주소를 가리키고 있다.

USXGameInstance* GameInstance;
-> 실제 Class의 정보가 있는 주소. 멤버변수, 메서드 접근가능.

UClass* MyClass = StaticClass()
-> Class의 설계도 정보만 닮겨있는 부동산의 주소. 찾아가면 실질적인 값 같은건 없음. 크기, 이름, 상속 관계, 기본값 정도만 알 수 있음.

UClass*가 읽을 수 있는 것

  1. 이름 -> 클래스의 이름이 문자열로 무엇인지? -> GetName()

  2. 상속 관계 -> 특정 클래스를 상속받아서 만든건지? -> IsChildOf()

  3. 변수/함수 목록 -> UPROPERTY로 지정된 변수가 몇개고 이름이 뭔지? -> 리플렉션 조사

  4. 기본값 -> 생성자에 적힌 값이 얼만지? -> GetDefaultObject()


UClass*는 언제쓸까

1.월드에 무언가를 새로 만들 때(Spawn)

UClass* BulletClass = ABullet::StaticClass(); 
GetWorld()->SpawnActor<ABullet>(BulletClass, Location, Rotation);

객체에 올라타지않은, 만들어지지 않은 클래스의 주소를 줄 순 없다. 대신 설계도 주소 UClass*를 줘야 엔진이 객체를 만든다.

에디터에서 어떤걸 만들지 선택하게 할 때 (TSubclassOf)

 UPROPERTY(EditAnywhere)
TSubclassOf<AWeapon> WeaponToSpawn; 

3.종류 판별(IsChildOf)

if (OtherActor->GetClass()->IsChildOf(ABoss::StaticClass()))




0개의 댓글