Many experiences use player stats to track player experience data over time. Statistics like high score, total games won, total play time and collected items give players a sense of progression and are all great ways to encourage players to return to your experience.
Verse Persistence is a powerful tool that allows you to add persistable data to your Verse scripts. Persistable data is saved on a per-player, per-island basis, and stays the same between gameplay sessions. Persistable data will allow you to track player progress between play sessions and will open up a variety of unique and interesting play experiences previously unavailable in UEFN.
This tutorial will show you how to create a custom table of player stats using Verse and set them up to be persistent across multiple plays of your experience. After finishing this tutorial, check out Make Your Own In-Game Leaderboard in Verse to learn how to use persistence to build in-game leaderboards!
Verse Language Features Used
Class: This example creates a Verse class that manages a single stat as well as a persistable class that tracks a group of stats for a single player.
Constructor: A constructor is a special function that creates an instance of the class it is associated with.
Weak_map: A weak_map is a simple map that cannot be iterated over. Verse persistable data is required to be stored in a weak_map.
Setting Up the Level
This example uses the following props and devices.
2 x Button devices: When the player interacts with the device, they’ll add a point to their current Score. You’ll use another button device to simulate the end of a game, adding to the player’s wins or losses depending on their current score.
1 x Billboard device: It is often important to display persistent data to the player. Sometimes this is done for testing purposes and other times to boost player engagement or show progress. While requirements of when to show data and what data to show will vary from experience to experience, in this example you’ll show the stat data for score, high score, wins, and losses on a billboard device.
Tracking Persistable Player Stats
First, it’s important to define what stats you want to track per player. For example, you might want to track a player’s all-time score, their current rank, or their best time in a lap. In this example, you’re going to track score, wins, and losses in a table of stat values for each player. You’ll do this in a new class, player_stats_table which will be your main persistable class.
Follow these steps to create your player_stats_table class:
Create a new verse file using Verse Explorer named
player_stats_table.verse.In your new Verse file create a new class named
player_stats_table. Add both the<persistable>and<final>modifiers to your class. The<persistable>modifier allows the data in the class to be persistable and requires the<final>modifier since persistable data cannot be overridden or subclassed from.Verseusing { /Fortnite.com/Devices } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } # Tracks different persistable stats for each player. player_stats_table := class<final><persistable>:To your
player_stats_table, add threeintvalues namedScore,Wins, andLosses. These track the lifetime score, wins, and losses for each player respectively. Also, add anintnamedVersionto track the current version of yourplayer_stats_table.Verse# Tracks different persistable stats for each player. player_stats_table := class<final><persistable>: # The version of the current stats table. Version<public>:int = 0 # The score of a player. Score<public>:int = 0 # The number of wins for a player. Wins<public>:int = 0To create an instance of the
player_stats_table class, you’re going to use a<constructor>function. This constructor is required because Verse persistence does not allow classes containing variable fields to be persistable. Using a constructor will allow you to update your persistable class values by creating a copy of an existing persistable stat that is variable, update the copy, and then replace the original instance of the class with the changed values. Add a new constructor functionMakePlayerStatsTable()to your file. This constructor will take an original (previous) instance of theplayer_stats_tableclass and create a new one from the original given values.Verse# Creates a new player_stats_table with the same values as the previous player_stats_table. MakePlayerStatsTable<constructor>(OldTable:player_stats_table)<transacts> := player_stats_table: Version := OldTable.Version Score := OldTable.Score Wins := OldTable.Wins Losses := OldTable.LossesTo track all of your
player_stats_tables, you’ll use a persistableweak_mapofplayertoplayer_stats_tableinstances. Add this weak map to your file.Verse# Maps players to a table of their player stats. var PlayerStatsMap:weak_map(player, player_stats_table) = map{}Your complete
player_stats_tableclass should now look like this:Verseusing { /Fortnite.com/Devices } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } # Tracks different persistable stats for each player. player_stats_table := class<final><persistable>: # The version of the current stats table. Version<public>:int = 0 # The score of a player.
Managing Player Stats for all Players
Your player_stats_table class allows you to track stats for an individual player, but you don’t yet have a way to manage them. You need to update the stat tables for each player whenever they gain score, and depending on the design of your experience there may be many players at once.
To solve this, you’ll use another class to manage stats for all players, and record stat changes whenever a player gains a win, loss, or scores. Follow the steps below to set up your manager class.
Create a new Verse file using Verse Explorer named
player_stats_manager. In that file, create a new classplayer_stats_manager.Verseusing { /Fortnite.com/Devices } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } # Manages and updates player_stat_tables for each player. player_stats_manager := class():Your
player_stats_managerneeds to do several things. It needs to set up aplayer_stats_tablefor a player, update theScore,Wins, andLossesper player, and return theplayer_stats_tablefor a player. You’ll handle each of these in separate functions. Add a new functionInitializePlayer()to yourplayer_stats_managerclass definition. This function will initialize the stats for the given player.Verse# Initialize stats for the given player. InitializePlayer(Player:player):void=In
InitializePlayer(), check if the given player already exists in thePlayerStatsMap. If not, set the value of that player in the map to a newplayer_stats_table. Your completedInitializePlayer()function should look like the following:Verse# Initialize stats for the given player. InitializePlayer(Player:player):void= if: not PlayerStatsMap[Player] set PlayerStatsMap[Player] = player_stats_table{} else: Print("Unable to initialize player stats")Add a new function
InitializeAllPlayers()to yourplayer_stats_managerclass definition. This function takes an array of players and callsInitializePlayer()on all of them. Your completedInitializeAllPlayers()function should look like the following:Verse# Initialize stats for all current players. InitializeAllPlayers(Players:[]player):void = for (Player : Players): InitializePlayer(Player)To return the stats for a particular player, you need a function that returns that player’s
player_stats_table. Add a new functionGetPlayerStats()to theplayer_stats_managerclass definition that takes an agent. Add the<decides><transacts>modifier to allow this function to fail and roll back in the case where a player’s stat table doesn’t exist. InGetPlayerStats(), create a newplayer_stats_tablevariablePlayerStats.Verse# Return the player_stats_table for the provided Agent. GetPlayerStats(Agent:agent)<decides><transacts>:player_stats_table= var PlayerStats:player_stats_table = player_stats_table{}In an
ifexpression, cast theAgentpassed to this function to aPlayer, and then retrieve theplayer_stats_tablefor that player from thePlayerStatsMap. Then setPlayerStatsto that table by callingMakePlayerStatsTable(). Finally, returnPlayerStats. Your completedGetPlayerStats()function should look like the following:Verse# Return the player_stats_table for the provided Agent. GetPlayerStats(Agent:agent)<decides><transacts>:player_stats_table= var PlayerStats:player_stats_table = player_stats_table{} if: Player := player[Agent] PlayerStatsTable := PlayerStatsMap[Player] set PlayerStats = MakePlayerStatsTable(PlayerStatsTable) PlayerStatsTo update each of the Score, Wins, and Losses stats, you’re going to create functions for each respective stat. Add a new function named
AddScore()to yourplayer_stats_managerfile. This function takes the agent to award score to and theintnumber of points to award them.Verse# Adds to the given Agent's score and updates both their stats table # in PlayerStatsManager and the billboard in the level. AddScore<public>(Agent:agent, NewScore:int):void=Data is updated by first validating that the player has valid data in the persistable
weak_mapand then by replacing that data with an updated copy of the class. To handle this for the score, retrieve the player’s score from thePlayerStatsTable, then set the table in thePlayerStatsMapto the result of constructing a newplayer_stats_tableusingMakePlayerStatsTable(), passing the current score plus the new score. When you are working with a class that contains several fields, the class constructor allows you to easily update a single field without explicitly copying all fields every time you want to make an update. YourAddScore()function should look like the following:Verse# Adds to the given Agent's score and updates both their stats table # in PlayerStatsManager and the billboard in the level. AddScore<public>(Agent:agent, NewScore:int):void= if: Player := player[Agent] PlayerStatsTable := PlayerStatsMap[Player] CurrentScore := PlayerStatsTable.Score set PlayerStatsMap[Player] = player_stats_table: MakePlayerStatsTable<constructor>(PlayerStatsTable) Score := CurrentScore + NewScoreRepeat this process for wins and losses, adding
NewWinsandNewLossesto the player’s wins or losses respectively when callingMakePlayerStatsTable().Verse# Adds to the given Agent's wins and updates both their stats table # in PlayerStatsManager and the billboard in the level. AddWin<public>(Agent:agent, NewWins:int):void= if: Player := player[Agent] PlayerStatsTable := PlayerStatsMap[Player] CurrentWins := PlayerStatsTable.Wins set PlayerStatsMap[Player] = player_stats_table: MakePlayerStatsTable<constructor>(PlayerStatsTable) Wins := CurrentWins + NewWinsYour final
player_stats_managerfile should look like the following.Verseusing { /Fortnite.com/Devices } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } # Manages and updates player_stat_tables for each player. player_stats_manager := class(): # Return the player_stats_table for the provided Agent. GetPlayerStats(Agent:agent)<decides><transacts>:player_stats_table= var PlayerStats:player_stats_table = player_stats_table{}
Testing Persistence with Devices
Now that you’ve set up your persistence classes, it’s time to test them in your level.
Create a new Verse device named player_stats_example. See Create Your Own Device Using Verse for steps.
At the top of the
player_stats_exampleclass definition, add the following fields:An editable
button_devicenamedScorePointsButton. This button adds to the player’s score whenever it’s activated.Verse# Adds to the activating player's score. @editable ScorePointsButton:button_device = button_device{}An editable
billboard_devicenamedStatsBillboard. This will display the player’s Score, High Score, Wins, and Losses.Verse# Displays the player's Score, High Score, Wins, and Losses @editable StatsBillboard:billboard_device = billboard_device{}An editable button_device named
CheckWinButton. This button resets each player’s score and gives them a win or a loss depending on their player’s score.Verse# Resets the player's score and award them a win or a loss # depending if their current score is greater than WinScore. @editable CheckWinButton:button_device = button_device{}An editable
intnamedWinScore. This is the score players need to reach to be awarded a win after theCheckWinButtonis activated.Verse# The score players need to reach to be awarded a win after # the CheckWinButton is activated. @editable WinScore:int = 10An editable
intnamedAwardScore. This is the score players are awarded when interacting with the button.Verse# The amount of score to award per button press. @editable AwardScore:int = 1A
player_stats_managernamedPlayerStatsManager. This will manage and update stats for all players.Verse# Manages and updates stats for each player. PlayerStatsManager:player_stats_manager = player_stats_manager{}A message named StatsMessage that takes an agent four integers: Score, MaxScore, Wins, and Losses. You’ll use this message to display a player’s stats on the billboard.
Verse# Displays a player's stats on a billboard. StatsMessage<localizes>(Player:agent, Score:int, Wins:int, Losses:int):message = "{Player}, Stats:\n Score: {Score}\n Wins: {Wins}\n Losses: {Losses}"
Compile your code and drag your Verse-authored device onto your island. See Adding Your Verse Device to Your Level for steps.
In your device’s Details panel, assign the Button device in your level to ScorePointsButton and assign the Billboard device to StatsBillboard.
To display a given player’s stats on the StatsBillboard, add a new function
UpdateStatsBillboard()to yourplayer_stats_exampleclass definition. This function takes the agent whose stats to display.Verse# Retrieves the stats of the given player and displays their stats # on the StatsBillboard. UpdateStatsBillboard(Agent:agent):void=In
UpdateStatsBillboard(), get the current stats of the given agent by calling the stats manager’sGetPlayerStats[]function. Then callSetText()on the StatsBillboard passing a newStatsMessage(). To construct thisStatsMessage(), get the agent’s Score, Wins, and Losses by accessing them from the agent’s current stats. Your completedUpdateStatsBillboard()function should look like the following:Verse# Retrieves the stats of the given player and displays their stats # on the StatsBillboard. UpdateStatsBillboard(Agent:agent):void= if: # Get the current stats of the given agent. CurrentPlayerStats := PlayerStatsManager.GetPlayerStats[Agent] then: StatsBillboard.SetText( StatsMessage( Player := Agent,Add a new function
AddScore()to yourplayer_stats_exampleclass definition. This function takes an agent and adds to that agent’s score whenever they interact with the ScorePointsButton.Verse# Adds to the given player's score and updates both their stats table # in PlayerStatsManager and the billboard in the level. AddScore(Agent:agent):void=In
AddScore(), get the current stats of the given agent, as well as their current score. Then callAddScore()from thePlayerStatsManager, passing the agent and theAwardScoreto award them a new score. Finally callUpdateStatsBillboard(), passing the given agent. Your completedAddScore()function should look like the following:Verse# Adds to the given player's score and updates both their stats table # in PlayerStatsManager and the billboard in the level. AddScore(Agent:agent):void= if: CurrentPlayerStats := PlayerStatsManager.GetPlayerStats[Agent] CurrentScore := CurrentPlayerStats.Score then: Print("Current Score is: {CurrentScore}") PlayerStatsManager.AddScore(Agent, AwardScore) UpdateStatsBillboard(Agent)To award a player a win or a loss when the CheckWin button is interacted with, add a new function
CheckWin()to the player_stats_manager class definition.Verse# Awards a player a win or a loss when they interact # with the CheckWinButton. CheckWin(Agent:agent):void=First, define a variable
CurrentScoreto track the agent’s current score. Then as with theAddScore()function, retrieve their current score from their player stats table.Verse# Awards a player a win or a loss when they interact # with the CheckWinButton. CheckWin(Agent:agent):void= var CurrentScore:int = 0 if: PlayerStats := PlayerStatsManager.GetPlayerStats[Agent] set CurrentScore = PlayerStats.ScoreIf the agent’s current score is greater than the
WinScore, you need to record a win in thePlayerStatsManager. Otherwise, record a loss. Finally, reset the agent’s score by callingAddScore()with a negativeCurrentScorethen display the agent’s stats on the stats billboard. Your completedCheckWin()function should look like the following:Verse# Awards a player a win or a loss when they interact # with the CheckWinButton. CheckWin(Agent:agent):void= var CurrentScore:int = 0 if: PlayerStats := PlayerStatsManager.GetPlayerStats[Agent] set CurrentScore = PlayerStats.Score then: Print("Current Score is: {CurrentScore}") if:In
OnBegin(), subscribe theScorePointsButton.InteractedWithEventtoAddScore(), and theCheckWinButton.InteractedWithEventtoCheckWin(). Then get the array of each player in the game by callingGetPlayers(), and initialize them all using the stats manager’sInitializeAllPlayers()function.Verse# Runs when the device is started in a running game OnBegin<override>()<suspends>:void= # Register Button Events ScorePointsButton.InteractedWithEvent.Subscribe(AddScore) CheckWinButton.InteractedWithEvent.Subscribe(CheckWin) Players := GetPlayspace().GetPlayers() # Initialize player stats PlayerStatsManager.InitializeAllPlayers(Players)Save your code, compile it.
Testing Persistence in your Level
You can test your persistent data in an edit session, but this data will be reset when you exit and relaunch the session. To have your data persist across sessions, you’ll have to launch a playtest session and change certain settings in your Island Settings. For info on setting up your island to test persistable data both in edit and playtest sessions, see Testing with Persistent Data on the persistable data page.
After setting up your session, when you playtest your level, interacting with the ScorePoints button should add to the player’s score, and display that update on the billboard. Interacting with the CheckWin button should add to the player’s wins or losses depending on the player’s score. After returning to the lobby and re-entering your island, the player’s stats should persist and their total wins/losses and high score should display on the billboard whenever it updates.
On Your Own
By completing this guide, you’ve learned how to use Verse to create persistable data tracked per player that persists across gameplay sessions. Now go and see how you can adapt persistence to elevate your own experience
Can you create a save file system that remembers the last checkpoint a player got to?
What about a system that remembers which characters you’ve talked to and your current relationship with them?
What about a system that only gives players a limited amount of time across sessions to reach goals, and resets their progress if they fail?
Complete Code
Here is the complete code built in this section tutorial.
player_stats_table.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Tracks different persistable stats for each player.
player_stats_table := class<final><persistable>:
# The version of the current stats table.
Version<public>:int = 0
# The score of a player.
player_stats_manager.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Manages and updates player_stat_tables for each player.
player_stats_manager := class():
# Return the player_stats_table for the provided Agent.
GetPlayerStats(Agent:agent)<decides><transacts>:player_stats_table=
var PlayerStats:player_stats_table = player_stats_table{}
player_stats_example.verse
using { /Fortnite.com/Devices }
using { /Fortnite.com/Game }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# A Verse-authored creative device that can be placed in a level
player_stats_example := class(creative_device):
# Adds to the activating player's score.
@editable