나만의 작은 도서관

[TIL][C++] 250916 MMO 서버 개발 102일차: Visual Studio의 Edit and Continue, OpenLevel의 반환시점에는 Default Pawn이 없다. 본문

Today I Learn

[TIL][C++] 250916 MMO 서버 개발 102일차: Visual Studio의 Edit and Continue, OpenLevel의 반환시점에는 Default Pawn이 없다.

pledge24 2025. 9. 17. 18:56
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

Visual Studio의 Edit and Continue

  • 디버그 하던 도중 알게 된 내용인데, 디버그 중에 코드 파일을 수정하고 계속 진행하면 수정된 내용이 적용된 상태로 디버그가 진행된다. 즉, 디버그 상태 및 순서를 유지하면서 빌드가 진행된다는 것!

 

모든 경우에서 다 될까?

  • 대부분의 경우에 잘 작동하지만, 모든 종류의 코드 변경을 지원하지는 않는다고 한다. 예외적으로 안 되는 경우들을 나열해 보면 아래와 같은 것들이 있다.
    • 새로운 클래스나 구조체 추가
    • 함수의 시그니처 변경
    • 전역 변수나 static 변수 추가 또는 삭제
    • 헤더 파일 변경
    • 람다식 변경
    • 일부 최적화된 코드 변경

 

Edit and Continue는 언리얼 환경에서도 사용할 수 있을까?

  • 언리얼이 Visual Studio를 사용한다고는 하지만 Edit and Continue의 사용 가능 여부와는 또다른 이야기이다. 그래서 좀 찾아봤는데, 다행히도 Edit and Continue 기능을 지원한다고 한다.
  • 엔진의 핫 리로드 기능과는 별개로, 디버그 모드에서 똑같이 중단점을 설정하고 코드를 수정한 후, 디버거를 진행시키면 변경된 내용이 즉시 적용된다.

 

 

OpenLevel의 반환시점에는 Default Pawn이 없다.

  • OpenLevel로 레벨을 열면 기존 레벨을 언로드 → 새로운 레벨 로드를 하게 된다.
  • 그렇다면 OpenLevel() 함수의 작동 방식이 “동기 함수”이고, 레벨을 로드하는 역할을 한다고 했으니 당연히 다음 코드로 넘어가는 시점에는 레벨이 완전히 준비되어 있을까? 그렇지 않다. 여기에는 함정이 있다.

 

왜 동기 함수임에도 OpenLevel() 함수의 반환 시점에는 레벨이 로드되어 있지 않을까?

  • OpenLevel의 역할은 “레벨을 로드하는 역할”이 아니다. 정확히는 “레벨 로드를 요청하는 역할”이다. 그래서 OpenLevel은 동기함수임에도 레벨 로드를 엔진의 레벨 전환 시스템에 “이 레벨로 전환해 달라”는 요청만 등록해 두고 바로 빠져나오게 된다.
UGameplayStatics::OpenLevel(GetWorld(), TEXT("NewLevel"));
// OpenLevel 내부에서 일어나는 일 요약
// 1. 레벨 전환 "요청"을 엔진의 내부 큐에 등록
// 2. 등록 후 즉시 반환됨
  • 사실 그래서 레벨 자체는 비동기적으로 열린다고 보는게 맞다. 애초에 OpenLevel() 함수는 요청하는 게 역할인지라 진행 완료 여부와는 상관없이 동기 함수임에도 다음 코드로 진행된다. 따라서 아래와 같이 바로 Default Pawn을 찾아달라고 하면 nullptr이 반환된다.
if (GWorld) 
{ 
	UGameplayStatics::OpenLevel(GWorld, FName("InGameMap")); 
	// 실제 레벨 전환은 이후 프레임들에서 진행되는 중...
	
	// Pawn이 없을 가능성이 높다.
	auto* PC = UGameplayStatics::GetPlayerController(this, 0); 
	AP1Player* Player = Cast<AP1Player>(PC->GetPawn());
}

 

 

그렇다면 레벨이 완전히 로드된 시점을 알고 싶다면 어떻게 해야하는가?

  • 레벨이 완전히 로드된 시점에서 로직을 실행시키고 싶다면, (Ex. 위처럼 GameMode의 Default Pawn을 가져오고 싶은 경우) GameInstance에서 델리게이트를 바인딩하면 된다.
  • 언리얼에는 FWorldDelegates::OnPostLoadMapWithWorld나 UGameInstance::OnWorldChanged 같은 콜백이 존재하는데 이 콜백들은 새 월드가 준비된 후에 호출된다는 특징이 있다. 그래서 아래와 같이 GameInstance에서 바인딩을 한다면 안전하게 레벨이 완전히 로드된 시점에서 로직을 실행시킬 수 있다.

FWorldDelegates::OnPostLoadMapWithWorld와 UGameInstance::OnWorldChanged 사용하는 시점의 차이 및 예제(잘못된 정보)

 

FWorldDelegates::OnPostLoadMapWithWorld

  • 호출 시점: 새 레벨의 UWorld 가 로드되고, 액터들이 월드에 배치된 직후.
  • 특징: 맵이 로딩 완료된 이후 한 번 호출됨. 레벨 안의 액터 접근 가능.
  • 용도: 레벨 전환 후 월드/액터 초기화, 커스텀 스폰 처리 등.

 

예제

// GameInstance.cpp
void UP1GameInstance::Init()
{
    Super::Init();

    FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UP1GameInstance::SendEnterMapCompletePacket);
}

void UP1GameInstance::SendEnterMapCompletePacket(class UWorld* inWorld)
{
    // ✅ 가능: 월드 정보 조회, 액터 검색
    // ❌ 주의: GameMode 관련 로직 (아직 완전 초기화 안됨)
}

UGameInstance::OnWorldChanged

  • 호출 시점: GWorld 가 교체될 때 (즉, 현재 사용 중인 UWorld 가 바뀔 때).
  • 특징: 레벨이 실제로 교체될 때마다 호출되며, 이전 월드와 새로운 월드를 둘 다 받을 수 있음.
  • 용도: 전환 직전/직후에 처리할 전역 상태 관리.
    • 예: 기존 월드 클린업, 새로운 월드 준비, 월드 전환 시점에 맞춘 리소스 관리.

예제

// GameInstance.cpp
void UMyGameInstance::Init()
{
    Super::Init();
    OnWorldChanged().AddUObject(this, &UMyGameInstance::HandleWorldChanged);
}

void UMyGameInstance::HandleWorldChanged(UWorld* OldWorld, UWorld* NewWorld)
{
    UE_LOG(LogTemp, Log, TEXT("World changed: %s -> %s"),
        OldWorld ? *OldWorld->GetName() : TEXT("None"),
        NewWorld ? *NewWorld->GetName() : TEXT("None"));

    // NewWorld가 막 교체된 시점
    // 아직 액터는 초기화 중일 수 있음
}


핵심 차이

  • OnWorldChanged → 월드 교체 이벤트 (OldWorld → NewWorld). 액터가 다 준비됐다는 보장은 없음.
  • OnPostLoadMapWithWorld → 새 맵 로드 완료 이벤트. 액터가 전부 배치된 후 실행되므로 실제 작업(스폰, 액터 참조)에 더 적합.