How do I make a round system for a tower defense game in UEFN?


Hey everyone, do you all know what UEFN devices did vysena use for swisscom hero city tower defense? I’m trying to make a round system for a project I’m working, it’s called 20ALIENZ TOWER DEFENSE, it’s where players have to be on the turrets to shot the enemies before reaching the precious item and they have to survive 30 rounds. So I’ll share two verse codes and I share the video example of what I’m trying to do and what went wrong, and let me if y’all have any helpful thoughts, tips, and advices. It would be really helpful, because I’ve tried to make it exactly like swisscom hero city tower defense and COD zombie wave system, it’s going wrong.

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

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{}



\# 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{}



\# 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



\# 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



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

            \# 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()):

                FixedPointCamera.Disable()

                spawn{HandleTimerExpiration()}

                TowerDefenseDialog.Hide(Player)

            set PlayerDialogSubscriptions = map{}

            \# Proceed with starting the game...



            \# 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()

    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

    else:

        Print("Can't find 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.")



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

        if (CurrentRoundIndex = 29):



    \# RoundSettings: notify round end

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

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

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

    set CurrentRound += 1

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 }

# Manages round-based gameplay similar to COD Zombies

round_system_manager := class(creative_device):

\# Editable device references

@editable

Levels : \[\]trigger_device = array{}



@editable

ReadyUpTimer : timer_device = timer_device{}



@editable

MainRound : tracker_device = tracker_device{}



@editable

EliminateEnemies : tracker_device = tracker_device{}



@editable

EndRoundDevice : end_game_device = end_game_device{}



@editable

Rounds : \[\]round_settings_device = array{}



@editable

GateSequencer : cinematic_sequence_device = cinematic_sequence_device{}



@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

RoundTimer : timer_device = timer_device{}



\# Internal state

var CurrentRound : int = 0

var IsRoundActive : logic = false



\# Initialize the round system

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

    \# Initial setup - disable all spawners and timers

    FastNPCSpawner.Disable()

    MediumNPCSpawner.Disable()

    StrongNPCSpawner.Disable()

    RoundTimer.Reset()

    RoundTimer.Disable()



    \# Subscribe to device events

    RoundTimer.SuccessEvent.Subscribe(OnRoundTimerComplete)

    ReadyUpTimer.SuccessEvent.Subscribe(OnReadyUpComplete)

    EliminateEnemies.CompleteEvent.Subscribe(OnAllEnemiesEliminated)

    \# Start first round

    StartNextRound()



\# Handle ready up phase completion

OnReadyUpComplete(Agent : ?agent) : void =

    if (IsRoundActive = false):

    StartRound()



\# Start a new round

StartRound() : void =



    set IsRoundActive = true

    set CurrentRound += 1



    \# Update round tracker

    MainRound.SetValue(CurrentRound)



    \# Start round timer

    RoundTimer.Start()



    \# 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

    GateSequencer.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.")



                \# Start round timer

                RoundTimer.Enable()

                RoundTimer.Reset()

                RoundTimer.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()



\# 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 round for all players

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

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

            RoundDevice.EndRound(Player)



    \# Check if game should end

    if (CurrentRound >= 30):

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

            EndRoundDevice.Activate(FirstPlayer)



    else:

        \# Start ready up phase for next round

        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()