Graceful Goodbyes: Detecting Player Leave

In this lesson you'll learn to

  • βœ“Student can handle a player leaving mid-round (PlayersLeftEvent / participant tracking) and clean up their shells/score so the round stays fair.

Student can handle a player leaving mid-round (PlayersLeftEvent / participant tracking) and clean up their shells/score so the round stays fair.

πŸ“– Reference & full walkthrough: Graceful Goodbyes: Detecting Player Leave

🧩 Your capstone piece: Mid-round dropout handling so Shell-Hunt never soft-locks waiting on a gone player


Overview

Every multiplayer island eventually meets the same uninvited guest: the mid-round dropout. A player rage-quits, loses connection, or gets called to dinner β€” and suddenly your round logic is waiting on someone who is never coming back. In Shell-Hunt terms: the golden shell walks out the door in a gone player's pocket, the scoreboard still counts their shells, and the "last collector standing" check never fires. Coral the flamingo has seen it all from her perch on South Shores, and her advice is simple: say a graceful goodbye, tidy up, and keep the round moving.

The real Verse API for this is fort_playspace.PlayerRemovedEvent() β€” a listenable(player) that fires whenever a human player leaves your island (the concept some creators call a "players left event"). Its sibling ParticipantRemovedEvent() covers any participant agent, including AI-controlled characters β€” handy when your round mixes players and NPCs. You will subscribe to the player event, and when it fires you will run a short, ordered cleanup routine: return their held loot to the round, drop their score entry, and check whether the round should end early.

What you will build

The mid-round dropout handler for the Shell-Hunt capstone β€” the piece that guarantees Shell-Hunt never soft-locks waiting on a player who already left. One creative_device that:

  1. Tracks every player's shell count in a [player]int map (seeded at OnBegin, extended when players join late).
  2. Tracks who currently holds the golden shell (an ?player option, set from an item_spawner_device's ItemPickedUpEvent).
  3. Subscribes to GetPlayspace().PlayerRemovedEvent() and, on a leave, runs cleanup in order: golden shell back into play β†’ score entry removed β†’ end-of-round check.
  4. Ends the round via an end_game_device if only one hunter remains, instead of waiting forever.

Walkthrough

Step 1 β€” Place the devices

In UEFN, place on your island:

  • An Item Spawner configured to spawn your golden shell item (this device both spawns the shell and tells us who picked it up).
  • A HUD Message device with its message set to something like "The golden shell washes back ashore!".
  • An End Game device that declares the activating player's team the winner.

Step 2 β€” The complete dropout handler

Create a new Verse file, paste this in, build, and wire the three @editable slots in the Details panel:

using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /Fortnite.com/Game }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Shell-Hunt dropout handler: keeps the round fair and un-stuck
# when a player leaves mid-round.
shell_hunt_dropout_device := class(creative_device):

    # Item Spawner configured with the golden shell item.
    @editable
    GoldenShellSpawner : item_spawner_device = item_spawner_device{}

    # HUD Message announcing the shell is back in play.
    @editable
    DropoutMessage : hud_message_device = hud_message_device{}

    # End Game device that crowns the last hunter standing.
    @editable
    WinnerScreen : end_game_device = end_game_device{}

    # Per-player shell tally. A leaver's entry must be removed so
    # averages/leaderboards don't count a ghost.
    var ShellCounts : [player]int = map{}

    # Who is holding the golden shell right now (nobody at round start).
    var GoldenHolder : ?player = false

    OnBegin<override>()<suspends>:void =
        # Seed a zero tally for everyone already on the island.
        for (P : GetPlayspace().GetPlayers()):
            if (set ShellCounts[P] = 0) {}

        # Playspace events use () before .Subscribe (unlike device events).
        GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerJoined)
        GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerLeft)

        # Track who picked up the golden shell.
        GoldenShellSpawner.ItemPickedUpEvent.Subscribe(OnGoldenShellPickedUp)

    # Late joiners get a fresh tally so the map always mirrors the lobby.
    OnPlayerJoined(NewPlayer : player):void =
        if (set ShellCounts[NewPlayer] = 0) {}
        Print("A new hunter wades ashore.")

    # Remember the current golden-shell holder.
    OnGoldenShellPickedUp(Agent : agent):void =
        # Bots are agents but not players β€” narrow before storing.
        if (P := player[Agent]):
            set GoldenHolder = option{P}

    # THE graceful goodbye. Order matters here:
    # 1) loot back into play, 2) score cleanup, 3) round-end check.
    OnPlayerLeft(Leaver : player):void =
        # Step 1: if the leaver held the golden shell, respawn it
        # so the objective is never stranded in a gone inventory.
        if (Holder := GoldenHolder?, Holder = Leaver):
            set GoldenHolder = false
            GoldenShellSpawner.SpawnItem()
            DropoutMessage.Show()
            Print("Golden shell returned to the beach.")

        # Step 2: drop the leaver's tally from the score map.
        RemoveFromCounts(Leaver)

        # Step 3: never wait on a ghost β€” end the round if only
        # one hunter remains.
        CheckRemainingHunters()

    # Verse maps have no delete β€” rebuild the map without the leaver.
    RemoveFromCounts(Leaver : player):void =
        var Rebuilt : [player]int = map{}
        for (Key -> Value : ShellCounts, Key <> Leaver):
            if (set Rebuilt[Key] = Value) {}
        set ShellCounts = Rebuilt

    # If one player is left, crown them instead of soft-locking.
    CheckRemainingHunters():void =
        Remaining := GetPlayspace().GetPlayers()
        Print("Hunters remaining: {Remaining.Length}")
        if (Remaining.Length = 1, LastHunter := Remaining[0]):
            WinnerScreen.Activate(LastHunter)

Step 3 β€” Read the cleanup order like Coral does

Order Step Why this order
1 GoldenShellSpawner.SpawnItem() The objective item must come back first β€” everything else can wait, but a stranded objective soft-locks the round.
2 RemoveFromCounts(Leaver) Score math (leaderboards, averages, "most shells wins" ties) must stop counting the ghost before anything reads the map.
3 CheckRemainingHunters() Run the end-of-round check last, against the already-cleaned state, so the winner screen reflects reality.

A few Verse details worth pausing on:

  • PlayerRemovedEvent() is a playspace event, so it takes () before .Subscribe β€” device events like ItemPickedUpEvent do not. Mixing these up is the number-one compile error on this lesson.
  • Verse maps have no remove-key call. The proven pattern is to rebuild: loop Key -> Value with a Key <> Leaver filter into a fresh map, then set the variable to the rebuilt map.
  • GoldenHolder : ?player is an option: false means "nobody", option{P} means "P holds it". Unwrapping with GoldenHolder? inside if is failable, which is exactly what we want β€” the whole golden-shell branch simply skips when nobody held it.

Common patterns

Pattern 1 β€” Participant tracking (players and NPCs)

Shell-Hunt's later rounds add AI beachcombers. Human players are players, but AI-controlled characters are agent participants β€” so the round-end check should count participants, and the leave handling should also hear ParticipantRemovedEvent():

using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /Fortnite.com/Game }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

participant_watch_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        # Participants = human players + registered AI agents.
        Everyone := GetPlayspace().GetParticipants()
        Print("Round starts with {Everyone.Length} participants.")

        # Fires for ANY participant leaving β€” human or AI.
        GetPlayspace().ParticipantRemovedEvent().Subscribe(OnParticipantLeft)

    OnParticipantLeft(Gone : agent):void =
        # player[] narrows: succeeds only for humans.
        if (P := player[Gone]):
            Print("A human hunter left the shores.")
        else:
            Print("An AI beachcomber wandered off.")
        Print("Participants remaining: {GetPlayspace().GetParticipants().Length}")

Pattern 2 β€” Await-style leave watching

If you prefer a linear coroutine over a subscription (useful inside a round loop that also races timers), Await() the same event:

using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /Fortnite.com/Game }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

leave_await_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        loop:
            # Suspends here until SOME player leaves, then hands us who.
            Leaver := GetPlayspace().PlayerRemovedEvent().Await()
            Print("Goodbye, hunter. Tidying the beach...")
            if (GetPlayspace().GetPlayers().Length < 2):
                Print("Too few hunters to keep hunting.")
                break

Where this goes next

This is the Shell-Hunt capstone's dropout-resilience piece. When you assemble the full minigame at the end of South Shores, shell_hunt_dropout_device slots in beside the shell-spawning and scoring pieces from the earlier lessons: the capstone's round manager imports the same [player]int tally shape you built here, subscribes its own listeners in OnBegin right next to these, and calls the same rebuild-the-map cleanup whenever PlayerRemovedEvent() fires. Because you handled the golden-shell edge case here, the capstone's win condition can trust two things unconditionally: the objective item is always somewhere reachable, and every entry in the score map belongs to a hunter who is actually still on the beach. That is what a graceful goodbye buys you β€” a round that never waits on someone who already sailed home.

πŸ”’

Keep going β€” free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/detect-player-leave-cleanup
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the 🦩 South Shores (Free) expedition.

β›΅ Take me there