Unreal Nets
Unreal의 네트워크 구조
- 계층 정리도
UWorld (월드)
└── UNetDriver
└── UNetConnection (Client 1개당 1개)
└── UChannel (한 Connection에 여러 개)
└── FOutBunch / FInBunch (전송할 논리 단위)
└── Packet (실제 UDP 패킷에 여러 Bunch가 합쳐져 전송됨)
- UWorld
- 언리얼의 네트워크는 ‘월드’를 기준으로 동작
- 서버 / 클라 실행시 각각의 World가 자신의 NetDriver를 가짐
- Tick에 따라 NetDriver에게 ‘업데이트’ 요청
- 언리얼의 네트워크는 ‘월드’를 기준으로 동작
- UNetDriver
- 네트워크 전송 / 수신, 연결 유지, 타임 아웃 처리를 담당
- Packet을 보내고 받음
- 서버는 클라이언트 만큼 NetConnection 관리
- 네트워크 전송 / 수신, 연결 유지, 타임 아웃 처리를 담당
- UNetConnection (연결 객체)
- 클라이언트 1명당 서버엔 1개의 NetConnection 생성
- 서버는 접속한 클라만큼, 클라는 서버와의 통신을 위한 1개의 NetConnection만 생성
- 플레이어 1명에 대한 네트워크 상태 관리
- 클라이언트 1명당 서버엔 1개의 NetConnection 생성
- UChannel(채널)
- 네트워크의 데이터를 ‘기능’별로 분리하는 단위
- NetConnection 내부에 여러 채널 존재
- ex) ControlChannel, ActorChannel 등
- ex) ControlChannel, ActorChannel 등
- 네트워크의 데이터를 ‘기능’별로 분리하는 단위
- Bunch
- 언리얼 네트워크의 ‘논리적 전송 단위’
- 채널 안에 생성되며 여러 정보가 담김
- ex) RPC 호출 정보, Replication 정보 등
- ex) RPC 호출 정보, Replication 정보 등
- 언리얼 네트워크의 ‘논리적 전송 단위’
- Packet(RUDP)
- 여러 Bunch들이 모아두었다가 한번에 Packet에 합쳐 전송
(네트워크 관련 포스팅)
- 여러 Bunch들이 모아두었다가 한번에 Packet에 합쳐 전송
TMI - 서버 와 클라의 World 시작
서버의 관점
클라이언트가 접속을 하지 않더라도 진행
- UWorld 생성
- GameMode/State/Actor Spawn 진행
- World::Beginplay 호출
- GameMode가 규칙/매치 흐름 진행
클라 관점
- 서버에 접속
- 클라가 같은 맵을 로딩하며 자신의 UWorld 생성
- 로딩 이후, Client 내부의 Beginplay 호출
- 또한 서버에서 GameState, PlayerController, PlayerState 등과
필요한 Replicate 데이터들을 받음
- 또한 서버에서 GameState, PlayerController, PlayerState 등과
- 이후 받아온 데이터를 기반으로 GameState 등을 생성하고 상태 동기화
이러한 방식으로 ‘늦게 접속한’ 클라이언트 역시
현재 서버의 상태에 맞게 동기화하여 문제없이 진행 가능
게임 시작에 따른 전반적인 흐름
-
- 서버 월드/레벨 시작
- 서버 월드가 돌아가며 GameMode,GameState 등이 BeginPlay를 호출
- 서버 월드/레벨 시작
UGameInstance::StartGameInstance()
→ UGameEngine::LoadMap()
→ UWorld 생성
→ 기본 Actor들 스폰 (GameMode, GameState, PlayerStart 등)
→ UWorld::BeginPlay()
→ World 안의 모든 Actor에 대해 AActor::BeginPlay() 호출
-
- 서버의 게임 시작
- GameMode/GameState의 게임 시작
- GameMode::StartPlay()
- GameState::HandleBeginPlay()
(이 타이밍에 State의 Replicated 플래그를 세팅하고, 클라로 복제)
- 서버의 게임 시작
- 클라 월드/레벨 시작
- 클라는 GameMode가 없기에 GameState의 복제 값과 OnRep를 통해 시작을 확인
- 레벨을 열고, 처리 자체는 생성하고 초기화하며 BeginPlay()를 호출
- 클라는 GameMode가 없기에 GameState의 복제 값과 OnRep를 통해 시작을 확인
서버로부터 Travel 정보 수신
→ UGameEngine::LoadMap()
→ 클라용 UWorld 생성
→ 기본 Actor들 스폰 (GameState, PlayerController, DefaultPawn 등 *클라 버전*)
→ UWorld::BeginPlay()
→ Actor::BeginPlay() (클라 쪽에서도 전체 호출)
- GameState의 OnRep_ReplicatedHasBegunPlay() 함수를 통해
게임 시작 확인
- GameState의 Replicate 변수의 값 변경을 통해 클라에 OnRep_ 함수가 호출
- 늦게 접속한 클라이언트들도 이 함수가 호출되며, 서버와 동기화
- OnRep 는 ‘이전 값 vs 이번 값’의 비교를 통해 호출되기에
새로 들어온 클라의 Replicate를 초기화하며 false인 걸 확인하고
OnRep를 바로 호출
- OnRep 는 ‘이전 값 vs 이번 값’의 비교를 통해 호출되기에
- GameState의 Replicate 변수의 값 변경을 통해 클라에 OnRep_ 함수가 호출
- PlayerController? PlayerState?
bool AGameModeBase::PreLogin(...); // 접속 허가 여부 - ErrorMessage에 임의의 문자열 넣을시, 로그인 중인 플레이어의 연결이 거부됨 (설정에 따라 StandAlone으로 유지하거나, 다른 Level 로드)
APlayerController* AGameModeBase::Login(...); // PC 생성
void AGameModeBase::PostLogin(APlayerController* NewPC);
void AGameModeBase::HandleStartingNewPlayer(APlayerController* NewPC);
void AGameModeBase::RestartPlayer(AController* NewPC); // Pawn 스폰 & Possess
-
RestartPlayer 타이밍에 PlayerController,PlayerState, Pawn 등이 서버에서 Spawn되고
NetDriver가 이들을 클라에 Replicate -
이후, Client 쪽에서 PostNetInit() 등이 호출
(이미 시작했다면 Beginplay도 바로 호출) -
RestartPlayer 타이밍에 추가적으로
Pawn에 컨트롤러가 빙의되기에- AController::Possess
- Pawn의 PossessedBy() 호출(서버에서)
- 내부에서 SetOwner를 통해 Owner 설정
- 컨트롤러 설정
- 내부에서 SetOwner를 통해 Owner 설정
- 변경된 Owner로 인해 OnRep_Owner (클라) 호출
- AController::Possess
Controller.Possess(Pawn)
→ Pawn::PossessedBy(ServerController)
→ Pawn.SetOwner(ServerController)
→ Replication 시스템에서 Owner 변경 감지
- 타이밍에 따라 다를수 있으나
PostNetInit() 호출 후, 추가적으로 OnRep_Owner()가 호출될 수 있음
(Owner 역시 클라에 복제되어 있다면)
(처음 Pawn 생성될 때)
Pawn.PostNetInit()
Pawn.OnRep_Owner() // Owner가 이미 있는 상태에서 생성되었다면 호출됨
(그 이후 서버에서 Owner 변경 패킷 도착)
Pawn.OnRep_Owner() // Possess 효과가 클라이언트에서 여기서 일어남
NetLoadOnClient
-
레벨 디자인을 통해
레벨에 고정적으로 배치되는 액터는 해당 옵션을 true로 지정해
모든 Client가 스스로 Spawn 시키도록 설정
(로그인 시 호출되는 PlayerController와 혼동하지 말것!) -
클라리언트가 접속할 때, 월드에 이미 존재하는 해당 옵션이 true인
Actor를 클라이언트 쪽에도 생성 -
Level에 이미 배치한 Actor 나
런타임 중 생성한 Actor 등에 true를 주어 사용
Replication Notify
-
서버에서 실행되지 않고 클라에서만 실행되는 특징이 있음
(서버에서 실행 시, 명시적인 호출 필요) -
속성값이 변경되어서 변경된 값이 클라에 복제되는 시점에
Callback 함수 바인딩 가능- 이것을 Replication Notify 함수라 함
- C++ : OnRep_
- BP : RepNotify
- 이것을 Replication Notify 함수라 함
| 구분 | C++ OnRep 함수 | Blueprint RepNotify |
|---|---|---|
| 호출 위치 | 클라이언트에서만 자동 호출 | 서버 + 클라이언트 모두 호출 |
| 명시적 호출 | C++에서 직접 호출 가능 | 명시적 호출 불가능 |
| 호출 조건 | 값이 변경된 경우에만 호출 | 서버: 항상 호출 / 클라이언트: 값 변경 시 호출 |
NetUpdateFrequency
- 서버에서 클라이언트로 1초당 몇 번 Replication 정보를 전송할지를 표현하는 변수
- 기본값 100(초당 100번)
- 다만 ‘이론상’의 값이며, 이를 보장하진 않음
(서버의 Tick Rate에 따라 Replication이 발생하나,
서버의 성능에 따라 달라짐)
-> 그렇기에 렌더링 기능이 없는 Dedicated Server가 비교적 좋은 성능을 발휘 가능
- 기본값 100(초당 100번)
-
Pawn, PlayerController 등의 기본값 : 100
GameState : 10
PlayerState : 1 -
해당 변수의 비율을 낮추어 서버의 성능을 개선할 수 있음
-
또한 액터의 처음 서버 위치와 속도 값을 안다면
이를 이용하여 ‘예측’하여 동기화가 가능
(보간) - MinNetUpdateFrequency?
- Replication 업데이트가 ‘느리게’ 발생하지 않도록 제한하는 Actor 변수
- 최소한의 업데이트 빈도를 보장하는 용도의 변수이다
(실제 업데이트 Frequency = max(NetUpdateFrequency,MinNetUpdateFrequency))
- Replication 업데이트가 ‘느리게’ 발생하지 않도록 제한하는 Actor 변수
Relevancy(연관성/관련성)
- 레벨에 있는 모든 액터의 정보를 클라에게 실시간으로 전송하는 것은
꽤나 무거운 작업
- 그렇기에 해당 클라이언트 커넥션과 ‘연관성’이 있는 액터만을 Replication을 해준다
- 그렇기에 해당 클라이언트 커넥션과 ‘연관성’이 있는 액터만을 Replication을 해준다
연관성의 판단 기준(Actor)
- Owner
- 해당 Actor를 소유하고 있는 Actor(Owner)
(ex : 무기 액터를 소유한 캐릭터) - 거리 기반 Relevancy를 무시하고 항상 Replicate 됨
(내 소유 Actor는 항상 레플리케이션)
- 해당 Actor를 소유하고 있는 Actor(Owner)
- Instigator
- 해당 Actor에 영향을 끼친 Pawn
(ex : 데미지를 가한 Pawn) - 보통은 OwnerShip Chain을 따라 연관성에 포함되는 편
- 해당 Actor에 영향을 끼친 Pawn
- AlwaysRelevant
- 해당 Actor가 모든 Client에 연관성이 있게끔 설정
(ex : 맵 전체에 보여야 하는 보스 몬스터 등)
- 해당 Actor가 모든 Client에 연관성이 있게끔 설정
- NetUseOwnerRelevancy
- Owner 액터의 연관성으로 해당 Actor 연관성을 대신할 때 사용
(소유한 무기 Actor 등은 장비한 캐릭터가 보일때 같이 보여야 함)
- Owner 액터의 연관성으로 해당 Actor 연관성을 대신할 때 사용
- OnlyRelevantToOwner
- Owner 액터에게만 연관성을 가짐 (다른 액터와는 연관성 x)
(길찾기 Actor - 다른 플레이어가 나의 편의성 액터를 볼 필요 없음)
- Owner 액터에게만 연관성을 가짐 (다른 액터와는 연관성 x)
- NetCullDistance
- View와의 거리에 따라 연관성 여부 결정
(아주 먼 곳의 고블린 Actor를 가까우면 레플리케이션, 멀면 안하기)
- View와의 거리에 따라 연관성 여부 결정
연관성의 추가 판단 기준 (Pawn/Character/PlayerController)
- Viewer
- 클라이언트 커넥션이 소유한 Player Controller
- 클라이언트 커넥션이 소유한 Player Controller
- ViewTarget
- 플레이어 컨트롤러가 빙의한 Pawn
- Viewer 에 의해 빙의된 ViewTarget이 레벨을 돌아다님
- 플레이어 컨트롤러가 빙의한 Pawn
- 관련 함수
virtual bool AActor::IsNetRelevantFor(
const AActor* RealViewer, // Connection이 가진 Pawn (Player의 아바타)
const AActor* ViewTarget, // PlayerCameraManager가 바라보는 대상
const FVector& SrcLocation
) const;
-
플레이어가 빙의한 RealViewer는 항상 연관성이 있기에
Replicate 됨 -
Player의 ViewTarget 역시 항상 연관성이 있는 편
(Replicate 됨)
NetPriority
- 한정된 ‘대역폭’(NetBandWidth) 내에서
서버가 어떤 Actor를 먼저 Replicate 할 지 결정하는 가중치
- 순서를 정하는데만 사용
(우선순위를 정하는 것이며, 서버 상황에 따라 모두 정상적으로 보내지기도 함)
(‘포화 상태’가 아니라면 기본적으로 모두 보내질 수 있음)
- 순서를 정하는데만 사용
- 단독으로 쓰이기 보단
NetUpdateFrequency와 마지막으로 갱신되었는지 등의 요소도 같이 사용됨
- 또한 NetCullDistance 같은 ‘연관성’ 속성도 반영될 수 있음
- 또한 NetCullDistance 같은 ‘연관성’ 속성도 반영될 수 있음
-
- 포화 상태?
- 보낼 액터 데이터들의 총량이 커서 ‘대역폭’을 넘어선 경우를 뜻함
(패킷 용량이 꽉찼다!)
- 이 경우에 NetPriority에 따라 보낼 데이터를 결정
- 포화 상태?
NetDormancy (휴면)
- Actor의 Replication과 RPC를 멈추게 하는 설정(NetDormancy 설정)
- 자주 수정되지 않는 Actor에 적합
- 너무 자주 수정되는 Actor에 적용시, 오히려 오버헤드가 발생 가능함
- 자주 수정되지 않는 Actor에 적합
enum class ENetDormancy : uint8
{
DORM_Never, // 절대 Dormant 하지 않음. 항상 Replicate.
DORM_Awake, // 활성 상태, Replicate 대상, 휴면 가능 (기본)
DORM_DormantPartial, // 일부 클라만 Dormant
DORM_Initial, // 휴면 상태로 시작하고, 필요할 때 깨울 수 있는 상태
DORM_DormantAll, // 모든 클라이언트에게 Dormant
};
-
- Conditional Property Replication
- 특정 UPROPERTY의 레플리케이트 변수가 ‘조건’에서만 전송되도록 하는 기능
(일반적으론 Replication 등록되면, 해제할 수 없기에 이러한 조건 분류를 통해
세밀한 조정을 함)
- 변수마다 Replication 전송 조건을 설정
- Conditional Property Replication
COND_None // 항상 전송(기본)
COND_InitialOnly // Actor 최초 생성 시에만 전송
COND_OwnerOnly // Owner 클라이언트에게만 전송
COND_SkipOwner // Owner 제외 모든 클라이언트에게만
COND_SimulatedOnly // SimulatedProxy에게만
COND_AutonomousOnly // AutonomousProxy에게만
COND_SimulatedOrPhysics // SimulatedProxy + Physics
COND_InitialOrOwner // 초기 + Owner에게만
COND_Custom // C++ Override 시 직접 조건 선택
댓글남기기