TIL: Unreal Engine 네트워크 초기화 흐름과 NetLoadOnClient 이해하기
오늘 배운 내용
오늘은 Unreal Engine 멀티플레이에서 GameState의 BeginPlay 동기화 흐름, PlayerController가 Pawn을 Possess하는 과정, Owner 복제, PostNetInit, 그리고 NetLoadOnClient 속성의 의미를 정리했다.
핵심은 서버와 클라이언트가 같은 시점에 모든 객체를 동시에 가지는 것이 아니라, 서버에서 먼저 상태를 만들고 그 상태가 복제를 통해 클라이언트에 전달된다는 점이었다.
또 레벨에 미리 배치된 액터와 게임 중 서버가 Spawn하는 액터는 네트워크 설정을 다르게 봐야 한다는 점도 중요했다.
4. HandleBeginPlay()의 의미
HandleBeginPlay()는 GameState에서 게임 시작 상태를 처리하는 함수다.
virtual void HandleBeginPlay() override;
이 함수는 서버에서 GameMode가 게임 시작을 처리할 때 호출된다.
흐름으로 보면 다음과 같다.
서버에서 게임 시작
↓
GameMode::StartPlay()
↓
GameState::HandleBeginPlay()
HandleBeginPlay()는 서버 기준으로 “게임이 시작되었다”는 상태를 GameState에 반영한다.
이 과정에서 bReplicatedHasBegunPlay 같은 값이 설정되고, 클라이언트로 복제될 준비가 된다.
즉, HandleBeginPlay()는 서버가 BeginPlay 시작 상태를 확정하는 단계라고 볼 수 있다.
5. OnRep_ReplicatedHasBegunPlay()의 의미
OnRep_ReplicatedHasBegunPlay()는 서버에서 설정된 BeginPlay 상태가 클라이언트에 복제되었을 때 호출되는 함수다.
virtual void OnRep_ReplicatedHasBegunPlay() override;
서버에서 bReplicatedHasBegunPlay 값이 바뀌고 이 값이 클라이언트로 복제되면, 클라이언트에서 OnRep_ReplicatedHasBegunPlay()가 호출된다.
흐름으로 보면 다음과 같다.
서버에서 bReplicatedHasBegunPlay 설정
↓
값이 클라이언트로 복제됨
↓
클라이언트에서 OnRep_ReplicatedHasBegunPlay() 호출
즉, 클라이언트는 이 함수를 통해 “서버에서 게임이 시작되었구나”라는 사실을 전달받는다.
정리하면 HandleBeginPlay()는 서버에서 게임 시작을 처리하는 함수이고, OnRep_ReplicatedHasBegunPlay()는 클라이언트가 서버의 게임 시작 상태를 복제받아 반응하는 함수다.
6. BeginPlay 동기화 흐름
서버와 클라이언트의 BeginPlay 흐름은 다음과 같이 진행된다.
서버
GameMode::StartPlay()
↓
GameState::HandleBeginPlay()
↓
bReplicatedHasBegunPlay 설정
↓
WorldSettings::NotifyBeginPlay()
↓
서버 월드 액터들의 BeginPlay 실행
서버는 StartPlay()를 통해 게임 시작을 시작하고, GameState의 HandleBeginPlay()를 호출한다.
그 안에서 BeginPlay가 시작되었다는 상태를 기록하고, 서버 월드의 액터들에게 BeginPlay를 실행시킨다.
이후 해당 상태가 클라이언트로 복제된다.
클라이언트
bReplicatedHasBegunPlay 값 복제 받음
↓
GameState::OnRep_ReplicatedHasBegunPlay()
↓
WorldSettings::NotifyBeginPlay()
↓
클라이언트 월드 액터들의 BeginPlay 실행
클라이언트는 서버에서 복제된 값을 보고 자신의 월드에서도 BeginPlay를 실행한다.
따라서 서버와 클라이언트는 직접 동시에 BeginPlay를 시작하는 것이 아니라, 서버가 먼저 시작 상태를 만들고 클라이언트가 그 상태를 복제받아 따라가는 구조다.
7. 서버와 클라이언트의 BeginPlay 흐름 정리
서버와 클라이언트를 나누어 보면 다음과 같다.
[서버] [클라이언트]
GameMode::StartPlay()
↓
GameState::HandleBeginPlay()
↓
bReplicatedHasBegunPlay = true
↓
WorldSettings::NotifyBeginPlay()
↓
서버 액터 BeginPlay 실행
↓
bReplicatedHasBegunPlay 복제
↓
GameState::OnRep_ReplicatedHasBegunPlay()
↓
WorldSettings::NotifyBeginPlay()
↓
클라이언트 액터 BeginPlay 실행
핵심은 서버는 HandleBeginPlay()로 BeginPlay 시작을 결정하고, 클라이언트는 OnRep_ReplicatedHasBegunPlay()로 그 사실을 전달받는다는 것이다.
8. PlayerController와 Character의 Possess 흐름
플레이어가 접속하면 서버에서는 PlayerController가 생성되고, 이후 Pawn 또는 Character를 Possess한다.
흐름은 다음과 같다.
서버: PreLogin
↓
서버: Login
↓
서버: PlayerController 생성
↓
서버: PostLogin
↓
서버: Character 생성
↓
서버: PlayerController::OnPossess()
↓
서버: Character::PossessedBy()
OnPossess()는 PlayerController가 어떤 Pawn 또는 Character를 소유하게 될 때 호출된다.
PossessedBy()는 Pawn 또는 Character 입장에서 자신이 어떤 Controller에게 Possess 되었는지 알게 되는 함수다.
즉, PlayerController 쪽에서는 OnPossess()가 호출되고, Character 쪽에서는 PossessedBy()가 호출된다.
9. PossessedBy()와 Owner 설정
PossessedBy()는 서버에서 호출된다.
서버에서 PlayerController가 Character를 Possess하면 Character의 Owner 관련 정보가 설정된다.
서버에서 PlayerController::OnPossess()
↓
서버에서 Character::PossessedBy()
↓
서버에서 Character Owner 설정
처음 Character가 생성되었을 때는 Owner가 아직 없을 수 있다.
이후 Possess 과정에서 Owner가 설정되고, 이 Owner 정보가 클라이언트로 복제된다.
즉, Owner는 서버에서 정하고 클라이언트는 복제를 통해 그 결과를 전달받는다.
10. OnRep_Owner()의 의미
OnRep_Owner()는 서버에서 변경된 Owner 정보가 클라이언트에 복제되었을 때 호출되는 함수다.
서버에서 Character Owner 변경
↓
Owner 속성이 클라이언트로 복제됨
↓
클라이언트에서 OnRep_Owner() 호출
클라이언트는 OnRep_Owner()를 통해 이 액터의 Owner가 누구인지 알게 된다.
예를 들어 Character의 Owner가 특정 PlayerController로 설정되면, 클라이언트는 복제된 Owner 값을 통해 해당 Character가 누구에게 소유되었는지 알 수 있다.
즉, OnRep_Owner()는 클라이언트가 서버에서 정한 Owner 정보를 전달받고 반영하는 함수다.
11. PostNetInit()의 의미
PostNetInit()은 액터의 초기 네트워크 복제가 끝난 뒤 호출되는 함수다.
네트워크로 생성되거나 복제되는 액터는 처음부터 모든 정보를 가지고 있는 것이 아니라, 서버에서 필요한 초기 정보들이 클라이언트로 복제되어야 한다.
이 초기 복제가 끝나면 PostNetInit()이 호출된다.
흐름으로 보면 다음과 같다.
서버에서 액터 생성
↓
액터 정보가 클라이언트로 복제됨
↓
Owner 같은 초기 속성 복제
↓
클라이언트에서 PostNetInit() 호출
중요한 점은 Owner 속성도 복제 대상이기 때문에, Owner 정보가 복제된 뒤 PostNetInit 흐름과 연결될 수 있다는 것이다.
즉, 클라이언트는 서버에서 만들어진 액터의 기본 네트워크 정보를 받은 뒤 초기화 과정을 진행한다.
12. BeginPlay 흐름과 Possess 흐름을 합쳐서 보기
오늘 본 로그를 합치면 두 가지 흐름이 함께 진행된다.
1. 게임 시작 / BeginPlay 동기화 흐름
2. 플레이어 접속 / Possess / Owner 복제 흐름
전체 흐름은 다음과 같이 정리할 수 있다.
[서버 월드 시작]
GameMode 생성
GameState 생성
↓
GameMode::StartPlay()
↓
GameState::HandleBeginPlay()
↓
bReplicatedHasBegunPlay 설정
↓
WorldSettings::NotifyBeginPlay()
↓
서버 월드 액터 BeginPlay 실행
이후 플레이어 접속 흐름이 진행된다.
[플레이어 접속]
PreLogin
↓
Login
↓
PlayerController 생성
↓
PostLogin
↓
Character 생성
↓
PlayerController::OnPossess()
↓
Character::PossessedBy()
↓
Character Owner 설정
그리고 이 정보들이 클라이언트로 복제된다.
[클라이언트 복제]
bReplicatedHasBegunPlay 복제
↓
GameState::OnRep_ReplicatedHasBegunPlay()
↓
클라이언트 BeginPlay 실행
Owner 복제
↓
Character::OnRep_Owner()
↓
PostNetInit()
정리하면 서버는 게임 시작 상태와 Owner 정보를 만들고, 클라이언트는 그 정보를 복제받아 자신의 월드에서 초기화를 진행한다.
13. NetLoadOnClient 속성의 의미
NetLoadOnClient는 레벨에 미리 배치된 액터를 클라이언트도 맵 로드 시 함께 로드할지 결정하는 속성이다.
쉽게 말하면 다음과 같다.
이 액터가 레벨에 배치되어 있을 때
클라이언트도 이 액터를 같이 로드할 것인가?
모든 액터 정보를 서버가 모든 클라이언트에게 복제하는 것은 비효율적이다.
그래서 레벨 디자인을 통해 고정적으로 배치되는 액터는 NetLoadOnClient를 true로 지정해서 클라이언트가 스스로 로드하도록 할 수 있다.
레벨에 미리 배치되어 있고
모든 클라이언트가 알아야 하는 액터
→ NetLoadOnClient = true
반대로 서버에서만 필요한 액터라면 클라이언트가 로드할 필요가 없다.
서버만 알아도 되는 액터
→ NetLoadOnClient = false
14. NetLoadOnClient를 true로 둘 액터 예시
NetLoadOnClient를 true로 두면 좋은 액터는 클라이언트가 맵을 볼 때 반드시 존재해야 하는 액터다.
예를 들면 다음과 같다.
벽
바닥
나무
바위
건물
울타리
가로등
문
엘리베이터
스위치
레버
버튼
보물상자
퀘스트 오브젝트
이런 액터들은 맵에 미리 배치되어 있고 모든 플레이어가 볼 수 있어야 한다.
특히 문, 레버, 버튼, 상자처럼 상호작용이 필요한 액터는 클라이언트에도 존재해야 한다.
다만 상태가 변하는 액터라면 NetLoadOnClient만으로는 부족하고 Replicates도 함께 고려해야 한다.
예를 들어 문이 열리고 닫히는 상태를 모든 클라이언트가 알아야 한다면 다음처럼 생각할 수 있다.
문 액터
NetLoadOnClient = true
Replicates = true
반대로 바위나 벽처럼 고정되어 있고 상태가 변하지 않는 장식물은 다음처럼 둘 수 있다.
바위 액터
NetLoadOnClient = true
Replicates = false
15. NetLoadOnClient를 false로 둘 수 있는 액터 예시
클라이언트가 직접 볼 필요가 없고 서버에서만 로직을 처리하면 되는 액터는 NetLoadOnClient를 false로 둘 수 있다.
예를 들면 다음과 같다.
몬스터 스폰 매니저
라운드 매니저
점수 계산 매니저
게임 규칙 처리 액터
서버 판정용 트리거
아이템 스폰 구역
보스방 입장 감지 트리거
퀘스트 지역 진입 체크 트리거
숨겨진 트랩 로직 액터
랜덤 보상 위치 계산 액터
이런 액터들은 클라이언트가 직접 알 필요가 없다.
서버에서만 계산하고 결과만 필요한 경우가 많기 때문이다.
예를 들어 몬스터 스폰 매니저는 서버에서 몬스터를 언제 어디에 생성할지만 결정하면 된다.
클라이언트가 이 매니저 액터를 직접 가지고 있을 필요는 없다.
서버 전용 스폰 매니저
NetLoadOnClient = false
Replicates = false
16. NetLoadOnClient와 Replicates의 차이
NetLoadOnClient와 Replicates는 서로 다른 개념이다.
NetLoadOnClient
= 맵 로드 시 클라이언트도 이 액터를 로드할지 결정
Replicates
= 서버에서 변경된 액터 상태를 클라이언트에게 동기화할지 결정
예를 들어 레벨에 배치된 문이 있다고 하면,
NetLoadOnClient = true
는 클라이언트도 맵을 로드할 때 문 액터를 가지게 한다는 뜻이다.
하지만 문이 열렸는지 닫혔는지 같은 상태 변화까지 자동으로 동기화되는 것은 아니다.
상태 변화까지 서버에서 클라이언트로 보내려면 다음 설정도 필요하다.
Replicates = true
따라서 둘은 다음처럼 구분해야 한다.
클라이언트도 액터를 가지고 있어야 한다
→ NetLoadOnClient
서버에서 바뀐 상태를 클라이언트도 알아야 한다
→ Replicates
17. 게임 중 SpawnActor로 생성되는 액터는 다르게 봐야 한다
NetLoadOnClient는 주로 레벨에 미리 배치된 액터와 관련이 있다.
반면 게임 중 서버가 SpawnActor로 생성하는 액터는 NetLoadOnClient보다 Replicates 설정이 더 중요하다.
예를 들어 플레이어 캐릭터는 보통 맵에 미리 배치되는 것이 아니다.
플레이어 접속
↓
서버가 Pawn 또는 Character Spawn
↓
PlayerController가 Possess
↓
복제를 통해 클라이언트에 전달
이런 경우에는 다음 설정이 더 중요하다.
Replicates = true
Replicate Movement = true
Owner 설정
Possess 흐름
총알, 투사체, 아이템처럼 게임 중 생성되는 액터도 마찬가지다.
서버에서 생성한 액터를 클라이언트에게 보여줘야 한다면 해당 액터가 복제되어야 한다.
게임 중 생성되는 액터
→ NetLoadOnClient보다 Replicates를 먼저 확인
18. 실제 게임 제작 시 판단 기준
게임을 만들 때 모든 액터마다 무조건 NetLoadOnClient를 바꿀 필요는 없다.
대부분은 기본값을 사용하고, 특별히 서버 전용으로만 필요한 액터인지 판단하면 된다.
기준은 다음과 같다.
플레이어 눈에 보여야 한다
→ NetLoadOnClient = true
맵에 고정적으로 배치되어 있다
→ NetLoadOnClient = true
모든 클라이언트가 위치를 알아야 한다
→ NetLoadOnClient = true
서버만 계산하면 된다
→ NetLoadOnClient = false
클라이언트가 몰라도 되는 로직 액터다
→ NetLoadOnClient = false
게임 중 서버가 SpawnActor로 생성한다
→ Replicates를 먼저 확인
정리하면 NetLoadOnClient는 모든 액터마다 고민하는 옵션이라기보다, 레벨에 미리 배치된 액터를 클라이언트도 로드해야 하는지 판단할 때 사용하는 옵션이다.
오늘 헷갈렸던 점
오늘 헷갈렸던 부분은 서버와 클라이언트에서 BeginPlay가 어떻게 동기화되는지였다.
처음에는 서버와 클라이언트가 각자 BeginPlay를 바로 실행한다고 생각할 수 있었지만, 실제로는 서버에서 bReplicatedHasBegunPlay 상태를 만들고 그 값이 클라이언트로 복제되면서 클라이언트의 BeginPlay 흐름이 이어진다.
또 Possess 과정에서도 서버가 먼저 Character를 Possess하고 Owner를 설정한 뒤, 그 Owner 정보가 클라이언트로 복제되어 OnRep_Owner()가 호출된다는 점이 중요했다.
NetLoadOnClient도 처음에는 모든 액터에 설정해야 하는 옵션처럼 보였지만, 실제로는 레벨에 미리 배치된 액터를 클라이언트도 로드할지 결정하는 옵션이었다.
따라서 게임 중 서버가 생성하는 액터는 NetLoadOnClient보다 Replicates 설정을 먼저 확인해야 한다.
오늘의 핵심 정리
- HandleBeginPlay()는 서버에서 GameState의 BeginPlay 시작 상태를 처리하는 함수다.
- OnRep_ReplicatedHasBegunPlay()는 클라이언트가 서버의 BeginPlay 상태를 복제받았을 때 호출된다.
- 서버는 GameMode::StartPlay()에서 GameState::HandleBeginPlay()를 호출한다.
- 서버는 bReplicatedHasBegunPlay를 설정하고 서버 월드의 BeginPlay를 실행한다.
- 클라이언트는 bReplicatedHasBegunPlay를 복제받아 OnRep_ReplicatedHasBegunPlay()를 실행한다.
- 클라이언트는 이 흐름을 통해 자신의 월드 액터 BeginPlay를 실행한다.
- PlayerController가 Pawn 또는 Character를 소유할 때 서버에서 OnPossess()가 호출된다.
- Character 입장에서는 서버에서 PossessedBy()가 호출된다.
- 서버에서 설정된 Owner 정보는 클라이언트로 복제된다.
- 클라이언트는 Owner 복제를 받으면 OnRep_Owner()를 호출한다.
- PostNetInit()은 액터의 초기 네트워크 복제가 끝난 뒤 호출되는 초기화 단계다.
- NetLoadOnClient는 레벨에 배치된 액터를 클라이언트도 맵 로드 시 함께 로드할지 결정한다.
- 모든 클라이언트가 봐야 하는 레벨 배치 액터는 NetLoadOnClient = true가 적절하다.
- 서버에서만 필요한 매니저나 판정용 액터는 NetLoadOnClient = false로 둘 수 있다.
- NetLoadOnClient는 액터 존재 여부와 관련 있고, Replicates는 상태 동기화와 관련 있다.
- 게임 중 서버가 SpawnActor로 생성하는 액터는 NetLoadOnClient보다 Replicates 설정이 더 중요하다.
정리하면 서버는 게임 시작 상태, Possess, Owner 같은 중요한 네트워크 상태를 먼저 만들고 클라이언트는 그 정보를 복제받아 자신의 초기화 흐름을 진행한다.
그리고 레벨에 미리 배치된 액터는 NetLoadOnClient로 클라이언트 로드 여부를 판단하고, 게임 중 생성되거나 상태가 바뀌는 액터는 Replicates를 함께 고려해야 한다.
'unreal 8기' 카테고리의 다른 글
| 2026-6-18 TIL (0) | 2026.06.19 |
|---|---|
| 2026-6-17 TIL (0) | 2026.06.17 |
| 2026-6-15 TIL (0) | 2026.06.15 |
| 2026-6-11 TIL (0) | 2026.06.11 |
| 2026-6-10 TIL (0) | 2026.06.10 |