분류: Unreal Engine 5.7 / GAS / Gameplay Cue
발생 시점: 액터 파괴(Destroy) 시
증상: ApplyOriginalMaterial 내부에서 크래시 발생, 로그상 "MyTarget is null"처럼 보이지만 실제 원인은 다른 곳
레벨을 정리할 때 어노말리 액터를 파괴하는 로직
AGameplayCueNotify_Actor 기반의 머티리얼 교체 Cue를 구현하였다.
OnRemove에서 MyTarget과 CachedMaterialCueData 유효성을 모두 체크한 뒤 원본 머티리얼 복원 함수를 호출했음에도, 액터가 파괴될 때 크래시가 발생하였다.
bool ADZGCN_AnomalyChangeMaterial::OnRemove_Implementation(
AActor* MyTarget, const FGameplayCueParameters& Parameters)
{
if (!MyTarget) return false; // ← 통과
if (CachedMaterialCueData.Num() == 0) return false; // ← 통과
for (auto& MaterialData : CachedMaterialCueData)
{
if (IsValid(MyTarget))
ApplyOriginalMaterial(MyTarget, MaterialData); // ← 여기서 크래시
}
return true;
}
void ADZGCN_AnomalyChangeMaterial::ApplyOriginalMaterial(
AActor* MyTarget, FDZMaterialCueData& CueData)
{
UStaticMeshComponent* TargetMesh = CastChecked<UStaticMeshComponent>(
MyTarget->FindComponentByTag(..., CueData.TargetMeshTag)); // ← 실제 크래시 지점
...
}
OnRemove에서의 IsValid(MyTarget) 체크는 통과하였다.
그러나 액터가 파괴 진행 중(BeginDestroy 이후) 상태일 경우, 액터 자체는 아직 유효한 포인터를 가지고 있더라도 내부 컴포넌트는 이미 정리된 상태일 수 있다.
이 상황에서 FindComponentByTag는 nullptr을 반환하고, 이 nullptr을 CastChecked에 넘기면 즉시 assert 크래시가 발생한다.
OnRemove 진입
↓
IsValid(MyTarget) → true (액터 포인터는 아직 유효)
↓
FindComponentByTag → nullptr (컴포넌트는 이미 정리됨)
↓
CastChecked<UStaticMeshComponent>(nullptr) → 💥 크래시
CastChecked는 내부적으로 입력값이 절대null이 아님을 전제로 동작한다.
외부 조회 결과처럼null가능성이 있는 값에는 사용하면 안 된다.
CastChecked → Cast 로 교체하고, 결과에 대해 명시적인 IsValid 체크를 추가하였다.
void ADZGCN_AnomalyChangeMaterial::ApplyOriginalMaterial(
AActor* MyTarget, FDZMaterialCueData& CueData)
{
UStaticMeshComponent* TargetMesh = Cast<UStaticMeshComponent>(
MyTarget->FindComponentByTag(UStaticMeshComponent::StaticClass(), CueData.TargetMeshTag));
if (!IsValid(TargetMesh))
{
UE_LOG(LogTemp, Warning,
TEXT("ApplyOriginalMaterial: TargetMesh not found. Tag: %s"),
*CueData.TargetMeshTag.ToString());
return;
}
for (auto& SlotOverride : CueData.SlotOverrides)
{
TargetMesh->SetMaterial(SlotOverride.SlotIndex, SlotOverride.OriginalMaterial);
}
}
OnRemove에서도 !MyTarget 대신 !IsValid(MyTarget)으로 교체하고, 중복 체크를 제거하였다.
bool ADZGCN_AnomalyChangeMaterial::OnRemove_Implementation(
AActor* MyTarget, const FGameplayCueParameters& Parameters)
{
if (!IsValid(MyTarget)) return false;
if (CachedMaterialCueData.Num() == 0) return false;
for (auto& MaterialData : CachedMaterialCueData)
{
ApplyOriginalMaterial(MyTarget, MaterialData);
}
return true;
}
| 구분 | 내용 |
|---|---|
CastChecked | 입력이 절대 null이 아님을 개발자가 보장할 수 있을 때만 사용 |
Cast + IsValid | FindComponentByTag, GetComponentByClass 등 외부 조회 결과에는 반드시 이 조합 사용 |
!MyTarget vs !IsValid(MyTarget) | raw null 체크만으로는 GC 수거 중인 객체를 잡지 못함. IsValid 사용이 안전 |
| 액터 파괴 순서 | OnRemove가 호출되는 시점에 액터 포인터는 유효해도, 내부 컴포넌트는 이미 정리되어 있을 수 있음 |