상황
오늘 UGameplayMessageSubsystem을 가져오는 과정에서 크래시가 발생했다.
문제가 된 흐름은 GameplayAbility 내부에서 Subsystem을 바로 Get하려고 한 부분이었다.
특히 Editor에서 PIE 실행 후 게임을 종료할 때 해당 지점에서 터지는 문제가 있었다.
UGameplayMessageSubsystem& MessageSubsystem =
UGameplayMessageSubsystem::Get(this);
또는 이와 비슷하게 this를 WorldContextObject처럼 사용한 코드가 문제였다.
원인
UGameplayAbility는 AActor나 UActorComponent처럼 명확한 World 소유 객체가 아니다.GameplayAbility는 ASC에 의해 관리되는 UObject 계열 객체이고, Ability 인스턴스의 생성 방식이나 실행 시점에 따라 GetWorld()가 안정적으로 유효하지 않을 수 있다.
PIE 종료, Ability 종료, Actor 파괴, StateTree 정지 같은 타이밍에서는 Ability가 참조하던 Actor나 World가 이미 정리 중일 수 있다.
this는 WorldContextObject로 쓰기에는 안전하지 않다.
| 대상 | WorldContext로 적합한가 |
|---|---|
this as UGameplayAbility |
부적합 |
GetAvatarActorFromActorInfo() |
적합 |
GetOwningActorFromActorInfo() |
상황에 따라 가능 |
GetWorld()가 확실한 Actor/Component |
적합 |
문제 코드
void UFTGameplayAbility::SomeFunction()
{
UGameplayMessageSubsystem& MessageSubsystem =
UGameplayMessageSubsystem::Get(this);
MessageSubsystem.BroadcastMessage(MessageTag, Payload);
}
this가 UGameplayAbility이기 때문에 WorldContextObject로 안정적이지 않다.
PIE 종료 시점이나 Ability 정리 시점에서는 내부적으로 World를 찾지 못하거나, 이미 정리 중인 객체를 통해 Subsystem에 접근하면서 크래시가 날 수 있다.
수정 방향
Subsystem을 바로 Get하지 않고, Ability ActorInfo에서 AvatarActor를 가져온 뒤 그 Actor를 WorldContextObject로 사용한다.
AActor* AvatarActor = GetAvatarActorFromActorInfo();
if (!IsValid(AvatarActor))
{
return;
}
UGameplayMessageSubsystem& MessageSubsystem =
UGameplayMessageSubsystem::Get(AvatarActor);
GetAvatarActorFromActorInfo()로 실제 게임 월드에 존재하는 Actor를 가져오고, IsValid()로 nullptr와 삭제 대기 상태를 함께 검사한다.
수정 코드
void UFTGameplayAbility::BroadcastGameplayMessage()
{
AActor* AvatarActor = GetAvatarActorFromActorInfo();
if (!IsValid(AvatarActor))
{
return;
}
UGameplayMessageSubsystem& MessageSubsystem =
UGameplayMessageSubsystem::Get(AvatarActor);
MessageSubsystem.BroadcastMessage(MessageTag, Payload);
}
| 처리 | 이유 |
|---|---|
GetAvatarActorFromActorInfo() 사용 |
Ability를 실제 실행 중인 Actor 기준으로 World를 찾기 위해 |
IsValid() 사용 |
nullptr뿐 아니라 Destroy / PendingKill 상태까지 거르기 위해 |
UGameplayMessageSubsystem::Get(AvatarActor) |
올바른 WorldContextObject를 넘기기 위해 |
유효하지 않으면 return |
PIE 종료, Actor 파괴, Ability 종료 타이밍 방어 |
GetAvatarActorFromActorInfo를 사용한 이유
GameplayAbility에는 보통 두 Actor 개념이 있다.
| 함수 | 의미 |
|---|---|
GetOwningActorFromActorInfo() |
ASC를 소유한 Actor |
GetAvatarActorFromActorInfo() |
Ability가 실제로 적용되는 Actor |
GetCurrentActorInfo() |
Ability 실행에 필요한 ActorInfo |
GetAbilitySystemComponentFromActorInfo() |
현재 Ability의 ASC |
PlayerState가 ASC를 소유하고 Character가 Avatar인 구조라면, OwningActor와 AvatarActor가 다를 수 있다.
| 구조 | 예시 |
|---|---|
| OwningActor | APlayerState |
| AvatarActor | ACharacter |
GameplayMessageSubsystem을 가져올 때 주체는 Ability가 실행 중인 World Context다.
실제 월드에 배치되어 있는 AvatarActor를 사용하는 쪽이 더 안전하다.
IsValid
IsValid()는 nullptr뿐 아니라 언리얼 객체의 삭제 대기 상태까지 함께 확인한다.
if (IsValid(AvatarActor))
{
// 사용 가능
}
| 검사 | 확인 범위 |
|---|---|
if (Ptr) |
nullptr 여부 |
IsValid(Ptr) |
nullptr + 삭제 대기 상태 |
IsValidLowLevel() |
일반 게임 코드에서 권장하지 않음 |
GameplayAbility는 종료 타이밍, 취소 타이밍, PIE 종료 타이밍에서 호출될 수 있으므로 IsValid() 방어가 필요하다.
PIE 종료 시 터진 이유
Editor에서 Play를 종료하면 World, Actor, Component, Subsystem이 순차적으로 정리된다.
이때 Ability나 StateTree 쪽 로직이 아직 정리 중에 호출될 수 있다.
이런 타이밍에서 Ability 내부 코드가 Subsystem을 가져오려고 하면, 잘못된 WorldContextObject 때문에 문제가 발생할 수 있다.
WorldContextObject 기준
Unreal에서 Subsystem이나 GameplayStatics 계열 함수는 WorldContextObject를 넣을때는
엔진이 객체를 통해 UWorld를 찾을 수 있는 ContextObject를 넣어줘야한다.
| WorldContextObject 후보 | 안전도 | 설명 |
|---|---|---|
AActor* |
높음 | World에 직접 속함 |
UActorComponent* |
높음 | Owner를 통해 World 접근 가능 |
UGameInstance* |
높음 | GameInstanceSubsystem 접근에 적합 |
UWorld* |
높음 | 직접적인 World |
UGameplayAbility* this |
낮음 | Ability는 World 소유 객체가 아님 |
| 임시 UObject | 낮음 | World를 찾지 못할 수 있음 |
'TIL' 카테고리의 다른 글
| LevelDataAssetGenerator 수정 (0) | 2026.07.15 |
|---|---|
| IK (0) | 2026.07.14 |
| Clean Code (0) | 2026.06.09 |
| Unreal EQS(Environment Query System) (0) | 2026.06.08 |
| Unreal Engine AI Controller (0) | 2026.06.05 |