728x90

Bunch부터 TickFlush까지 — Actor의 값은 어떻게 네트워크로 전달되는가

앞선 글에서 Unreal은 UNetConnection을 통해 특정 상대방과의 논리적인 연결을 관리한다고 설명했다.

그렇다면 실제로 Actor의 값이나 RPC 같은 데이터는 이 연결을 통해 어떻게 전달될까?

이때 등장하는 개념이

Channel → Bunch → Packet

이다.

1. Channel은 연결 안의 논리적인 통로다

하나의 UNetConnection 안에는 여러 개의 UChannel이 존재할 수 있다.

UNetConnection
      │
      ├─ UControlChannel
      │
      ├─ UActorChannel
      │
      ├─ UActorChannel
      │
      └─ ...

Channel은 네트워크 연결 자체가 아니다.

UNetConnection
→ 상대방과의 연결

UChannel
→ 그 연결 안에서 특정 네트워크 데이터를 처리하는 논리적인 통로

2. 대표적인 Channel

대표적으로 다음과 같은 Channel이 있다.

UControlChannel

연결 제어 메시지를 처리한다.

공식 API에서도 UControlChannel을 "A channel for exchanging connection control messages"​라고 설명한다.

UNetConnection
      ↓
UControlChannel
      ↓
Control Message

UActorChannel

Actor와 Subobject의 네트워크 데이터를 처리한다.

공식 API에서는 UActorChannel을 Actor 및 Subobject의 properties와 RPC를 교환하는 Channel이라고 설명한다. 또한 Actor Channel 내부의 실제 Property/RPC replication은 FObjectReplicator가 담당한다고 설명한다.

UNetConnection
      ↓
UActorChannel
      ↓
Actor / Subobject

3. UControlChannel이 연결을 만드는 것은 아니다

이 부분은 자주 혼동된다.

UNetConnection
      ↓
UControlChannel

순서다.

즉,

UControlChannel
→ 연결을 만드는 객체

가 아니다.

정확하게는

UNetConnection
→ 연결 관리

UControlChannel
→ 그 연결 위에서 Control Message 처리

라고 이해해야 한다.

4. FNetworkNotify와 Control Message

Control Channel을 통해 들어온 메시지는 World 등의 네트워크 알림 인터페이스와 연결된다.

FNetworkNotify에는 대표적으로 다음 네 가지 함수가 있다.

NotifyAcceptingConnection()
NotifyAcceptedConnection()
NotifyAcceptingChannel()
NotifyControlMessage()

각각의 역할을 단순화하면:

NotifyAcceptingConnection()
→ 연결 요청을 받아들일 것인가?

NotifyAcceptedConnection()
→ 연결이 만들어졌음을 알림

NotifyAcceptingChannel()
→ 새로운 Channel을 열어도 되는가?

NotifyControlMessage()
→ Control Message를 처리/알림

공식 문서에서도 NotifyAcceptingChannel()은 원격 요청으로 새로운 Channel이 생성될 때 이를 받아들일지 결정하는 notification이라고 설명한다.

따라서 FNetworkNotify는

네트워크 시스템과 World/PendingNetGame 등의 상위 객체를 연결하는 notification interface

라고 이해하는 것이 가장 정확하다.

접속 허용 여부는 게임 규칙에 맞게 결정 해야 하는데. 이 판단을 네트워크에 둘것이냐, 아니면 게임 코드에 둘것이냐를 언리얼은 게임 코드에 결정하는 쪽을 사용하고 이 판단의 과정을 게임 코드에서 FNetworkNotify라고 하는 4개의 함수로 이루어진 인터페이스를 통해 처리한다.

5. Bunch란 무엇인가?

Channel을 이해했다면 이제 Bunch를 볼 차례다.

쉽게 말하면 Bunch는 Unreal의 네트워크 프로토콜에서 Channel을 통해 전달되는 데이터 단위다.

개념적으로:

UActorChannel
      ↓
   FOutBunch
      ↓
UNetConnection
      ↓
   Packet
      ↓
    UDP

반대로 수신 측에서는:

UDP
 ↓
Packet
 ↓
Bunch
 ↓
ChIndex
 ↓
Channel
 ↓
데이터 처리

하나의 Packet에는 여러 개의 Bunch가 포함될 수 있다.

UDP Packet
 ├─ Bunch A
 ├─ Bunch B
 └─ Bunch C

각 Bunch는 어떤 Channel에 속하는지 식별할 수 있는 정보와 함께 신뢰성, Open/Close 등의 프로토콜 정보를 가진다.

6. ChIndex는 왜 필요한가?

Bunch에는 Channel을 식별하기 위한 Channel Index가 있다.

Bunch
 ├─ ChIndex
 ├─ Reliable
 ├─ Open / Close
 └─ Payload

수신 측에서는 이 값을 사용해 해당 Channel을 찾는다.

개념적으로:

ChIndex = 5
     ↓
Channels[5]
     ↓
해당 Channel

Channel을 매번 별도의 ID 검색 구조로 찾는 대신 배열 인덱스를 이용하면 빠르게 접근할 수 있다.

Channels[ChIndex]

그리고 해당 슬롯에 Channel 객체가 존재하는지 여부도 바로 확인할 수 있다.

7. Open / Close는 Connection의 종료가 아니다

Bunch에서 Open이나 Close가 등장한다고 해서 Connection 자체가 열리고 닫히는 것은 아니다.

여기서의 의미는 Channel의 상태다.

Connection
   │
   ├─ Channel A
   │
   └─ Channel B

Channel Bunch의 Close:

Channel B 종료

Connection의 Close:

전체 Connection 종료

서로 완전히 다른 개념이다.

8. Actor가 변경됐다고 즉시 Send되는 것은 아니다

이제 Replication의 핵심으로 들어간다.

게임 코드에서:

Health = 80;

이라고 했다고 해서 그 순간 바로 UDP 패킷을 보내는 것은 아니다.

Unreal은 네트워크 Tick 흐름에서 복제할 데이터를 계산하고 전송한다.

개념적으로:

게임 코드
   ↓
Actor 상태 변경
   ↓
다음 네트워크 처리 시점
   ↓
Replication 대상 판단
   ↓
Actor Channel
   ↓
Bunch 생성
   ↓
Connection
   ↓
Packet
   ↓
UDP

9. 서버의 TickFlush

서버에서 중요한 지점이 TickFlush다.

UWorld Tick
     ↓
게임 로직 처리
     ↓
TickFlush
     ↓
NetDriver
     ↓
ServerReplicateActors()

언리얼은 프레임 끝의 TickFlush에서 이번 프레임에 무엇을 복제할지를 모아서 네트워크 데이터로 만든다.

UWorld의 프레임이 끝나면 NetDriver의 TickFlush가 실행되고 서버에서는
ServerReplicateActors()가 복제할 엑터를 고르고 UActorChannel의 SendBunch가 번치를 커넥션에 넘긴다.

공식 API에서는 ServerReplicateActors() 를
NetDriver에 포함된 Connection으로 관련 Actor를 복제하기 위해 호출되는 함수라고 설명한다.

따라서 개념적으로:

ServerReplicateActors()
        ↓
복제 대상 Actor 판단
        ↓
각 Connection의 상태 확인
        ↓
UActorChannel
        ↓
네트워크 데이터 생성
        ↓
FOutBunch
        ↓
Connection
        ↓
Packet

이라고 볼 수 있다.

단, ServerReplicateActors()가 단순히 "변경된 Actor만 찾아서 보낸다"고 이해하면 안 된다.

실제 Replication에는 relevancy, update frequency, channel 상태, NetUpdate 관련 정책 등 여러 조건이 관여한다.

10. Bunch와 Packet은 같은 것이 아니다

이 부분은 반드시 구분해야 한다.

Bunch
≠
Packet

송신 방향에서는:

Actor
 ↓
UActorChannel
 ↓
FOutBunch
 ↓
UNetConnection
 ↓
Packet
 ↓
UDP

하나의 Packet에 여러 Bunch가 들어갈 수도 있다.

Packet
 ├─ Bunch
 ├─ Bunch
 └─ Bunch

따라서 Bunch를

"UDP Packet 그 자체"

라고 표현하면 안 된다.

Bunch는 Unreal의 네트워크 프로토콜 계층에서 사용되는 데이터 단위이고, Packet은 실제 전송되는 네트워크 패킷이다.

11. 전체 Replication 흐름

이제 전체 과정을 연결해보자.

[게임 로직]

Actor 값 변경
     ↓
Health = 80
     ↓
[World Tick]
     ↓
[NetDriver TickFlush]
     ↓
ServerReplicateActors()
     ↓
복제 대상 / Connection 판단
     ↓
UActorChannel
     ↓
FObjectReplicator 등을 통한 데이터 직렬화
     ↓
FOutBunch
     ↓
UNetConnection
     ↓
Packet 구성
     ↓
UDP Socket
     ↓
Client

여기서 최신 UE에서는 Actor Channel이 Actor의 생성과 lifetime을 관리하는 반면 실제 Property/RPC replication은 FObjectReplicator가 담당한다는 점도 기억해두면 좋다.

12. Tick Rate와 Network Speed는 다르다

게임 네트워크 성능을 이해할 때 반드시 구분해야 하는 것이 있다.

Tick Rate
vs
Network Speed

Tick Rate

NetServerMaxTickRate

서버가 초당 몇 번 Tick할 수 있는지를 제한한다.

예를 들어:

NetServerMaxTickRate = 30

이면 Dedicated Server에서 엔진 Tick Rate를 최대 30 ticks/sec 수준으로 제한하는 의미다.

공식 API에서도 NetServerMaxTickRate를 Dedicated Server에서 엔진 Tick Rate를 제한하는 설정으로 설명한다.

13. Tick이 30이라고 Packet도 30개는 아니다

이 둘은 1:1 관계가 아니다.

30 Tick/sec

이라고 해서

30 Packet/sec

이라는 의미는 아니다.

한 Tick에서:

Packet 0개

일 수도 있고,

Packet 여러 개

가 나갈 수도 있다.

즉,

Tick Rate
→ 얼마나 자주 게임/네트워크 처리를 수행하는가

Packet Rate
→ 실제로 몇 개의 Packet이 전송되는가

는 서로 다른 개념이다.

14. CurrentNetSpeed

이번에는 데이터의 양을 제한하는 값이다.

CurrentNetSpeed는 현재 Connection에 적용된 네트워크 전송률을 나타내며 일반적으로 bytes/sec 단위로 다룬다.

예를 들어:

CurrentNetSpeed = 50,000

이라면 대략적으로

초당 50,000 bytes 수준

의 전송률을 허용하는 설정으로 이해할 수 있다.

하지만 이것이

"매초 반드시 50,000 bytes를 전송한다"

는 의미는 아니다.

실제로 보낼 데이터가 없다면 더 적게 보낸다.

허용 전송률
       ↓
50,000 bytes/sec

실제 전송량
       ↓
보낼 데이터에 따라
0 ~ 50,000 수준

Epic의 공식 지원 답변도 관련 bandwidth 설정값을 bytes/sec로 설명하고, 실제 per-frame bandwidth limit은 frame rate의 영향을 받는다고 설명한다.

15. MaxClientRate와 MaxInternetClientRate

UNetDriver에는 다음과 같은 설정이 있다.

MaxClientRate
MaxInternetClientRate

개념적으로는 클라이언트에 적용할 수 있는 전송률의 상한을 설정하는 값이다.

Epic 개발자 설명에서는:

MaxClientRate
→ LAN에서 사용할 수 있는 최대 데이터 전송률

MaxInternetClientRate
→ Internet 연결에서 사용할 수 있는 최대 데이터 전송률

이라는 의미로 설명해 왔다.

즉 LAN과 Internet에 서로 다른 bandwidth 정책을 적용하기 위한 값이라고 이해할 수 있다.

16. 두 값을 사용하는 이유

예를 들어:

MaxClientRate=100000
MaxInternetClientRate=50000

이라면 Internet 환경에서는 Internet용 상한이 더 낮다.

따라서 Internet 환경에서 적용되는 최대 전송률은 50,000 수준을 넘지 않도록 제한된다.

반대로:

MaxClientRate=50000
MaxInternetClientRate=100000

이라면

MaxClientRate = 50,000
MaxInternetClientRate = 100,000

이므로 Internet 상한이 높다고 해서 일반 상한인 MaxClientRate를 100,000으로 올리는 것은 아니다.

즉 더 낮게 설정된 상한보다 높여서 사용할 수는 없다.

개념적으로 보면:

Internet 환경

MaxClientRate          50,000
MaxInternetClientRate 100,000
                         ↓
실제로 허용 가능한 상한
                         ↓
                      50,000

반대라면:

MaxClientRate         100,000
MaxInternetClientRate  50,000
                         ↓
실제로 허용 가능한 상한
                         ↓
                      50,000

Epic의 과거 네트워크 설명에서도 두 값이 LAN/Internet 환경별 최대 데이터 전송률을 제한하는 용도로 설명되며, 최신 Epic 지원 답변에서도 ConfiguredInternetSpeed, ConfiguredLanSpeed, MaxClientRate, MaxInternetClientRate가 bandwidth 제한에 관여한다고 설명한다.

17. Tick Rate와 Network Speed를 함께 보면

결국 두 값은 서로 다른 제한이다.

┌─────────────────────────────┐
│     Server Tick Rate     
│    NetServerMaxTickRate   
│                             
│  얼마나 자주 처리하는가?     
└─────────────────────────────┘
              +
┌─────────────────────────────┐
│    Network Bandwidth      
│    CurrentNetSpeed        
│                              
│  얼마나 많은 데이터를         
│  전송할 수 있는가?           
└─────────────────────────────┘

따라서

Tick Rate = 처리 빈도

NetSpeed = 전송량 제한

이라고 기억하면 된다.

18. Unreal 네트워크 전체 구조

지금까지 세 편의 내용을 모두 연결하면 다음과 같다.

                         FURL
                          ↓
                   UEngine::Browse()
                          │
                ┌─────────┴─────────┐
                ↓                   ↓
             Local               Remote
                ↓                   ↓
             LoadMap()       UPendingNetGame
                ↓                   ↓
             UWorld              NetDriver
                │                   ↓
                │              InitConnect()
                │                   ↓
                │             UNetConnection
                │                   ↓
                │              Handshake
                │
                ↓
             NetDriver
                ↓
           InitListen()
                ↓
              Server
                │
                ↓
            UDP Socket
                │
       ┌────────┼────────┐
       ↓        ↓        ↓
   Connection Connection Connection
       A          B          C
       │          │          │
     Channel    Channel    Channel
       │
   ┌───┴──────────┐
   ↓              ↓
Control         Actor
Channel         Channel
                  ↓
             Replication
                  ↓
               Bunch
                  ↓
               Packet
                  ↓
                 UDP

이 구조를 이해하면 Unreal의 네트워크를 단순히

"Replication을 하면 서버에서 클라이언트로 값이 복사된다."

정도로 보는 것이 아니라,

World → NetDriver → Connection → Channel → Bunch → Packet → UDP Socket

이라는 여러 계층의 흐름으로 볼 수 있게 된다.

그리고 반대 방향의 데이터는

UDP Socket
   ↓
Packet
   ↓
Bunch
   ↓
Channel
   ↓
Connection
   ↓
Actor / Control 처리

순서로 올라온다.

이것이 Unreal 네트워크를 소스 코드 레벨에서 따라가기 위한 기본적인 골격이다.

728x90
Posted by 정망스
,
728x90

UDP부터 UNetConnection까지 — 언리얼은 클라이언트 접속을 어떻게 관리하는가

1편에서는 FURL에서 시작하여 UNetDriver까지 도달하는 흐름을 살펴봤다.

이번에는 한 단계 더 내려가서 실제 네트워크 통신을 담당하는 UDP Socket → NetDriver → NetConnection의 관계를 살펴본다.

1. Unreal의 기본 네트워크는 UDP 기반이다

일반적인 Unreal 게임 네트워크의 Replication은 UDP/IP 기반으로 동작한다.

다만 중요한 점은

LAN = UDP, Internet = TCP

처럼 이해하면 안 된다는 것이다.

LAN과 Internet은 네트워크 환경을 구분하는 개념이고,

UDP와 TCP는 전송 프로토콜을 구분하는 개념이다.

LAN
 ├─ UDP
 └─ TCP

Internet
 ├─ UDP
 └─ TCP

Unreal의 일반적인 게임 네트워킹은 UDP를 사용하지만, 프로젝트에서는 별도로 TCP/HTTP 등의 다른 통신을 사용할 수도 있다.

2. TCP와 UDP의 가장 큰 차이

TCP 서버는 일반적으로 다음 구조를 가진다.

Listen Socket
     │
     ├─ accept()
     │      ↓
     │   Client A Socket
     │
     ├─ accept()
     │      ↓
     │   Client B Socket
     │
     └─ accept()
            ↓
         Client C Socket

TCP는 연결이 성립되면 클라이언트마다 별도의 연결 소켓이 만들어진다.

반면 UDP는 기본적으로 Connectionless 프로토콜이다.

UDP Socket
    │
    ├─ Packet from A
    ├─ Packet from B
    └─ Packet from C

TCP처럼 accept()를 통해 클라이언트별 연결 소켓을 만드는 과정이 없다.

3. 그렇다면 Unreal은 클라이언트를 어떻게 구분할까?

UDP에는 TCP와 같은 연결 객체가 없기 때문에 Unreal이 논리적인 연결을 직접 관리한다.

서버에서 패킷이 들어오면 대략 다음과 같은 흐름으로 처리할 수 있다.

UDP Packet
    ↓
Source Address
    ↓
이미 알고 있는 주소인가?
    │
 ┌──┴──┐
Yes    No
 │      │
 ↓      ↓
기존    새로운
Connection  Handshake

Epic의 엔진 설명에서도 UNetDriver::TickDispatch()가 패킷을 받으면 패킷의 주소를 검사하고, 해당 주소에 대한 기존 UNetConnection이 있는지 FInternetAddr → UNetConnection 형태의 Map으로 확인한다고 설명한다.

개념적으로는 다음과 같다.

Server NetDriver
   │
   ├─ UDP Socket
   │
   └─ Connection Map
         ├─ Address A → Connection A
         ├─ Address B → Connection B
         └─ Address C → Connection C

따라서 클라이언트가 늘어난다고 해서

Client 1 → Socket 1
Client 2 → Socket 2
Client 3 → Socket 3

처럼 서버의 UDP Socket이 클라이언트마다 하나씩 증가하는 것은 아니다.

하나의 UDP Socket을 사용하면서 Unreal이 논리적인 UNetConnection을 클라이언트별로 관리하는 구조다.

4. UIpNetDriver는 무엇인가?

UNetDriver는 네트워크 전체를 관리하는 추상적인 기반 클래스다.

실제 IP 네트워크 통신을 담당하는 구현체가 UIpNetDriver다.

UNetDriver
    ↑
UIpNetDriver

공식 API에서도 UIpNetDriver는 UNetDriver를 상속하며 InitConnect(), InitListen(), LowLevelSend(), TickDispatch() 등의 IP 네트워크 관련 구현을 제공한다.
주소 선택에 사용하는 IP 주소나 호스트 이름을 실제 네트워크 주소로 해석하는 Resolver를 멤버로 들고있다.

개념적으로 보면:

UNetDriver
    │
    └─ UIpNetDriver
           │
           └─ IP 네트워크
                │
                └─ Socket

5. Socket은 어디에서 만들어지는가?

플랫폼마다 네트워크 API가 다르기 때문에 Unreal은 플랫폼별 Socket 구현을 직접 게임 코드에서 사용하지 않는다.

그 사이에 공통 인터페이스가 존재한다.

Unreal Network Code
        ↓
ISocketSubsystem
        ↓
Platform Socket Implementation
        ↓
OS Networking API

Windows라면 Winsock 계열 API를 사용하고, 다른 플랫폼 역시 해당 플랫폼의 네트워크 시스템에 맞는 구현을 사용한다.

따라서 게임 코드 입장에서는 플랫폼이 Windows인지 Linux인지 등에 따라 Socket API를 전부 다르게 작성할 필요가 없다.

6. 서버의 InitListen()

서버가 네트워크를 시작할 때는 InitListen()이 핵심이다.

UWorld
   ↓
NetDriver
   ↓
UIpNetDriver::InitListen()
   ↓
Socket 생성
   ↓
Local Address / Port 설정
   ↓
Bind
   ↓
Listening 상태

UNetDriver::InitListen()은 공식적으로 서버 모드에서 NetDriver를 초기화하는 함수다. UIpNetDriver에서도 InitListen()이 IP 기반 서버 초기화 함수로 제공된다.

여기서 TCP의 listen()과 Unreal의 InitListen()을 혼동하면 안 된다.

InitListen()은 Unreal의 NetDriver 초기화 함수 이름이고, 내부적으로 사용하는 실제 Socket API의 동작과 동일한 이름이라는 뜻은 아니다.

7. 클라이언트의 InitConnect()

클라이언트는 반대로 InitConnect()를 통해 원격 서버에 연결하기 위한 네트워크 환경을 준비한다.

UPendingNetGame
      ↓
InitNetDriver()
      ↓
UIpNetDriver::InitConnect()
      ↓
Socket 준비
      ↓
Server Address
      ↓
UNetConnection 생성
      ↓
Handshake 시작

Epic의 엔진 설명에서도 클라이언트가 서버에 Join할 때 UPendingNetGame에서 NetDriver를 초기화하고, 그 과정에서 서버를 대상으로 하는 UNetConnection을 설정한 뒤 데이터를 보내며 Handshake를 시작한다고 설명한다.

8. UNetConnection은 Socket인가?

아니다.

이 구분이 상당히 중요하다.

OS / Platform
      ↓
   FSocket
      ↓
실제 네트워크 송수신

Unreal
      ↓
UNetConnection
      ↓
특정 상대방과의 논리적인 연결 관리

즉,

Socket = 실제 네트워크 I/O

UNetConnection = Unreal이 관리하는 논리적인 연결

이라고 생각하면 된다.

UDP 서버에서는 하나의 Socket을 여러 클라이언트가 공유할 수 있지만,

UDP Socket
    │
    ├── UNetConnection A
    ├── UNetConnection B
    └── UNetConnection C

처럼 논리적인 Connection은 클라이언트별로 존재할 수 있다.

UDP는 accept()가 없기 때문에 클라이언트마다 소켓을 만들지 않고 서버 소켓은 하나이고 각 패킷의 송신자 주소를 보고 구분한다
NetDriver는 이 주소를 키로 하는 NetConnection 맵으로 각 클라이언트를 구분하여 관리하기 때문에 커넥션이 늘어나도 소켓 수가 늘지 않는다.

9. Connection이 만들어지기 전에 검증하는 이유

UDP는 TCP와 달리 연결을 위한 accept() 과정이 없다.

따라서 외부에서 UDP 패킷을 보내는 것만으로도 서버의 네트워크 처리 경로에 들어올 수 있다.

그렇다면 이런 문제가 생길 수 있다.

악성 패킷
   ↓
UDP 서버
   ↓
Connection 생성
   ↓
메모리 / CPU 사용

만약 공격자가 수많은 가짜 접속을 만들어낸다면 서버 자원이 빠르게 소모될 수 있다.

따라서 Unreal은 정식 UNetConnection을 만들기 전에 연결 요청을 검증하는 단계를 둔다.

10. StatelessConnectHandlerComponent

이 역할에 사용되는 것이 StatelessConnectHandlerComponent다.

공식 API는 이를

stateless (non-memory-consuming) connection handshake

를 구현하는 PacketHandler Component라고 설명한다.

핵심 아이디어는 다음과 같다.

Client
  ↓
초기 접속 요청
  ↓
Server
  ↓
Challenge / Cookie 처리
  ↓
Client 응답
  ↓
Server 검증
  ↓
통과
  ↓
Connection 처리 진행

여기서 "stateless"라는 것은 검증이 끝나기 전에 클라이언트별 연결 상태를 서버가 무조건 저장해두는 방식이 아니라는 것이 핵심이다.

최신 UE 문서에서도 내부적으로 활성 Secret, Authorised Cookie 등의 상태를 관리하는 것을 확인할 수 있다. 따라서 단순히 "아무 상태도 전혀 사용하지 않는다"라고 이해하면 안 된다.

11. TCP SYN Cookie와 비슷한 이유

이 방식은 개념적으로 TCP의 SYN Cookie와 비슷한 목적을 가진다.

TCP에서는

Client
  ↓ SYN
Server
  ↓ SYN-ACK + Cookie
Client
  ↓ ACK
Server
  ↓
Cookie 검증

을 통해 연결 요청을 검증할 수 있다.

Unreal의 UDP Handshake도

Client
  ↓
초기 요청
  ↓
Server
  ↓
Challenge / Cookie
  ↓
Client
  ↓
응답
  ↓
Server 검증

클라 요청에 따른 입력값을 통해 쿠키를 생성해서 회신하고, 클라의 응답으로 다시 받는 입력값을 통해 다시 쿠키를 계산해서 받은 값과 비교해서 검증한다.

NetDriver는 마지막으로 검증 결과를 확인해서 연결 처리를 진행한다.

이라는 방식으로
정식 연결 상태를 만들기 전에 요청을 검증한다는 점에서 비슷한 철학을 가진다.

다만 두 시스템의 프로토콜이 동일한 것은 아니다.

앞서 말햇듯 UDP에서는 accept()가 없기 때문에 주소를 기준으로 클라이언트별 연결 객체인 UIpConnection(UNetConnection)을 만들어서 관리 한다.

12. 검증이 끝나면 UNetConnection

검증 이후에는 실제 Unreal 연결 처리가 진행된다.

UDP Packet
    ↓
Stateless Handshake
    ↓
검증 성공
    ↓
UNetDriver
    ↓
UNetConnection
    ↓
Channel

즉 StatelessConnectHandlerComponent가 UNetConnection을 대신하는 것이 아니다.

역할이 다르다.

StatelessConnectHandlerComponent
→ 초기 접속 검증

UNetConnection
→ 검증 이후 특정 상대방과의 네트워크 연결 관리

13. UDP에는 TCP의 FIN이 없다

TCP는 연결 종료를 위한 명확한 프로토콜 절차가 있다.

대표적으로 FIN을 이용한다.

반면 일반적인 UDP에는 TCP와 같은 연결 종료 handshake가 없다.

따라서 서버 입장에서 다음 상황은 비슷하게 보일 수 있다.

정상적으로 게임 종료
        ↓
패킷 중단

인터넷 끊김
        ↓
패킷 중단

프로세스 강제 종료
        ↓
패킷 중단

컴퓨터 전원 종료
        ↓
패킷 중단

UDP 자체만으로는 "상대방이 정상적으로 종료했다"와 "갑자기 사라졌다"를 TCP FIN처럼 구분해 알려주지 않는다.

14. 그렇다면 서버는 어떻게 연결 종료를 알아내는가?

Unreal은 네트워크 계층에서 시간과 상태를 관리하고, 일정 시간 동안 통신이 이루어지지 않는 경우 Timeout으로 연결 종료를 판단한다.

개념적으로 보면:

UNetDriver Tick
      ↓
UNetConnection
      ↓
마지막 수신 시각 확인
      ↓
현재 시간과 비교
      ↓
Timeout?
   ┌──┴──┐
  No     Yes
   │       │
계속      Close
통신

여기서 Close()는 단순히 "UDP FIN 패킷을 보낸다"는 의미가 아니다.

Unreal 내부에서 해당 Connection의 종료 및 정리 과정을 진행하는 것이다.

15. Timeout과 정상 종료는 구분해야 한다

정상적인 종료와 갑작스러운 연결 끊김은 처리 목적이 다르다.
일반적인 Timeout과, Graceful을 구분하여 Close 처리를 한다.

정상 종료
   ↓
종료 절차 진행
   ↓
남아 있는 처리 확인
   ↓
Connection 종료

비정상 종료
   ↓
패킷 수신 중단
   ↓
Timeout
   ↓
Connection 종료

따라서

"UDP는 FIN이 없으니 바로 Close한다"

라고 표현하는 것은 정확하지 않다.

더 정확하게는

UDP 자체에는 TCP와 같은 연결 종료 handshake가 없기 때문에 Unreal이 별도의 상태 관리와 Timeout 등의 메커니즘으로 연결 종료를 판단한다.

라고 설명하는 것이 좋다.

16. 전체 연결 구조

지금까지를 하나로 연결하면 다음과 같다.

                Client
                  │
                  ↓
              UDP Packet
                  │
                  ↓
        Stateless Handshake
                  │
             검증 성공
                  ↓
             UNetDriver
                  │
                  ↓
          UNetConnection
                  │
                  ↓
              Channel
                  │
        ┌─────────┴─────────┐
        ↓                   ↓
 UControlChannel       UActorChannel
        │                   │
  제어 메시지          Actor 네트워크 데이터
728x90
Posted by 정망스
,
728x90

FURL에서 UNetDriver까지 — 맵 로딩과 서버 접속은 어떻게 시작되는가

언리얼의 네트워크 구조를 이해하려면 먼저 "게임이 맵을 여는 과정"과 "서버에 접속하는 과정"이 어디에서 갈라지는가​를 이해할 필요가 있다.

언리얼에서는 이 두 요청을 모두 FURL이라는 하나의 형태로 표현하고, UEngine::Browse()가 해당 URL을 해석하여 이후의 경로를 결정한다.

1. FURL은 맵과 서버 접속 정보를 함께 표현한다

FURL은 단순히 웹 주소를 표현하는 문자열이 아니다.

언리얼에서는 맵 이름, 호스트, 포트, 옵션 등의 정보를 하나의 URL 구조로 전달한다.

개념적으로 보면 다음과 같다.

FURL
 ├─ Map
 ├─ Host
 ├─ Port
 └─ Options

예를 들어 로컬 맵을 여는 경우와 원격 서버에 접속하는 경우는 같은 FURL을 사용하지만 내용이 다르다.

로컬 맵
FURL
 └─ Host가 없음
       ↓
     LoadMap

원격 서버
FURL
 └─ Host가 있음
       ↓
   서버 접속 경로
       ↓
 UPendingNetGame

UEngine::Browse()는 지정된 URL을 기준으로 이동을 처리하는 핵심 진입점이다. 공식 API에서도 Browse()는 현재 URL을 기준으로 지정된 URL로 이동하는 함수로 정의되어 있다.

2. Browse()에서 로컬과 원격 경로가 갈린다

전체적인 개념은 다음과 같다.

게임 시작 / Travel 요청
        ↓
       FURL
        ↓
   UEngine::Browse()
        │
        ├─ 로컬 맵
        │      ↓
        │   LoadMap()
        │      ↓
        │    UWorld
        │
        └─ 원격 서버
               ↓
        UPendingNetGame
               ↓
          NetDriver 생성
               ↓
          서버 접속 시작

여기서 중요한 점은 클라이언트가 서버에 접속하기 위해 반드시 먼저 완성된 게임 World를 가지고 있어야 하는 것은 아니라는 것이다.

UPendingNetGame은 바로 이 접속 준비 상태를 담당한다.

공식 API에서도 UPendingNetGame은 서버와의 연결에 사용할 UNetDriver를 가지고 있으며, InitNetDriver()를 통해 서버 연결용 NetDriver를 초기화한다고 설명한다.

3. 서버와 클라이언트의 시작 경로

이를 조금 더 구체적으로 나누면 다음과 같다.

서버

FURL
 ↓
Browse()
 ↓
LoadMap()
 ↓
UWorld 생성 및 초기화
 ↓
NetDriver 생성
 ↓
UWorld / NetDriver 연결
 ↓
InitListen()
 ↓
Listen 상태
 ↓
클라이언트 접속 대기

클라이언트

FURL
 ↓
Browse()
 ↓
UPendingNetGame 생성
 ↓
NetDriver 초기화
 ↓
InitConnect()
 ↓
UNetConnection 생성
 ↓
서버와 Handshake
 ↓
서버 접속 완료
 ↓
맵 로딩 / World 연결

실제 엔진에는 Travel, PIE, Demo, Seamless Travel 등의 추가 경로가 존재하기 때문에 이를 "언리얼 전체의 유일한 실행 순서"라고 보기보다는 일반적인 게임 서버 접속 흐름을 이해하기 위한 핵심 경로로 보는 것이 좋다.

Epic의 엔진 설명에서도 클라이언트가 서버에 Join할 때 UEngine::Browse()에서 UPendingNetGame을 만들고, 그 과정에서 서버 연결을 위한 UNetDriver와 UNetConnection을 설정한 뒤 Handshake를 시작한다고 설명한다.

4. 왜 UPendingNetGame이 필요한가?

클라이언트가 서버에 접속하는 순간에는 아직 서버의 게임 World가 클라이언트에 완성되어 있지 않을 수 있다.

그렇다면 이런 상태가 필요하다.

[접속 준비 상태]

클라이언트
 ├─ 기존 게임 World
 │
 └─ UPendingNetGame
       └─ NetDriver
            └─ UNetConnection
                 ↓
              서버와 통신

UPendingNetGame은 이 "아직 새로운 게임 World에 완전히 들어가지 않았지만 서버와 접속 절차를 진행하고 있는 상태"​를 관리한다.

공식 API에서도 UPendingNetGame의 NetDriver를 "새 서버에 접촉하기 위해 생성된 NetDriver"라고 설명하고 있으며, 연결이 성공하면 World로 전달된다고 명시한다.

5. 서버는 언제 Listen하는가?

서버의 경우 맵을 준비한 뒤 NetDriver를 서버 모드로 초기화한다.

LoadMap()
   ↓
World 준비
   ↓
NetDriver
   ↓
InitListen()
   ↓
서버가 접속을 받을 수 있는 상태

UNetDriver::InitListen()의 공식 설명도 "Initialize the network driver in server mode (listener)"​라고 되어 있다.

즉 Listen은 단순히 "맵을 연다"는 의미가 아니라,

현재 World를 네트워크 서버로 동작시키기 위해 NetDriver를 Listener 상태로 초기화하는 과정

으로 이해하는 것이 좋다.

6. UNetDriver는 네트워크 전체를 관리한다

이제 NetDriver가 등장한다.

핵심 관계는 다음과 같다.

UWorld
  │
  └─ UNetDriver
       │
       ├─ NetConnection
       │
       ├─ Channel
       │
       └─ 네트워크 송수신 / 복제 처리

각 객체의 역할을 단순화하면 다음과 같다.

객체역할

UNetDriver 네트워크 전체를 관리
UNetConnection 특정 상대방과의 논리적인 연결 관리
UChannel 하나의 연결 안에서 네트워크 데이터를 논리적으로 분리하여 처리

UNetDriver는 서버와 클라이언트 모두에 존재할 수 있다.

서버에서는 여러 클라이언트 연결을 관리하고,

Server NetDriver
 ├─ Connection A
 ├─ Connection B
 └─ Connection C

클라이언트에서는 접속 중인 서버와의 연결을 관리한다.

Client NetDriver
 └─ Connection
      ↓
    Server

7. UWorld와 UNetDriver는 어떻게 연결되는가?

World가 네트워크를 사용할 때 NetDriver는 해당 World와 연결되어 있어야 한다.

이 역할을 하는 것이 SetWorld()다.

UWorld
   ↓
SetNetDriver()
   ↓
UNetDriver::SetWorld()
   ↓
NetDriver가 해당 World를 사용

공식 API에서는 SetWorld()를 "Associate a world with this net driver", 즉 NetDriver에 World를 연결하는 함수로 설명한다. 또한 기존 World가 있다면 먼저 기존 World와의 연결을 해제한다고 명시한다.

개념적으로 보면 다음과 같다.

기존 World
    ↓
기존 연결 해제
    ↓
새 World 연결
    ↓
Tick 이벤트 등록

새로 만들어진 NetDriver라면 기존 World가 없기 때문에 기존 World를 해제하는 과정은 사실상 할 일이 없다.

8. NetDriver가 World Tick에 참여하는 방법

NetDriver는 World의 Tick 흐름에 네트워크 처리를 연결한다.

공식 API에는 다음 함수가 존재한다.

RegisterTickEvents(UWorld* InWorld)

이 함수는 공식적으로

Register all TickDispatch, TickFlush, PostTickFlush to tick in World

라고 설명되어 있다.

개념적으로는 다음과 같다.

UWorld
   │
   ├─ TickDispatch
   │      ↓
   │   네트워크 수신 처리
   │
   ├─ 일반적인 게임 Tick
   │
   └─ TickFlush
          ↓
       네트워크 송신 처리

따라서 NetDriver는 World의 Tick 흐름에 네트워크 처리 시점을 등록해 놓고 매 프레임 해당 처리에 참여한다.

9. FNetworkNotify는 무엇인가?

여기서 하나 더 등장하는 것이 FNetworkNotify다.

UNetDriver에는 다음과 같은 멤버가 있다.

FNetworkNotify* Notify;

중요한 것은 이것이 UWorld*가 아니라는 점이다.

FNetworkNotify는 네트워크 코드가 다른 객체에 네트워크 상태나 이벤트를 알리기 위한 인터페이스 성격의 타입이다.

공식 API도 이를

The net code uses this to send notifications.

라고 설명한다. 또한 UNetDriver::Notify를 네트워크 상태를 다른 객체, 일반적으로 World와 통신하기 위한 인터페이스라고 설명한다.

대표적인 구현체가 바로 UWorld와 UPendingNetGame이다.

FNetworkNotify
      ↑
      ├─ UWorld
      │
      └─ UPendingNetGame

즉,

UNetDriver
 ├─ World
 └─ Notify → FNetworkNotify

라는 관계를 통해 네트워크 시스템과 게임/World 측 코드가 연결된다.
World와 Notify 모두 비소유 참조 이기 떄문에 중간에 월드가 변경되도 NetConnection과 Channel의 정의는 유지 된다.

10. 여기까지의 전체 흐름

지금까지를 한 번에 연결하면 다음과 같다.

                 FURL
                  ↓
             UEngine::Browse()
                  │
          ┌───────┴────────┐
          ↓                ↓
       Local             Remote
          ↓                ↓
      LoadMap()      UPendingNetGame
          ↓                ↓
       UWorld          NetDriver
          ↓                ↓
      NetDriver       InitConnect()
          ↓                ↓
      InitListen()    UNetConnection
          ↓                ↓
       Server          Handshake

그리고 World와 NetDriver는 다음처럼 연결된다.

UWorld
   │
   └── NetDriver
          │
          ├── NetConnection
          │      └── Channel
          │
          └── FNetworkNotify
728x90
Posted by 정망스
,
728x90

• LoadingScreenWidget
• 타입: FSoftClassPath
• 설명: 로딩 스크린에 표시할 위젯 클래스(블루프린트 또는 C++ UserWidget)를 지정합니다.
이 위젯이 실제 로딩 화면에 표시됩니다.

• LoadingScreenZOrder
• 타입: int32
• 설명: 로딩 스크린 위젯이 뷰포트에 추가될 때의 Z-Order(레이어 우선순위)입니다.
값이 높을수록 다른 UI 위에 표시됩니다. 기본값은 10000입니다.

• HoldLoadingScreenAdditionalSecs
• 타입: float
• 설명: 로딩이 끝난 후에도 추가로 로딩 스크린을 유지할 시간(초)입니다.
텍스처 스트리밍 등으로 인한 블러 현상을 방지하기 위해 사용합니다.

• LoadingScreenHeartbeatHangDuration
• 타입: float
• 설명: 이 값(초)보다 더 오래 로딩 스크린이 유지되면 "로딩이 멈췄다"고 간주합니다.
0이면 비활성화됩니다.

• LogLoadingScreenHeartbeatInterval
• 타입: float
• 설명: 로딩 스크린이 유지되는 동안, 이 간격(초)마다 "무엇이 로딩 스크린을 붙잡고 있는지" 로그를 남깁니다.
0이면 비활성화됩니다.

• LogLoadingScreenReasonEveryFrame
• 타입: bool
• 설명: true로 설정하면, 매 프레임마다 로딩 스크린이 표시/숨겨지는 이유를 로그로 출력합니다.

• ForceLoadingScreenVisible
• 타입: bool
• 설명: true로 설정하면, 항상 로딩 스크린을 강제로 표시합니다.
디버깅 용도로 사용합니다.

• HoldLoadingScreenAdditionalSecsEvenInEditor
• 타입: bool
• 설명: true로 설정하면, 에디터 환경에서도 HoldLoadingScreenAdditionalSecs 지연을 적용합니다.
로딩 스크린을 반복적으로 테스트할 때 유용합니다.

• ForceTickLoadingScreenEvenInEditor
• 타입: bool
• 설명: true로 설정하면, 에디터 환경에서도 로딩 스크린의 틱(Tick)을 강제로 한번 호출하여 로딩 스크린이 즉시 표시 되도록 합니다. 기본값은 true입니다.

“지금 로딩이 필요한가?”를 판단하는 주체가 ILoadingProcessInterface(로딩 프로세서) 구현체
로딩 매니저가 매 프레임 이 구현체들을 체크해서 필요하면 지정된 로딩 위젯을 띄웁니다.
아래 코드 구문처럼 인터페이스를 상속받고 있는 리스트를 체크 해서 필요 여부 체크 GameState or Gamestate안에 구성하고있는 각 모듈의 컴포넌트(UGameStateComponent) or 외부에서 등록된 것들

// Ask the game state if it needs a loading screen	
if (ILoadingProcessInterface::ShouldShowLoadingScreen(GameState, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
	return true;
}

// Ask any game state components if they need a loading screen
for (UActorComponent* TestComponent : GameState->GetComponents())
{
	if (ILoadingProcessInterface::ShouldShowLoadingScreen(TestComponent, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
	{
		return true;
	}
}

// Ask any of the external loading processors that may have been registered.  These might be actors or components
// that were registered by game code to tell us to keep the loading screen up while perhaps something finishes
// streaming in.
for (const TWeakInterfacePtr<ILoadingProcessInterface>& Processor : ExternalLoadingProcessors)
{
	if (ILoadingProcessInterface::ShouldShowLoadingScreen(Processor.GetObject(), /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
	{
		return true;
	}
}
  • ‘어떤 배경을 쓸지’ 결정하는 데이터 소스 준비
    • 라이라처럼 모드/맵 선택 데이터(Primary Asset)를 만들고, TSoftClassPtr<UUserWidget>로 로딩 배경 위젯을 지정.
  • 레벨 전환 트리거에서 서브시스템에 저장
    • 트래블 직전에 LoadingScreenSubsystem->SetLoadingWidgetClass(ChosenClass) 같은 식으로 “교체할 클래스” 전달.
  • Common Loading Screen 세팅에서 Host 위젯만 지정
    • Host는 항상 동일한 틀, “배경”은 Construct에서 교체되므로 Host만 플러그인 세팅에 등록.
  • Host 위젯 Construct에서 Subsystem 조회 → SetContent
    • 서브시스템에서 클래스를 꺼내 새 배경 위젯 생성 → 네임드 슬롯/패널에 SetContent.
  • 해제 조건 신호 정리
    • 라이라처럼 Experience 시스템을 쓰지 않는다면, 맵 로드 완료 + 게임플레이 준비 완료를 알리는 별도 이벤트/플래그를 만들어 ShouldShowLoadingScreen() 로직(혹은 등가의 판정)에서 참고하도록 해야 “무한 로딩”을 피합니다. 커뮤니티에서도 라이라 신호를 그대로 기대해 붙였다가 로딩이 안 사라지는 이슈가 보고됩니다.

이뿐만이 아니라 조건 체크 종류에는 맵 로딩중인지, 월드가 시작이 아직 안됬는지. 로컬플레이어의 플레이어 컨트롤러에 해당 인터페이스가 있다면 로컬플레이어마다 체크를 한다던지 체크를 하는 조건이 다양하게 설정되어있다.

만약 멀티 게임이라면 지금 드는 생각은  AGameState가 모든 PlayerState가 Ready인지 집계하고, ILoadingProcessInterface 를 구현하여 “모두 Ready 될 때까지 true(계속 보여 줘)”를 반환하게 하면, 로딩 화면이 자연스럽게 전원 준비 완료까지 유지가 될거 같다.
RPC를 이용해서 각 클라에서 Ready상태값을 서버에 요청해서 갱신해주고 서버에서 모든 플레이어들의 Ready상태값을 확인한다 식의?...

아래 코드 느낌처럼.. 이거 재대로 돌아가는 코드 아니다.. 그냥 생각나는대로 정리만 해본것

//PlayerState
.h
UCLASS()
class AMyPlayerState : public APlayerState
{
    GENERATED_BODY()
public:
    AMyPlayerState();

    UPROPERTY(ReplicatedUsing=OnRep_IsReady, BlueprintReadOnly, Category="Ready")
    bool bIsReady = false;
}

.cpp
AMyPlayerState::AMyPlayerState()
{
    bReplicates = true;
}

void AMyPlayerState::SetReady(bool bInReady)
{
    if (!HasAuthority())
    {
        return; // 서버에서만 상태 확정
    }

    const bool bChanged = (bIsReady != bInReady);
    bIsReady = bInReady;
    if (bChanged)
    {
        OnRep_IsReady();

        // 서버에서 집계 갱신
        if (AGameStateBase* GS = GetWorld()->GetGameState())
        {
            if (AMyGameState* MGS = Cast<AMyGameState>(GS))
            {
                MGS->RecalcAllPlayersReady();
            }
        }
    }
}

void AMyPlayerState::OnRep_IsReady()
{
    // 클라에서 UI 갱신이 필요하면 여기서 브로드캐스트 등
}

void AMyPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
    Super::GetLifetimeReplicatedProps(Out);
    DOREPLIFETIME(AMyPlayerState, bIsReady);
}

//PlayerController
.h
UCLASS()
class AMyPlayerController : public APlayerController
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category="Ready")
    void MarkClientReady(bool bReady = true);

protected:
    UFUNCTION(Server, Reliable)
    void ServerSetClientReady(bool bReady);
    void ServerSetClientReady_Implementation(bool bReady);
};

.cpp
void AMyPlayerController::MarkClientReady(bool bReady)
{
    if (IsLocalController())
    {
        ServerSetClientReady(bReady);
    }
}

void AMyPlayerController::ServerSetClientReady_Implementation(bool bReady)
{
    if (AMyPlayerState* PS = GetPlayerState<AMyPlayerState>())
    {
        PS->SetReady(bReady); // 서버에서 복제 + 집계 트리거
    }
}

//GameState
.h
UCLASS()
class AMyGameState : public AGameState, public ILoadingProcessInterface
{
    GENERATED_BODY()
public:
    AMyGameState();

    // 집계 결과(복제): 클라에서도 UI가 참고할 수 있게끔
    UPROPERTY(ReplicatedUsing=OnRep_AllPlayersReady, BlueprintReadOnly, Category="Ready")
    bool bAllPlayersReady = false;

    // (선택) 기대 인원. 매치메이킹/세션 정보에서 세팅하면
    // "예상 인원 다 찰 때까지"도 함께 기다릴 수 있음.
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Ready")
    int32 ExpectedNumPlayers = 0; // 0이면 무시

    // 서버에서 호출: PlayerState 추가/변경/제거 시 집계
    void RecalcAllPlayersReady();

    // AGameState overrides
    virtual void AddPlayerState(APlayerState* PlayerState) override;
    virtual void RemovePlayerState(APlayerState* PlayerState) override;

    // ILoadingProcessInterface
    virtual bool ShouldShowLoadingScreen(FString& OutReason) const override;

protected:
    UFUNCTION()
    void OnRep_AllPlayersReady();

    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const override;
};

.cpp
AMyGameState::AMyGameState()
{
    bReplicates = true;
}

void AMyGameState::AddPlayerState(APlayerState* PlayerState)
{
    Super::AddPlayerState(PlayerState);
    if (HasAuthority())
    {
        RecalcAllPlayersReady();
    }
}

void AMyGameState::RemovePlayerState(APlayerState* PlayerState)
{
    Super::RemovePlayerState(PlayerState);
    if (HasAuthority())
    {
        RecalcAllPlayersReady();
    }
}

void AMyGameState::RecalcAllPlayersReady()
{
    if (!HasAuthority())
    {
        return;
    }

    const int32 NumPlayers = PlayerArray.Num();

    // 1) 인원수 조건: 기대 인원이 있으면 그 수만큼 모일 때까지 대기
    if (ExpectedNumPlayers > 0 && NumPlayers < ExpectedNumPlayers)
    {
        if (bAllPlayersReady != false)
        {
            bAllPlayersReady = false;
            OnRep_AllPlayersReady();
        }
        return;
    }

    // 2) 각 PlayerState의 Ready 플래그 확인
    bool bEveryoneReady = (NumPlayers > 0); // 0명이면 false로(원하면 정책 조정)
    for (APlayerState* PS : PlayerArray)
    {
        const AMyPlayerState* MyPS = Cast<AMyPlayerState>(PS);
        if (!MyPS || !MyPS->bIsReady)
        {
            bEveryoneReady = false;
            break;
        }
    }

    if (bAllPlayersReady != bEveryoneReady)
    {
        bAllPlayersReady = bEveryoneReady;
        OnRep_AllPlayersReady();
    }
}

bool AMyGameState::ShouldShowLoadingScreen(FString& OutReason) const
{
    // 로딩 화면 매니저가 매 프레임 호출
    if (!bAllPlayersReady)
    {
        OutReason = TEXT("Waiting for all players to be ready");
        return true; // 아직 붙잡아 둔다
    }
    return false; // 모두 준비됨 → 해제
}

void AMyGameState::OnRep_AllPlayersReady()
{
    // 클라 UI에 브로드캐스트하거나 로그 남기기 등
}

void AMyGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
    Super::GetLifetimeReplicatedProps(Out);
    DOREPLIFETIME(AMyGameState, bAllPlayersReady);
    DOREPLIFETIME(AMyGameState, ExpectedNumPlayers);
}

 

만약 로딩화면이 떠야할 시점 전에 외부에서 로딩화면의 조건을 설정해야할 경우가 있다면 ULoadingProcessTask::CreateLoadingScreenProcessTask을 사용해서 외부 task를 등록하면 된다.
CreateLoadingScreenProcessTask()로 만드는 객체(ULoadingProcessTask)가 이미 ILoadingProcessInterface를 구현하고 있어서, 함수를 호출하는 쪽 클래스는 인터페이스를 구현할 필요가 없다. 태스크를 만들어 두고, 일이 끝났을 때 Unregister()만 호출하면 된다.
인자로 UObject* 타입의 WorldContextObject자리에 this가 들어가고 있는건데 당연하게도 UWorld(월드) 객체를 얻으려 하는거기때문에 대부분은 this면 될것이다. 

ULoadingProcessTask* LoadTask =
    ULoadingProcessTask::CreateLoadingScreenProcessTask(this, TEXT("Travel to Arena"));

// 2) 실제 작업 시작 (예: 맵 전환)
UGameplayStatics::OpenLevel(this, FName(TEXT("L_Arena")));

// 3) 작업이 정말 끝났을 때 Unregister
if (LoadTask)
{
    LoadTask->Unregister();   // <- 해제 신호 (이 호출 전까지는 로딩 화면 유지)
}


728x90
Posted by 정망스
,
728x90
[UGameInstance]   // 게임 전체에서 단 하나, 앱 실행~종료까지 유지
     │
     └─[UWorld]   // 맵(레벨) 단위로 존재, 맵이 바뀌면 새로 생성
          │
          ├─[AGameModeBase / AGameMode]   // 서버(Authority)에서만 존재, 게임 규칙/진행 관리
          │     │
          │     └─[AGameStateBase / AGameState]   // 서버/클라이언트 모두에 존재, 게임 상태 동기화
          │           │
          │           └─[APlayerState]   // 각 플레이어별로 존재, 점수/팀/닉네임 등 상태 동기화
          │
          └─[APlayerController]   // 각 플레이어별로 존재, 입력/카메라/조작 담당
                │
                ├─[APlayerState]   // PlayerController가 소유(참조)함 (상속X, 소유/참조 관계)
                │
                └─[APawn / ACharacter]   // 실제 월드에서 움직이는 캐릭터/오브젝트, PlayerController가 소유(조작)

각 클래스의 역할 및 관계
• UGameInstance
• 게임 전체에서 단 하나만 존재 (앱 실행~종료까지)
• 레벨이 바뀌어도 유지됨
• 글로벌 매니저, 네트워크, 로그인 등 전역 데이터 관리


• UWorld
• 하나의 맵(레벨) 단위로 존재
• 맵이 바뀌면 새로 생성됨


• AGameModeBase / AGameMode
• 서버(Authority)에서만 존재
• 게임 규칙, 승패, 플레이어 입장/퇴장, 매치 흐름 등 관리
• 한 맵에 하나만 존재


• AGameStateBase / AGameState
• 서버/클라이언트 모두에 존재
• 게임의 현재 상태(점수, 남은 시간 등)를 동기화
• 한 맵에 하나만 존재


• APlayerController
• 각 플레이어(로컬/원격)마다 하나씩 존재
• 입력 처리, 카메라, UI, Pawn 조작 등 담당
• 서버/클라이언트 모두에 존재
• PlayerState를 소유(참조)함
(상속 관계가 아니라, 멤버 변수로 참조)


• APlayerState
• 각 플레이어마다 하나씩 존재
• 점수, 팀, 닉네임 등 플레이어 상태 동기화
• 서버/클라이언트 모두에 존재
• PlayerController와 1:1로 연결
(PlayerController가 PlayerState를 참조)


• APawn / ACharacter
• 실제 월드에서 움직이는 캐릭터/오브젝트
• PlayerController가 소유(조작)함

728x90
Posted by 정망스
,


맨 위로
홈으로 ▲위로 ▼아래로 ♥댓글쓰기 새로고침