영상을 불러오지 못했습니다

휴면 액터 깨우기: FlushNetDormancy, ForceNetUpdate, SetNetDormancy

2413 단어
12 분
휴면 액터 깨우기: FlushNetDormancy, ForceNetUpdate, SetNetDormancy
  1. 1휴면 액터 깨우기: FlushNetDormancy, ForceNetUpdate, SetNetDormancy이 글
휴면 액터 깨우기: FlushNetDormancy, ForceNetUpdate, SetNetDormancy

휴면(dormant) 상태인 액터의 replicated property를 바꾸면 그 변경은 클라이언트로 가지 않는다. 휴면을 다룰 때는 두 가지를 구분해야 한다.

  • 휴면 정책: 액터의 NetDormancy 값. DORM_Awake 이하면 깨어 있고, 그보다 크면(DORM_DormantAll 등) 휴면하려 한다.
  • 휴면 상태: 연결마다 지금 실제로 휴면 중인지. 휴면 중인 연결에는 복제 검토(NetDriver가 보낼 액터를 고르고 값을 비교하는 단계)도 하지 않는다.

이 글에서 액터를 깨운다는 것은 휴면 상태를 푸는 것을 말한다. 휴면 정책을 그대로 둔 채 깨우면, 액터는 변경을 보낸 뒤 다시 휴면한다. FlushNetDormancy는 휴면 상태를, SetNetDormancy는 휴면 정책을 바꾼다. ForceNetUpdate는 복제 시점을 바꾸고, 휴면 중이면 조건부로 flush도 덤으로 한다.

세 함수 비교#

FlushNetDormancyForceNetUpdateSetNetDormancy
바꾸는 것휴면 상태복제 시점휴면 정책
휴면 정책유지유지지정한 값
휴면 상태잠깐 깨운다휴면 중이면 덤으로 flush(조건부)DORM_Awake 이하면 깨워 둔다
다시 휴면하는 때다음 검토와 ack를 거친 뒤flush했다면 다음 검토와 ack를 거친 뒤DORM_DormantAll로 되돌린 뒤
복제 시점앞당기지 않는다(Iris 제외)다음 복제 검토에 바로 넣는다앞당기지 않는다
DORM_Initial 액터DORM_DormantAll로 바뀐다게임 NetDriver가 있을 때만 바뀐다지정한 값
클라이언트에서 호출무시게임 NetDriver에는 무시무시
BlueprintBlueprintAuthorityOnlyBlueprintCallableBlueprintAuthorityOnly

각 함수가 하는 일#

FlushNetDormancy#

휴면 정책은 그대로 두고 휴면 상태만 잠깐 푼다. 휴면을 끄는 것이 아니라, 바뀐 값을 보내는 동안만 깨우는 것이다. 정책이 여전히 휴면이므로 액터는 변경을 보낸 뒤 스스로 다시 휴면한다. 계속 깨워 두려면 정책 자체를 바꾸는 SetNetDormancy를 쓴다.

DORM_Initial이면 DORM_DormantAll로 바꾸는데, 둘 다 휴면하는 값이라 휴면은 이어진다. 이 변경은 게임 NetDriver가 없어도 일어난다. 그다음 액터를 복제하는 모든 NetDriver에서 휴면 상태를 푼다.

레거시 복제와 Replication Graph는 휴면 중이던 연결마다 replicator를 현재 상태로 다시 만든다(Engine/Private/NetConnection.cpp). 복제 시점은 건드리지 않고, 연결마다 다음 과정을 거쳐 다시 휴면한다.

  1. 다음 복제 검토에서 flush 뒤에 바뀐 값을 보낸다.
  2. 그다음 검토에서 다시 비교해 바뀐 값이 없음을 확인한다.
  3. 1에서 보낸 것이 ack되면 Actor channel을 닫고 휴면한다.

데이터가 나가는 것은 보통 한 번이다. 기본 설정에서 다시 휴면하는 시점은 두 번째 검토와 ack 중 늦은 쪽이고, 그 사이 값이 또 바뀌면 검토마다 보내고 다시 기다린다. Iris는 channel을 닫지 않는다. flush하면 다음 업데이트에 한 번 폴링한 뒤 바로 폴링 대상에서 빠진다.

ForceNetUpdate#

복제 시점을 다루는 함수다. NetUpdateFrequency 주기를 기다리지 않고 다음 복제 검토에 넣는다. Replication Graph는 우선순위도 올리지만 레거시 복제는 올리지 않는다.

보장하는 것은 검토 시점뿐이다. 관련성 검사, 대역폭 포화, 프로퍼티 비교는 그대로 거치고, Push Model의 마킹을 대신하지 않는다.

깨우는 것은 덤이다. 휴면 액터면 FlushNetDormancy도 부르는데, 휴면 중인 연결은 복제 검토에서 빠져 시점만 앞당겨서는 보낼 수 없기 때문으로 보인다. 이 flush에는 조건이 있다(아래 자주 하는 실수).

SetNetDormancy#

휴면 정책 자체를 바꾼다. DORM_Awake 이하로 바꾸면 각 NetDriver에서 휴면 상태도 푼다. 정책이 깨어 있으므로 다시 휴면하지 않고, 따로 FlushNetDormancy를 부를 필요도 없다. DORM_DormantAll로 되돌리면 보낸 것이 모두 ack됐고 바뀐 값이 없을 때 다음 검토 직후 휴면한다.

언제 무엇을 부르나#

상황호출그 뒤 액터
휴면 액터의 값을 가끔 바꾼다값을 바꾸기 전에 FlushNetDormancy보낸 뒤 다시 휴면
그 변경을 다음 복제 주기보다 빨리 보내야 한다FlushNetDormancy → 값 변경 → ForceNetUpdate보낸 뒤 다시 휴면
깨어 있는 액터의 변경을 빨리 보내야 한다값 변경과 함께 ForceNetUpdate깨어 있음
한동안 값이 자주 바뀐다시작할 때 SetNetDormancy(DORM_Awake), 끝나면 SetNetDormancy(DORM_DormantAll)끝낼 때까지 깨어 있음

ForceNetUpdate의 flush는 조건부 덤이므로, 휴면 액터를 깨우는 일은 FlushNetDormancy에 맡긴다. 둘을 함께 쓰면 flush가 두 번 일어나지만, 두 번째 flush는 replicator를 다시 만들지 않아 비용이 거의 없다.

자주 하는 실수#

ForceNetUpdate가 flush하지 않는 경우#

ForceNetUpdate는 authority이고, 게임 NetDriver가 있고, 그 NetDriver가 서버일 때만 덤으로 flush한다.

Engine/Private/Actor.cpp
void AActor::ForceNetUpdate()
{
UNetDriver* NetDriver = GetNetDriver();
if (GetLocalRole() == ROLE_Authority)
{
if (NetDriver && NetDriver->GetNetMode() < ENetMode::NM_Client)
{
NetDriver->ForceNetUpdate(this);
if (NetDormancy > DORM_Awake)
{
FlushNetDormancy();
}
}
}
// (생략) 게임 NetDriver가 아닌 NetDriver에는 ForceNetUpdate만 전달한다.
}

데디케이티드 서버와 맵을 리슨 서버로 연 경우는 문제가 없다. UEngine::LoadMap이 Listen을 액터 초기화보다 먼저 호출하기 때문이다.

문제는 게임 NetDriver가 없는 월드다. 혼자 플레이하던 호스트가 맵을 다시 열지 않고 UWorld::Listen으로 친구를 받는 협동 게임에서, 맵에 배치된 문(DORM_Initial)을 연다고 하자.

Listen 전에는 게임 NetDriver가 없어 ForceNetUpdate가 아무것도 하지 않는다. 결과가 갈리는 것은 DORM_Initial과 DORM_DormantAll이 둘 다 휴면이지만 새로 접속한 연결을 다르게 다루기 때문이다.

DORM_InitialDORM_DormantAll
휴면인 연결처음부터 모든 연결. 나중에 접속한 연결도 포함그 연결에 한 번 복제한 뒤부터
새 연결이 받는 값없음. 레벨 패키지의 기본값을 쓴다접속 시점의 서버 값

DORM_Initial 배치 액터는 클라이언트도 레벨에서 이미 갖고 있다고 보고 복제 검토에서 아예 뺀다. 그래서 친구는 레벨에 저장된 닫힌 문을 보고, 다음 flush 전까지 상태가 어긋난다.

Standalone에서 리플레이를 녹화할 때도 같은 분기를 탄다.

FlushNetDormancy는 게임 NetDriver가 없어도 DORM_Initial을 DORM_DormantAll로 바꾸고, 데모 NetDriver를 포함해 액터를 복제하는 모든 NetDriver에서 휴면 상태를 푼다.

값을 바꾼 뒤 깨우면 FastArray 변경이 사라진다#

레거시 복제와 Replication Graph 기준이다. 휴면 액터의 FastArray에 항목을 추가한 뒤 깨우면 그 항목은 클라이언트로 가지 않는다.

void AChestActor::AddItem(const FChestItem& NewItem)
{
FChestItem& Item = Items.Items.Add_GetRef(NewItem);
Items.MarkItemDirty(Item);
// 휴면 중이었다면 위 항목은 전달되지 않는다.
ForceNetUpdate();
}

결과는 서버와 클라이언트의 배열 불일치다. 이후 같은 순서로 추가한 항목도 전달되지 않고, Push Model로 등록하고 마킹한 FastArray도 같다.

ForceNetUpdate가 덤으로 부르는 FlushNetDormancy도 값을 바꾼 뒤라 “변경 → flush” 순서가 된다. 레거시 복제와 Replication Graph는 기본 설정에서 flush할 때 휴면 중이던 연결마다 replicator를 현재 상태를 기준선으로 다시 만든다. 그래서 flush 전에 추가한 항목은 이미 보낸 것으로 취급되는 것으로 보인다.

일반 프로퍼티는 값을 바꾼 뒤 flush해도 지금은 전달되지만, Epic은 이를 기대면 안 되는 구현 세부로 본다(Actor Network Dormancy).

값을 바꾸기 전에 깨우면 된다.

void AChestActor::AddItem(const FChestItem& NewItem)
{
FlushNetDormancy();
FChestItem& Item = Items.Items.Add_GetRef(NewItem);
Items.MarkItemDirty(Item);
}

이 뒤에 ForceNetUpdate를 더해도 변경은 그대로 전달된다.

실수를 막는 방법#

Setter 패턴#

위 두 문제는 휴면할 수 있는 액터의 replicated property를 setter로만 바꾸면 막을 수 있다.

void ADoorActor::SetOpen(bool bNewOpen)
{
check(HasAuthority());
if (bOpen == bNewOpen)
{
return;
}
FlushNetDormancy();
bOpen = bNewOpen;
// 바로 보내야 하는 변경일 때만
ForceNetUpdate();
}
  • 같은 값이면 반환한다. 값이 그대로여도 flush는 휴면 중이던 연결을 깨우고 replicator를 다시 만든다. 값이 한동안 자주 바뀌면 변경마다 flush하지 말고 SetNetDormancy로 깨워 둔다.
  • FlushNetDormancy를 직접, 값을 바꾸기 전에 부른다. 게임 NetDriver가 없는 월드와 FastArray 순서 문제를 함께 막는다.
  • 프로퍼티를 private로 두고 setter만 공개한다.

Push Model과 함께 쓸 때#

마킹도 setter 안에서 값을 바꾼 직후에 한다. 마킹하지 않은 Push Model 프로퍼티는 flush해도 전달되지 않는다. 반대로 마킹은 dirty 표시일 뿐 액터를 깨우지 않으므로, 마킹했더라도 flush는 그대로 필요하다.

Push Model은 값이 그대로인 flush의 비용을 줄여 주지 않는다. 건너뛰는 것은 dirty가 아닌 프로퍼티의 비교뿐이고, 깨우고 다시 재우는 비용은 setter의 early-return으로 막는다. 이 패턴에서 Push Model의 역할은 비용보다 강제에 가깝다. 마킹 없이는 변경이 전달되지 않으니 값을 바꾸는 경로가 setter로 모이고, 그 setter가 flush 순서까지 고정한다.

참고 자료#

글 공유하기

이 글이 도움이 되셨다면 다른 사람과 공유해 주세요!

휴면 액터 깨우기: FlushNetDormancy, ForceNetUpdate, SetNetDormancy
https://hon454.github.io/posts/unreal-engine-waking-dormant-actors/
작성자
Jihoon Jeon
게시일
2026-10-22
라이선스
CC BY-NC-SA 4.0
관련 게시글스마트
1
Unreal Engine - Push Model Networking
Unreal EngineUE 5.8.3 기준으로 레거시 복제의 Push Model이 켜지는 조건, dirty 비트가 소비되는 경로, net.PushModelSkipUndirtiedReplication이 건너뛰는 범위를 엔진 소스로 확인하고, ThirdPerson 템플릿에 액터 1000개를 띄워 Unreal Insights, Networking Insights, Network Profiler로 CPU 시간, 대역폭, 비교 횟수를 측정한다. 같은 조건에서 Dormancy와도 비교한다.
2
면접 오답노트: 말하지 못한 주요 개념, Net Relevancy
Unreal Engine면접에서 제대로 답하지 못한 Net Relevancy를 다시 정리한다. AActor::IsNetRelevantFor의 판정 순서와 문서와 다른 부분, Owner가 판정을 바꾸는 방식, 연관성을 잃고 되찾을 때 RelevantTimeout과 액터 파괴, BeginPlay, OnRep, RPC에 생기는 일, ForceNetRelevant와 Replication Graph, Iris에서의 주의점, Dormancy, Priority와의 차이를 다룬다.
3
Unreal Engine PIE에서 클라이언트와 서버 값을 한 줄로 비교하기
Unreal EnginePIE에서 클라이언트 코드가 같은 프로세스의 서버 월드와 서버 측 PlayerController를 찾아 위치·속도를 한 줄로 비교하는 에디터 전용 디버그 코드와, 네트워크 에뮬레이션으로 확인한 측정 결과를 정리합니다.
4
언리얼 엔진의 리플리케이션 및 RPC 순서 보장
Unreal Engine프로퍼티 복제와 OnRep, RPC의 순서 보장 범위를 살펴보고, 일시적인 상태 불일치와 액터 간 준비 순서를 처리하는 방법을 예시로 정리한다.
5
Unreal Engine - Replication Graph
Unreal EngineUE 5.8.3 소스와 테스트 프로젝트 실측으로 Replication Graph가 기본 드라이버의 무엇을 줄이고 무엇을 줄이지 않는지 확인한다. 액터 5,000개와 클라이언트 16개까지의 측정, 효과가 있는 설정값과 이름과 반대로 동작하는 설정값, 커스텀 그래프의 라우팅과 Team Node, 5.8.3에서 재현한 버그 다섯 가지, 도입 기준과 Iris와의 관계를 다룬다.
무작위 게시글무작위

댓글

Profile Image of the Author
Jihoon Jeon
안녕하세요! 서브컬처와 자전거를 사랑하는 개발자 전지훈입니다.
공지사항
제 블로그에 오신 것을 환영합니다! 예시 공지입니다.
카테고리
태그
최신 일상
사이트 통계
게시글
73
카테고리
10
태그
143
총 단어 수
133,527
운영 일수
0일
최근 활동
0일 전
사이트 정보
빌드 플랫폼
GitHub Actions
블로그 버전
Firefly v6.16.9
라이선스
CC BY-NC-SA 4.0
목차