Quick answer: When an actor is destroyed, EndPlay runs on the actor and its components, but the order between sibling components is not something to rely on, and anything you reach through raw pointers may already be on its way out. Do clean-up in EndPlay (not destructors), check IsValid() before touching other components or actors, clear timers and unbind delegates there, and hold cross-object references as TWeakObjectPtr.
Destroying an actor crashes the editor, or logs an access to a pending-kill object, from inside a component's EndPlay or OnComponentDestroyed. Teardown in Unreal happens in stages, and code that assumes everything is still alive until the very end runs into objects that are already partly gone.
How teardown proceeds
When Destroy() is called on an actor (or its level unloads, or PIE ends):
- The actor's
EndPlay(EEndPlayReason)runs. The base implementation ends play on its components, which calls each component'sEndPlay. - Components are unregistered and destroyed;
OnComponentDestroyedruns. - The actor and components are marked for garbage collection and later freed.
EndPlay is the right place for game-level clean-up. Destructors and BeginDestroy run during garbage collection, when the world and other objects may already be gone.
Cause 1: one component uses another in EndPlay
void UHealthComponent::EndPlay(const EEndPlayReason::Type Reason)
{
Inventory->DropAll(); // Inventory may have already ended play
Super::EndPlay(Reason);
}
Do not depend on the order in which sibling components end play. Either move cross-component clean-up into the owning actor's EndPlay, before calling Super::EndPlay, while all components are still valid, or check first:
if (IsValid(Inventory)) { Inventory->DropAll(); }
Cause 2: DestroyComponent at runtime
Calling DestroyComponent() on one component ends play for it immediately, while the actor keeps running. Other components that cached a raw pointer to it now hold a dangling or pending-kill reference. Store references as UPROPERTY() pointers (so they are nulled or tracked by GC) or TWeakObjectPtr, and check validity before use.
Cause 3: timers and latent actions
A timer set by a component fires after its owner has been destroyed if nothing clears it. Clear timers in EndPlay:
GetWorld()->GetTimerManager().ClearAllTimersForObject(this);
The same applies to async tasks and lambdas capturing this: capture a TWeakObjectPtr and check it before running.
Cause 4: delegates from other actors
If a component bound to another actor's delegate with AddDynamic or AddUObject, unbind in EndPlay with RemoveDynamic/RemoveAll(this). Otherwise the other actor may broadcast to an object that is being destroyed.
Cause 5: using the EndPlay reason
EEndPlayReason tells you why play is ending: Destroyed, LevelTransition, EndPlayInEditor, RemovedFromWorld or Quit. Spawning effects, saving or dropping loot on EndPlayInEditor or Quit is a common source of teardown crashes; gate gameplay reactions on Destroyed.
Cause 6: Blueprint equivalents
In Blueprints, use Event End Play on the actor for clean-up that touches several components, use Is Valid nodes before accessing references, and clear timers with Clear and Invalidate Timer by Handle.