UEFN 게임이 종료되도 유지되는 Persistable Data

임혁진·2025년 1월 9일
0

uefn

목록 보기
8/11

https://dev.epicgames.com/documentation/en-us/uefn/using-persistable-data-in-verse#persistabletypesinverse

게임에 사용되는 데이터들은 영구적이지 않고 대부분 라운드가 종료되면 초기화된다. 그렇다면 라운드마다 누적되는 데이터 혹은 세션을 나갔다 들어왔을 때 업적이나 레벨처럼 계속 유지되어야 하는 데이터들은 어떻게 관리할까? 이때 사용할 수 있는 데이터베이스와 같은 역할을 하는게 Persistable data다

Persistable data는 세션 당 데이터를 저장하는 weak_map(session, t) 와 게임을 나가도 유지되는 각 플레이어에게 데이터를 저장하는 weak_map(player, t) 가 있다.

Persistable Data

예시: https://dev.epicgames.com/documentation/en-us/uefn/custom-round-logic-in-verse

weak_map(session, t)

세션마다 저장되며 라운드가 바뀌어도 유지해야 할 정보들을 넣을 수 있다. 예를 들어 이전 라운드의 정보를 이용해 다음 라운드에 필요한 데이터를 만들 수 있다.

위의 예시에선 전 라운드 기록에 따라 순위를 기록하고 라운드가 시작 될 때 이를 바탕으로 시작 위치를 재배치한다.

weak_map(player, t)

플레이어마다 저장되며 섬을 나가도 유지해야 할 정보들을 넣을 수 있다. 플레이어 레벨, 경험치 등 진행상황을 넣을 수 있고 꼭 섬을 나가서 유지되는 데이터가 아니라도 플레이어 별로 기록이 필요한 데이터라면 넣을 수 있다.

구현

RPG 게임을 만든다고 가정할 때 몬스터의 킬 수와 경험치를 저장하는 시스템을 구현해보기로 했다. 게임을 나가도 유지될 수 있도록 weak_map(player, t)를 사용해 구현했다.

player_save.verse

# 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를 만들었다.

Table Map

player_infos_table에 persistable옵션을 넣고 저장하고 싶은 데이터를 넣었다. 이번에 사용할 데이터는 XP와 KillCount다.

Manager

  • MakePlayerInfosTable: 데이터 테이블을 transacts를 위해 원자적으로 구성하기 위한 함수다. 트랜잭션이 수행되기 위해선 데이터가 딥카피 되어야 한다.
  • GetPlayerInfos: 저장되어 있는 PlayerInfos를 통해 데이터를 가져온다.
  • InitializeAllPlayerInfos & InitializePlayerStat
    게임이 시작될 때 호출될 수 있으며 테이블이 없는 유저에게 테이블을 만들어준다.
  • AddXP & AddKillCount: 변화가 필요한 Agent가 플레이어일 때 값을 변화시킨다.

score_creature_manager.verse

간단하게 크리쳐를 스폰시키고 해당 크리쳐가 죽을 때 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.verse

마지막으로 위 장치들을 등록하고 실행시켜줄 custom_round_manager를 만들고 레벨에 배치한다.

  • 시작 시 존재하는 플레이어의 데이터 init
  • 이후에 입장한 플레이어의 데이터도 init할 수 있게 이벤트 등록
  • 등록된 SourceCreatureManager OnBegin 실행
# 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가 유지되는 것을 확인할 수 있다.

TIL

  • 라운드마다 혹은 플레이어마다 점수가 아닌 추가적인 작업이 필요한 데이터를 어떻게 관리할지 알 수 있었다.
  • UEFN에서 다른 장르의 게임을 구현하려고 한다면 매우 유용하게 쓸 수 있을 것 같다.
  • 만약 라운드 방식이 아니라면 중간에 입장하는 플레이어를 위해 playerAddedEvent에서 데이터를 세팅해주는 작업을 해줘야 한다
profile
로스트빌드(lostbuilds.com) 개발자, UEFN 도전, 게임이 재밌는 이유를 찾아서

1개의 댓글

comment-user-thumbnail
2025년 4월 30일

와 저도 UEFN하는 1인입니다! 전문적으로 코딩해본적은 없이 verse하고 있는데 큰 도움 될거 같아요~ 업데이트 기다리겠습니다!

답글 달기