Back
How We Improved the Speedway Race Template in Fortnite with Verse Persistence, and More
The Fortnite Team
The introduction of Verse Persistence enabled an opportunity to upgrade the Fortnite Creative “Design a Speedway Race” template island to UEFN. From updates like adding cinematics and landscaping, to moving away from triggers and into Verse, we’re excited to share the journey of this template's development with you. Welcome to Speedway Race with Verse Persistence!
The “Design a Speedway Race” template was initially created to use the newly created race track assets and build a distinct racing experience with some nice quality-of-life features.
With the latest update, we took a holistic approach and replaced and upgraded many features throughout the map. Let’s take a look at some of the updates we’ve made that really take advantage of UEFN’s powerful capabilities.
The legacy day/night cycle of the original project was replaced with the more advanced Fortnite Battle Royale Chapter 4 lighting. This new cycle enabled us to use Lumen, creating softer shadows and realistic global illumination. We also added features like a waterfall and made the track itself more visually interesting.
Verse Persistence provided the solution, enabling data tracking across game sessions. This functionality allowed us to monitor lifetime player stats and create local leaderboards.
Our goal was to update a player’s best lap time upon finishing a lap and also record their points and wins whenever they finished a race.
Updating lap times was straightforward, as we could wait on the Race Manager device’s LapCompletedEvent to detect whenever a player finished a lap. When the race begins, we start a Timer device that runs per player. The WaitForPlayerToFinishLap function waits for a player to finish a lap, calculates their lap time, updates their stats table, and then resets the timer for the next lap recording.
To solve this, we used the ArraySync function. ArraySync calls an asynchronous function on each element in the array passed to it and waits for all those functions to complete. By passing an array of players and the WaitForPlayerToFinishRace function, we could call the function on each of them and wait until it had finished for everyone.
This enabled us to award points and a win to players based on their placement when they finished the race, then update their stats table accordingly. When ArraySync finishes, we know all players have finished the race and can end the game.
Since we already had persistent stats for each player, we could retrieve those stats and display them using billboards. In the pregame waiting area, we added Billboard and Player Reference devices for each player to showcase both their stats and outfits used at the time. However, sorting these players based on performance remained a challenge.
To achieve this, we implemented the Merge Sort algorithm. Merge Sort is a common divide-and-conquer algorithm that recursively divides an array into two, sorts each sub-array, and merges them back together. We also added the ability to pass in a comparison function to the algorithm, and we created comparison functions for each of the different player stats.
Whenever we needed to sort players, we could retrieve each player’s stats from their stats table, add them all to an array, and pass both it and a comparison function to the Merge Sort algorithm. By choosing which comparison function we passed, we could get back the array of players sorted by any stat. We’ve included both the Merge Sort algorithm and a testing file in the new template, so you can test the algorithm yourself and tailor it to your own functions!
With sorting mechanisms in place, we finalized our leaderboards. During the first round of the game, players spawn in a pregame waiting area with their player references and billboards. We sort these player references by each player’s lifetime points and display each of their stats on the billboard in front of them. This gives players a chance to scope out the competition before the race and know who to watch out for if they want to win.
Keep playing, and you might find yourself at the top of the leaderboard!
To do this, we created a second player weak map variable named CircuitInfo. This CircuitInfo variable holds all the information we need to reset at the end of the game or when a player leaves.
We used a second weak map variable to make it clear which info should be reset (circuit info) and which should persist forever (player stats).
For each player, we record the following information in the CircuitInfo player weak map variable:
In the OnBegin function of the Verse device (which runs at the beginning of each round), we figure out which round we’re in by iterating through all active players and getting the highest last completed round value from their persistent data. We only do this once per round and record the round info in a session weak map variable so all Verse code in the project can access this value at any time without needing to recompute it.
When a player completes the race, detected by waiting on the Race Manager device’s RaceCompletedEvent, we record their finish order and the current round in the CircuitInfo weak map variable associated with the player. We reset this information under two conditions:
In the updated template, we replace the Pulse Trigger with Sequencer to achieve our opening cinematic. Using Sequencer, we were able to add different cameras, heads-up display (HUD) elements, and a dynamic lineup view that adjusts based on the amount of active players.
Similar to how we configured the Pulse Trigger, we connected devices to the sequence at important moments, enabling us to determine when to display the next player’s score or when to cut the intro.
We look forward to updating this template as new features are released that enhance its design. In the meantime, you can download the template, explore its components, and integrate its functionality into your projects through the template tab in UEFN. We can’t wait to see your races, leaderboards, and more take shape!
The “Design a Speedway Race” template was initially created to use the newly created race track assets and build a distinct racing experience with some nice quality-of-life features.
With the latest update, we took a holistic approach and replaced and upgraded many features throughout the map. Let’s take a look at some of the updates we’ve made that really take advantage of UEFN’s powerful capabilities.
Visual Design
Landscape mode in UEFN enabled us to go back to the roots of racing and create an off-road track using the new landscape editing tools. The mountains made of rock assets in the background of our original island were replaced with a more natural landscape design.The legacy day/night cycle of the original project was replaced with the more advanced Fortnite Battle Royale Chapter 4 lighting. This new cycle enabled us to use Lumen, creating softer shadows and realistic global illumination. We also added features like a waterfall and made the track itself more visually interesting.
Verse Persistence: A Better Leaderboard
In the original map, a tower using the Player Reference device displayed the player in first place and how many points they had. However, the data would not persist from session to session.Verse Persistence provided the solution, enabling data tracking across game sessions. This functionality allowed us to monitor lifetime player stats and create local leaderboards.
Tracking Player Stats
We developed a persistable player stats table class that tracks a player’s lifetime wins, their best lap time, and the points earned per race finish. We also used a persistable weak map named PlayerStatsMap to map players to their player stats tables, enabling these stats to persist across rounds and sessions. Then, we created a stats manager class to handle initializing, retrieving, and updating these stats per player.Our goal was to update a player’s best lap time upon finishing a lap and also record their points and wins whenever they finished a race.
Updating lap times was straightforward, as we could wait on the Race Manager device’s LapCompletedEvent to detect whenever a player finished a lap. When the race begins, we start a Timer device that runs per player. The WaitForPlayerToFinishLap function waits for a player to finish a lap, calculates their lap time, updates their stats table, and then resets the timer for the next lap recording.
Recording Wins and Points
Recording wins and points proved to be more complex. We knew we could use the similar “WaitForPlayerToFinishRace” function to wait on the Race Manager’s “RaceCompletedEvent” and know when any player finished. However, we wanted the game to end only when all players finished, and the Race Manager didn’t have a way of tracking this or ending the game when they did.To solve this, we used the ArraySync function. ArraySync calls an asynchronous function on each element in the array passed to it and waits for all those functions to complete. By passing an array of players and the WaitForPlayerToFinishRace function, we could call the function on each of them and wait until it had finished for everyone.
This enabled us to award points and a win to players based on their placement when they finished the race, then update their stats table accordingly. When ArraySync finishes, we know all players have finished the race and can end the game.
Displaying Results
Recording stats was one thing, but we also needed a way to display these stats to players. We wanted to create in-level leaderboards that were visible, and sorted players by their lifetime points to highlight the top players.Since we already had persistent stats for each player, we could retrieve those stats and display them using billboards. In the pregame waiting area, we added Billboard and Player Reference devices for each player to showcase both their stats and outfits used at the time. However, sorting these players based on performance remained a challenge.
To achieve this, we implemented the Merge Sort algorithm. Merge Sort is a common divide-and-conquer algorithm that recursively divides an array into two, sorts each sub-array, and merges them back together. We also added the ability to pass in a comparison function to the algorithm, and we created comparison functions for each of the different player stats.
Whenever we needed to sort players, we could retrieve each player’s stats from their stats table, add them all to an array, and pass both it and a comparison function to the Merge Sort algorithm. By choosing which comparison function we passed, we could get back the array of players sorted by any stat. We’ve included both the Merge Sort algorithm and a testing file in the new template, so you can test the algorithm yourself and tailor it to your own functions!
With sorting mechanisms in place, we finalized our leaderboards. During the first round of the game, players spawn in a pregame waiting area with their player references and billboards. We sort these player references by each player’s lifetime points and display each of their stats on the billboard in front of them. This gives players a chance to scope out the competition before the race and know who to watch out for if they want to win.
Keep playing, and you might find yourself at the top of the leaderboard!
Racer Order at the Start Line
The Fortnite Creative version of the map puts players in a random order at the start of each game. To motivate players to achieve higher rankings on the leaderboard, we set the starting line order based on their finishing position from the previous round.Tracking Round Info
Unlike the persistable data we used for the leaderboard, we needed the racer order and circuit information to persist across all the rounds but not across all game sessions. A session weak map variable in Verse resets its data every round, so we have to store this information for each player and reset it after the game ends.To do this, we created a second player weak map variable named CircuitInfo. This CircuitInfo variable holds all the information we need to reset at the end of the game or when a player leaves.
We used a second weak map variable to make it clear which info should be reset (circuit info) and which should persist forever (player stats).
For each player, we record the following information in the CircuitInfo player weak map variable:
- Finish order
- Last completed round
Round-Specific Logic
We need to know which round we’re in to apply round-specific logic (such as ordering players by their last finish order if it’s not the first round) and to know when to reset the player data. There’s currently no API for getting the current round which is why we have to record it in the persistable data for each player.In the OnBegin function of the Verse device (which runs at the beginning of each round), we figure out which round we’re in by iterating through all active players and getting the highest last completed round value from their persistent data. We only do this once per round and record the round info in a session weak map variable so all Verse code in the project can access this value at any time without needing to recompute it.
When a player completes the race, detected by waiting on the Race Manager device’s RaceCompletedEvent, we record their finish order and the current round in the CircuitInfo weak map variable associated with the player. We reset this information under two conditions:
- The player leaves during the game. We subscribe to the playspace’s PlayerRemovedEvent to know when they leave and call our ResetCircuitInfo function.
- At the start of each round, we compute the last completed round. If it was the final round, we invoke ResetCircuitInfo to refresh the player’s data. We know how many rounds there are by having an editable property on the Verse device that is set to the total number of rounds for a game. This is on the island creator to make sure it matches with the round settings.
When to Use Session Weak Maps vs. Player Weak Maps
This new Speedway Race is a great example to show the differences and reasoning for using the session weak map variable and player weak map variable in your code:- Session weak map variables are useful for singletons and storing data for the current round that you don’t want to recompute every time.
- The player weak map variables are designed for information that needs to persist across multiple rounds and game sessions but must be associated with individual players.
Pulse Trigger to Opening Sequence
In the original version of the Speedway Race template, we utilized a device called a Pulse Trigger to orchestrate the “ready - set - go” part of the race. The Pulse Trigger played a sequence of events over a set period of time by activating triggers to display text and enable lights on the starting line.In the updated template, we replace the Pulse Trigger with Sequencer to achieve our opening cinematic. Using Sequencer, we were able to add different cameras, heads-up display (HUD) elements, and a dynamic lineup view that adjusts based on the amount of active players.
Similar to how we configured the Pulse Trigger, we connected devices to the sequence at important moments, enabling us to determine when to display the next player’s score or when to cut the intro.
What’s Next?
As UEFN continues to advance, we hope to also evolve this template. Our goal is to empower you to continue to build more sophisticated content with UEFN, leveraging the latest features to build fun, engaging islands.We look forward to updating this template as new features are released that enhance its design. In the meantime, you can download the template, explore its components, and integrate its functionality into your projects through the template tab in UEFN. We can’t wait to see your races, leaderboards, and more take shape!