Multiplayer UI button issue.


Hey Everyone, I am having problems with UI working for single player instead of multiplayers. So I’m almost finished with a project I’m working in UEFN and the problem that I’m trying to fix is that each player presses the UI button, and the game starts and leaving the other players be stuck in the dialog until pressing the UI button to get started in the middle of the game. So I have a video example of the issue, and let me know if y’all have any helpful thoughts, tips, advices. It would be really helpful, because I’ve tried figuring out what to make it work, but nothing’s working.

Hello @NoahD1 how are you?

It seems that when a player clicks on the button, it starts the match for every player but it only removes the UI for the player that clicked the button.

This problem should be solved if you use a “for” to disable the UI for every player that is currently in your island when anybody click on the button.

Other thing you can do, is to check if all the players already clicked the button before starting the match.

If you need more help, please share your code here, so we can check it and make the proper corrections for you!

1 Like

Hello @BRGJuanCruzMK, I still have the same problem after using for in verse and it’s still not working for multiplayers. But the good thing is it works for one player, but not for other players that are joining the private playtest session and even includes a public release game, and I still need more help. So I’ll share the playtest code from Fortnite Creator Portal, and thank you so much for tips.

Here’s the code:

3780-3345-8791

1 Like

Hello again @NoahD1 !

When I said “share your code” I meant the Verse code you are using for this feature, so I can check where the error is and fix it for you.

I understand the problem you are having, but I cannot help if I don’t know your Verse code.

I’ll be waiting for it!

Oh, I’m sorry about that, man. I thought you mean like Island code or playtest code, but my bad. I’ll go ahead and share the verse code. Well, I have nine of them in my island, six of them are creative devices and three of them are NPC behaviors.

Here’s two verse code scripts:

title_screen_handler script:

using { /Fortnite.com/Devices }

using { /Verse.org/Simulation }

using { /UnrealEngine.com/Temporary/Diagnostics }

# Device to handle the title screen dialog logic and respond to button presses

title_screen_handler := class(creative_device):

\# Editable reference to the main title screen dialog device

@editable

TitleScreenDialog : popup_dialog_device = popup_dialog_device{}



\# Editable reference to the tower defense dialog device

@editable

TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}



\# Editable reference to the end game device

@editable

EndGameDevice : end_game_device = end_game_device{}



@editable

Player1Spawner : player_spawner_device = player_spawner_device{}

@editable

Player2Spawner : player_spawner_device = player_spawner_device{}

@editable

Player3Spawner : player_spawner_device = player_spawner_device{}

@editable

Player4Spawner : player_spawner_device = player_spawner_device{}



\# Indices for dialog buttons

StartGameButtonIndex : int = 0

ExitGameButtonIndex : int = 1



\# Track whether the title screen dialog has been shown already

var TitleScreenShown : logic = false

var TowerDefenseDialogShown : \[agent\]logic = map{}



\# Subscribe to button press events from the TitleScreenDialog when the device starts

OnBegin<override>()<suspends>:void=

    if (TitleScreenShown = false):

        TitleScreenDialog.Show() # Show only once

        set TitleScreenShown = true

        TitleScreenDialog.RespondingButtonEvent.Subscribe(OnTitleButtonPressed)



\# Handle button presses in the title screen dialog

OnTitleButtonPressed(Data:tuple(agent, int)):void=

    Agent := Data(0)

    ButtonIndex := Data(1)

    if (ButtonIndex = 0):

        \# Close the dialog for the player who pressed the button

        TitleScreenDialog.Hide(Agent)



    \# Show TowerDefenseDialog only from Start Game button

    if (ButtonIndex = StartGameButtonIndex):

        Player1Spawner.Enable()

        Player2Spawner.Enable()

        Player3Spawner.Enable()

        Player4Spawner.Enable()

        if (not TowerDefenseDialogShown\[Agent\]?):

            Print("Start_Game_Button pressed - enabling and showing TowerDefenseDialog for agent.")

            TowerDefenseDialog.Enable()

            TowerDefenseDialog.Show(Agent)

            Print("Start_Game_Button - TowerDefenseDialog enabled/shown.")

            if (set TowerDefenseDialogShown\[Agent\] = true) {}

            \# Enable all referenced player spawners

            PlayerSpawners : \[\]player_spawner_device = array{

                Player1Spawner,

                Player2Spawner,

                Player3Spawner,

                Player4Spawner

            }

            for (Spawner : PlayerSpawners):

                Spawner.Enable()

        else:

            Print("TowerDefenseDialog already shown to this agent. Not showing again.")



    \# Handle exit button

    else if (ButtonIndex = ExitGameButtonIndex):

        EndGameDevice.Enable()

        EndGameDevice.Activate(Agent)

        Print("Exit_Game_Button pressed - EndGameDevice enabled.")



HideAllDialogs():void=

    Players := GetPlayspace().GetPlayers()

    for (Player : Players):

        TitleScreenDialog.Hide(Player)

tower_defesne_manager script:

using { /Fortnite.com/Devices }

using { /Fortnite.com/Characters }

using { /Verse.org/Simulation }

using { /UnrealEngine.com/Temporary/Diagnostics }

using { /UnrealEngine.com/Temporary/SpatialMath }

using { /UnrealEngine.com/Temporary/UI }

using { /Fortnite.com/UI }

# Persistable data structure for round stats

round_data := class:

RoundNumber : int = 1

PointScore : int = 0

CoinScore : int = 500

# Device to manage tower defense rounds and settings

tower_defense_manager := class(creative_device):

\# Dialogs & Round infrastructure

@editable

TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}

@editable

GateSequencer : cinematic_sequence_device = cinematic_sequence_device{}



\# NPC Spawners

@editable

FastNPCSpawner : npc_spawner_device = npc_spawner_device{}

@editable

MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}

@editable

StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}



\# End round

@editable

EndRound : end_game_device = end_game_device{}



\# Stats

@editable

PointsStat : stat_creator_device = stat_creator_device{}

@editable

CoinsStat : stat_creator_device = stat_creator_device{}



@editable

RoundTimer : timer_device = timer_device{}



@editable

FixedPointCamera : gameplay_camera_fixed_point_device = gameplay_camera_fixed_point_device{}

@editable

ThirdPersonControls : gameplay_controls_third_person_device = gameplay_controls_third_person_device{}

@editable

Objective : objective_device = objective_device{}



@editable

Player1Spawner : player_spawner_device = player_spawner_device{}

@editable

Player2Spawner : player_spawner_device = player_spawner_device{}

@editable

Player3Spawner : player_spawner_device = player_spawner_device{}

@editable

Player4Spawner : player_spawner_device = player_spawner_device{}



@editable

Rounds : \[\]round_settings_device = array{}



\# Round timer and logic

var CurrentRound : int = 1

TotalRounds : int = 30

RoundDuration : float = 300.0 # seconds (5 min)

var RoundActive : logic = false



var IsRoundActive : logic = false



var DialogShown : logic = false

var RoundStarted : logic = false

var DialogSubscription : ?cancelable = false



\# Persistent storage for player round data

var PlayerRoundData : weak_map(player, round_data) = map{}



\# Array of round settings devices for 30 rounds

var RoundSettings : \[\]round_settings_device = array{}



var CurrentRoundIndex : int = 0

var AgentsInZone : \[agent\]logic = map{}

var PlayerDialogSubscriptions : \[player\]cancelable = map{}

var ActiveDialogPlayer : ?player = false



OnBegin<override>()<suspends>:void=

    \# Start: disable all spawners (idle)

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    \# Subscribe to the 1Player spawner's spawn event

    Player1Spawner.SpawnedEvent.Subscribe(OnPlayer1Spawned)

    Player2Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    Player3Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    Player4Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    \# Subscribe to dialog button interaction

    TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)

    \# Cancel and hide timer at game start—do this FIRST no matter what!

    Self.RoundTimer.Reset()

    Self.RoundTimer.Disable()

    set Self.DialogSubscription = option{Self.TowerDefenseDialog.RespondingButtonEvent.Subscribe(Self.OnDialogButtonPressed)}

    \# Notice: No call to Self.TowerDefenseDialog.Show() or Enable() here

    \# Keep Player1 enabled, and leave Player2, Player3, and Player4 disabled until the game starts.

    Self.Player1Spawner.Enable()

    Self.Player2Spawner.Disable()

    Self.Player1Spawner.Disable()

    Self.Player1Spawner.Disable()



OnPlayer1Spawned(Player: agent): void =

    \# Show dialog and subscribe only for the player spawned from 1Player spawner

    if (FirstPlayer := player\[Player\]):

        TowerDefenseDialog.Show(FirstPlayer)

        Subscription := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)

        if (set PlayerDialogSubscriptions\[FirstPlayer\] = Subscription) {}

        set ActiveDialogPlayer = option{FirstPlayer}

        Print("Waiting for round start...")



OnOtherPlayerSpawned(Player: agent): void =

    Print("Player from non-priority spawner joined, dialog not shown.")

    \# No dialog/subscription for 2/3/4Player



OnDialogButtonPressed(Agent:agent, ButtonIndex:int):void =

    if (ActivePlayer := ActiveDialogPlayer?, Agent = ActivePlayer, ButtonIndex = 0):

        if (ButtonIndex = 0 and Self.RoundStarted = false):

            set Self.RoundStarted = true

            if (DialogSub := Self.DialogSubscription?):

                DialogSub.Cancel()

            set Self.DialogSubscription = false

            \# Dismiss dialog and clean up only for the active player

            \# Only the 1Player spawner player can trigger start logic!

           if (Subscription := PlayerDialogSubscriptions\[ActivePlayer\]):

                Subscription.Cancel()

                TowerDefenseDialog.Hide(ActivePlayer)

            set PlayerDialogSubscriptions = map{}

            set ActiveDialogPlayer = false

            Print("Game Started!")

            \# Proceed with starting the game...

            \# Switch camera back to Fortnite's default third-person view

            Self.FixedPointCamera.Disable()

            Self.ThirdPersonControls.Enable()

            Self.RoundTimer.Enable()

            Self.RoundTimer.Reset()

            Self.RoundTimer.Start()

            \# Reset camera for all players

            for (Player : GetPlayspace().GetPlayers()):

                TowerDefenseDialog.Show(Player)

                DialogSub := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)

                if (set PlayerDialogSubscriptions\[Player\] = DialogSub) {}

                    FixedPointCamera.Disable()

                    spawn{HandleTimerExpiration()}

                    TowerDefenseDialog.Hide(Player)

                set PlayerDialogSubscriptions = map{}

                \# Proceed with starting the game...



            for (Player -> DialogSub : PlayerDialogSubscriptions):

                if (DialogSubscription?):

                    if (Sub := DialogSubscription?):

                        Sub.Cancel()             # Cancel this player's dialog event subscription

                TowerDefenseDialog.Hide(Player)  # Hide the dialog for this player



            set PlayerDialogSubscriptions = map{} # Clear the map afterwards



            \# Create 30 round settings devices and store them in the array

            set RoundSettings = for (Index := 0..29):

                round_settings_device{}



            \# Example: Configure each round's settings

            for (RoundIndex -> RoundDevice : RoundSettings):

                \# Set up round-specific settings here

                \# For example, you could enable/disable certain features per round

                \# or configure round-specific rules

                Print("Configured round {RoundIndex + 1}")



\# Example function to get a specific round's settings

GetRoundDevice(RoundIndex : int) : void =

    if (RoundDevice := RoundSettings\[RoundIndex\]):

        RoundDevice



\# Call this when a round ends

HandleRoundEnd() : void =

    for (Player : GetPlayspace().GetPlayers()):

        if (RoundDevice := RoundSettings\[CurrentRoundIndex\]):

            Players := GetPlayspace().GetPlayers()

            if (FirstPlayer := Players\[0\]):

                RoundDevice.EndRound(FirstPlayer) # Pass agent type!

       

HandleTimerExpiration()<suspends>:void =

    Self.RoundTimer.SuccessEvent.Await()

    for (Player : Self.GetPlayspace().GetPlayers()):

        Self.EndRound.Activate(Player)

    Self.RoundTimer.Reset()

    Self.RoundTimer.Disable()

    \# Implementation for timer expiration handling

    Print("Timer expired")



    Players := Self.GetPlayspace().GetPlayers()

    if (Players.Length > 0):

        \# Start the configured round timer (automatically shows on player HUD)

        Self.RoundTimer.Start()

        \# Wait for the timer to complete

        Self.RoundTimer.SuccessEvent.Await()

        \# After the timer finishes, end the round for all players

        for (Player : Players):

            Self.EndRound.Activate(Player)

    else:

        Print("Can't find player")



\# Initialize or load persistent data for a player

InitializePlayerData(Player:player):void =

    if (ExistingData := PlayerRoundData\[Player\]):

        \# Data already exists, do nothing

        Print("Loaded existing data for player")

    else:

        \# Create new data with default values

        NewData := round_data{}

        if (set PlayerRoundData\[Player\] = NewData):

            Print("Initialized new data for player")



\# Save updated round data for a player

SavePlayerRoundData(Player:player):void =

    if (CurrentData := PlayerRoundData\[Player\]):

        \# Create new data instance with updated values

        NewData := round_data{

            RoundNumber := CurrentData.RoundNumber + 1,

            PointScore := CurrentData.PointScore + 100,

            CoinScore := CurrentData.CoinScore + 50

        }

        if (set PlayerRoundData\[Player\] = NewData):

            Print("Saved updated data for player")



OnTowerDefenseButtonPressed(Data:tuple(agent, int)):void=

    ButtonIndex := Data(1)

    case(ButtonIndex):

        \# 0: Start round, enable all spawners, move players, keep dialog shown

        0 => HandleStartRound()

        \_ => Print("Unknown button pressed")



HandleStartRound():void=

    if (CurrentRound > TotalRounds):

        Print("All rounds complete.")

        return

    if (RoundActive = false):

        set RoundActive = true

        FastNPCSpawner.Enable()

        MediumNPCSpawner.Enable()

        StrongNPCSpawner.Enable()

        FastNPCSpawner.Spawn()

        MediumNPCSpawner.Spawn()

        StrongNPCSpawner.Spawn()

        GateSequencer.Play()

        Print("Round {CurrentRound} started.")

        \# Move all players to new location & keep dialog shown

        for (Player : GetPlayspace().GetPlayers()):

            if (Character := Player.GetFortCharacter\[\]):

                TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}

                if (Character.TeleportTo\[TargetLocation, rotation{}\]):

                    Print("Moved player to target location at round start.")

        \# Start timer for this round

        spawn{RoundTimerLoop()}



\# Each round lasts 5 min, up to 30 rounds; stops spawners & activates EndRound each time.

RoundTimerLoop()<suspends>:void=

    Print("Started round timer for {RoundDuration} seconds.")

    Sleep(RoundDuration)

    set RoundActive = false

    \# Stop spawners

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    Print("Round {CurrentRound} ended.")

    \# Activate EndRound device for all players

    EndRound.Enable()



    for (Player : GetPlayspace().GetPlayers()):

        if (CurrentRoundIndex = 29):

            EndRound.Activate(Player)

    \# RoundSettings: notify round end

    for (Player : GetPlayspace().GetPlayers()):

        if (RoundDevice := RoundSettings\[CurrentRoundIndex\]):

            RoundDevice.EndRound(Player) # Pass Player (agent)

    set CurrentRound += 1

No worries, I didn’t calrify it haha

I’ll be checking your code and reach you again when I have a solution! :smiley:

Sound good, thank you so much dude. :+1:

Hey @NoahD1 how are you?

I’ve been trying to fix this issue but I realized that I don’t understand the flow the game is trying to follow.

Currently, you have the Main Menu, with Start Game and Exit Game buttons. When any player press “Start Game”, the main manu is closed and a new menu with only a “Start Round” button.

Pressing the “Start Game” button on the main menu also starts the round, and that is one of the problems, right?

I assume you want to start the round after everybody is ready (all the players pressed Start Game), is that correct?

I also assume that your don’t want to be able to start the round if there is any “unready” player, do you?

Another thing I’d like to understand is if you want all the players to be able to start the round or onlye the Player 1.

And the last thing, are you sure you need a second “Start” button? Maybe you can start a timer and start the round automatically when everybody is ready.

I’ll be waiting for your answers to keep working on this!

So the game is a tower defense game, it’s where players have to be on the turrets to shot the enemies before reaching the precious item, and yes, I want all players to be able to press the button widget instead of having each individual players press the button widget after one of the players presses it first. Oh, and another thing, I can’t seem to find what’s enabling the exit game button to pop up TowerDefenseDialog, which is the main menu while it ends the game. So I was wondering if you can help me with that problem too? I’ll share the video example of the different issue. Thank you so much for trying. Oh, and I sorry if I haven’t answer back to you.

Hey @NoahD1 hope you are doing great!

I’ve been working on this for the last two days and I did a couple of things to try to solve the problem. Lets start with the main issue!

This is the list of things I did:

  1. Added new screen between title and round start
  2. New management to “show” and “hide” screens for each player, not using “HideAllDialogs” function
  3. Added a map and a counter variable to know how many players are ready
  4. Showing “Start Round” screen only if all the players are ready
  5. All the screens are managed inside the “title_screen_handler” device, avoiding duplicates (for example “TowerDefenseDialog”)
  6. “title_screen_handler” decides which screen is shown at any moment, instead of managing “TowerDefenseDialog” also on “tower_defense_manager”
  7. To achieve that, I added a reference to the “title_screen_handler” on the “tower_defense_manager”, allowing me to use the variables from the first class (I added “ScreensHandler.” at the front of each “TowerDefenseDialog” calling)
  8. I fixed some typos on the spawner variables

With this changes, the flow for starting a new game changes a little bit! When you press “Start Game” (I would change it by “Ready” instead) in the Title Screen, you go to a new screen (you should create a new popup message device for this with a “Cancel” or “Unready” button) that should say something like “Waiting for other players to be ready.” and if you click on its only button, you will be back to title screen.
I’m using a variable to check how many players are ready, and when all players are ready, the “TowerDefenseDialog” spawns, allowing any player to start the game immediately.

This is the code for “title_screen_handler”:

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

# Device to handle the title screen dialog logic and respond to button presses
title_screen_handler := class(creative_device):

    # Editable reference to the main title screen dialog device
    @editable
    TitleScreenDialog : popup_dialog_device = popup_dialog_device{}
    @editable
    ReadyDialog : popup_dialog_device = popup_dialog_device{}

    # Editable reference to the tower defense dialog device
    @editable
    TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}

    # Editable reference to the end game device
    @editable
    EndGameDevice : end_game_device = end_game_device{}
    @editable
    Player1Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player2Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player3Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player4Spawner : player_spawner_device = player_spawner_device{}

    # Indices for dialog buttons
    StartGameButtonIndex : int = 0
    ExitGameButtonIndex : int = 1

    # Track whether the title screen dialog has been shown already
    #var TitleScreenShown : logic = false
    var TowerDefenseDialogShown : [agent]logic = map{}

    # Track players ready
    var PlayersReady : [agent]logic = map{}
    var ReadyCounter : int = 0


    # Subscribe to button press events from the TitleScreenDialog when the device starts
    OnBegin<override>()<suspends>:void=
        #if (TitleScreenShown = false):
            TitleScreenDialog.Show() # Show only once
            #set TitleScreenShown = true
            TitleScreenDialog.RespondingButtonEvent.Subscribe(OnTitleButtonPressed)
            ReadyDialog.RespondingButtonEvent.Subscribe(OnReadyDialogButtonPressed)
        
        for ( Player : GetPlayspace().GetPlayers() ):
            InitializePlayersMap(Player)

    
    InitializePlayersMap( Player : agent ) : void =
        # Store player and initialize it "false"
        if ( set PlayersReady[ Player ] = false ):

    # Handle button presses in the title screen dialog
    OnTitleButtonPressed(Data:tuple(agent, int)):void=
        Agent := Data(0)
        ButtonIndex := Data(1)
        if (ButtonIndex = 0):
            # Close the dialog for the player who pressed the button
            # TitleScreenDialog.Hide(Agent)
            #HideAllDialogs()
            TitleScreenDialog.Hide(Agent)
            
            if(set PlayersReady[Agent] = true):
            set ReadyCounter += 1

            if ( ReadyCounter >= PlayersReady.Length ):
                Print("All Players Ready")
            # Show TowerDefenseDialog only from Start Game button
            #if (ButtonIndex = StartGameButtonIndex):
                Player1Spawner.Enable()
                Player2Spawner.Enable()
                Player3Spawner.Enable()
                Player4Spawner.Enable()
                #if (not TowerDefenseDialogShown[Agent]?):
                    #Print("Start_Game_Button pressed - enabling and showing TowerDefenseDialog for agent.")
                    ReadyDialog.Hide() # Hide ready dialog for all the ready players
                    #TowerDefenseDialog.Enable()
                    TowerDefenseDialog.Show() # Show tower defense dialog for all the players so any player can start the round
                    #Print("Start_Game_Button - TowerDefenseDialog enabled/shown.")
                    #if (set TowerDefenseDialogShown[Agent] = true) {}
                    # Enable all referenced player spawners
                    #PlayerSpawners : []player_spawner_device = array{
                        #Player1Spawner,
                        #Player2Spawner,
                        #Player3Spawner,
                        #Player4Spawner
                    #}
                    #for (Spawner : PlayerSpawners):
                        #Spawner.Enable()
                    
                #else:
                    #Print("TowerDefenseDialog already shown to this agent. Not showing again.")
            else:
                Print("Players Ready: {ReadyCounter}")
                ReadyDialog.Show(Agent) # Show Ready screen to the player that clicked "Ready"/"Start Game" button

        # Handle exit button
        else if (ButtonIndex = ExitGameButtonIndex):
            EndGameDevice.Enable()
            EndGameDevice.Activate(Agent)
            Print("Exit_Game_Button pressed - EndGameDevice enabled.")

    
    # Function to go back to title screen from the Ready screen
    OnReadyDialogButtonPressed(Data:tuple(agent, int)):void=
        Agent := Data(0)
        ButtonIndex := Data(1)

        if (ButtonIndex = 0):
            # Set the readyness to false and discount one ready player from the tracker variable
            if(set PlayersReady[Agent] = false):
            set ReadyCounter -= 1
            Print("Players Ready: {ReadyCounter}")
            # Back to title screen
            ReadyDialog.Hide(Agent)
            TitleScreenDialog.Show(Agent)

    HideAllDialogs():void=
        Players := GetPlayspace().GetPlayers()
        for (Player : Players):
            TitleScreenDialog.Hide(Player)

And this is the code for “tower_defense_manager”:

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }

# Persistable data structure for round stats
round_data := class:
    RoundNumber : int = 1
    PointScore : int = 0
    CoinScore : int = 500

# Device to manage tower defense rounds and settings
tower_defense_manager := class(creative_device):

    # Reference to the Title Screen Manager custom device
    ScreensHandler : title_screen_handler = title_screen_handler{}
    
    # Dialogs & Round infrastructure
    #@editable
    #TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}
    @editable
    GateSequencer : cinematic_sequence_device = cinematic_sequence_device{}

    # NPC Spawners
    @editable
    FastNPCSpawner : npc_spawner_device = npc_spawner_device{}
    @editable
    MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}
    @editable
    StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}

    # End round
    @editable
    EndRound : end_game_device = end_game_device{}

    # Stats
    @editable
    PointsStat : stat_creator_device = stat_creator_device{}
    @editable
    CoinsStat : stat_creator_device = stat_creator_device{}
    
    @editable
    RoundTimer : timer_device = timer_device{}
    @editable
    FixedPointCamera : gameplay_camera_fixed_point_device = gameplay_camera_fixed_point_device{}
    @editable
    ThirdPersonControls : gameplay_controls_third_person_device = gameplay_controls_third_person_device{}
    @editable
    Objective : objective_device = objective_device{}
    @editable
    Player1Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player2Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player3Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player4Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Rounds : []round_settings_device = array{}


    # Round timer and logic
    var CurrentRound : int = 1
    TotalRounds : int = 30
    RoundDuration : float = 300.0 # seconds (5 min)
    var RoundActive : logic = false
    var IsRoundActive : logic = false
    var DialogShown : logic = false
    var RoundStarted : logic = false
    var DialogSubscription : ?cancelable = false

    # Persistent storage for player round data
    var PlayerRoundData : weak_map(player, round_data) = map{}

    # Array of round settings devices for 30 rounds
    var RoundSettings : []round_settings_device = array{}
    var CurrentRoundIndex : int = 0
    var AgentsInZone : [agent]logic = map{}
    var PlayerDialogSubscriptions : [player]cancelable = map{}
    var ActiveDialogPlayer : ?player = false


    OnBegin<override>()<suspends>:void=
        
        # Start: disable all spawners (idle)
        FastNPCSpawner.Disable()
        MediumNPCSpawner.Disable()
        StrongNPCSpawner.Disable()

        # Subscribe to the 1Player spawner's spawn event
        Player1Spawner.SpawnedEvent.Subscribe(OnPlayer1Spawned)
        Player2Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)
        Player3Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)
        Player4Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

        # Subscribe to dialog button interaction
        #TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)
        ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)

        # Cancel and hide timer at game start—do this FIRST no matter what!
        Self.RoundTimer.Reset()
        Self.RoundTimer.Disable()
        set Self.DialogSubscription = option{ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(Self.OnDialogButtonPressed)}

        # Notice: No call to Self.TowerDefenseDialog.Show() or Enable() here
        # Keep Player1 enabled, and leave Player2, Player3, and Player4 disabled until the game starts.
        Self.Player1Spawner.Enable()
        Self.Player2Spawner.Disable()
        Self.Player3Spawner.Disable()# previously Self.Player1Spawner.Disable()
        Self.Player4Spawner.Disable()# previously Self.Player1Spawner.Disable()


    OnPlayer1Spawned(Player: agent): void =

        # Show dialog and subscribe only for the player spawned from 1Player spawner
        if (FirstPlayer := player[Player]):
            ScreensHandler.TowerDefenseDialog.Show(FirstPlayer)
            Subscription := ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)
            if (set PlayerDialogSubscriptions[FirstPlayer] = Subscription) {}
            set ActiveDialogPlayer = option{FirstPlayer}
            Print("Waiting for round start...")


    OnOtherPlayerSpawned(Player: agent): void =
        Print("Player from non-priority spawner joined, dialog not shown.")
        # No dialog/subscription for 2/3/4Player


    OnDialogButtonPressed(Agent:agent, ButtonIndex:int):void =
        if (ActivePlayer := ActiveDialogPlayer?, Agent = ActivePlayer, ButtonIndex = 0):
            if (ButtonIndex = 0 and Self.RoundStarted = false):
                set Self.RoundStarted = true
                if (DialogSub := Self.DialogSubscription?):
                    DialogSub.Cancel()
                set Self.DialogSubscription = false
                # Dismiss dialog and clean up only for the active player

                # Only the 1Player spawner player can trigger start logic!
            if (Subscription := PlayerDialogSubscriptions[ActivePlayer]):
                    Subscription.Cancel()
                    ScreensHandler.TowerDefenseDialog.Hide(ActivePlayer)
                set PlayerDialogSubscriptions = map{}
                set ActiveDialogPlayer = false
                Print("Game Started!")
                # Proceed with starting the game...

                # Switch camera back to Fortnite's default third-person view
                Self.FixedPointCamera.Disable()
                Self.ThirdPersonControls.Enable()
                Self.RoundTimer.Enable()
                Self.RoundTimer.Reset()
                Self.RoundTimer.Start()

                # Reset camera for all players
                for (Player : GetPlayspace().GetPlayers()):
                    ScreensHandler.TowerDefenseDialog.Show(Player)
                    DialogSub := ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)
                    if (set PlayerDialogSubscriptions[Player] = DialogSub) {}
                        FixedPointCamera.Disable()
                        spawn{HandleTimerExpiration()}
                        ScreensHandler.TowerDefenseDialog.Hide(Player)
                    set PlayerDialogSubscriptions = map{}
                    
                # Proceed with starting the game...
                for (Player -> DialogSub : PlayerDialogSubscriptions):
                    if (DialogSubscription?):
                        if (Sub := DialogSubscription?):
                            Sub.Cancel()             # Cancel this player's dialog event subscription
                    ScreensHandler.TowerDefenseDialog.Hide(Player)  # Hide the dialog for this player
                set PlayerDialogSubscriptions = map{} # Clear the map afterwards

                # Create 30 round settings devices and store them in the array
                set RoundSettings = for (Index := 0..29):
                    round_settings_device{}


                # Example: Configure each round's settings
                for (RoundIndex -> RoundDevice : RoundSettings):
                    # Set up round-specific settings here
                    # For example, you could enable/disable certain features per round
                    # or configure round-specific rules
                    Print("Configured round {RoundIndex + 1}")

    # Example function to get a specific round's settings
    GetRoundDevice(RoundIndex : int) : void =
        if (RoundDevice := RoundSettings[RoundIndex]):
            RoundDevice


    # Call this when a round ends
    HandleRoundEnd() : void =
        for (Player : GetPlayspace().GetPlayers()):
            if (RoundDevice := RoundSettings[CurrentRoundIndex]):
                Players := GetPlayspace().GetPlayers()
                if (FirstPlayer := Players[0]):
                    RoundDevice.EndRound(FirstPlayer) # Pass agent type!


    HandleTimerExpiration()<suspends>:void =
        Self.RoundTimer.SuccessEvent.Await()
        for (Player : Self.GetPlayspace().GetPlayers()):
            Self.EndRound.Activate(Player)
        Self.RoundTimer.Reset()
        Self.RoundTimer.Disable()

        # Implementation for timer expiration handling
        Print("Timer expired")

        Players := Self.GetPlayspace().GetPlayers()
        if (Players.Length > 0):
            # Start the configured round timer (automatically shows on player HUD)
            Self.RoundTimer.Start()
            # Wait for the timer to complete
            Self.RoundTimer.SuccessEvent.Await()
            # After the timer finishes, end the round for all players
            for (Player : Players):
                Self.EndRound.Activate(Player)
        else:
            Print("Can't find player")


    # Initialize or load persistent data for a player
    InitializePlayerData(Player:player):void =
        if (ExistingData := PlayerRoundData[Player]):
            # Data already exists, do nothing
            Print("Loaded existing data for player")
        else:
            # Create new data with default values
            NewData := round_data{}
            if (set PlayerRoundData[Player] = NewData):
                Print("Initialized new data for player")


    # Save updated round data for a player
    SavePlayerRoundData(Player:player):void =
        if (CurrentData := PlayerRoundData[Player]):
            # Create new data instance with updated values
            NewData := round_data{
                RoundNumber := CurrentData.RoundNumber + 1,
                PointScore := CurrentData.PointScore + 100,
                CoinScore := CurrentData.CoinScore + 50
            }
            if (set PlayerRoundData[Player] = NewData):
                Print("Saved updated data for player")


    OnTowerDefenseButtonPressed(Data:tuple(agent, int)):void=
        ButtonIndex := Data(1)
        case(ButtonIndex):
            # 0: Start round, enable all spawners, move players, keep dialog shown
            0 => HandleStartRound()
            _ => Print("Unknown button pressed")


    HandleStartRound():void=
        if (CurrentRound > TotalRounds):
            Print("All rounds complete.")
            return

        if (RoundActive = false):
            set RoundActive = true
            FastNPCSpawner.Enable()
            MediumNPCSpawner.Enable()
            StrongNPCSpawner.Enable()
            FastNPCSpawner.Spawn()
            MediumNPCSpawner.Spawn()
            StrongNPCSpawner.Spawn()
            GateSequencer.Play()
            Print("Round {CurrentRound} started.")
            # Move all players to new location & keep dialog shown
            for (Player : GetPlayspace().GetPlayers()):
                if (Character := Player.GetFortCharacter[]):
                    TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}
                    if (Character.TeleportTo[TargetLocation, rotation{}]):
                        Print("Moved player to target location at round start.")

            # Start timer for this round
            spawn{RoundTimerLoop()}


    # Each round lasts 5 min, up to 30 rounds; stops spawners & activates EndRound each time.
    RoundTimerLoop()<suspends>:void=
        Print("Started round timer for {RoundDuration} seconds.")
        Sleep(RoundDuration)
        set RoundActive = false

        # Stop spawners
        FastNPCSpawner.Disable()
        MediumNPCSpawner.Disable()
        StrongNPCSpawner.Disable()
        Print("Round {CurrentRound} ended.")

        # Activate EndRound device for all players
        EndRound.Enable()

        for (Player : GetPlayspace().GetPlayers()):
            if (CurrentRoundIndex = 29):
                EndRound.Activate(Player)

        # RoundSettings: notify round end
        for (Player : GetPlayspace().GetPlayers()):
            if (RoundDevice := RoundSettings[CurrentRoundIndex]):
                RoundDevice.EndRound(Player) # Pass Player (agent)

        set CurrentRound += 1

As you can see, I didn’t delete any part of your code, only commented it, so you can see what I’m replacing and/or deleting to make it work!

One additional comment: I’d use the same “ready” logic before each round instead of using the “TowerDefenseDialog” to start it. I mean waiting for all players to be ready and then start the round automatically after a few secconds. It could be more clear and and feel more “natural”.

Please, test it and let me know if you find any issues with it!

1 Like

Sweet, I’ll go ahead and test it and let you know if there are issues. But thank you so much for the help, dude. Super appreciated. :+1:

1 Like

Hey Dude, I found an issue after I’ve made a new dialog widget for ReadyDialog and tested it. So what I have in the video is where I press the start game button and the ReadyDialog has been popped up for one second and then after press start round button I’m stuck in the fixed point camera perspective mode.

1 Like

Hey @NoahD1 how are you doing?

Sorry for my error with that!

I’ve been testing and, aparently, you cannot save subscriptions of a device from another Class. I’ve reverted that change on the following script!

You should replace this and add the start round popup dialog device to the “tower_defense_manager” again.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }

# Persistable data structure for round stats
round_data := class:
    RoundNumber : int = 1
    PointScore : int = 0
    CoinScore : int = 500

# Device to manage tower defense rounds and settings
tower_defense_manager := class(creative_device):

    # Reference to the Title Screen Manager custom device
    ScreensHandler : title_screen_handler = title_screen_handler{}
    
    # Dialogs & Round infrastructure
    @editable
    TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}
    #@editable
    #GateSequencer : cinematic_sequence_device = cinematic_sequence_device{}

    # NPC Spawners
    #@editable
    #FastNPCSpawner : npc_spawner_device = npc_spawner_device{}
    #@editable
    #MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}
    #@editable
    #StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}

    # End round
    @editable
    EndRound : end_game_device = end_game_device{}

    # Stats
    #@editable
    #PointsStat : stat_creator_device = stat_creator_device{}
    #@editable
    #CoinsStat : stat_creator_device = stat_creator_device{}
    
    @editable
    RoundTimer : timer_device = timer_device{}
    @editable
    FixedPointCamera : gameplay_camera_fixed_point_device = gameplay_camera_fixed_point_device{}
    @editable
    ThirdPersonControls : gameplay_controls_third_person_device = gameplay_controls_third_person_device{}
    @editable
    Objective : objective_device = objective_device{}
    @editable
    Player1Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player2Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player3Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Player4Spawner : player_spawner_device = player_spawner_device{}
    @editable
    Rounds : []round_settings_device = array{}


    # Round timer and logic
    var CurrentRound : int = 1
    TotalRounds : int = 30
    RoundDuration : float = 300.0 # seconds (5 min)
    var RoundActive : logic = false
    var IsRoundActive : logic = false
    var DialogShown : logic = false
    var RoundStarted : logic = false
    var DialogSubscription : ?cancelable = false

    # Persistent storage for player round data
    var PlayerRoundData : weak_map(player, round_data) = map{}

    # Array of round settings devices for 30 rounds
    var RoundSettings : []round_settings_device = array{}
    var CurrentRoundIndex : int = 0
    var AgentsInZone : [agent]logic = map{}
    var PlayerDialogSubscriptions : [player]cancelable = map{}
    var ActiveDialogPlayer : ?player = false


    OnBegin<override>()<suspends>:void=
        
        # Start: disable all spawners (idle)
        #FastNPCSpawner.Disable()
        #MediumNPCSpawner.Disable()
        #StrongNPCSpawner.Disable()

        # Subscribe to the 1Player spawner's spawn event
        Player1Spawner.SpawnedEvent.Subscribe(OnPlayer1Spawned)
        Player2Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)
        Player3Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)
        Player4Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

        # Subscribe to dialog button interaction
        TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)
        #ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)

        # Cancel and hide timer at game start—do this FIRST no matter what!
        Self.RoundTimer.Reset()
        Self.RoundTimer.Disable()
        set Self.DialogSubscription = option{#ScreensHandler.
        TowerDefenseDialog.RespondingButtonEvent.Subscribe(Self.OnDialogButtonPressed)}

        # Notice: No call to Self.TowerDefenseDialog.Show() or Enable() here
        # Keep Player1 enabled, and leave Player2, Player3, and Player4 disabled until the game starts.
        Self.Player1Spawner.Enable()
        Self.Player2Spawner.Disable()
        Self.Player3Spawner.Disable()# previously Self.Player1Spawner.Disable()
        Self.Player4Spawner.Disable()# previously Self.Player1Spawner.Disable()


    OnPlayer1Spawned(Player: agent): void =

        # Show dialog and subscribe only for the player spawned from 1Player spawner
        if (FirstPlayer := player[Player]):
            #ScreensHandler.TowerDefenseDialog.Show(FirstPlayer)
            TowerDefenseDialog.Show(FirstPlayer)
            Subscription := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)
            #Subscription := ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)
            if (set PlayerDialogSubscriptions[FirstPlayer] = Subscription) {}
            set ActiveDialogPlayer = option{FirstPlayer}
            Print("Waiting for round start...")


    OnOtherPlayerSpawned(Player: agent): void =
        Print("Player from non-priority spawner joined, dialog not shown.")
        # No dialog/subscription for 2/3/4Player


    OnDialogButtonPressed(Agent:agent, ButtonIndex:int):void =
        if (ActivePlayer := ActiveDialogPlayer?, Agent = ActivePlayer, ButtonIndex = 0):
            if (ButtonIndex = 0 and Self.RoundStarted = false):
                set Self.RoundStarted = true
                if (DialogSub := Self.DialogSubscription?):
                    DialogSub.Cancel()
                set Self.DialogSubscription = false
                # Dismiss dialog and clean up only for the active player

                # Only the 1Player spawner player can trigger start logic!
            if (Subscription := PlayerDialogSubscriptions[ActivePlayer]):
                    Subscription.Cancel()
                    #ScreensHandler.TowerDefenseDialog.Hide(ActivePlayer)
                    TowerDefenseDialog.Hide(ActivePlayer)
                set PlayerDialogSubscriptions = map{}
                set ActiveDialogPlayer = false
                Print("Game Started!")
                # Proceed with starting the game...

                # Switch camera back to Fortnite's default third-person view
                Self.FixedPointCamera.Disable()
                Self.ThirdPersonControls.Enable()
                Self.RoundTimer.Enable()
                Self.RoundTimer.Reset()
                Self.RoundTimer.Start()

                # Reset camera for all players
                for (Player : GetPlayspace().GetPlayers()):
                    #ScreensHandler.TowerDefenseDialog.Show(Player)
                    TowerDefenseDialog.Show(Player)
                    #DialogSub := ScreensHandler.TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)
                    DialogSub := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)
                    if (set PlayerDialogSubscriptions[Player] = DialogSub) {}
                        FixedPointCamera.Disable()
                        spawn{HandleTimerExpiration()}
                        #ScreensHandler.
                        TowerDefenseDialog.Hide(Player)
                    set PlayerDialogSubscriptions = map{}
                    
                # Proceed with starting the game...
                for (Player -> DialogSub : PlayerDialogSubscriptions):
                    if (DialogSubscription?):
                        if (Sub := DialogSubscription?):
                            Sub.Cancel()             # Cancel this player's dialog event subscription
                    #ScreensHandler.TowerDefenseDialog.Hide(Player)  # Hide the dialog for this player
                    TowerDefenseDialog.Hide(Player)  # Hide the dialog for this player
                set PlayerDialogSubscriptions = map{} # Clear the map afterwards

                # Create 30 round settings devices and store them in the array
                set RoundSettings = for (Index := 0..29):
                    round_settings_device{}


                # Example: Configure each round's settings
                for (RoundIndex -> RoundDevice : RoundSettings):
                    # Set up round-specific settings here
                    # For example, you could enable/disable certain features per round
                    # or configure round-specific rules
                    Print("Configured round {RoundIndex + 1}")

    # Example function to get a specific round's settings
    GetRoundDevice(RoundIndex : int) : void =
        if (RoundDevice := RoundSettings[RoundIndex]):
            RoundDevice


    # Call this when a round ends
    HandleRoundEnd() : void =
        for (Player : GetPlayspace().GetPlayers()):
            if (RoundDevice := RoundSettings[CurrentRoundIndex]):
                Players := GetPlayspace().GetPlayers()
                if (FirstPlayer := Players[0]):
                    RoundDevice.EndRound(FirstPlayer) # Pass agent type!


    HandleTimerExpiration()<suspends>:void =
        Self.RoundTimer.SuccessEvent.Await()
        for (Player : Self.GetPlayspace().GetPlayers()):
            Self.EndRound.Activate(Player)
        Self.RoundTimer.Reset()
        Self.RoundTimer.Disable()

        # Implementation for timer expiration handling
        Print("Timer expired")

        Players := Self.GetPlayspace().GetPlayers()
        if (Players.Length > 0):
            # Start the configured round timer (automatically shows on player HUD)
            Self.RoundTimer.Start()
            # Wait for the timer to complete
            Self.RoundTimer.SuccessEvent.Await()
            # After the timer finishes, end the round for all players
            for (Player : Players):
                Self.EndRound.Activate(Player)
        else:
            Print("Can't find player")


    # Initialize or load persistent data for a player
    InitializePlayerData(Player:player):void =
        if (ExistingData := PlayerRoundData[Player]):
            # Data already exists, do nothing
            Print("Loaded existing data for player")
        else:
            # Create new data with default values
            NewData := round_data{}
            if (set PlayerRoundData[Player] = NewData):
                Print("Initialized new data for player")


    # Save updated round data for a player
    SavePlayerRoundData(Player:player):void =
        if (CurrentData := PlayerRoundData[Player]):
            # Create new data instance with updated values
            NewData := round_data{
                RoundNumber := CurrentData.RoundNumber + 1,
                PointScore := CurrentData.PointScore + 100,
                CoinScore := CurrentData.CoinScore + 50
            }
            if (set PlayerRoundData[Player] = NewData):
                Print("Saved updated data for player")


    OnTowerDefenseButtonPressed(Data:tuple(agent, int)):void=
        ButtonIndex := Data(1)
        case(ButtonIndex):
            # 0: Start round, enable all spawners, move players, keep dialog shown
            0 => HandleStartRound()
            _ => Print("Unknown button pressed")


    HandleStartRound():void=
        if (CurrentRound > TotalRounds):
            Print("All rounds complete.")
            return

        if (RoundActive = false):
            set RoundActive = true
            #FastNPCSpawner.Enable()
            #MediumNPCSpawner.Enable()
            #StrongNPCSpawner.Enable()
            #FastNPCSpawner.Spawn()
            #MediumNPCSpawner.Spawn()
            #StrongNPCSpawner.Spawn()
            # GateSequencer.Play()
            Print("Round {CurrentRound} started.")
            # Move all players to new location & keep dialog shown
            for (Player : GetPlayspace().GetPlayers()):
                if (Character := Player.GetFortCharacter[]):
                    TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}
                    if (Character.TeleportTo[TargetLocation, rotation{}]):
                        Print("Moved player to target location at round start.")

            # Start timer for this round
            spawn{RoundTimerLoop()}


    # Each round lasts 5 min, up to 30 rounds; stops spawners & activates EndRound each time.
    RoundTimerLoop()<suspends>:void=
        Print("Started round timer for {RoundDuration} seconds.")
        Sleep(RoundDuration)
        set RoundActive = false

        # Stop spawners
        #FastNPCSpawner.Disable()
        #MediumNPCSpawner.Disable()
        #StrongNPCSpawner.Disable()
        Print("Round {CurrentRound} ended.")

        # Activate EndRound device for all players
        EndRound.Enable()

        for (Player : GetPlayspace().GetPlayers()):
            if (CurrentRoundIndex = 29):
                EndRound.Activate(Player)

        # RoundSettings: notify round end
        for (Player : GetPlayspace().GetPlayers()):
            if (RoundDevice := RoundSettings[CurrentRoundIndex]):
                RoundDevice.EndRound(Player) # Pass Player (agent)

        set CurrentRound += 1

I tried to use that reference to group all the dialogs in the same script but it seems we can’t do that if we want to store the subscriptions.

Sorry again!

Let me know if it works now and if you need more help!!

1 Like

No worries dude, and I’ll let you know if it works after I’ve testing it and thank you so much for your help. Oh by the way, I’ve made some changes to tower_defense_manager script and made a new script called round_system_manager, just trying to make an actual tower defense game like, “Hero City Tower Defense,” which it’s created by vysena, and just want to let you know about that.

Here’s the two verse scripts, just want to show them to you:

tower_defense_manager script:

using { /Fortnite.com/Devices }

using { /Fortnite.com/Characters }

using { /Verse.org/Simulation }

using { /UnrealEngine.com/Temporary/Diagnostics }

using { /UnrealEngine.com/Temporary/SpatialMath }

using { /UnrealEngine.com/Temporary/UI }

using { /Fortnite.com/UI }

# Device to manage tower defense rounds and settings

tower_defense_manager := class(creative_device):

\# Dialogs & Round infrastructure

@editable

TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}

@editable

Rounds : \[\]cinematic_sequence_device = array{}



\# 30 levels prerounds

@editable

Levels : \[\]trigger_device = array{}



\# NPC Spawners

@editable

FastNPCSpawner : npc_spawner_device = npc_spawner_device{}

@editable

MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}

@editable

StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

MainRound : tracker_device = tracker_device{}

@editable

EliminateEnemies : \[\]tracker_device = array{}



\# Stats

@editable

PointsStat : stat_creator_device = stat_creator_device{}

@editable

CoinsStat : stat_creator_device = stat_creator_device{}



@editable

ReadyUpTimer : timer_device = timer_device{}



@editable

FixedPointCamera : gameplay_camera_fixed_point_device = gameplay_camera_fixed_point_device{}

@editable

ThirdPersonControls : gameplay_controls_third_person_device = gameplay_controls_third_person_device{}

@editable

Objective : objective_device = objective_device{}



@editable

Player1Spawner : player_spawner_device = player_spawner_device{}

@editable

Player2Spawner : player_spawner_device = player_spawner_device{}

@editable

Player3Spawner : player_spawner_device = player_spawner_device{}

@editable

Player4Spawner : player_spawner_device = player_spawner_device{}



\# Round timer and logic

var CurrentRound : int = 1

var EliminationCount : int = 0

var SpawnLimitsPerRound : \[\]spawn_limits = array{}

ReadyDuration : float = 30.0 # seconds (30 second)

var RoundActive : logic = false



var IsRoundActive : logic = false



var DialogShown : logic = false

var RoundStarted : logic = false

var DialogSubscription : ?cancelable = false



var CurrentRoundIndex : int = 0

var AgentsInZone : \[agent\]logic = map{}

var PlayerDialogSubscriptions : \[player\]cancelable = map{}

var ActiveDialogPlayer : ?player = false



OnBegin<override>()<suspends>:void=

    \# Initialize spawn limits for each round

    set SpawnLimitsPerRound = array{

        spawn_limits{FastNPC := 30, MediumNPC := 10, StrongNPC := 3},  # Round 1

        spawn_limits{FastNPC := 60, MediumNPC := 20, StrongNPC := 5},  # Round 2

        spawn_limits{FastNPC := 100, MediumNPC := 30, StrongNPC := 8},  # Round 3

        spawn_limits{FastNPC := 140, MediumNPC := 40, StrongNPC := 10},  # Round 4

        spawn_limits{FastNPC := 180, MediumNPC := 50, StrongNPC := 12},  # Round 5

        spawn_limits{FastNPC := 230, MediumNPC := 60, StrongNPC := 15},  # Round 6

        spawn_limits{FastNPC := 270, MediumNPC := 75, StrongNPC := 18},  # Round 7

        spawn_limits{FastNPC := 315, MediumNPC := 90, StrongNPC := 20},  # Round 8

        spawn_limits{FastNPC := 360, MediumNPC := 105, StrongNPC := 22},  # Round 9

        spawn_limits{FastNPC := 400, MediumNPC := 120, StrongNPC := 24},  # Round 10

        spawn_limits{FastNPC := 440, MediumNPC := 130, StrongNPC := 26},  # Round 11

        spawn_limits{FastNPC := 480, MediumNPC := 140, StrongNPC := 28},  # Round 12

        spawn_limits{FastNPC := 520, MediumNPC := 150, StrongNPC := 30},  # Round 13

        spawn_limits{FastNPC := 560, MediumNPC := 160, StrongNPC := 32},  # Round 14

        spawn_limits{FastNPC := 600, MediumNPC := 175, StrongNPC := 35},  # Round 15

        spawn_limits{FastNPC := 640, MediumNPC := 190, StrongNPC := 38},  # Round 16

        spawn_limits{FastNPC := 680, MediumNPC := 205, StrongNPC := 40},  # Round 17

        spawn_limits{FastNPC := 720, MediumNPC := 220, StrongNPC := 42},  # Round 18

        spawn_limits{FastNPC := 760, MediumNPC := 235, StrongNPC := 44},  # Round 19

        spawn_limits{FastNPC := 800, MediumNPC := 250, StrongNPC := 46},  # Round 20

        spawn_limits{FastNPC := 840, MediumNPC := 265, StrongNPC := 48},  # Round 21

        spawn_limits{FastNPC := 880, MediumNPC := 280, StrongNPC := 50},  # Round 22

        spawn_limits{FastNPC := 920, MediumNPC := 295, StrongNPC := 52},  # Round 23

        spawn_limits{FastNPC := 960, MediumNPC := 310, StrongNPC := 54},  # Round 24

        spawn_limits{FastNPC := 1000, MediumNPC := 325, StrongNPC := 56},  # Round 25

        spawn_limits{FastNPC := 1040, MediumNPC := 340, StrongNPC := 58},  # Round 26

        spawn_limits{FastNPC := 1080, MediumNPC := 355, StrongNPC := 60},  # Round 27

        spawn_limits{FastNPC := 1120, MediumNPC := 370, StrongNPC := 62},  # Round 28

        spawn_limits{FastNPC := 1160, MediumNPC := 385, StrongNPC := 64},  # Round 29

        spawn_limits{FastNPC := 1200, MediumNPC := 400, StrongNPC := 66} # Round 30

    }



    \# Start: disable all spawners (idle)

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    ReadyUpTimer.Disable()



    \# Subscribe to the 1Player spawner's spawn event

    Player1Spawner.SpawnedEvent.Subscribe(OnPlayer1Spawned)

    Player2Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    Player3Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    Player4Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)



    \# Subscribe to dialog button interaction

    TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)



    \# Subscribe to ReadyUpTimer's SuccessEvent (fires when timer ends)

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpTimerExpired)

    set Self.DialogSubscription = option{Self.TowerDefenseDialog.RespondingButtonEvent.Subscribe(Self.OnDialogButtonPressed)}

    \# Notice: No call to Self.TowerDefenseDialog.Show() or Enable() here



OnOtherPlayerSpawned(Player: agent): void =

    Print("Other player spawned, dialog not shown and not subscribed.")



OnPlayer1Spawned(Player: agent): void =

    \# Show dialog and subscribe only for the player spawned from 1Player spawner

    if (FirstPlayer := player\[Player\]):

        TowerDefenseDialog.Show(FirstPlayer)

        Subscription := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)

        if (set PlayerDialogSubscriptions\[FirstPlayer\] = Subscription) {}

        set ActiveDialogPlayer = option{FirstPlayer}

        Print("Waiting for round start...")



OnDialogButtonPressed(Agent:agent, ButtonIndex:int):void =

    if (ActivePlayer := ActiveDialogPlayer?, Agent = ActivePlayer, ButtonIndex = 0):

        if (ButtonIndex = 0 and Self.RoundStarted = false):

            set Self.RoundStarted = true

            if (DialogSub := Self.DialogSubscription?):

                DialogSub.Cancel()

            set Self.DialogSubscription = false

            if (ButtonIndex = 0):

                \# When ready-up begins, keep round/spawners/timer disabled

                FastNPCSpawner.Disable()

                MediumNPCSpawner.Disable()

                StrongNPCSpawner.Disable()

                for (Cinematic : Rounds):

                    Cinematic.Stop()

                ReadyUpTimer.Enable()

                ReadyUpTimer.Start()

                \# Optionally, show a UI message to notify the ready up countdown has started



                \# Enable and show MainRound + EliminateEnemies for all players

                for (Player : GetPlayspace().GetPlayers()):

                    MainRound.Assign(Player)

                    MainRound.SetTitleText(StringToMessage("Complete Rounds"))

                    MainRound.SetDescriptionText(StringToMessage("Complete all 30 rounds and defend the item from aliens."))

                    MainRound.SetValue(0)  # Start at round 1



            \# Dismiss dialog and clean up only for the active player

            \# Only the 1Player spawner player can trigger start logic!

           if (Subscription := PlayerDialogSubscriptions\[ActivePlayer\]):

                Subscription.Cancel()

                TowerDefenseDialog.Hide(ActivePlayer)

            set PlayerDialogSubscriptions = map{}

            set ActiveDialogPlayer = false

            Print("Game Started!")

            \# Proceed with starting the game...

            \# Switch camera back to Fortnite's default third-person view

            Self.FixedPointCamera.Disable()

            Self.ThirdPersonControls.Enable()

           

            \# Reset camera for all players

            for (Player : GetPlayspace().GetPlayers()):

                FixedPointCamera.Disable()

                TowerDefenseDialog.Hide(Player)

            set PlayerDialogSubscriptions = map{}

            \# Proceed with starting the game...



            \# Subscribe trigger events for Levels

            for (LevelTrigger : Levels):

                LevelTrigger.TriggeredEvent.Subscribe(OnLevelTriggered)



OnLevelTriggered(Agent : ?agent): void =

    \# Each time any Level trigger fires, update trackers

    set CurrentRound += 1

    MainRound.SetValue(CurrentRound)



OnEliminateEnemiesComplete(Agent:agent):void =

    for (Player : GetPlayspace().GetPlayers()):

        for (Tracker : EliminateEnemies):

            Tracker.SetValue(0)

            Tracker.Assign(Player)

            Tracker.Remove(Player)

            Tracker.SetTitleText(StringToMessage("Alien Eliminations"))

            Tracker.SetDescriptionText(StringToMessage("Eliminate 43 aliens from reaching the item"))

            Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

            Print("Tracker completed for agent!")



    for (Tracker : EliminateEnemies):

        Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

    

OnReadyUpTimerExpired(Agent: ?agent): void =

    \# After 30 seconds, enable round timer and spawners

    FastNPCSpawner.Enable()

    MediumNPCSpawner.Enable()

    StrongNPCSpawner.Enable()

    for (Cinematic : Rounds):

        Cinematic.Play()



OnTowerDefenseButtonPressed(Data:tuple(agent, int)):void=

    ButtonIndex := Data(1)

    case(ButtonIndex):

        \# 0: Start round, enable all spawners, move players, keep dialog shown

        0 => HandleStartRound()

        \_ => Print("Unknown button pressed")



HandleStartRound():void=

    if (RoundActive = false):

        set RoundActive = true

        Print("Round {CurrentRound} started.")

        \# Move all players to new location & keep dialog shown

        for (Player : GetPlayspace().GetPlayers()):

            if (Character := Player.GetFortCharacter\[\]):

                TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}

                if (Character.TeleportTo\[TargetLocation, rotation{}\]):

                    Print("Moved player to target location at round start.")



StringToMessage<localizes>(InString: string): message = "{InString}"

round_system_manager script:

using { /Fortnite.com/Devices }

using { /Fortnite.com/Characters }

using { /Verse.org/Simulation }

using { /UnrealEngine.com/Temporary/Diagnostics }

using { /UnrealEngine.com/Temporary/SpatialMath }

using { /UnrealEngine.com/Temporary/UI }

using { /Fortnite.com/UI }

# Struct to hold spawn limits for each NPC type per round

spawn_limits := struct:

FastNPC : int

MediumNPC : int

StrongNPC : int

# Manages round-based gameplay similar to COD Zombies

round_system_manager := class(creative_device):

\# Editable device references



\# 30 levels prerounds

@editable

Levels : \[\]trigger_device = array{}



@editable

ReadyUpTimer : timer_device = timer_device{}



@editable

MainRound : tracker_device = tracker_device{}



@editable

EliminateEnemies : \[\]tracker_device = array{}



@editable

EndRoundDevice : end_game_device = end_game_device{}



@editable

Rounds : \[\]cinematic_sequence_device = array{}



@editable

FastNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}



\# Internal state

var CurrentRound : int = 0

var EliminationCount : int = 0

var SpawnLimitsPerRound : \[\]spawn_limits = array{}

var IsRoundActive : logic = false

var Players : \[\]player = array{}

ReadyDuration : float = 30.0 # seconds (30 second)



\# Initialize the round system

OnBegin<override>()<suspends> : void =

    \# Initialize spawn limits for each round

    set SpawnLimitsPerRound = array{

        spawn_limits{FastNPC := 30, MediumNPC := 10, StrongNPC := 3},  # Round 1

        spawn_limits{FastNPC := 60, MediumNPC := 20, StrongNPC := 5},  # Round 2

        spawn_limits{FastNPC := 100, MediumNPC := 30, StrongNPC := 8},  # Round 3

        spawn_limits{FastNPC := 140, MediumNPC := 40, StrongNPC := 10},  # Round 4

        spawn_limits{FastNPC := 180, MediumNPC := 50, StrongNPC := 12},  # Round 5

        spawn_limits{FastNPC := 230, MediumNPC := 60, StrongNPC := 15},  # Round 6

        spawn_limits{FastNPC := 270, MediumNPC := 75, StrongNPC := 18},  # Round 7

        spawn_limits{FastNPC := 315, MediumNPC := 90, StrongNPC := 20},  # Round 8

        spawn_limits{FastNPC := 360, MediumNPC := 105, StrongNPC := 22},  # Round 9

        spawn_limits{FastNPC := 400, MediumNPC := 120, StrongNPC := 24},  # Round 10

        spawn_limits{FastNPC := 440, MediumNPC := 130, StrongNPC := 26},  # Round 11

        spawn_limits{FastNPC := 480, MediumNPC := 140, StrongNPC := 28},  # Round 12

        spawn_limits{FastNPC := 520, MediumNPC := 150, StrongNPC := 30},  # Round 13

        spawn_limits{FastNPC := 560, MediumNPC := 160, StrongNPC := 32},  # Round 14

        spawn_limits{FastNPC := 600, MediumNPC := 175, StrongNPC := 35},  # Round 15

        spawn_limits{FastNPC := 640, MediumNPC := 190, StrongNPC := 38},  # Round 16

        spawn_limits{FastNPC := 680, MediumNPC := 205, StrongNPC := 40},  # Round 17

        spawn_limits{FastNPC := 720, MediumNPC := 220, StrongNPC := 42},  # Round 18

        spawn_limits{FastNPC := 760, MediumNPC := 235, StrongNPC := 44},  # Round 19

        spawn_limits{FastNPC := 800, MediumNPC := 250, StrongNPC := 46},  # Round 20

        spawn_limits{FastNPC := 840, MediumNPC := 265, StrongNPC := 48},  # Round 21

        spawn_limits{FastNPC := 880, MediumNPC := 280, StrongNPC := 50},  # Round 22

        spawn_limits{FastNPC := 920, MediumNPC := 295, StrongNPC := 52},  # Round 23

        spawn_limits{FastNPC := 960, MediumNPC := 310, StrongNPC := 54},  # Round 24

        spawn_limits{FastNPC := 1000, MediumNPC := 325, StrongNPC := 56},  # Round 25

        spawn_limits{FastNPC := 1040, MediumNPC := 340, StrongNPC := 58},  # Round 26

        spawn_limits{FastNPC := 1080, MediumNPC := 355, StrongNPC := 60},  # Round 27

        spawn_limits{FastNPC := 1120, MediumNPC := 370, StrongNPC := 62},  # Round 28

        spawn_limits{FastNPC := 1160, MediumNPC := 385, StrongNPC := 64},  # Round 29

        spawn_limits{FastNPC := 1200, MediumNPC := 400, StrongNPC := 66} # Round 30

    }



    \# Save player list

    set Players = GetPlayspace().GetPlayers()



    \# Initial setup - disable all spawners and timers

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    ReadyUpTimer.Disable()

   

    \# Subscribe to device events

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpTimerComplete)



    \# Start with ready up for round 1

    ReadyUpTimer.Enable()

    ReadyUpTimer.Start() # Start Ready Up at game start



OnReadyUpTimerComplete(Agent:?agent):void =

    \# When ready up ends, update spawner limits and start round timer

    UpdateSpawnerLimits()



OnReadyUpEnded(Agent : ?agent) : void =

    \# Subscribe to ReadyUpTimer

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpEnded)

    ReadyUpTimer.Start() # Start the 30s timer



    \# Subscribe trigger events for Levels

    for (LevelTrigger : Levels):

        LevelTrigger.TriggeredEvent.Subscribe(OnLevelTriggered)



    for (Player : GetPlayspace().GetPlayers()):

        MainRound.Remove(Player)



    \# Enable and show MainRound + EliminateEnemies for all players

    for (Player : GetPlayspace().GetPlayers()):

        MainRound.Assign(Player)

        MainRound.SetTitleText(StringToMessage("Complete Rounds"))

        MainRound.SetDescriptionText(StringToMessage("Complete all 30 rounds and defend the item from aliens."))

        MainRound.SetValue(0) # Start at round 1



    for (Player : GetPlayspace().GetPlayers()):

        MainRound.Remove(Player)



OnLevelTriggered(Agent : ?agent): void =

    \# Each time any Level trigger fires, update trackers

    set CurrentRound += 1

    MainRound.SetValue(CurrentRound)

    Print("Level trigger activated, updated round trackers.")



OnEliminateEnemiesComplete(Agent:agent):void =

    for (Player : GetPlayspace().GetPlayers()):

        for (Tracker : EliminateEnemies):

            Tracker.SetValue(0)

            Tracker.Assign(Player)

            Tracker.Remove(Player)

            Tracker.SetTitleText(StringToMessage("Alien Eliminations"))

            Tracker.SetDescriptionText(StringToMessage("Eliminate 43 aliens from reaching the item"))

            Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

            Print("Tracker completed for agent!")



    for (Tracker : EliminateEnemies):

        Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)



\# Handle ready up phase completion

OnReadyUpComplete(Agent : ?agent) : void =

    if (IsRoundActive = false):

    StartRound()

    EndRound()



\# Start a new round

StartRound() : void =



    set IsRoundActive = true

    set CurrentRound += 1



    \# Update round tracker

    MainRound.SetValue(CurrentRound)



    \# Spawn enemies based on round

    SpawnEnemiesForRound()



    \# Enable and spawn NPCs

    FastNPCSpawner.Enable()

    MediumNPCSpawner.Enable()

    StrongNPCSpawner.Enable()

    FastNPCSpawner.Spawn()

    MediumNPCSpawner.Spawn()

    StrongNPCSpawner.Spawn()



    \# Play round start cinematic

    for (Cinematic : Rounds):

        Cinematic.Play()



    \# Teleport players to start position

    for (Player : GetPlayspace().GetPlayers()):

        if (Character := Player.GetFortCharacter\[\]):

            TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}

            if (Character.TeleportTo\[TargetLocation, rotation{}\]):

                Print("Moved player to target location at round start.")



\# Spawn enemies scaled to current round

SpawnEnemiesForRound() : void =

    \# Calculate spawn counts based on round

    var FastCount : int = 50 + CurrentRound

    var MediumCount : int = 10 + CurrentRound

    var StrongCount : int = 0

    if (CurrentRound >= 5):

    set StrongCount = CurrentRound - 1



    \# Spawn enemies

    for (Index := 50..FastCount):

        FastNPCSpawner.Spawn()



    for (Index := 10..MediumCount):

        MediumNPCSpawner.Spawn()



    for (Index := 1..StrongCount):

        StrongNPCSpawner.Spawn()



OnDialogButtonPressed(Agent: agent, ButtonIndex: int): void =

    if (ButtonIndex = 0):

        \# When ready-up begins, keep round/spawners/timer disabled

        FastNPCSpawner.Disable()

        MediumNPCSpawner.Disable()

        StrongNPCSpawner.Disable()

        for (Cinematic : Rounds):

            Cinematic.Stop()

        \# Set durations to avoid incorrect settings

        ReadyUpTimer.SetMaxDuration(30.0) # 30 seconds ready-up

        ReadyUpTimer.Enable()

        ReadyUpTimer.Start()



OnReadyUpTimerExpired(Agent: ?agent): void =

    \# After 30 seconds, enable round timer and spawners

    FastNPCSpawner.Enable()

    MediumNPCSpawner.Enable()

    StrongNPCSpawner.Enable()

    for (Cinematic : Rounds):

        Cinematic.Play()

    

    set IsRoundActive = true

    set CurrentRound += 1

    MainRound.SetValue(CurrentRound)



    \# Update round tracker and reset eliminations for all players

    set CurrentRound += 1

    for (Player : GetPlayspace().GetPlayers()):

        MainRound.SetValue(CurrentRound)



\# Handle round timer completion

OnRoundTimerComplete(Agent : ?agent) : void =

    if (IsRoundActive = true):

    EndRound()



\# Handle all enemies eliminated

OnAllEnemiesEliminated(Agent : agent) : void =

    if (IsRoundActive = true):

        EndRound()

       

\# End the current round

EndRound() : void =

    set IsRoundActive = false

    \# Stop all spawners

    FastNPCSpawner.DespawnAll(false)

    MediumNPCSpawner.DespawnAll(false)

    StrongNPCSpawner.DespawnAll(false)

    \# Check if game should end

    if (CurrentRound >= Rounds.Length):

        EndGame()



    \# Disable all spawners

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()



    \# End the game if this is Level 30

    if (CurrentRound = 30):

        RoundPlayers := GetPlayspace().GetPlayers()

        if (Players.Length > 0):

            if (FirstPlayer := Players\[0\]):

                EndRoundDevice.Activate(FirstPlayer)



    else:

        \# Start ready up phase for next round

        \# Reset timers and start ReadyUp again

        ReadyUpTimer.Reset()

        ReadyUpTimer.Start()



\# End the game

EndGame() : void =

    \# Activate end game device for the first player

    if (FirstPlayer := GetPlayspace().GetPlayers()\[0\]):

        EndRoundDevice.Activate(FirstPlayer)



\# Start next round

StartNextRound() : void =

    ReadyUpTimer.Start()



\# Update spawner limits based on current round

UpdateSpawnerLimits():void =

    if (CurrentRound <= SpawnLimitsPerRound.Length):

        if (Limits := SpawnLimitsPerRound\[CurrentRound - 1\]):

            \# Enable/disable spawners based on limits

            if (Limits.FastNPC > 0):

                FastNPCSpawner.Enable()

            else:

                FastNPCSpawner.Disable()



            if (Limits.MediumNPC > 0):

                MediumNPCSpawner.Enable()

            else:

                MediumNPCSpawner.Disable()



            if (Limits.StrongNPC > 0):

                StrongNPCSpawner.Enable()

            else:

                StrongNPCSpawner.Disable()



StringToMessage<localizes>(InString: string): message = "{InString}"

But I’ll still try the updated code you share to me and let you know if it works or not, and again thank you so much for your help, super appreciated. I’ll reach out too you if I need more help anytime . :+1:

1 Like

Hey dude, So it worked and there’s an issue with it, the readydialog didn’t pop up before have the TowerDefenseDialog be pop up and NPC enemies are not spawning and the gate is not open after testing it and I think it’s because they’ve been commented, so I think that’s causing the issue. So just want to let you know about it, and I’ll share the video for it. Oh, and another thing, I need help on how to make a tower defense game in UEFN and I trying to make it like “Hero City Tower Defense,” which it’s created by vysena, but it’s different then that. So I got 30 trigger devices, 31 tracker devices which is 30 trackers for EliminateEnemies and 1 tracker for MainRound, 1 timer device for 30 second readyup, 3 end game devices which is 1 is EndRoundDevice to end the game at round 30, 1 is Exit Button from TitleScreenDialog, and 1 is End Game for game over, 30 cinematic sequence devices, and etc. So I’ll share my two scripts for you to look at, and let me know if you have any thoughts, tips, and advice.

tower_defense_manager script:

using { /Fortnite.com/Devices }

using { /Fortnite.com/Characters }

using { /Verse.org/Simulation }

using { /UnrealEngine.com/Temporary/Diagnostics }

using { /UnrealEngine.com/Temporary/SpatialMath }

using { /UnrealEngine.com/Temporary/UI }

using { /Fortnite.com/UI }

# Device to manage tower defense rounds and settings

tower_defense_manager := class(creative_device):

\# Dialogs & Round infrastructure

@editable

TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}

@editable

Rounds : \[\]cinematic_sequence_device = array{}



\# 30 levels prerounds

@editable

Levels : \[\]trigger_device = array{}



\# NPC Spawners

@editable

FastNPCSpawner : npc_spawner_device = npc_spawner_device{}

@editable

MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}

@editable

StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

MainRound : tracker_device = tracker_device{}

@editable

EliminateEnemies : \[\]tracker_device = array{}



\# Stats

@editable

PointsStat : stat_creator_device = stat_creator_device{}

@editable

CoinsStat : stat_creator_device = stat_creator_device{}



@editable

ReadyUpTimer : timer_device = timer_device{}



@editable

FixedPointCamera : gameplay_camera_fixed_point_device = gameplay_camera_fixed_point_device{}

@editable

ThirdPersonControls : gameplay_controls_third_person_device = gameplay_controls_third_person_device{}

@editable

Objective : objective_device = objective_device{}



@editable

Player1Spawner : player_spawner_device = player_spawner_device{}

@editable

Player2Spawner : player_spawner_device = player_spawner_device{}

@editable

Player3Spawner : player_spawner_device = player_spawner_device{}

@editable

Player4Spawner : player_spawner_device = player_spawner_device{}



\# Round timer and logic

var CurrentRound : int = 1

var EliminationCount : int = 0

var SpawnLimitsPerRound : \[\]spawn_limits = array{}

ReadyDuration : float = 30.0 # seconds (30 second)

var RoundActive : logic = false

var Players : \[\]player = array{}



var IsRoundActive : logic = false



var DialogShown : logic = false

var RoundStarted : logic = false

var DialogSubscription : ?cancelable = false



var CurrentRoundIndex : int = 0

var AgentsInZone : \[agent\]logic = map{}

var PlayerDialogSubscriptions : \[player\]cancelable = map{}

var ActiveDialogPlayer : ?player = false



OnBegin<override>()<suspends>:void=

    \# Initialize spawn limits for each round

    set SpawnLimitsPerRound = array{

        spawn_limits{FastNPC := 30, MediumNPC := 10, StrongNPC := 3},  # Round 1

        spawn_limits{FastNPC := 60, MediumNPC := 20, StrongNPC := 5},  # Round 2

        spawn_limits{FastNPC := 100, MediumNPC := 30, StrongNPC := 8},  # Round 3

        spawn_limits{FastNPC := 140, MediumNPC := 40, StrongNPC := 10},  # Round 4

        spawn_limits{FastNPC := 180, MediumNPC := 50, StrongNPC := 12},  # Round 5

        spawn_limits{FastNPC := 230, MediumNPC := 60, StrongNPC := 15},  # Round 6

        spawn_limits{FastNPC := 270, MediumNPC := 75, StrongNPC := 18},  # Round 7

        spawn_limits{FastNPC := 315, MediumNPC := 90, StrongNPC := 20},  # Round 8

        spawn_limits{FastNPC := 360, MediumNPC := 105, StrongNPC := 22},  # Round 9

        spawn_limits{FastNPC := 400, MediumNPC := 120, StrongNPC := 24},  # Round 10

        spawn_limits{FastNPC := 440, MediumNPC := 130, StrongNPC := 26},  # Round 11

        spawn_limits{FastNPC := 480, MediumNPC := 140, StrongNPC := 28},  # Round 12

        spawn_limits{FastNPC := 520, MediumNPC := 150, StrongNPC := 30},  # Round 13

        spawn_limits{FastNPC := 560, MediumNPC := 160, StrongNPC := 32},  # Round 14

        spawn_limits{FastNPC := 600, MediumNPC := 175, StrongNPC := 35},  # Round 15

        spawn_limits{FastNPC := 640, MediumNPC := 190, StrongNPC := 38},  # Round 16

        spawn_limits{FastNPC := 680, MediumNPC := 205, StrongNPC := 40},  # Round 17

        spawn_limits{FastNPC := 720, MediumNPC := 220, StrongNPC := 42},  # Round 18

        spawn_limits{FastNPC := 760, MediumNPC := 235, StrongNPC := 44},  # Round 19

        spawn_limits{FastNPC := 800, MediumNPC := 250, StrongNPC := 46},  # Round 20

        spawn_limits{FastNPC := 840, MediumNPC := 265, StrongNPC := 48},  # Round 21

        spawn_limits{FastNPC := 880, MediumNPC := 280, StrongNPC := 50},  # Round 22

        spawn_limits{FastNPC := 920, MediumNPC := 295, StrongNPC := 52},  # Round 23

        spawn_limits{FastNPC := 960, MediumNPC := 310, StrongNPC := 54},  # Round 24

        spawn_limits{FastNPC := 1000, MediumNPC := 325, StrongNPC := 56},  # Round 25

        spawn_limits{FastNPC := 1040, MediumNPC := 340, StrongNPC := 58},  # Round 26

        spawn_limits{FastNPC := 1080, MediumNPC := 355, StrongNPC := 60},  # Round 27

        spawn_limits{FastNPC := 1120, MediumNPC := 370, StrongNPC := 62},  # Round 28

        spawn_limits{FastNPC := 1160, MediumNPC := 385, StrongNPC := 64},  # Round 29

        spawn_limits{FastNPC := 1200, MediumNPC := 400, StrongNPC := 66} # Round 30

    }



    \# Save player list

    set Players = GetPlayspace().GetPlayers()



    \# Initialize first round

    EnableCurrentLevel()

    PlayCurrentRoundCinematic()



    \# Start: disable all spawners (idle)

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    ReadyUpTimer.Disable()



    \# Subscribe to the 1Player spawner's spawn event

    Player1Spawner.SpawnedEvent.Subscribe(OnPlayer1Spawned)

    Player2Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    Player3Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

    Player4Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)



    \# Subscribe to dialog button interaction

    TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)



    \# Subscribe to ReadyUpTimer's SuccessEvent (fires when timer ends)

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpTimerExpired)

    set Self.DialogSubscription = option{Self.TowerDefenseDialog.RespondingButtonEvent.Subscribe(Self.OnDialogButtonPressed)}

    \# Notice: No call to Self.TowerDefenseDialog.Show() or Enable() here



OnOtherPlayerSpawned(Player: agent): void =

    Print("Other player spawned, dialog not shown and not subscribed.")



OnPlayer1Spawned(Player: agent): void =

    \# Show dialog and subscribe only for the player spawned from 1Player spawner

    if (FirstPlayer := player\[Player\]):

        TowerDefenseDialog.Show(FirstPlayer)

        Subscription := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)

        if (set PlayerDialogSubscriptions\[FirstPlayer\] = Subscription) {}

        set ActiveDialogPlayer = option{FirstPlayer}

        Print("Waiting for round start...")



OnDialogButtonPressed(Agent:agent, ButtonIndex:int):void =

    if (ActivePlayer := ActiveDialogPlayer?, Agent = ActivePlayer, ButtonIndex = 0):

        if (ButtonIndex = 0 and Self.RoundStarted = false):

            set Self.RoundStarted = true

            if (DialogSub := Self.DialogSubscription?):

                DialogSub.Cancel()

            set Self.DialogSubscription = false

            if (ButtonIndex = 0):

                \# When ready-up begins, keep round/spawners/timer disabled

                FastNPCSpawner.Disable()

                MediumNPCSpawner.Disable()

                StrongNPCSpawner.Disable()

                for (Cinematic : Rounds):

                    Cinematic.Stop()

                ReadyUpTimer.Enable()

                ReadyUpTimer.Start()

                \# Optionally, show a UI message to notify the ready up countdown has started



                \# Enable and show MainRound + EliminateEnemies for all players

                for (Player : GetPlayspace().GetPlayers()):

                    MainRound.Assign(Player)

                    MainRound.SetTitleText(StringToMessage("Complete Rounds"))

                    MainRound.SetDescriptionText(StringToMessage("Complete all 30 rounds and defend the item from aliens."))

                    MainRound.SetValue(0)  # Start at round 1



            \# Dismiss dialog and clean up only for the active player

            \# Only the 1Player spawner player can trigger start logic!

           if (Subscription := PlayerDialogSubscriptions\[ActivePlayer\]):

                Subscription.Cancel()

                TowerDefenseDialog.Hide(ActivePlayer)

            set PlayerDialogSubscriptions = map{}

            set ActiveDialogPlayer = false

            Print("Game Started!")

            \# Proceed with starting the game...

            \# Switch camera back to Fortnite's default third-person view

            Self.FixedPointCamera.Disable()

            Self.ThirdPersonControls.Enable()

           

            \# Reset camera for all players

            for (Player : GetPlayspace().GetPlayers()):

                FixedPointCamera.Disable()

                TowerDefenseDialog.Hide(Player)

            set PlayerDialogSubscriptions = map{}

            \# Proceed with starting the game...



            \# Subscribe trigger events for Levels

            for (LevelTrigger : Levels):

                LevelTrigger.TriggeredEvent.Subscribe(OnLevelTriggered)



OnLevelTriggered(Agent : ?agent): void =

    \# Each time any Level trigger fires, update trackers

    set CurrentRound += 1

    MainRound.SetValue(CurrentRound)



\# Enable only the current round's level trigger, disable others

EnableCurrentLevel():void =

    for (Index -> Level : Levels):

        if (Index = CurrentRound - 1):

            Level.Enable()

        else:

            Level.Disable()



\# Play the cinematic for the current round if it exists

PlayCurrentRoundCinematic():void =

    if (CurrentRound - 1 >= 0 and CurrentRound - 1 < Rounds.Length):

        if (CurrentCinematic := Rounds\[CurrentRound - 1\]):

            CurrentCinematic.Play()



EnableRoundDevices(RoundIndex:int):void =

    \# Enable the trigger for this round

    if (RoundIndex >= 0 and RoundIndex < Levels.Length):

        if (Trigger := Levels\[RoundIndex\]):

            Trigger.Enable()



    \# Play the cinematic for this round

    if (RoundIndex >= 0 and RoundIndex < Rounds.Length):

        if (Cinematic := Rounds\[RoundIndex\]):

            Cinematic.Play()



\# Completely resets all triggers and cinematics, then enables/plays just one of each

ActivateRoundDevices(RoundNumber : int):void =

    \# Disable all triggers and stop all cinematics

    for (Trigger : Levels):

        Trigger.Disable()

    for (Cine : Rounds):

        Cine.Stop()



    \# Only enable & play what matches this round

    if (RoundNumber > 0 and RoundNumber <= Levels.Length):

        if (Level := Levels\[RoundNumber - 1\]):

            Level.Enable()

    if (RoundNumber > 0 and RoundNumber <= Rounds.Length):

        if (SelectedCine := Rounds\[RoundNumber - 1\]):

            SelectedCine.Play()



\# Advance to the next round safely

AdvanceRound():void =

    set CurrentRound += 1

    ActivateRoundDevices(CurrentRound)



OnEliminateEnemiesComplete(Agent:agent):void =

    for (Player : GetPlayspace().GetPlayers()):

        for (Tracker : EliminateEnemies):

            Tracker.SetValue(0)

            Tracker.Assign(Player)

            Tracker.Remove(Player)

            Tracker.SetTitleText(StringToMessage("Alien Eliminations"))

            Tracker.SetDescriptionText(StringToMessage("Eliminate 43 aliens from reaching the item"))

            Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

            Print("Tracker completed for agent!")



    for (Tracker : EliminateEnemies):

        Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)



\# Call this when starting a new round

StartNewRound():void =

    if (CurrentRound < 30):

        set CurrentRound += 1

        EnableCurrentLevel()

        PlayCurrentRoundCinematic()

   

OnReadyUpTimerExpired(Agent: ?agent): void =

    \# After 30 seconds, enable round timer and spawners

    FastNPCSpawner.Enable()

    MediumNPCSpawner.Enable()

    StrongNPCSpawner.Enable()

    for (Cinematic : Rounds):

        Cinematic.Play()



OnTowerDefenseButtonPressed(Data:tuple(agent, int)):void=

    ButtonIndex := Data(1)

    case(ButtonIndex):

        \# 0: Start round, enable all spawners, move players, keep dialog shown

        0 => HandleStartRound()

        \_ => Print("Unknown button pressed")



HandleStartRound():void=

    if (RoundActive = false):

        set RoundActive = true

        Print("Round {CurrentRound} started.")

        \# Move all players to new location & keep dialog shown

        for (Player : GetPlayspace().GetPlayers()):

            if (Character := Player.GetFortCharacter\[\]):

                TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}

                if (Character.TeleportTo\[TargetLocation, rotation{}\]):

                    Print("Moved player to target location at round start.")



StringToMessage<localizes>(InString: string): message = "{InString}"

@editableeditable

MainRound : tracker_device = tracker_de@editableice{}

@editable

EliminateEnemies : \[\]tracker_device = arr@editabley{}

\# Stats

@editable

PointsStat : stat_creator_device =@editablestat_creator_device{}

@editable

CoinsStat : stat_creator_@editableevice = stat_creator_device{}

@editable

ReadyU@editableTimer : timer_device = timer_device{}

@editable

FixedPointCamera : gameplay_camera_fixed_point@editabledevice = gameplay_camera_fixed_point_device{}

@editable

ThirdPersonControls : gameplay_controls_third_p@editablerson_device = gameplay_controls_third_person_device{}@editable

@editable

Objective : objective_device = objective_device{}

@edit@editableble

Player1Spawner : player_spawner_device = player_spawner_device{@editable

@editable

Player2Spawner : player_spawner_device = player_spawner@editabledevice{}

@editable

Player3Spawner : player_spawner_device = player_spawner_device{}

@editable

Player4Spawner : player_spawner_device = player_spawner_device{}

\# Round timer and logic

var CurrentRound : int = 1

var EliminationCount : int = 0

var SpawnLimitsPerRound : \[\]spawn_limits = array{}

ReadyDuration : float = 30.0 # seconds (30 second)

var RoundActive : logic = false

var IsRoundActive : logic = false

var DialogShown : logic = false

var RoundStarted : logic = false

var DialogSubscription : ?cancelable = false

var CurrentRoundIndex : int = 0

var AgentsInZone : \[agent\]logic = map{}

var PlayerDialogSubscriptions : \[player\]cancelable = map{}

var ActiveDialogPlayer : ?player = false

OnBegin():void=

\\# Initialize spawn limits for each round

set SpawnLimitsPerRound = array{

    spawn_limits{FastNPC := 30, MediumNPC := 10, StrongNPC := 3},  # Round 1

    spawn_limits{FastNPC := 60, MediumNPC := 20, StrongNPC := 5},  # Round 2

    spawn_limits{FastNPC := 100, MediumNPC := 30, StrongNPC := 8},  # Round 3

    spawn_limits{FastNPC := 140, MediumNPC := 40, StrongNPC := 10},  # Round 4

    spawn_limits{FastNPC := 180, MediumNPC := 50, StrongNPC := 12},  # Round 5

    spawn_limits{FastNPC := 230, MediumNPC := 60, StrongNPC := 15},  # Round 6

    spawn_limits{FastNPC := 270, MediumNPC := 75, StrongNPC := 18},  # Round 7

    spawn_limits{FastNPC := 315, MediumNPC := 90, StrongNPC := 20},  # Round 8

    spawn_limits{FastNPC := 360, MediumNPC := 105, StrongNPC := 22},  # Round 9

    spawn_limits{FastNPC := 400, MediumNPC := 120, StrongNPC := 24},  # Round 10

    spawn_limits{FastNPC := 440, MediumNPC := 130, StrongNPC := 26},  # Round 11

    spawn_limits{FastNPC := 480, MediumNPC := 140, StrongNPC := 28},  # Round 12

    spawn_limits{FastNPC := 520, MediumNPC := 150, StrongNPC := 30},  # Round 13

    spawn_limits{FastNPC := 560, MediumNPC := 160, StrongNPC := 32},  # Round 14

    spawn_limits{FastNPC := 600, MediumNPC := 175, StrongNPC := 35},  # Round 15

    spawn_limits{FastNPC := 640, MediumNPC := 190, StrongNPC := 38},  # Round 16

    spawn_limits{FastNPC := 680, MediumNPC := 205, StrongNPC := 40},  # Round 17

    spawn_limits{FastNPC := 720, MediumNPC := 220, StrongNPC := 42},  # Round 18

    spawn_limits{FastNPC := 760, MediumNPC := 235, StrongNPC := 44},  # Round 19

    spawn_limits{FastNPC := 800, MediumNPC := 250, StrongNPC := 46},  # Round 20

    spawn_limits{FastNPC := 840, MediumNPC := 265, StrongNPC := 48},  # Round 21

    spawn_limits{FastNPC := 880, MediumNPC := 280, StrongNPC := 50},  # Round 22

    spawn_limits{FastNPC := 920, MediumNPC := 295, StrongNPC := 52},  # Round 23

    spawn_limits{FastNPC := 960, MediumNPC := 310, StrongNPC := 54},  # Round 24

    spawn_limits{FastNPC := 1000, MediumNPC := 325, StrongNPC := 56},  # Round 25

    spawn_limits{FastNPC := 1040, MediumNPC := 340, StrongNPC := 58},  # Round 26

    spawn_limits{FastNPC := 1080, MediumNPC := 355, StrongNPC := 60},  # Round 27

    spawn_limits{FastNPC := 1120, MediumNPC := 370, StrongNPC := 62},  # Round 28

    spawn_limits{FastNPC := 1160, MediumNPC := 385, StrongNPC := 64},  # Round 29

    spawn_limits{FastNPC := 1200, MediumNPC := 400, StrongNPC := 66} # Round 30

}



\\# Start: disable all spawners (idle)

FastNPCSpawner.Disable()

MediumNPCSpawner.Disable()

StrongNPCSpawner.Disable()

ReadyUpTimer.Disable()



\\# Subscribe to the 1Player spawner's spawn event

Player1Spawner.SpawnedEvent.Subscribe(OnPlayer1Spawned)

Player2Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

Player3Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)

Player4Spawner.SpawnedEvent.Subscribe(OnOtherPlayerSpawned)



\\# Subscribe to dialog button interaction

TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnTowerDefenseButtonPressed)



\\# Subscribe to ReadyUpTimer's SuccessEvent (fires when timer ends)

ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpTimerExpired)

set Self.DialogSubscription = option{Self.TowerDefenseDialog.RespondingButtonEvent.Subscribe(Self.OnDialogButtonPressed)}

\\# Notice: No call to Self.TowerDefenseDialog.Show() or Enable() here

OnOtherPlayerSpawned(Player: agent): void =

Print("Other player spawned, dialog not shown and not subscribed.")

OnPlayer1Spawned(Player: agent): void =

\\# Show dialog and subscribe only for the player spawned from 1Player spawner

if (FirstPlayer := player\\\[Player\\\]):

    TowerDefenseDialog.Show(FirstPlayer)

    Subscription := TowerDefenseDialog.RespondingButtonEvent.Subscribe(OnDialogButtonPressed)

    if (set PlayerDialogSubscriptions\\\[FirstPlayer\\\] = Subscription) {}

    set ActiveDialogPlayer = option{FirstPlayer}

    Print("Waiting for round start...")

OnDialogButtonPressed(Agent:agent, ButtonIndex:int):void =

if (ActivePlayer := ActiveDialogPlayer?, Agent = ActivePlayer, ButtonIndex = 0):

    if (ButtonIndex = 0 and Self.RoundStarted = false):

        set Self.RoundStarted = true

        if (DialogSub := Self.DialogSubscription?):

            DialogSub.Cancel()

        set Self.DialogSubscription = false

        if (ButtonIndex = 0):

            \\# When ready-up begins, keep round/spawners/timer disabled

            FastNPCSpawner.Disable()

            MediumNPCSpawner.Disable()

            StrongNPCSpawner.Disable()

            for (Cinematic : Rounds):

                Cinematic.Stop()

            ReadyUpTimer.Enable()

            ReadyUpTimer.Start()

            \\# Optionally, show a UI message to notify the ready up countdown has started



            \\# Enable and show MainRound + EliminateEnemies for all players

            for (Player : GetPlayspace().GetPlayers()):

                MainRound.Assign(Player)

                MainRound.SetTitleText(StringToMessage("Complete Rounds"))

                MainRound.SetDescriptionText(StringToMessage("Complete all 30 rounds and defend the item from aliens."))

                MainRound.SetValue(0)  # Start at round 1



        \\# Dismiss dialog and clean up only for the active player

        \\# Only the 1Player spawner player can trigger start logic!

       if (Subscription := PlayerDialogSubscriptions\\\[ActivePlayer\\\]):

            Subscription.Cancel()

            TowerDefenseDialog.Hide(ActivePlayer)

        set PlayerDialogSubscriptions = map{}

        set ActiveDialogPlayer = false

        Print("Game Started!")

        \\# Proceed with starting the game...

        \\# Switch camera back to Fortnite's default third-person view

        Self.FixedPointCamera.Disable()

        Self.ThirdPersonControls.Enable()

       

        \\# Reset camera for all players

        for (Player : GetPlayspace().GetPlayers()):

            FixedPointCamera.Disable()

            TowerDefenseDialog.Hide(Player)

        set PlayerDialogSubscriptions = map{}

        \\# Proceed with starting the game...



        \\# Subscribe trigger events for Levels

        for (LevelTrigger : Levels):

            LevelTrigger.TriggeredEvent.Subscribe(OnLevelTriggered)

OnLevelTriggered(Agent : ?agent): void =

\\# Each time any Level trigger fires, update trackers

set CurrentRound += 1

MainRound.SetValue(CurrentRound)

EnableRoundDevices(RoundIndex:int):void =

\\# Enable the trigger for this round

if (RoundIndex >= 0 and RoundIndex < Levels.Length):

    if (Trigger := Levels\\\[RoundIndex\\\]):

        Trigger.Enable()



\\# Play the cinematic for this round

if (RoundIndex >= 0 and RoundIndex < Rounds.Length):

    if (Cinematic := Rounds\\\[RoundIndex\\\]):

        Cinematic.Play()

OnEliminateEnemiesComplete(Agent:agent):void =

for (Player : GetPlayspace().GetPlayers()):

    for (Tracker : EliminateEnemies):

        Tracker.SetValue(0)

        Tracker.Assign(Player)

        Tracker.Remove(Player)

        Tracker.SetTitleText(StringToMessage("Alien Eliminations"))

        Tracker.SetDescriptionText(StringToMessage("Eliminate 43 aliens from reaching the item"))

        Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

        Print("Tracker completed for agent!")



for (Tracker : EliminateEnemies):

    Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

StartNewRound(): void =

\\# Disable previous round if needed

\\# Enable new round devices

EnableRoundDevices(CurrentRound - 1)

OnReadyUpTimerExpired(Agent: ?agent): void =

\\# After 30 seconds, enable round timer and spawners

FastNPCSpawner.Enable()

MediumNPCSpawner.Enable()

StrongNPCSpawner.Enable()

for (Cinematic : Rounds):

    Cinematic.Play()

OnTowerDefenseButtonPressed(Data:tuple(agent, int)):void=

ButtonIndex := Data(1)

case(ButtonIndex):

    \\# 0: Start round, enable all spawners, move players, keep dialog shown

    0 => HandleStartRound()

    \\\_ => Print("Unknown button pressed")

HandleStartRound():void=

if (RoundActive = false):

    set RoundActive = true

    Print("Round {CurrentRound} started.")

    \\# Move all players to new location & keep dialog shown

    for (Player : GetPlayspace().GetPlayers()):

        if (Character := Player.GetFortCharacter\\\[\\\]):

            TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}

            if (Character.TeleportTo\\\[TargetLocation, rotation{}\\\]):

                Print("Moved player to target location at round start.")

StringToMessage(InString: string): message = “{InString}”

round_system_manager script:

using { /Fortnite.com/Devices }

using { /Fortnite.com/Characters }

using { /Verse.org/Simulation }

using { /UnrealEngine.com/Temporary/Diagnostics }

using { /UnrealEngine.com/Temporary/SpatialMath }

using { /UnrealEngine.com/Temporary/UI }

using { /Fortnite.com/UI }

# Struct to hold spawn limits for each NPC type per round

spawn_limits := struct:

FastNPC : int

MediumNPC : int

StrongNPC : int

# Manages round-based gameplay similar to COD Zombies

round_system_manager := class(creative_device):

\# Editable device references



\# 30 levels prerounds

@editable

Levels : \[\]trigger_device = array{}



@editable

ReadyUpTimer : timer_device = timer_device{}



@editable

MainRound : tracker_device = tracker_device{}



@editable

EliminateEnemies : \[\]tracker_device = array{}



@editable

EndRoundDevice : end_game_device = end_game_device{}



@editable

Rounds : \[\]cinematic_sequence_device = array{}



@editable

FastNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

MediumNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

StrongNPCSpawner : npc_spawner_device = npc_spawner_device{}



@editable

TowerDefenseDialog : popup_dialog_device = popup_dialog_device{}



\# Internal state

var CurrentRound : int = 0

var EliminationCount : int = 0

var SpawnLimitsPerRound : \[\]spawn_limits = array{}

var LevelTriggered : \[\]logic = array{}

var IsRoundActive : logic = false

var Players : \[\]player = array{}

ReadyDuration : float = 30.0 # seconds (30 second)



\# Initialize the round system

OnBegin<override>()<suspends> : void =

    \# Initialize spawn limits for each round

    set SpawnLimitsPerRound = array{

        spawn_limits{FastNPC := 81, MediumNPC := 25, StrongNPC := 4},  # Round 1

        spawn_limits{FastNPC := 60, MediumNPC := 20, StrongNPC := 5},  # Round 2

        spawn_limits{FastNPC := 100, MediumNPC := 30, StrongNPC := 8},  # Round 3

        spawn_limits{FastNPC := 140, MediumNPC := 40, StrongNPC := 10},  # Round 4

        spawn_limits{FastNPC := 180, MediumNPC := 50, StrongNPC := 12},  # Round 5

        spawn_limits{FastNPC := 230, MediumNPC := 60, StrongNPC := 15},  # Round 6

        spawn_limits{FastNPC := 270, MediumNPC := 75, StrongNPC := 18},  # Round 7

        spawn_limits{FastNPC := 315, MediumNPC := 90, StrongNPC := 20},  # Round 8

        spawn_limits{FastNPC := 360, MediumNPC := 105, StrongNPC := 22},  # Round 9

        spawn_limits{FastNPC := 400, MediumNPC := 120, StrongNPC := 24},  # Round 10

        spawn_limits{FastNPC := 440, MediumNPC := 130, StrongNPC := 26},  # Round 11

        spawn_limits{FastNPC := 480, MediumNPC := 140, StrongNPC := 28},  # Round 12

        spawn_limits{FastNPC := 520, MediumNPC := 150, StrongNPC := 30},  # Round 13

        spawn_limits{FastNPC := 560, MediumNPC := 160, StrongNPC := 32},  # Round 14

        spawn_limits{FastNPC := 600, MediumNPC := 175, StrongNPC := 35},  # Round 15

        spawn_limits{FastNPC := 640, MediumNPC := 190, StrongNPC := 38},  # Round 16

        spawn_limits{FastNPC := 680, MediumNPC := 205, StrongNPC := 40},  # Round 17

        spawn_limits{FastNPC := 720, MediumNPC := 220, StrongNPC := 42},  # Round 18

        spawn_limits{FastNPC := 760, MediumNPC := 235, StrongNPC := 44},  # Round 19

        spawn_limits{FastNPC := 800, MediumNPC := 250, StrongNPC := 46},  # Round 20

        spawn_limits{FastNPC := 840, MediumNPC := 265, StrongNPC := 48},  # Round 21

        spawn_limits{FastNPC := 880, MediumNPC := 280, StrongNPC := 50},  # Round 22

        spawn_limits{FastNPC := 920, MediumNPC := 295, StrongNPC := 52},  # Round 23

        spawn_limits{FastNPC := 960, MediumNPC := 310, StrongNPC := 54},  # Round 24

        spawn_limits{FastNPC := 1000, MediumNPC := 325, StrongNPC := 56},  # Round 25

        spawn_limits{FastNPC := 1040, MediumNPC := 340, StrongNPC := 58},  # Round 26

        spawn_limits{FastNPC := 1080, MediumNPC := 355, StrongNPC := 60},  # Round 27

        spawn_limits{FastNPC := 1120, MediumNPC := 370, StrongNPC := 62},  # Round 28

        spawn_limits{FastNPC := 1160, MediumNPC := 385, StrongNPC := 64},  # Round 29

        spawn_limits{FastNPC := 1200, MediumNPC := 400, StrongNPC := 66} # Round 30

    }



    \# Save player list

    set Players = GetPlayspace().GetPlayers()



    \# Initialize first round

    EnableCurrentLevel()

    PlayCurrentRoundCinematic()

    ActivateRoundDevices(CurrentRound)



    \# Initial setup - disable all spawners and timers

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    ReadyUpTimer.Disable()

   

    \# Subscribe to device events

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpTimerComplete)



    \# Start with ready up for round 1

    ReadyUpTimer.Enable()

    ReadyUpTimer.Start() # Start Ready Up at game start



OnReadyUpTimerComplete(Agent:?agent):void =

    \# When ready up ends, update spawner limits and start round timer

    UpdateSpawnerLimits()



OnReadyUpEnded(Agent : ?agent) : void =

    \# Subscribe to ReadyUpTimer

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpEnded)

    ReadyUpTimer.Start() # Start the 30s timer



    \# Enable and show MainRound + EliminateEnemies for all players

    for (Player : GetPlayspace().GetPlayers()):

        MainRound.Assign(Player)

        MainRound.SetTitleText(StringToMessage("Complete Rounds"))

        MainRound.SetDescriptionText(StringToMessage("Complete all 30 rounds and defend the item from aliens."))

        MainRound.SetValue(0) # Start at round 1



    for (Player : GetPlayspace().GetPlayers()):

        MainRound.Remove(Player)



OnLevelTriggered(Agent : ?agent): void =

    \# Each time any Level trigger fires, update trackers

    set CurrentRound += 1

    MainRound.SetValue(CurrentRound)



    \# Subscribe trigger events for Levels

    for (LevelTrigger : Levels):

        LevelTrigger.TriggeredEvent.Subscribe(OnLevelTriggered)



    Print("Level trigger activated, updated round trackers.")



\# Enable only the current round's level trigger, disable others

EnableCurrentLevel():void =

    for (Index -> Level : Levels):

        if (Index = CurrentRound - 1):

            Level.Enable()

        else:

            Level.Disable()



\# Play the cinematic for the current round if it exists

PlayCurrentRoundCinematic():void =

    if (CurrentRound - 1 >= 0 and CurrentRound - 1 < Rounds.Length):

        if (CurrentCinematic := Rounds\[CurrentRound - 1\]):

            CurrentCinematic.Play()



EnableRoundDevices(RoundIndex:int):void =

    \# Enable the trigger for this round

    if (RoundIndex >= 0 and RoundIndex < Levels.Length):

        if (Trigger := Levels\[RoundIndex\]):

            Trigger.Enable()



    \# Play the cinematic for this round

    if (RoundIndex >= 0 and RoundIndex < Rounds.Length):

        if (Cinematic := Rounds\[RoundIndex\]):

            Cinematic.Play()

            

\# Completely resets all triggers and cinematics, then enables/plays just one of each

ActivateRoundDevices(RoundNumber : int):void =

    \# Disable all triggers and stop all cinematics

    for (Trigger : Levels):

        Trigger.Disable()

    for (Cine : Rounds):

        Cine.Stop()



    \# Only enable & play what matches this round

    if (RoundNumber > 0 and RoundNumber <= Levels.Length):

        if (Level := Levels\[RoundNumber - 1\]):

            Level.Enable()

    if (RoundNumber > 0 and RoundNumber <= Rounds.Length):

        if (SelectedCine := Rounds\[RoundNumber - 1\]):

            SelectedCine.Play()



\# Advance to the next round safely

AdvanceRound():void =

    set CurrentRound += 1

    ActivateRoundDevices(CurrentRound)



OnEliminateEnemiesComplete(Agent:agent):void =

    for (Player : GetPlayspace().GetPlayers()):

        for (Tracker : EliminateEnemies):

            Tracker.SetValue(0)

            Tracker.Assign(Player)

            Tracker.Remove(Player)

            Tracker.SetTitleText(StringToMessage("Alien Eliminations"))

            Tracker.SetDescriptionText(StringToMessage("Eliminate 43 aliens from reaching the item"))

            Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)

            Print("Tracker completed for agent!")



    for (Tracker : EliminateEnemies):

        Tracker.CompleteEvent.Subscribe(OnEliminateEnemiesComplete)



\# Handle ready up phase completion

OnReadyUpComplete(Agent : ?agent) : void =

    if (IsRoundActive = false):

    StartRound()

    EndRound()



\# Call this when starting a new round

StartNewRound():void =

    if (CurrentRound < 30):

        set CurrentRound += 1

        EnableCurrentLevel()

        PlayCurrentRoundCinematic()



\# Start a new round

StartRound() : void =



    set IsRoundActive = true

    set CurrentRound += 1



    \# Update round tracker

    MainRound.SetValue(CurrentRound)



    \# Spawn enemies based on round

    SpawnEnemiesForRound()



    \# Enable and spawn NPCs

    FastNPCSpawner.Enable()

    MediumNPCSpawner.Enable()

    StrongNPCSpawner.Enable()

    FastNPCSpawner.Spawn()

    MediumNPCSpawner.Spawn()

    StrongNPCSpawner.Spawn()



    \# Play round start cinematic

    for (Cinematic : Rounds):

        Cinematic.Play()



    \# Teleport players to start position

    for (Player : GetPlayspace().GetPlayers()):

        if (Character := Player.GetFortCharacter\[\]):

            TargetLocation := vector3{X := 1000.0, Y := 1000.0, Z := 200.0}

            if (Character.TeleportTo\[TargetLocation, rotation{}\]):

                Print("Moved player to target location at round start.")



\# Spawn enemies scaled to current round

SpawnEnemiesForRound() : void =

    \# Calculate spawn counts based on round

    var FastCount : int = 50 + CurrentRound

    var MediumCount : int = 10 + CurrentRound

    var StrongCount : int = 0

    if (CurrentRound >= 5):

    set StrongCount = CurrentRound - 1



    \# Spawn enemies

    for (Index := 50..FastCount):

        FastNPCSpawner.Spawn()



    for (Index := 10..MediumCount):

        MediumNPCSpawner.Spawn()



    for (Index := 1..StrongCount):

        StrongNPCSpawner.Spawn()



OnDialogButtonPressed(Agent: agent, ButtonIndex: int): void =

    if (ButtonIndex = 0):

        \# When ready-up begins, keep round/spawners/timer disabled

        FastNPCSpawner.Disable()

        MediumNPCSpawner.Disable()

        StrongNPCSpawner.Disable()

        for (Cinematic : Rounds):

            Cinematic.Stop()

        \# Set durations to avoid incorrect settings

        ReadyUpTimer.SetMaxDuration(30.0) # 30 seconds ready-up

        ReadyUpTimer.Enable()

        ReadyUpTimer.Start()



OnReadyUpTimerExpired(Agent: ?agent): void =

    \# After 30 seconds, enable round timer and spawners

    FastNPCSpawner.Enable()

    MediumNPCSpawner.Enable()

    StrongNPCSpawner.Enable()

    for (Cinematic : Rounds):

        Cinematic.Play()

   

    set IsRoundActive = true

    set CurrentRound += 1

    MainRound.SetValue(CurrentRound)



    \# Update round tracker and reset eliminations for all players

    set CurrentRound += 1

    for (Player : GetPlayspace().GetPlayers()):

        MainRound.SetValue(CurrentRound)



\# Handle round timer completion

OnRoundTimerComplete(Agent : ?agent) : void =

    if (IsRoundActive = true):

    EndRound()



\# Handle all enemies eliminated

OnAllEnemiesEliminated(Agent : agent) : void =

    if (IsRoundActive = true):

        EndRound()

       

\# End the current round

EndRound() : void =

    set IsRoundActive = false

    \# Stop all spawners

    FastNPCSpawner.DespawnAll(false)

    MediumNPCSpawner.DespawnAll(false)

    StrongNPCSpawner.DespawnAll(false)

    \# Check if game should end

    if (CurrentRound >= Rounds.Length):

        EndGame()



    \# Disable all spawners

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()



    \# End the game if this is Level 30

    if (CurrentRound = 30):

        RoundPlayers := GetPlayspace().GetPlayers()

        if (Players.Length > 0):

            if (FirstPlayer := Players\[0\]):

                EndRoundDevice.Activate(FirstPlayer)



    else:

        \# Start ready up phase for next round

        \# Reset timers and start ReadyUp again

        ReadyUpTimer.Reset()

        ReadyUpTimer.Start()



\# End the game

EndGame() : void =

    \# Activate end game device for the first player

    if (FirstPlayer := GetPlayspace().GetPlayers()\[0\]):

        EndRoundDevice.Activate(FirstPlayer)



\# Start next round

StartNextRound() : void =

    ReadyUpTimer.Start()



\# Update spawner limits based on current round

UpdateSpawnerLimits():void =

    if (CurrentRound <= SpawnLimitsPerRound.Length):

        if (Limits := SpawnLimitsPerRound\[CurrentRound - 1\]):

            \# Enable/disable spawners based on limits

            if (Limits.FastNPC > 0):

                FastNPCSpawner.Enable()

            else:

                FastNPCSpawner.Disable()



            if (Limits.MediumNPC > 0):

                MediumNPCSpawner.Enable()

            else:

                MediumNPCSpawner.Disable()



            if (Limits.StrongNPC > 0):

                StrongNPCSpawner.Enable()

            else:

                StrongNPCSpawner.Disable()



StringToMessage<localizes>(InString: string): message = "{InString}"
1 Like

Hey @NoahD1 !!

So I assume the first question/issue is solved now, right?

Sorry for the mistake with the commented code, I did that to isolate the issue and completely forgot to uncomment that part of the code before sending it back to you!

Regarding your new question, as it is completly different from the first one, I need to ask you to create a new post in the forum and mark this one as solved in the corresponding comment!

Thanks in advance for that!

1 Like

You’re welcome dude, I’ll go ahead and create a new post in the forum about trying to make a tower defense game and about the solved for corresponding comment and someday I think I’ll do a tutorial about it too, and thank you so much for you help on multiplayer UI button topic. Super appreciated. So I’ll try the ReadyDialog and somethings you added in two scripts like # Reference to the Title Screen Manager custom device ScreensHandler: title_screen_handler=title_screen_handler{} and @editableeditable ReadyDialog: popup_dialog_device=popup_dialog_device{}, and if I’m having issues with it. I’ll let you know. :+1:

1 Like