Firefly Cove at Dusk: PlayerAddedEvent and the join lifecycle

In this lesson you'll learn to

  • βœ“Student can handle players joining mid-session (PlayerAddedEvent + GetPlayspace().GetPlayers()) and initialize per-player state.

Student can handle players joining mid-session (PlayerAddedEvent + GetPlayspace().GetPlayers()) and initialize per-player state.

πŸ“– Reference & full walkthrough: Firefly Cove at Dusk: PlayerAddedEvent and the join lifecycle

πŸ” Builds on: south-shores: player spawn pad basics

🧩 Your capstone piece: The race lobby: joiners registered into the Racers roster + assigned a boat dock (content/verse compile-PASSED β€” pin URL was wrongly flagged broken; sibling detect-player-leave also PASSES for the leave path)


Overview

Dusk settles over Firefly Cove, and racers keep trickling in β€” one at match start, two more while the fireflies are still waking up, another right as the countdown clears its throat. Inkbeard the kraken has watched a thousand lobbies from below the pier, and the ones that sink all drown the same way: they assume everyone arrives at once. Fortnite islands are drop-in worlds. Players join mid-session constantly, and any per-player state you build β€” rosters, scores, dock assignments β€” must be initialized for every player, whenever they arrive.

The real Verse API for this is fort_playspace.PlayerAddedEvent() β€” a listenable(player) that fires whenever a human player joins your island. Its mirror, PlayerRemovedEvent(), covers the leave path (that's the sibling lesson, Graceful Goodbyes). But here is the tentacle-grab most creators miss: PlayerAddedEvent only fires for players who join after you subscribe. Anyone already standing on the island when your OnBegin runs will never trigger it. The join lifecycle therefore always has two halves β€” subscribe to the event for future arrivals, and sweep GetPlayspace().GetPlayers() for everyone already aboard.

What you will build

The race lobby registrar for the Coastal Race capstone β€” the piece that guarantees every racer, early bird or latecomer, ends up in the roster with a boat waiting. One creative_device that:

  1. Subscribes to GetPlayspace().PlayerAddedEvent() so mid-session joiners are caught the moment they wash ashore.
  2. Sweeps GetPlayspace().GetPlayers() in OnBegin so players present before the device started are registered too.
  3. Reuses the player spawn pad from South Shores (player_spawner_device.SpawnedEvent) as a third registration path β€” Coral's spawn-pad basics, now working for Inkbeard.
  4. Registers each newcomer into the live Racers roster (the array you built in array add/remove) and assigns them a boat dock β€” a vehicle_spawner_boat_device chosen round-robin along the pier.
  5. Stays idempotent: with three registration paths, the same player can arrive twice β€” RegisterRacer must shrug and skip duplicates.

Walkthrough

Step 1 β€” Place the devices

In UEFN, place on your island:

  • A Player Spawn Pad at the lobby dock (the same device you configured back in South Shores β€” Team/Class filters, Visible In Game off).
  • Three or more Boat Spawners in a line along the pier β€” these are the docks. Set each one's Enabled During Phase to None so Verse controls when a boat appears.
  • A HUD Message device with its message set to something like "Welcome to the Coastal Race! Your boat awaits.".

Step 2 β€” The complete lobby registrar

Create a new Verse file, paste this in, build, and wire the @editable slots in the Details panel (add every dock spawner to the BoatDocks array in pier order):

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

# Coastal Race lobby registrar: every racer who washes into
# Firefly Cove gets a roster entry and a boat dock.
race_lobby_device := class(creative_device):

    # Lobby spawn pad β€” the South Shores basics, reused here.
    @editable
    LobbySpawnPad : player_spawner_device = player_spawner_device{}

    # The dock line: one boat spawner per mooring, in pier order.
    @editable
    BoatDocks : []vehicle_spawner_boat_device = array{}

    # Per-player welcome banner.
    @editable
    WelcomeMessage : hud_message_device = hud_message_device{}

    # The live Racers roster (built in the array add/remove lesson).
    var Racers : []player = array{}

    # Which dock index each racer owns.
    var DockAssignments : [player]int = map{}

    OnBegin<override>()<suspends>:void =
        # 1) Subscribe FIRST, so nobody slips through the gap
        #    between the sweep and the subscription.
        GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerJoined)

        # 2) The spawn pad is a second signal: whoever spawns at
        #    the lobby pad is definitely here to race.
        LobbySpawnPad.SpawnedEvent.Subscribe(OnSpawnedAtLobby)

        # 3) THEN sweep everyone already aboard. PlayerAddedEvent
        #    will never fire for these players β€” they joined
        #    before we subscribed.
        for (ExistingPlayer : GetPlayspace().GetPlayers()):
            RegisterRacer(ExistingPlayer)

    # Fires for every player who joins AFTER OnBegin subscribed.
    OnPlayerJoined(NewRacer : player):void =
        Print("A new racer drifts into Firefly Cove.")
        RegisterRacer(NewRacer)

    # Spawn-pad path: the pad reports an agent, so narrow to player.
    OnSpawnedAtLobby(SpawnedAgent : agent):void =
        if (SpawnedPlayer := player[SpawnedAgent]):
            RegisterRacer(SpawnedPlayer)

    # Idempotent registration: three paths can deliver the same
    # player, so a duplicate must be a harmless no-op.
    RegisterRacer(NewRacer : player):void =
        if (DockAssignments[NewRacer]):
            Print("Racer already registered - skipping.")
        else:
            set Racers += array{NewRacer}
            Slot := Racers.Length - 1
            # Round-robin dock pick: Mod[] and the array index are
            # both failable, so the whole assignment sits in one if.
            if:
                DockIndex := Mod[Slot, BoatDocks.Length]
                Dock := BoatDocks[DockIndex]
                set DockAssignments[NewRacer] = DockIndex
            then:
                Dock.Enable()
                Dock.RespawnVehicle()
                Dock.AssignDriver(NewRacer)
                Print("Racer {Racers.Length} assigned dock {DockIndex + 1}.")
            WelcomeMessage.Show(NewRacer)

Step 3 β€” Read the join lifecycle like Inkbeard does

Piece What it handles Why it exists
PlayerAddedEvent().Subscribe(...) Players who join after OnBegin The event is a function returning listenable(player) β€” note the () before .Subscribe, unlike device events such as SpawnedEvent which are plain fields.
for (P : GetPlayspace().GetPlayers()) Players already present when the device starts PlayerAddedEvent never fires retroactively. Skip this sweep and your first wave of racers has no boats.
LobbySpawnPad.SpawnedEvent Anyone spawning at the lobby pad The South Shores import. Also catches respawns back into the lobby.
duplicate guard in RegisterRacer The same player arriving via two or three paths DockAssignments[NewRacer] is a failable map lookup β€” if it succeeds, the racer is already registered.
Mod[Slot, BoatDocks.Length] Round-robin dock choice Mod[] is failable (division by zero fails), so an empty BoatDocks array degrades gracefully instead of crashing β€” the racer still gets a roster entry and a welcome.

One more habit worth stealing: the subscription happens before the sweep. If you swept first and subscribed second, a player joining in that sliver of time would be missed by both halves. Subscribing first means the worst case is a duplicate registration β€” which your idempotent RegisterRacer already shrugs off.

Common patterns

Pattern 1 β€” Counting NPCs too with ParticipantAddedEvent

PlayerAddedEvent is humans-only. When your lobby mixes players and AI-controlled characters (Inkbeard's kraken has friends), subscribe to ParticipantAddedEvent() instead β€” it fires for any participant agent:

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

participant_watcher_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        GetPlayspace().ParticipantAddedEvent().Subscribe(OnParticipantAdded)

    # Payload is agent, not player β€” narrow when you need a human.
    OnParticipantAdded(NewParticipant : agent):void =
        if (HumanPlayer := player[NewParticipant]):
            Print("A human joined.")
        else:
            Print("An AI participant joined.")

Pattern 2 β€” Closing the lobby once the race starts

Mid-session joiners are welcome in the lobby phase, but once boats launch, latecomers should spectate, not race. Gate registration behind a logic flag that your state machine (a later lesson) flips:

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

gated_lobby_device := class(creative_device):

    @editable
    LateArrivalMessage : hud_message_device = hud_message_device{}

    var LobbyOpen : logic = true

    OnBegin<override>()<suspends>:void =
        GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerJoined)

    OnPlayerJoined(NewPlayer : player):void =
        if (LobbyOpen?):
            Print("Lobby open - registering racer.")
        else:
            # Race already running: greet them, route to spectating.
            LateArrivalMessage.Show(NewPlayer)
            Print("Race in progress - late arrival becomes a spectator.")

    # Called by the race state machine when the countdown ends.
    CloseLobby():void =
        set LobbyOpen = false

Where this goes next

This registrar is lesson 17 of the West Coves ladder, and nearly everything downstream drinks from it:

  • DNF handling (elimination-event) removes eliminated racers from the same Racers roster this device fills.
  • The master state machine (simple-game-state-machine) calls the lobby's close-the-gates moment (Pattern 2) when Lobby β†’ Countdown transitions.
  • The Coastal Race capstone (west-coves-coastal-race) wires it all together: joiners registered here get a boat, race the Mangrove Channel past Inkbeard's patrol, and β€” if they leave mid-race β€” the sibling lesson's PlayerRemovedEvent cleanup keeps the fleet honest.

And the import ledger, paid in full: the South Shores spawn pad (player_spawner_device) you learned with Coral is doing grown-up work here as a registration signal β€” proof that beginner devices never stop being useful, they just get better co-workers.

πŸ”’

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-join
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 πŸ¦‘ West Coves expedition.

β›΅ Take me there