using { /Fortnite.com/Devices } using { /Fortnite.com/Playspaces } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } # ----------------------------------------------------------------------------- # tycoon_currency_manager — Indie Island tutorial 02: Player progress # # Per-player currency that survives leaving the island: restored on join, # autosaved on a timer, with a guard so the save loop stops when a player # leaves. Optional passive income on top. # # Setup: place this device in your level. Nothing else to wire. # # Other devices call GiveCurrency / TrySpend / GetBalance on this device via an # @editable field of type tycoon_currency_manager. # ----------------------------------------------------------------------------- # Everything a player keeps between sessions. # # APPEND-ONLY after your island's first publish: you may ADD fields, but never # remove or rename one -- publishing fails ("verse_validate_publish_error") # because live save data still references it. If you stop using a field, # leave it here with a comment saying it can never leave. player_save := class: Currency : int = 0 Level : int = 0 # Verse allows only a small number of persistent weak_map variables per island. # One map holding one save class is usually enough -- spend them deliberately. var PlayerSave : weak_map(player, player_save) = map{} tycoon_currency_manager := class(creative_device): @editable StartingCurrency : int = 0 # Passive income for everyone. Set PassiveAmount to 0 to switch it off. @editable PassiveAmount : int = 1 @editable PassiveSeconds : float = 6.0 # How often progress is written to save data. @editable SaveIntervalSeconds : float = 5.0 var PlayerCurrency : [player]int = map{} var PlayerLevel : [player]int = map{} OnBegin() : void = for (Player : GetPlayspace().GetPlayers()): OnPlayerAdded(Player) GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerAdded) OnPlayerAdded(Player : player) : void = InitPlayer(Player) spawn { SaveLoop(Player) } if (PassiveAmount > 0): spawn { PassiveIncomeLoop(Player) } # -- Restore on join --------------------------------------------------------- InitPlayer(Player : player) : void = if (Saved := PlayerSave[Player]): # Returning player: load what they had. if (set PlayerCurrency[Player] = Saved.Currency) {} if (set PlayerLevel[Player] = Saved.Level) {} Print("[tycoon_currency_manager] Progress restored: {Saved.Currency}") else: # New player: defaults. if (set PlayerCurrency[Player] = StartingCurrency) {} if (set PlayerLevel[Player] = 0) {} # -- Currency ---------------------------------------------------------------- GiveCurrency(Player : player, Amount : int) : void = Current := GetBalance(Player) if (set PlayerCurrency[Player] = Current + Amount) {} GetBalance(Player : player) : int = if (Current := PlayerCurrency[Player]): return Current return 0 # Fails (and changes nothing) if the player can't afford it: # if (CurrencyManager.TrySpend[Player, 100]): ... TrySpend(Player : player, Amount : int) : void = Current := PlayerCurrency[Player] Current >= Amount set PlayerCurrency[Player] = Current - Amount GetLevel(Player : player) : int = if (L := PlayerLevel[Player]): return L return 0 SetLevel(Player : player, NewLevel : int) : void = if (set PlayerLevel[Player] = NewLevel) {} PassiveIncomeLoop(Player : player) : void = loop: Sleep(PassiveSeconds) if (IsPlayerConnected(Player) = false): break GiveCurrency(Player, PassiveAmount) # -- Autosave ---------------------------------------------------------------- # A loop spawned per player does NOT stop by itself when that player leaves. # Without the connection check it keeps writing to an invalid key and throws # ErrRuntime_WeakMapInvalidKey. SaveLoop(Player : player) : void = loop: Sleep(SaveIntervalSeconds) if (IsPlayerConnected(Player) = false): break SavePlayer(Player) IsPlayerConnected(Player : player) : logic = for (P : GetPlayspace().GetPlayers()): if (P = Player): return true return false SavePlayer(Player : player) : void = if (set PlayerSave[Player] = player_save{ Currency := GetBalance(Player), Level := GetLevel(Player) }) {}