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.
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!
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
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!
Sound good, thank you so much dude.
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:
- Added new screen between title and round start
- New management to “show” and “hide” screens for each player, not using “HideAllDialogs” function
- Added a map and a counter variable to know how many players are ready
- Showing “Start Round” screen only if all the players are ready
- All the screens are managed inside the “title_screen_handler” device, avoiding duplicates (for example “TowerDefenseDialog”)
- “title_screen_handler” decides which screen is shown at any moment, instead of managing “TowerDefenseDialog” also on “tower_defense_manager”
- 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)
- 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!
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.
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!!
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 .
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}"
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!
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.