I spent two days on a bug that was, the whole time, telling me it was fine.
I'm building a Bomberman-style arena game. Hub with doors, you stand on one, it takes you to the arena, you drop bombs, you probably die. After you die there's a PLAY AGAIN button.
PLAY AGAIN did not work. Press it and you'd end up in the middle of the hub instead of back at your door. And the maddening part was that the other path through the same code — the automatic "return to the hub when the round ends" — worked perfectly.
Same function. Same argument. One worked, one didn't.
What was actually happening
Lobby and arenas live in the same place, 320 studs apart, and I move players between them with PivotTo. Here is the real sequence:
1. you get killed -> Humanoid.Health = 0
2. you press PLAY AGAIN -> server PivotTo's you to your door
3. ~2s later Roblox REPLACES your character (Players.RespawnTime)
and the new one spawns at the SpawnLocation
4. you end up in the middle of the hub
Step 2 was moving a corpse.
The character I so carefully repositioned was already scheduled for deletion. Two seconds later Roblox threw it away and built a fresh one at the SpawnLocation, and everything I'd done to the old one went with it. Not some placements of dead players were lost — all of them were, always, from the first day.
Why it took two days
Because the other path looked like it worked.
When a round ends normally, the game also sends everyone back to the hub, through the exact same function. And it looked flawless. Every single time, players ended up in the hub.
They ended up in the hub because the SpawnLocation is in the hub.
My code was doing nothing. Roblox's default respawn was producing the correct result on its own, and I was taking the credit. The feature "worked" in the sense that the observable outcome matched what I wanted, and for the entirely wrong reason.
That's a category of bug I now actively fear. A broken thing that looks broken is a morning's work. A broken thing that looks fine costs you two days, and you only find it because some unrelated path — the one where the right answer and the accidental answer happen to differ — finally disagrees.
The fix
Stop trying to place the player. Write down where they're supposed to end up.
TeleportManager.pendiente = {} -- [userId] = { destino, velocidad }
If they're alive, apply it now. If they're dead, apply it to the new character on CharacterAdded. It doesn't matter what caused the respawn, who killed them, or how long Roblox takes to get around to it. The intent survives the character.
The general shape, which I suspect is the real lesson: when the thing you're modifying has a shorter lifetime than the intent you're expressing, store the intent, not the modification.
Two things I'm still unsure about
- Is a pending-destination table the normal pattern here, or is everyone else setting
RespawnLocationand letting the engine put the player in the right spot to begin with? Mine feels like I'm working around the engine rather than with it. - Is there a less indirect way to know a
Humanoidis about to be replaced than checkingHealth? Branching on "is this dead" to answer "will this still exist in two seconds" is reading a symptom, not a cause. It works, but I don't love it.
If you've shipped something with a custom respawn flow, I'd like to hear which of those you landed on.
The game, if you want to see what it's for: BOMB MAYHEM. It's free and it's on Roblox, so you'll need an account; it's currently rated 16+. Neither of those is needed to have an opinion about the code.
Top comments (0)