I’m relying on the UE5Coro plugin to provide coroutine support for my game. Unfortunately, I’m experiencing an editor crash most of the time when I exit PIE with active coroutines. The underlying cause appears to be that the coroutines are still active, despite the game being over and a bunch of important state having been cleaned up. For example, in this Rider screenshot, you can see that the i
and j
variables are clearly inaccurate; the Grid
UObject is also null:
UE5Coro has functions for forcing coroutines to run in the main game thread, and for detecting coroutine cancellation, neither of which fix the issue. I suspect that when you exit PIE, a lot of stuff is just being forcibly cleaned up/unloaded, and it’s on me to recognize and respond to that (hence why I’m not bugging the UE5Coro dev for help).
I tried overriding the AActor::EndPlay function, so that my own code could recognize that the game is ending, and exit coroutines early; that doesn’t help either.
What’s the correct way to recognize and respond to PIE ending? Actually, a more appropriate question is probably: what’s the correct way to recognize and respond to an object being unloaded? After all, I might well get similar crashes when loading a new level. I only have one level at the moment, so it hasn’t come up.
Thanks for any advice or suggestions!
For the sake of completeness, here’s the function that’s most consistently crashing for me. It’s detecting matches in a match-3 game, clearing matched tokens, waiting for new tokens to fall in, and then scanning again, recursively.
UE5Coro::TCoroutine<> AGameBoard::InnerScanForMatches(FIntVector2 gravity, UMatchSequence* sequence) {
co_await UE5Coro::Async::MoveToGameThread();
bool topLevel = sequence == nullptr;
if (topLevel) {
sequence = NewObject<UMatchSequence>();
}
FSimultaneousMatchSet curSet;
FTokenMatch curMatch;
auto matchDirections = TArray<FIntVector2>{{0, 1}, {1, 0}};
for (int i = inactiveBorderSize; i < UTokenGrid::BOARD_SIZE - inactiveBorderSize; ++i) {
for (int j = inactiveBorderSize; j < UTokenGrid::BOARD_SIZE - inactiveBorderSize; ++j) {
for (auto dir : matchDirections) {
auto startToken = Grid->GetToken(i, j);
curMatch.tokens.Add(startToken);
curMatch.type = startToken->Type;
// Scan in dir until we encounter edge-of-grid or a different token.
for (int k = 1; k < UTokenGrid::BOARD_SIZE - inactiveBorderSize; ++k) {
auto p = FIntVector2(i, j) + dir * k;
if (p.X >= UTokenGrid::BOARD_SIZE || p.Y >= UTokenGrid::BOARD_SIZE ||
Grid->GetToken(p.X, p.Y)->Type != startToken->Type) {
// Ran out of match to make.
break;
}
// Valid token to add to match.
curMatch.tokens.Add(Grid->GetToken(p.X, p.Y));
}
if (curMatch.tokens.Num() < 3) {
// Not a valid match.
curMatch.tokens.Empty();
continue;
}
// Successful match!
curSet.matches.Add(curMatch);
curMatch = FTokenMatch();
}
}
}
// Done scanning for immediate matches.
if (curSet.matches.Num() == 0) {
co_return;
}
sequence->AddSet(curSet);
UE_LOGFMT(LogTemp, Log, "Made {0} matches", curSet.matches.Num());
co_await ClearMatches(gravity, curSet);
if (mustCancelCoroutines || UE5Coro::IsCurrentCoroutineCanceled()) co_return;
co_await InnerScanForMatches(gravity, sequence);
if (topLevel && sequence->matches.Num() > 0) {
// TODO flow popped token particles to the player, then cast spells
}
}```