게임에 사용되는 데이터들은 영구적이지 않고 대부분 라운드가 종료되면 초기화된다. 그렇다면 라운드마다 누적되는 데이터 혹은 세션을 나갔다 들어왔을 때 업적이나 레벨처럼 계속 유지되어야 하는 데이터들은 어떻게 관리할까? 이때 사용할 수 있는 데이터베이스와 같은 역할을 하는게 Persistable data다
Persistable data는 세션 당 데이터를 저장하는 weak_map(session, t) 와 게임을 나가도 유지되는 각 플레이어에게 데이터를 저장하는 weak_map(player, t) 가 있다.
예시: https://dev.epicgames.com/documentation/en-us/uefn/custom-round-logic-in-verse
세션마다 저장되며 라운드가 바뀌어도 유지해야 할 정보들을 넣을 수 있다. 예를 들어 이전 라운드의 정보를 이용해 다음 라운드에 필요한 데이터를 만들 수 있다.
위의 예시에선 전 라운드 기록에 따라 순위를 기록하고 라운드가 시작 될 때 이를 바탕으로 시작 위치를 재배치한다.
플레이어마다 저장되며 섬을 나가도 유지해야 할 정보들을 넣을 수 있다. 플레이어 레벨, 경험치 등 진행상황을 넣을 수 있고 꼭 섬을 나가서 유지되는 데이터가 아니라도 플레이어 별로 기록이 필요한 데이터라면 넣을 수 있다.
RPG 게임을 만든다고 가정할 때 몬스터의 킬 수와 경험치를 저장하는 시스템을 구현해보기로 했다. 게임을 나가도 유지될 수 있도록 weak_map(player, t)를 사용해 구현했다.
# player_save.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Table Map
var PlayerInfosMap:weak_map(player, player_infos_table) = map{}
# Table Definition
player_infos_table<public>:= class<final><persistable>:
Version<public>:int = 0
Level<public>:int = 0
XP<public>:int = 0
KillCount<public>:int = 0
# Manager
MakePlayerInfosTable<constructor>(OldTable:player_infos_table)<transacts> := player_infos_table:
Version := OldTable.Version
Level := OldTable.Level
XP := OldTable.XP
KillCount := OldTable.KillCount
GetPlayerInfos<public>(Agent:agent)<decides><transacts>:player_infos_table=
var PlayerInfos:player_infos_table = player_infos_table{}
if:
Player := player[Agent]
Player.IsActive[]
PlayerInfosTable := PlayerInfosMap[Player]
set PlayerInfos = MakePlayerInfosTable(PlayerInfosTable)
PlayerInfos
InitializeAllPlayerInfos<public>(Players:[]player):void=
for (Player : Players):
InitializePlayerStat(Player)
InitializePlayerStat<public>(Player:player):void=
if:
Player.IsActive[]
not PlayerInfosMap[Player]
set PlayerInfosMap[Player] = player_infos_table{}
TableLog("Player Stat Initialized")
else:
TableLog("Player is not active", ?Level := log_level.Warning)
AddXP<public>(Agent:agent, NewXP:int):void=
if:
Player := player[Agent]
Player.IsActive[]
PlayerInfosTable := PlayerInfosMap[Player]
CurrentXP := PlayerInfosTable.XP
set PlayerInfosMap[Player] = player_infos_table:
MakePlayerInfosTable<constructor>(PlayerInfosTable) # <constructor> is used
XP := CurrentXP + NewXP
Print("XP Changed: {CurrentXP} + {NewXP} = {PlayerInfosMap[Player].XP}")
TableLog("XP Changed: {CurrentXP} + {NewXP} = {PlayerInfosMap[Player].XP}")
else:
TableLog("Unable to add XP", ?Level := log_level.Warning)
AddKillCount<public>(Agent:agent, NewKillCount:int):void=
if:
Player := player[Agent]
Player.IsActive[]
PlayerInfosTable := PlayerInfosMap[Player]
CurrentKillCount := PlayerInfosTable.KillCount
set PlayerInfosMap[Player] = player_infos_table:
MakePlayerInfosTable<constructor>(PlayerInfosTable) # <constructor> is used
KillCount := CurrentKillCount + NewKillCount
Print("Kill Count Changed: {CurrentKillCount} + {NewKillCount} = {PlayerInfosMap[Player].KillCount}")
TableLog("Kill Count Changed: {CurrentKillCount} + {NewKillCount} = {PlayerInfosMap[Player].KillCount}")
else:
TableLog("Unable to add Kill Count", ?Level := log_level.Warning)
# Table Logger
player_info_table_log<public> := class(log_channel):
TableLog<public>(Message:[]char, ?Level:log_level = log_level.Normal)<transacts>:void=
Logger := log{Channel := player_info_table_log}
Logger.Print(Message, ?Level := Level)
테이블을 정의하고 상호작용 할 수 있는 Manager method들이 있다. 또한 하단에 테이블의 상호작용을 기록하기 위해 Table logger를 만들었다.
player_infos_table에 persistable옵션을 넣고 저장하고 싶은 데이터를 넣었다. 이번에 사용할 데이터는 XP와 KillCount다.
간단하게 크리쳐를 스폰시키고 해당 크리쳐가 죽을 때 KillCount와 XP가 증가하도록 하는 score_creature_manager.verse를 만들었다.

크리쳐 생성 장치를 통해 크리쳐를 생성하고 생성장치의 EliminatedEvent를 활용해 죽인 플레이어의 KillCount와 XP를 증가시키기로했다.
# score_creature_manager.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Fortnite.com/FortPlayerUtilities }
using { PlayerSave }
score_creature_manager<public> := class<concrete>():
@editable
CreatureSpawnerToScore : creature_spawner_device := creature_spawner_device{}
OnBegin()<suspends>:void=
CreatureSpawnerToScore.EliminatedEvent.Subscribe(OnMatchingCreatureTypeEliminated)
Print("Score Creature Manager Activated")
OnMatchingCreatureTypeEliminated(InteractionResult:device_ai_interaction_result):void=
if (Agent := InteractionResult.Source?):
AddKillCount(Agent, 1)
AddXP(Agent, 100)
Print("Creature eliminated by player")
CreatureSpawnerToScore에 레벨 크리쳐 생성 장치를 등록하고 OnBegin함수를 외부에서 실행하면(레벨의 시작 장치에 등록할 예정) 해당 장치에서 생성된 크리쳐가 사망 시 KillCount 1과 XP 100이 증가한다.
마지막으로 위 장치들을 등록하고 실행시켜줄 custom_round_manager를 만들고 레벨에 배치한다.
# custom_round_manager.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Fortnite.com/FortPlayerUtilities }
using { PlayerSave }
# 트리거(몬스터 때리기)로 경험치가 오르는지 테스트 해보기
custom_round_manager<public> := class(creative_device):
@editable
ScoreCreatureManagers:[]score_creature_manager = array{}
OnBegin<override>()<suspends>:void=
# initialize all player infos on begin
Players:[]player = GetPlayspace().GetPlayers()
ValidPlayers:[]player =
for:
Player : Players
Player.IsActive[]
not Player.IsSpectator[]
do:
Player
InitializeAllPlayerInfos(ValidPlayers)
# initialize on player added event
GetPlayspace().PlayerAddedEvent().Subscribe(InitializePlayerInfos)
# initialize all score creature managers(add events on kill)
for (ScoreCreatureManager : ScoreCreatureManagers):
ScoreCreatureManager.OnBegin()
Test.ProjectLog("Custom Round Manager Activated", ?Level := log_level.Normal)

사진과 같이 몬스터가 죽을 때 KillCount와 XP가 증가하는 것을 확인할 수 있다. 세션을 유지한 채 게임 끝내기 - 게임 시작을 하면 이전에 잡았던 KillCount가 유지되는 것을 확인할 수 있다.
와 저도 UEFN하는 1인입니다! 전문적으로 코딩해본적은 없이 verse하고 있는데 큰 도움 될거 같아요~ 업데이트 기다리겠습니다!