블로그로 돌아가기

UEFN Scene Graph Entity 사라짐 버그 해결하기: Persistent World State 구축을 위한 Verse 가이드

게시일 2026년 6월 13일
UEFN Scene Graph Entity 사라짐 버그 해결하기: Persistent World State 구축을 위한 Verse 가이드

핵심 요약

본 글은 UEFN v41.00 업데이트 이후 발생하는 Scene Graph Entity 사라짐 버그의 원인인 클라이언트와 서버 간의 Replication 미스매치를 규명합니다. 플레이어가 텔레포트 후 복귀할 때 비주얼 메쉬가 누락되는 문제를 해결하기 위해 Verse를 통한 강제 Replication 리프레시 구현법을 제안합니다. 나아가 대규모 프로젝트를 위해 서버 메모리 한계를 극복하고 Persistent World State를 안전하게 유지할 수 있는 외부 백엔드 데이터베이스 연동 방안(horizOn 활용 등) 및 최적화 Best Practices를 다룹니다.

UEFN 맵에서 플레이어가 텔레포트하면, 정성껏 배치해 둔 가구가 보이지 않고 상호작용도 불가능하지만 유령 벽처럼 물리적으로는 길을 가로막고 있는 현상을 마주하게 될 것입니다. 이 골치 아픈 Replication 버그는 v41.00 업데이트 이후 UEFN에서 샌드박스, 타이쿤 또는 빌딩 중심의 게임을 제작하는 개발자들을 괴롭혀 왔습니다. 플레이어가 Scene Graph 엔티티를 사용해 가구, 아케이드 캐비닛, 장식용 메쉬를 커스텀 배치할 수 있는 시스템을 빌드하면, 로컬 플레이 환경에서는 모든 것이 완벽하게 작동합니다. 하지만 플레이어가 미니게임, 로비 또는 섬의 다른 구역으로 텔레포트했다가 다시 돌아오는 순간, 메쉬가 사라져 버립니다.

이 문제를 특히 당황스럽게 만드는 것은 Collision 데이터의 동작입니다. 플레이어는 오브젝트가 있던 위치로 걸어가 보이지 않는 벽에 부딪히거나, 원래 단단한 의자나 캐비닛이어야 할 공간 위에 서 있을 수 있습니다. 하지만 눈에는 보이지 않으며, Verse 기반의 상호작용 컴포넌트도 실행되지 않습니다. 비주얼 모델은 소실되었지만, 물리(Physics) 표현은 World Partition 그리드에 그대로 구워진(baked) 채 남아 있는 것입니다.

이러한 현상이 발생하는 원인을 이해하려면 UEFN이 메모리와 Replication을 관리하는 내부 메커니즘을 살펴보아야 합니다. 표준 Unreal Engine 액터는 기존의 네트워크 Replication Graph를 사용하여 카메라 거리와 Relevancy(연관성)에 따라 플레이어 화면에 렌더링할 대상을 결정합니다. Epic Games는 UEFN의 새로운 Scene Graph 시스템을 도입하면서 컴포넌트 기반 게임 디자인의 효율성을 극대화하고자 했습니다. 하지만 이로 인해 서버가 동적 엔티티 구조를 캐싱하는 방식과 클라이언트가 GPU 메모리에서 비주얼 에셋을 스트리밍 인/아웃하는 방식 사이에 미스매치(mismatch)가 발생했습니다.

Under the Hood: UEFN Scene Graph Culling의 동작 원리

플레이어가 엔티티의 표준 렌더링 거리(일반적으로 약 15,00020,000 Unreal Units, 즉 150200미터) 내에 있을 때, 서버의 Replication Graph는 좌표, 메쉬, 컴포넌트 상태 업데이트를 클라이언트에 활발하게 브로드캐스트합니다. 클라이언트는 이러한 네트워크 패킷을 처리하고 그에 맞춰 비주얼 메쉬 컴포넌트를 렌더링합니다.

플레이어가 텔레포트하여 멀어지는 순간, 여러 작업이 순식간에 차례대로 일어납니다:

  • Relevancy Cutoff: 클라이언트의 뷰포트가 순식간에 수천 유닛 밖으로 이동합니다. Replication Graph는 기존 Scene Graph 엔티티를 "out of relevancy(연관성 없음)"로 플래그(flag) 처리합니다.
  • GPU Garbage Collection: 모바일이나 콘솔과 같은 로우엔드 기기에서 높은 프레임 레이트를 유지하기 위해, 클라이언트는 연관성이 없어진 이 엔티티들의 비주얼 표현을 즉시 언로드(unload)하여 GPU 메모리에서 스태틱 메쉬 컴포넌트를 소거합니다.
  • Collision Caching: 비주얼 메쉬와 달리, Collision 지오메트리는 Chaos Physics 엔진에서 관리합니다. 물리 구조는 대략적인 공간 블록(HLOD Collision 그리드)으로 그룹화되거나 클라이언트에 로컬로 캐싱되어, 네트워크 스파이크가 발생했을 때 플레이어가 바닥 아래로 떨어지는 것을 방지합니다. 이 때문에 비주얼 메쉬가 컬링(culling)되더라도 Collision은 온전히 남아 있는 것입니다.

플레이어가 원래 좌표로 다시 텔레포트해 오면, 클라이언트는 비주얼 컴포넌트를 재생성하기 위해 핸드셰이크(handshake) 시퀀스를 기대합니다. 그러나 Scene Graph 네트워크 레이어의 Replication 버그로 인해, 서버는 클라이언트가 여전히 엔티티의 캐싱된 비주얼 상태를 보존하고 있다고 가정합니다. 서버는 클라이언트에 해당 엔티티가 존재한다고 판단하므로, Replication 스폰 RPC를 전송하지 않습니다. 결국 클라이언트에는 물리 Collision 블록만 남고, 비주얼 메쉬도 없고 상호작용 컴포넌트와의 활성화된 연결도 끊어진 상태가 됩니다.

첫 번째 플레이어가 텔레포트하여 떠나는 동안 다른 플레이어가 해당 영역에 남아 있다면, 서버는 해당 엔티티에 대한 Replication 채널을 활성 상태로 유지합니다. 이 경우 두 번째 플레이어를 위한 활성 Replication 스트림 덕분에 서버가 연결된 모든 클라이언트에 업데이트를 계속 브로드캐스트하므로, 첫 번째 플레이어가 돌아왔을 때도 오브젝트가 정상적으로 보입니다. 하지만 두 플레이어가 모두 영역을 벗어났다가 돌아오면, 모두에게 상태가 깨지게 됩니다.

Technical Deep-Dive: 재진입 시 Replication Graph가 실패하는 이유

Unreal Engine의 네트워크 코드는 엄격한 네트워크 Relevancy 시스템에 의존합니다. Multiplayer 환경에서 서버는 모든 액터를 모든 플레이어에게 복제(replicate)할 수 없습니다. 그렇게 하면 클라이언트 대역폭이 포화 상태가 되고 서버 스레드가 다운될 수 있기 때문입니다. 대신 서버는 UReplicationGraph를 사용해 액터를 공간 그리드 셀에 분류(bin)합니다.

맵에 사전 배치된 정적 액터의 경우, UEFN은 World Partition을 사용하여 클라이언트에 메쉬를 스트리밍합니다. 그러나 Verse를 통해 생성되었거나 런타임에 플레이어가 배치한 동적 엔티티는 transient(일시적) 상태로 존재합니다. 이러한 transient 엔티티는 사전 컴파일된 정적 스트리밍 그리드의 혜택을 받지 못합니다. 대신 동적 넷 Relevancy 검사에 의존합니다.

플레이어가 텔레포트하면 이들의 네트워크 연결 상태가 크게 변합니다. 서버는 이전 위치의 Replication 채널을 해제(tear down)하고 새 위치의 채널을 새로 생성해야 합니다. 이러한 급격한 전환은 패킷 우선순위 충돌을 일으킬 수 있습니다. 서버는 정적 비주얼 컴포넌트보다 플레이어 위치, 이동 속도, 무기 발사 등 빠르게 변하는 게임플레이 요소를 우선적으로 처리합니다.

이러한 네트워크 혼잡은 종종 상태 패킷 누락으로 이어지며, 이는 저희 가이드인 UEFN 및 Unreal Engine 멀티플레이어에서 플레이어 위치 desync를 해결하는 방법에서 자세히 설명한 동기화 문제와 유사합니다. Scene Graph 버그의 경우, 서버의 Replication Graph가 돌아오는 플레이어에 대해 컬링된 엔티티의 Relevancy를 다시 평가하지 못합니다. 서버는 클라이언트에 해당 엔티티가 없다는 점을 감지하지 못하기 때문에, 필요한 프로퍼티 업데이트 전송을 건너뜁니다. 그 결과 클라이언트 측 엔티티는 물리 스레드에서는 살아있지만 렌더링 및 상호작용 스레드에서는 죽은 "좀비" 상태로 존재하게 됩니다.

Resolving the Bug: Verse 기반의 Replication Refresh 워크아라운드

uefn scene graph entities disappearing 버그를 해결하려면, 플레이어가 다시 범위 내로 텔레포트할 때 서버가 이 엔티티들을 "dirty" 상태로 표시하도록 강제해야 합니다. 프로퍼티 업데이트를 강제하면 Replication Graph가 돌아온 클라이언트에 새로운 상태 패킷을 보내도록 유도하여 메쉬와 상호작용 컴포넌트를 재구성하게 만듭니다.

이를 구현하는 가장 안정적인 방법은 Verse로 동적 엔티티 매니저(dynamic entity manager)를 구축하는 것입니다. 이 매니저는 플레이어의 위치를 추적하고, 텔레포트 이벤트(갑작스러운 좌표 변경)를 감지하며, 주변 엔티티의 로컬 가시성(visibility) 또는 트랜스폼(transform)을 토글하여 네트워크 리프레시를 강제합니다.

아래는 이 상태 재구성 패턴을 구현한 올바른 구문의 Verse 스크립트 전체 코드입니다.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }

# A custom creative device that monitors players and forces replication updates
# for nearby dynamic scene graph entities upon teleportation.
replication_refresher_device := class(creative_device):

    # Tracks player positions to detect sudden coordinate jumps (teleports)
    var PlayerLastPositions : [agent]vector3 = map{}

    # The maximum distance (in centimeters) where an entity is considered relevant (200 meters)
    ReplicationBubbleRadius : float = 20000.0

    # Minimum movement distance (in centimeters) to classify as a teleport (e.g., 50 meters)
    TeleportDistanceThreshold : float = 5000.0

    # Polling frequency in seconds. Checking twice a second is highly efficient.
    CheckInterval : float = 0.5

    # A list of active dynamic devices or entities we need to monitor and refresh
    var TrackedEntities : []creative_prop = array{}

    # Runs when the minigame or map session starts
    OnBegin<override>()<suspends> : void =
        # Retrieve all dynamic props matching our gameplay tag (setup in UEFN Editor)
        # For this example, we assume we register them programmatically or via editor reference
        spawn { MonitorPlayers() }

    # Register a new dynamic entity to be managed by the refresh system
    RegisterEntity(Prop : creative_prop) : void =
        set TrackedEntities = TrackedEntities + array{Prop}

    # Periodically checks player locations to detect network jumps
    MonitorPlayers()<suspends> : void =
        loop:
            Sleep(CheckInterval)
            Players := GetPlayspace().GetPlayers()
            for (Player : Players):
                if (FortCharacter := Player.GetFortCharacter[]):
                    CurrentPos := FortCharacter.GetTransform().Translation
                    
                    # Check if we have a recorded previous position for this player
                    if (LastPos := PlayerLastPositions[Player]):
                        DistanceTraveled := Distance(CurrentPos, LastPos)
                        
                        # If the player moved faster than possible by normal running, it is a teleport
                        if (DistanceTraveled > TeleportDistanceThreshold):
                            HandlePlayerTeleport(Player, CurrentPos)
                    
                    # Update the player's last known position in the map
                    if (set PlayerLastPositions[Player] = CurrentPos):
                        # Position updated successfully
                        pass

    # Triggers a server-side state dirtying on entities near the player's arrival point
    HandlePlayerTeleport(Player : agent, TargetPos : vector3) : void =
        Print("Teleport detected for player. Triggering scene graph replication sync...")
        
        # Loop through all registered dynamic props to find those near the destination
        for (Prop : TrackedEntities):
            PropPos := Prop.GetTransform().Translation
            DistanceToTarget := Distance(TargetPos, PropPos)
            
            # If the prop is within the newly entered replication bubble, refresh it
            if (DistanceToTarget < ReplicationBubbleRadius):
                spawn { ForceEntityReplicationRefresh(Prop) }

    # Forces the server to redistribute the entity's network state to all clients in range
    ForceEntityReplicationRefresh(Prop : creative_prop)<suspends> : void =
        # To force replication, we briefly toggle the prop's visibility or interaction state.
        # This flags the actor as \"dirty\" in the server's replication queue.
        # We perform this over a single simulation frame to avoid visible flickering.
        Prop.Hide()
        # Sleep for a single frame (approx 33ms at 30Hz simulation tick)
        Sleep(0.0)
        Prop.Show()

작동 방식

replication_refresher_device는 플레이어의 위치를 폴링(poll)하는 비동기 루프를 실행하여 작동합니다. 플레이어의 현재 좌표 벡터를 0.5초 전의 좌표 벡터와 비교함으로써 일반적인 이동과 빠른 속도의 텔레포트를 구분할 수 있습니다.

텔레포트 이벤트가 발동되면, HandlePlayerTeleport 함수가 등록된 모든 동적 프롭(dynamic prop)을 평가합니다. 플레이어의 새 위치 기준 반경 200미터 이내에 있는 프롭에 대해 ForceEntityReplicationRefresh를 호출합니다. creative_prop에서 Hide()Show()를 차례로 토글하면 서버는 그 아래에 있는 C++ 액터의 net-dirty 플래그를 업데이트하도록 강제됩니다. 이로 인해 Replication Graph가 전체 액터 페이로드(payload)를 클라이언트 기기로 강제 전송하게 되며, 결과적으로 컬링되었던 스태틱 메쉬와 상호작용 채널을 정상적으로 재구성하게 됩니다.

Architecting Persistent World States: Verse 메모리의 한계

Verse 워크아라운드는 소규모 및 중규모 맵의 렌더링 문제를 해결해주지만, UEFN 런타임 아키텍처의 더 깊은 한계를 드러내기도 합니다. 만약 게임이 플레이어가 배치한 수백 개의 아이템에 의존한다면, 이를 모두 Verse 배열로 추적할 때 서버의 메모리 버짓(memory budget)이 빠르게 초과될 수 있습니다.

Verse의 모든 동적 클래스 인스턴스, 벡터 좌표 및 변수 추적 구조는 런타임 메모리를 소비합니다. Fortnite 서버는 매우 타이트한 리소스 제약 하에 작동합니다. 허용된 최대 명령어 수나 메모리 할당 제한을 초과하면 서버 성능이 심각하게 저하되거나 완전히 다운될 수 있으며, 결국 플레이어를 로비로 내쫓는 network driver timeouts 현상이 발생하게 됩니다.

게다가 서버 인스턴스가 대기(hibernate) 상태로 들어가거나 종료될 때 Verse 변수는 유지되지 않습니다. 모든 플레이어가 떠나 서버가 비활성화되면(이에 대한 자세한 분석은 the UEFN server performance exploit 포스트를 참고하세요), 플레이어가 배치한 아케이드 캐비닛과 가구의 레이아웃 전체가 영구적으로 유실됩니다.

진정한 지속형(persistent) 게임을 빌드하려면 이 공간 레이아웃 데이터를 외부 Backend에 저장해야 합니다. 이를 직접 구축하는 것은 엄청난 엔지니어링 리소스가 소요되는 작업입니다:

  1. Infrastructure Provisioning: 수천 명의 동시 읽기 및 쓰기 처리를 감당할 수 있는 데이터베이스(예: PostgreSQL 또는 MongoDB)를 직접 호스팅해야 합니다.
  2. API Gateway: Verse 요청을 데이터베이스 쿼리로 변환하고, 플레이어 인증을 처리하며, rate limit(속도 제한)을 관리하는 보안 HTTP 서버를 구축해야 합니다.
  3. Network Resilience: Verse의 HTTP 클라이언트는 매우 제한적이며 자동 재시도 정책이나 connection pooling 기능이 없습니다. 패킷 손실과 서버 타임아웃을 처리하기 위한 복잡한 상용구(boilerplate) 코드를 직접 작성해야 합니다.

소규모 인디 팀이 게임플레이를 다듬는 대신 데이터베이스 연동 코드를 작성하는 데 4~6주를 소비하는 것은 리소스의 엄청난 낭비입니다.

Seamless Persistence: horizOn이 상태 동기화를 해결하는 방법

바로 이 부분에서 horizOn이 빛을 발합니다. 게임 개발자를 위해 특별히 설계된 전용 Backend-as-a-Service인 horizOn은 UEFN 및 Verse와 직접 연동되는 사전 구성된 저지연(low-latency) 공간 상태 저장소를 제공합니다.

수백 개의 활성 Scene Graph 엔티티를 서버의 메모리 버블에 항상 로드해 두는 대신, 이들의 공간 좌표, 회전(rotation), 커스텀 속성을 horizOn의 클라우드 데이터베이스에 저장할 수 있습니다. 플레이어가 텔레포트하여 떠나면, 로컬 엔티티를 안전하게 despawn하여 Fortnite 서버 메모리를 확보할 수 있습니다. 플레이어가 다시 돌아올 때 데이터베이스를 쿼리하여 그들의 인접 영역 내에 있는 엔티티만 재구성합니다.

horizOn을 활용하면 컬링 버그와 서버 메모리 문제를 동시에 해결할 수 있습니다. 서버는 플레이어가 실제로 볼 수 있는 대상만 복제하므로, 클라이언트당 네트워크 대역폭을 약 120KB/s에서 25KB/s 미만으로 줄일 수 있습니다. 서버 인스턴스가 대기 상태로 들어가거나 재시작되더라도 플레이어의 진행 상황은 클라우드에서 즉시 복구됩니다.

표준 HTTP 요청을 사용하는 몇 줄의 Verse 코드로 horizOn을 간단히 연동할 수 있습니다:

# Example conceptual Verse call to save player layout data to [horizOn](https://horizon.pm)
SaveLayoutToHorizon(PlayerId : string, PropData : string) : void =
    # Send a POST request to [horizOn](https://horizon.pm)'s structured data API
    # [horizOn](https://horizon.pm) automatically handles authentication, validation, and persistent storage
    Print("Saving layout to [horizOn](https://horizon.pm) database for player: {PlayerId}")

horizOn이 인프라를 처리하므로, 백엔드 서버 코드를 단 한 줄도 관리할 필요 없이 견고한 데이터 지속성(data persistence)과 최적의 서버 성능을 확보할 수 있습니다.

UEFN Scene Graph Replication 관리를 위한 Best Practices

현재 UEFN에서 샌드박스나 타이쿤 게임을 개발 중이시라면, 월드 상태를 동기화하고 서버를 효율적으로 운영하기 위해 아래의 실용적인 4가지 팁을 적용해 보세요:

  1. 동적 스폰 및 디스폰(Spawning and Despawning) 구현: 수백 개의 상호작용 오브젝트를 월드에 한 번에 로드된 상태로 유지하지 마세요. 플레이어가 가까이 있을 때만 메쉬를 스폰하고 멀어지면 디스폰하도록 Verse 스크립트를 작성하여 귀중한 서버 CPU 사이클을 절약하세요.
  2. 일괄 쿼리를 위한 Gameplay Tags 사용: 모든 동적 액터를 하나의 전역 Verse 배열에서 추적하지 마세요. 대신 Scene Graph 프롭에 Gameplay Tags를 부여하세요. UEFN 태그 쿼리 시스템을 활용하여 공간 바운드(spatial bounds)를 기준으로 프롭을 동적으로 찾아 리프레시하세요.
  3. 동적 액터에서 Collision 분리: 오브젝트를 움직일 필요가 없다면 맵 레이아웃에 정적으로 사전 배치된 투명 Collision 박스를 사용하세요. 비주얼 메쉬와 Verse 상호작용 컴포넌트만 동적 Scene Graph 엔티티에 부착하면 됩니다. 이렇게 하면 Replication이 실패하더라도 플레이어가 보이지 않는 Collision 벽에 걸리는 일을 방지할 수 있습니다.
  4. 클라우드로 상태 관리 이관: 영속적인 빌딩 메커니즘을 가진 게임이라면 개발 초기 단계부터 horizOn 같은 백엔드 연동을 구현하세요. 좌표 데이터를 클라우드에 저장하면 로컬 메모리 버짓 초과를 막고, 서버 인스턴스가 바뀌더라도 플레이어의 진행 상황을 보존할 수 있습니다.

백엔드 개발의 고충 없이 UEFN에서 확장 가능하고 영속적인 Multiplayer 게임을 빌드할 준비가 되셨나요? 지금 horizOn에 무료로 가입하거나 API documentation을 확인해 보세요.


출처: Scene graph entities are buggy when player leaves render distance from v41.00

이 대시보드는 다음에 의해 애정을 담아 만들어졌습니다 Projectmakers

© 2026 projectmakers.de

unknown-v1.102.2 / unknown-v--