🔎언리얼 엔진 파이프라인
Unreal Schematics | Unreal Engine Community Wiki
Unreal schematics featuring Animation, Blueprint, Character, Engine, Materials, Programming, Rendering & World Building
unrealcommunity.wiki
📌 Network Framwork
언리얼 엔진에서 네트워크 기능을 지원하는 핵심 시스템을 통합적으로 지칭하는 개념으로 볼 수 있다.
일반적으로 세션 관리, 연결 관리, 데이터 전송, 그리고 복제(Replication) 시스템이 포함된다.
✅ NetDriver
- 언리얼 엔진에서 서버와 클라이언트 간의 데이터 전송을 담당하는 핵심 네트워크 모듈
- 기본적으로 UNetDriver 클래스를 기반으로 하며, TCP/UDP 같은 프로토콜을 관리
- GameNetDriver(게임에서 사용)와 BeaconNetDriver(매치메이킹 등 비게임 요소 등에 사용) 등으로 확장
✅ NetConnection
- 각 클라이언트와 서버 간의 연결을 담당하는 클래스
- 서버는 UNetConnection을 통해 여러 클라이언트와의 연결을 관리하고, 클라이언트는 이를 통해 서버와 통신
✅ Online Subsystem
- 온라인 기능(매치메이킹, 친구 목록, 리더보드 등)을 처리하는 시스템
- OnlineSubsystemNull(로컬 환경) 부터 Steam, EOS(Epic Online Services) 같은 외부 플랫폼을 지원하는 모듈 존재
✅ Replication System
- 액터 및 데이터의 복제를 담당하는 시스템으로 Actor Replication과 Property Replication을 통해 동기화
- 복제는 NetDriver와 NetConnection을 통해 데이터를 주고 받으며 동작
📌 언리얼 Replication
Replication은 Network Framework 위에서 동작하는 기능으로, 위에서 언급한 NetDriver와 NetConnection을 활용하여 서버와 클라이언트 간의 액터 및 데이터를 동기화 하는 시스템이다.
정리하면, 언리얼 엔진에서 Replication은 서버와 클라이언트 간의 데이터 및 명령을 주고받는 프로셋를 의미하며 이를 통해 멀티플레이어 환경에서 객체의 상태나 이벤트를 동기화할 수 있다.

위의 이미지를 살펴보면 Replication 밑에 복잡하게 무언가 많아보이지만 크게 두 개의 개념으로 나눠서 살펴보면 조금 더 이해하기 쉬울 것 같다.
1️⃣서버와 클라이언트 동기화 기능 {Actor Replication, Property Replication, Ownership(소유권 결정)}
2️⃣이벤트 기반 네트워크 통신 방식 { RPCs, RepNotify, Reliability(신뢰성 옵션)}
1️⃣ 서버와 클라이언트 동기화 기능
✅ Actor Replication(Property Replication 같이 사용)
Actor Replication(액터 복제)은 서버에서 생성된 액터를 클라이언트와 동기화하는 기능이다.
즉, 서버에서만 존재하는 액터가 클라이언트에도 자동으로 생성되고 상태가 동기화되는 시스템이다.
멀티플레이어 환경에서 모든 플레이어가 동일한 게임 상태를 유지해야 하므로, 액터의 위치나 체력, 상태 등의 정보를 서버가 관리하고, 클라이언트는 이를 복제(Replication)받아 반영한다.
// 헤더 파일 (AMyCharacter.h)
public:
AMyCharacter();
protected:
// 복제할 변수 목록을 실제로 엔진에 등록하는 함수
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 복제될 수 있다고 엔진에 알리기 위해 선언
UPROPERTY(Replicated) // Property Replication
int32 PlayerHealth = 100;
// CPP 파일 (AMyCharacter.cpp)
AMyCharacter::AMyCharacter()
{
bReplicates = true; // 액터가 네트워크에서 복제되도록 설정(Actor Replication)
}
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
// 변수를 클라이언트와 동기화하기 위해 반드시 설정해야 하는 함수
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// PlayerHealth 변수를 네트워크에서 복제할 수 있도록 설정
DOREPLIFETIME(AMyCharacter, PlayerHealth);
}
서버에서 생성된 액터 자체를 클라이언트에도 복제하기위해 생성자에 bReplicated = true를 설정하여 Actor Replication을 사용했고, 개별 변수인 PlayerHealth를 UPROPERTY(Replicated)로 선언하므로써 서버에서 변경된 데이터가 클라이언트에 동기화 될 수 있도록 Property Replication을 사용했다.
📍GetLifeTimeReplicatedProps( )
UPROPERTY(Replicated)는 단순히 "복제 가능"하다고 선언하는 것이며, 실제 복제를 활성화 하려면
이 함수 내에서 DOREPLIFETIME(클래스명, 변수명); 함수를 사용해서 해당 변수를 복제 대상으로 지정해야한다.
✅ Ownership(소유권을 특정 클라이언트에게 할당)
서버는 기본적으로 모든 액터의 소유권을 가지고 있다.
멀티플레이어에서는 클라이언트가 자신의 캐릭터를 조작하려면 반드시 서버에서 소유권을 받아야한다.
클라이언트는 자신이 소유한 액터에서만 Server RPC를 호출할 수 있다. 소유권이 없다면 Server RPC 호출 등이 불가능하며, 입력을 받을 수 없다.
void AMyGameMode::PostLogin(APlayerController* NewPlayerController)
{
Super::PostLogin(NewPlayer);
// 새로운 플레이어 캐릭터를 생성하고 소유권을 설정
AMyCharacter* NewCharacter = GetWorld()->SpawnActor<AMyCharacter>(CharacterClass, FVector(0, 0, 100), FRotator::ZeroRotator);
NewCharacter->SetOwner(NewPlayerController); // 컨트롤러가 캐릭터를 소유하도록 설정
NewPlayerController->Possess(NewCharacter); // 컨트롤러가 캐릭터를 조작하도록 설정
}
- 서버에서 플레이어가 로그인하면 새로운 캐릭터 생성
- SetOwner(NewPlayerController);➡️ 해당 플레이어가 캐릭터를 소유하도록 설정
- NewPlayerController->Possess(NewCharacter); ➡️ 컨트롤러가 캐릭터를 조작하도록 설정
2️⃣이벤트 기반 네트워크 통신 방식
RPCs와 RepNotify는 둘 다 이벤트 기반이지만 동작 방식이 다르다.
RPCs는 함수 호출을 통해 수동으로 실행되고, RepNotify는 변수 값 변경을 통해 자동으로 실행된다.
Replication은 기본적으로 비신뢰성(Unreliable)으로 설정되어 있다. 이는 데이터 전송 중 일부 손실이 발생해도 큰 문제가 없는 경우에 적합하다.
중요한 게임 이벤트나 상태 변경 등 데이터 손실이 없어야 하는 경우에는 Reliable 키워드를 사용하여 신뢰성을 보장할 수 있다.
RepNotify는 RPC가 아니라 단순한 프로퍼티 값 동기화 이벤트이기 때문에 Reliable 설정은 RPC에서만 가능하다.
✅ RPCs(Reliability 옵션)
RPC는 서버와 클라이언트 간 특정 함수를 실행하는 방법이다.
즉, 한쪽에서 호출된 함수가 네트워크를 통해 다른 쪽(서버 혹은 클라이언트)에서 실행되는 방식.
Server, Client, Multicast 3가지 유형이 존재하며, 실행 위치가 다르며, 즉각적인 동작(공격, 점프, 채팅 메세지 등)이 필요할 때 사용하면 좋다.
앞서 말했듯이 Reliable 옵션을 설정하면 패킷 손실이 발생해도 반드시 실행되도록 하고 성능 비용이 있다. 그래서 점수나 플레이어 체력, 아이템 획득 등 중요한 게임 데이터에 사용된다.
Unreliable은 성능적으로 가볍지만 패킷 손실이 발생하면 그냥 무시하고 다음 패킷을 보낸다. 그래서 플레이어의 위치, 방향, 애니메이션 상태 등 빈번한 업데이트로 네트워크 부하가 크거나 과거의 데이터가 중요하지 않은 상황에서 사용된다.
Unreliable을 많이 사용해서 개발했을 때, 정상적으로 작동하고 있는지 검증이 어려울 수 있다.
그래서 대부분의 경우에 Reliable 옵션을 켜놓고 개발한 다음, 서버에 올려 테스트하고 추후에 최적화 단계에서 불필요하다고 판단되는 것에 Unreliable 옵션으로 변경하는 것이 더 효율적일 수 있다.

📍Server RPC(서버에서 실행되는 RPC)
클라이언트가 서버에게 특정 동작을 요청할 때 사용된다.
예를 들어 A클라이언트가 B클라이언트에게 총을 쏜다면, 서버에서 이를 검증한 후 처리해야된다.
// 헤더 파일 (MyCharacter.h)
UFUNCTION(Server, Reliable, WithValidation) // Reliable-> 반드시 실행, WithValidation-> 보안 검증
void Server_Fire();
bool Server_Fire_Validate(); // 클라이언트가 보낸 요청이 유효한지 검증하는 함수
void Server_Fire_Implementation(); // 실제 RPC 실행 로직을 포함하는 함수
// CPP 파일 (MyCharacter.cpp)
bool AMyCharacter::Server_Fire_Validate()
{
return (Ammo > 0); // 기본적으로 항상 허용하지만, 보안 검증 가능(탄약이 있는지 등)
}
void AMyCharacter::Server_Fire_Implementation()
{
UE_LOG(LogTemp, Log, TEXT("서버에서 총을 발사함"));
FireWeapon(); // 총을 발사하는 로직 실행
}
void AMyCharacter::Fire()
{
if (HasAuthority()) // 서버에서 실행 중이면 바로 실행
{
FireWeapon();
}
else // 클라이언트라면 서버에 RPC 요청
{
Server_Fire();
}
}
RPC를 호출하면 내부적으로 _Validate()가 먼저 실행되고, true를 반환 받으면 _Implementation()이 호출되며 실제 실행 로직이 동작한다.
만약 UFUNCTION(Server, Reliable)로 Server RPC를 선언하면 클라이언트가 무조건 실행 요청을 보낼 수 있으며 보안 검증 없이 바로 _Implementation()함수가 호출된다.
1️⃣ 클라이언트가 Server_Fire() 호출
2️⃣ 엔진이 자동으로 Server_Fire_Validate() 실행
3️⃣ Server_Fire_Validate()가 true를 반환하면
4️⃣ Server_Fire_Implementation()이 자동 실행됨
📍Client RPC(특정 클라이언트에서 실행되는 RPC)
서버가 특정 클라이언트에게 UI업데이트, 클라이언트에게 보여질 이펙트 효과 등을 실행하도록 요청할 때 사용된다.
A 클라이언트가 격발한 총알에 B 클라이언트가 피격되었다고 가정해보자.
// 헤더 파일 (MyCharacter.h)
UFUNCTION(Server, Reliable, WithValidation)
void Server_TakeDamage(float Damage); // 서버에서 실행되는 데미지 함수 (Server RPC)
void Server_TakeDamage_Implementation(float Damage);
bool Server_TakeDamage_Validate(float Damage);
UFUNCTION(Client, Reliable)
void Client_PlayHitEffect(); // 클라이언트에서 피격 이펙트를 실행하는 함수 (Client RPC)
void Client_PlayHitEffect_Implementation();
// CPP 파일 (MyCharacter.cpp)
void AMyCharacter::Server_TakeDamage_Implementation(float Damage)
{
if (Damage <= 0) return;
// 체력 감소
Health -= Damage;
// 클라이언트에서 피격 이펙트 실행 (Client RPC)
Client_PlayHitEffect();
}
bool AMyCharacter::Server_TakeDamage_Validate(float Damage)
{
return (Damage > 0); // 데미지는 0보다 커야 함
}
void AMyCharacter::Client_PlayHitEffect_Implementation()
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), HitEffect, GetActorLocation());
}
Client RPC는 서버에서 호출해야 하므로 HasAuthority() 체크가 필요 없다. 만약 클라이언트가 직접 Client RPC를 호출하면 무효화된다.
1️⃣ 클라이언트가 피격 당하면, Server RPC인 Server_TakeDamage()를 호출 (클라이언트 → 서버)
2️⃣ 서버에서 Validate() 검증을 거쳐 Server_TakeDamage_Implementation()을 실행
3️⃣ 이때 서버에서 Health 값을 감소하고 Client_PlayHitEffect()를 호출
4️⃣ 피격된 클라이언트에서 Client_PlayHitEffect_Implementation()이 자동으로 실행되어 이펙트 표시
5️⃣ 클라이언트에서 UI 업데이트 및 상태 반영
📍Multicast RPC(모든 클라이언트에서 실행되는 RPC)
서버가 모든 클라이언트에서 동일한 효과를 실행할 때 사용된다.
예를 들어 플레이어가 클라이언트에서 박격포 스킬을 사용한다고 했을 때의 과정으로 살펴보자.
// 헤더 파일 (MyCharacter.h)
public:
UFUNCTION(Server, Reliable, WithValidation)
void Server_UseMortar(FVector TargetLocation); // 박격포 스킬 사용 (클라이언트 → 서버 요청)
void Server_UseMortar_Implementation(FVector TargetLocation);
bool Server_UseMortar_Validate(FVector TargetLocation);
UFUNCTION(NetMulticast, Reliable)
void Multicast_Explode(FVector ExplosionLocation); // 모든 클라이언트에서 폭발 이펙트 실행 (Multicast RPC)
void Multicast_Explode_Implementation(FVector ExplosionLocation);
};
// CPP 파일 (MyCharacter.cpp)
// 서버에서 박격포 스킬 실행 (Server RPC)
void AMyCharacter::Server_UseMortar_Implementation(FVector TargetLocation)
{
// 박격포가 목표 지점에 도착 후 폭발 (모든 클라이언트에 이펙트 전송)
Multicast_Explode(TargetLocation);
// 서버에서 해당 위치에 있는 플레이어들에게 데미지 적용
float DamageRadius = 300.0f;
TArray<AActor*> HitActors;
UGameplayStatics::ApplyRadialDamage(GetWorld(), 50.0f, TargetLocation, DamageRadius, nullptr, HitActors, this, GetController(), true);
}
bool AMyCharacter::Server_UseMortar_Validate(FVector TargetLocation)
{
// 예: 맵의 특정 범위 내에서만 스킬 사용 허용
return TargetLocation.Z > 0 && TargetLocation.Z < 1000;
}
// 모든 클라이언트에서 폭발 이펙트 실행 (Multicast RPC)
void AMyCharacter::Multicast_Explode_Implementation(FVector ExplosionLocation)
{
// 파티클 이펙트, 사운드 실행 (모든 플레이어가 보게 됨)
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionEffect, ExplosionLocation);
}
1️⃣ 플레이어가 박격포 스킬을 사용 → Server_UseMortar(TargetLocation) 실행 (클라이언트 → 서버)
2️⃣ 서버에서 검증을 거쳐 Server_UseMortar_Implementation() 실행 → Multicast_Explode(TargetLocation) 호출 (서버 → 모든 클라이언트)
3️⃣ 모든 클라이언트에서 Multicast_Explode_Implementation() 실행 → 폭발 이펙트 발생
4️⃣ 서버에서 ApplyRadialDamage()를 사용해 박격포 데미지를 적용
✅ RepNotify
서버에서 변수 값을 변경하면, 클라이언트에서도 해당 값이 자동으로 동기화되고 추가적으로 값이 변경될 때 특정 함수를 실행한다.
일반적인 Replicated는 단순히 값만 동기화되지만, ReplicatedUsing을 사용하면 값 변경 시 동기화 + 자동으로 실행되는 함수를 실행한다. 그래서 RepNotify는 값 변경 시 실행될 함수가 필수적으로 존재해야한다.
// 헤더 파일 (MyCharacter.h)
public:
AMyCharacter();
protected:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
public:
UPROPERTY(ReplicatedUsing=OnRep_Health)
float Health; // 체력 변수 (RepNotify 적용)
UFUNCTION(Server, Reliable, WithValidation)
void Server_TakeDamage(float Damage); // 서버에서 체력 감소 처리 (Server RPC)
void Server_TakeDamage_Implementation(float Damage);
bool Server_TakeDamage_Validate(float Damage);
protected:
// 체력이 변경될 때 실행되는 함수 (RepNotify)
UFUNCTION()
void OnRep_Health();
// CPP 파일 (MyCharacter.cpp)
AMyCharacter::AMyCharacter()
{
bReplicates = true; // Actor Replication
Health = 100.0f; // 기본 체력
}
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, Health); // Health 변수를 복제 대상으로 설정
}
void AMyCharacter::Server_TakeDamage_Implementation(float Damage)
{
if (Damage <= 0) return;
Health -= Damage;
if (Health <= 0)
{
Die();
}
}
bool AMyCharacter::Server_TakeDamage_Validate(float Damage)
{
return (Damage > 0 && Damage < 100); // 데미지는 0보다 크고 100 미만이어야 함
}
void AMyCharacter::OnRep_Health() // 체력 값이 변경될 때 UI 업데이트 (RepNotify)
{
// UI 업데이트 (클라이언트에서만 실행)
UpdateHealthUI();
}
1️⃣ 서버에서 Health 값을 변경: Server_TakeDamage() 실행 → Health 감소
2️⃣ 서버에서 변경된 Health 값이 자동으로 클라이언트와 동기화, ReplicatedUsing을 설정했기 때문에 클라이언트도 동기화
3️⃣ 클라이언트에서 OnRep_Health()가 자동 실행, UI를 업데이트하거나 피격 애니메이션을 실행 가능
📌 언리얼의 객체 간 통신
1️⃣ 서버 - 클라이언트 간 통신 (네트워크 RPC)
앞서 서술했듯, 서버와 클라이언트 간 통신을 할 때는 RPCs(Remote Procedure Calls)를 사용한다.
블루프린트에서도 마찬가지로 커스텀 이벤트에 Replications 속성을 부여할 수 있다.


BP_Controller에 "OnLoginWithID"라는 커스텀 이벤트가 있다고 하면, Replicates 속성을 설정하여 RPC로 만들 수 있다.
✅Server RPC == Run On Server: 클라이언트에서 실행 요청을 보내고, 서버에서 실행
✅Client RPC == Run on owning Client: 서버에서 실행 요청을 보내고, 특정 클라이언트에서 실행
✅Mulicast RPC == Multicast: 서버에서 실행 요청을 보내고, 모든 클라이언트에서 실행
2️⃣ 객체 간 로컬 통신 (이벤트 디스패처)
이벤트 디스패처는 네트워크를 통해 호출되지 않기 때문에 로컬(같은 프로세스 내)에서만 동작한다.
이 때문에 서버 - 클라이언트 간에는 이벤트 디스패처를 사용할 수 없기에 RPC를 사용해 통신을 하는 것이고, 객체 간의 로컬 통신에서는 객체 간 직접적인 참조를 피할 수 있어 이벤트 디스패처를 사용하면 느슨한 결합이 가능해진다.
특히 UI, 애니메이션, 액터 간 이벤트 전달 등에 많이 활용된다.
❌이벤트 디스패처를 사용하지 않는다면?

이벤트 디스패처를 사용하지 않고, 객체 A에서 객체 B~C의 이벤트를 호출하고자 한다면 B와 C를 형 변환한 뒤 각각의 객체의 이벤트를 호출해야한다.
이 경우에 형 변환을 해야하고, 해당 객체의 구조를 알아야 하는 문제가 생긴다. 또한 객체 A에서 B와 C 뿐만아니라 아주 많은 객체들의 이벤트를 호출해야된다고 하면 그만큼 많은 캐스팅을 시도해야만 한다.
더군다나 캐스팅에 실패해 Null을 참조 했을 때 크래쉬가 발생하는 등, 강한 결합으로 인한 문제들이 발생하게 된다.
✔️ 이벤트 디스패처 사용

A에게 등록된 이벤트 디스패처를 B와 C가 구독하고 있다가, A의 이벤트 디스패처가 호출됐을 때, B와 C에게 바인딩된 이벤트를 자동 호출하므로 빠른 처리가 가능하고, 객체 간 직접적인 참조를 피할 수 있다.
🔎서버와 통신하는 주체는 누구인가?
RPC 자체는 서버와 클라이언트가 통신할 수 있게 해주는 역할을 한다.
그렇다면 실질적으로 누가 서버와 통신을 하는 주체가 되는 것일까?
바로 PlayerController가 RPC를 사용하여 서버와 통신하는 핵심적인 역할을 한다. 아래 그림을 보면서 자세하게 살펴보자.



네트워크 구조를 보면, Game Mode는 서버에만 존재하고, HUD와 UMG는 클라이언트에서만 존재한다. 이말은 서버에서는 HUD와 UMG의 존재를 모르고, 클라이언트에서는 Game Mode의 존재를 모른다는 얘기다.
하지만 Player Controller는 서버와 클라이언트 모두에 존재하는 공유된 객체이다. 따라서 서버가 클라이언트에게 데이터를 보내거나 클라이언트에서 서버에게 데이트를 보낼 때, 연결하는 통로 역할을 수행할 수 있다.
클라이언트에서 서버에 메세지를 보낸다고 했을 때, Pawn이나 Character에서 직접 서버 RPC를 호출하는 방식으로도 소통할 수 있지만, 서버에서 어떤 클라이언트에게서 온 메세지인지 정확하게 대상을 파악할 수 있도록 Player Controller를 거쳐서 데이터를 서버에 전달해야 한다.
그렇다면 왜 "어떤 클라이언트가 요청했는지" 서버에서 확인하는 것이 중요할까?
멀티 플레이에서는 여러 클라이언트가 동시에 접속하므로, 요청한 클라이언트를 정확히 식별해야한다.
예를 들어 GameState에서 점수를 관리한다고 했을 때, Player Character에서 GameState->UpdateScore(10)을 호출하게 되면 어떤 플레이어의 점수를 올려야하는지 알 수 없다.
Player Character는 게임 도중 사망으로 인해 교체되는 상황에서 기존 점수 데이터가 사라질 위험성이 있다. Player Controller는 게임 내내 유지되므로 데이터가 안정적으로 관리될 수 있어 신뢰성이 높다.
따라서 HUD나 UMG에서 서버 로직에 있는 Game Mode, Game State 등에 접근하려면 반드시 Player Controller를 거쳐야 하며, 반대의 경우도 마찬가지이다.
🔹 예시
플레이어가 코인을 획득해 최고 점수 기록하는 게임이라고 가정한다면,
Player Controller가 Score를 관리하고, 서버에 업데이트 요청
1️⃣ 코인 아이템에서 PlayerController를 찾아 점수를 서버에 요청
void ACoinItem::OnOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
if (APawn* Pawn = Cast<APawn>(OtherActor))
{
if (APlayerController* PC = Cast<APlayerController>(Pawn->GetController()))
{
AMyPlayerController* MyPC = Cast<AMyPlayerController>(PC);
if (MyPC)
{
MyPC->Server_RequestScoreUpdate(10); // 서버에 점수 업데이트 요청
Destroy(); // 코인 아이템 삭제
}
}
}
}
2️⃣ PlayerController에서 서버에 점수 업데이트 요청(Server RPC)
// PlayerController.h
UCLASS()
class AMyPlayerController : public APlayerController
{
GENERATED_BODY()
public:
// 서버(PlayerState)에서 Score 변경에 따른 UI업데이트 요청
UFUNCTION(Client, Reliable)
void UpdateHUDScore(int32 NewScore);
// 클라이언트에서 서버(PlayerState)에 Score 업데이트 요청
UFUNCTION(Server, Reliable, WithValidation)
void Server_RequestScoreUpdate(int32 ScoreAmount);
void Server_RequestScoreUpdate_Implementation(int32 ScoreAmount);
void Server_RequestScoreUpdate_Validate(int32 ScoreAmount);
};
// PlayerController.cpp
void AMyPlayerController::Server_RequestScoreUpdate_Implementation(int32 ScoreAmount)
{
if (HasAuthority()) // 유효성 검사
{
AMyPlayerState* MyPlayerState = GetPlayerState<AMyPlayerState>();
if (MyPlayerState)
{
MyPlayerState->AddScore(ScoreAmount); // PlayerState에 점수 업데이트
}
}
}
bool AMyPlayerController::Server_RequestScoreUpdate_Validate(int32 ScoreAmount)
{
return ScoreAmount > 0; // 잘못된 값 방지
}
- 서버에서만 PlayerState를 수정할 수 있음
- 각 클라이언트마다 개별적인 PlayerState가 존재해 자신만의 점수를 관리하며, 다른 플레이어의 데이터에는 영향을 주지 않음
3️⃣ PlayerState에서 점수 업데이트 및 RepNotify를 이용해 UI 업데이트 요청
// PlayerState.h
UCLASS()
class AMyPlayerState : public APlayerState
{
GENERATED_BODY()
public:
UPROPERTY(ReplicatedUsing = OnRep_Score, VisibleAnywhere, BlueprintReadOnly)
int32 Score;
UFUNCTION()
void OnRep_Score(); // 점수 변경 시 호출됨
void AddScore(int32 Amount);
};
// PlayerState.cpp
void AMyPlayerState::AddScore(int32 Amount)
{
if (HasAuthority()) // 서버에서 실행됨
{
Score += Amount;
}
}
void AMyPlayerState::OnRep_Score()
{
// 본인 클라이언트에서만 UI 업데이트 수행
if (GetOwner() && GetOwner()->IsLocallyControlled()) // 내 PlayerState일 때만 UI 업데이트
{
APlayerController* PC = Cast<APlayerController>(GetOwner());
if (PC)
{
AMyPlayerController* MyPC = Cast<AMyPlayerController>(PC);
if (MyPC)
{
MyPC->UpdateHUDScore(Score); // UI 업데이트 요청
}
}
}
}
- 서버에서 Score를 변경하면 OnRep_Scroe() 자동으로 호출되며 UI 업데이트 요청
- 클라이언트에서도 OnRep_Score()가 호출되므로, 점수가 자동으로 동기화
- UI 업데이트는 PlayerController에서 처리하도록 UpdateHUDScore() 호출
🔎GetOwner( ) -> IsLocallyControlled( )
현재 객체(AActor 혹은 Controller)가 내 클라이언트가 직접 컨트롤하는 객체인지 확인하는 함수이다.
내 클라이언트에서 내가 컨트롤하는 객체라면 true를 반환
서버에서는 false를 반환(서버는 특정 클라이언트를 직접 조작하지 않음)
다른 클라이언트에서 복제된 객체라면 false(예: 다른 플레이어의 PlayerState)
PlayerState는 개별 플레이어의 상태를 저장하지만, 서버는 모든 플레이어의 PlayerState를 소유하고 관리한다.
A, B, C 플레이어가 있다고 가정하면 서버에는 아래와 같이 PlayerState가 존재한다.
서버:
- A_PlayerState (Score: 0)
- B_PlayerState (Score: 0)
- C_PlayerState (Score: 0)
플레이어 A가 점수를 획득해서 AddScore(10)을 실행하면, 서버에서 플레이어 A의 PlayerState->Score를 통해 Score가 10점 증가한다.
이때 변경된 데이터는 A의 PlayerState가 복제된 모든 클라이언트에게 전파되며, B와 C 클라이언트에서도 A의 PlayerState->Score가 변경되며 OnRep_Score( )가 실행될 수 있다.
서버:
- A_PlayerState (Score: 10) - [변경됨]
- B_PlayerState (Score: 0)
- C_PlayerState (Score: 0)
클라이언트 A:
- A_PlayerState (Score: 10) - [본인, 변경됨] → `OnRep_Score()` 실행
- B_PlayerState (Score: 0)
- C_PlayerState (Score: 0)
클라이언트 B:
- A_PlayerState (Score: 10) - [복제된 A의 데이터] → `OnRep_Score()` 실행
- B_PlayerState (Score: 0) - [본인]
- C_PlayerState (Score: 0)
클라이언트 C:
- A_PlayerState (Score: 10) - [복제된 A의 데이터] → `OnRep_Score()` 실행
- B_PlayerState (Score: 0)
- C_PlayerState (Score: 0) - [본인]
B와 C 클라이언트에서 A의 OnRep_Score( )가 실행되며, A의 PlayerState에 저장된 Score 값을 동기화 시키는데,
B와 C의 UI는 A의 점수를 HUD에 표시할 필요가 없으므로, IsLocallyControlled( ) 체크를 하여 자신의 클라이언트에서 실행되는 PlayerController 인지 확인하여, 내 것일 때만 UI업데이트를 요청하도록 할 수 있다.
4️⃣ PlayerController에서 UI 업데이트
// PlayerController.h
UCLASS()
class AMyPlayerController : public APlayerController
{
GENERATED_BODY()
public:
UFUNCTION(Client, Reliable)
void UpdateHUDScore(int32 NewScore);
};
// PlayerController.cpp
void AMyPlayerController::UpdateHUDScore_Implementation(int32 NewScore)
{
if (APlayerHUD* HUD = Cast<APlayerHUD>(GetHUD()))
{
HUD->SetScore(NewScore); // HUD에서 점수 업데이트
}
}
🔔 정리
플레이어가 코인을 먹으면 PlayerController가 서버에 요청해 PlayerState의 Score를 업데이트한다.
Score 값이 변경되면 RepNotify로 OnRep_Score( ) 함수가 호출되고 본인 클라이언트에 있는 PlayerController에게 UI를 업데이트 하도록 요청한다.
📌 언리얼의 서버
언리얼 엔진에서 기본적으로 제공하는 네트워크 서버 유형은 Listen Server와 Dedicated Server 두 가지가 있다.
그러나 현업에서는 언리얼의 기본 네트워크 서버 기능을 그대로 활용하기보다는, 서버 구조를 직접 설계하거나 별도의 네트워크 솔루션을 도입하는 경우가 많다.
따라서 이 두 가지 서버 모델은 서버의 개념을 이해하는 용도로 학습하는 것이 좋을 것 같다.
✅ Listen Server (클라이언트 호스팅 서버)
클라이언트가 직접 서버 역할을 수행하는 방식
🔹 특징
- 한 플레이어가 서버 역할을 수행하고, 다른 플레이어들이 해당 서버로 접속
- 별도 서버가 필요 없으며, 서버를 따로 운영하지 않아도 플레이어끼리 게임을 할 수 있음
- 클라이언트와 서버 기능이 하나의 프로세스에서 동작
🔹 사용 사례
- LAN 환경에서의 멀티플레이
- 소규모 협동 게임
- 스팀 기반 멀티플레이 게임(스팀의 세션 브라우징 기능과 함께 사용)
🔹 한계점
- 한 플레이어가 서버 역할을 하면서 동시에 클라이언트 기능도 수행하는 구조, 따라서 네트워크 관련 코드를 작성할 때 현재 실행 중인 코드가 서버에서 실행되는 것인지 클라이언트에서 실행되는 것인지 구분하기 어려워 개발 및 테스트가 어려움
- 클라이언트가 서버 역할을 하므로 보안 취약, 권한 체크 필수적
- 호스트의 네트워크 상태에 따라 게임 품질에 영향을 끼침
- 서버 호스트가 게임을 종료하면 서버도 종료됨
✅ Dedicated Server (전용 서버)
클라이언트 기능 없이 오직 서버 역할만 수행하는 독립적인 서버
🔹 특징
- 서버 전용으로 동작하여 성능 최적화, 개발 시에 Listen Server에 비해 덜 복잡함
- 별도의 빌드 과정이 필요
- 대부분 Visual Studio로 별도로 빌드하지만 언리얼 엔진의 빌드 시스템으로도 가능함
- 클라이언트 기능이 포함되지 않아 보안성이 높고, 독립 서버를 갖춘 게임 환경에서 좋은 선택
🔹 사용 사례
- FPS, MOBA, 배틀로얄 등 대전게임 ( 16 vs 16, 32 vs 32 등 고정된 플레이어 수의 멀티 게임)
🔹 한계점
- 언리얼 엔진의 Dedicated Server 자체는 데이터 베이스 기능을 포함하지 않아 신뢰성이 떨어짐
- 대규모 MMORPG 같은 게임에서는 별도의 데이터베이스 서버 및 로직 서버가 필요
- MMORPG는 많게는 수천 명 이상의 플레이어가 동시 접속하는 구조여서, 한 대의 Dedicated Server만으로는 감당할 수 없음
- 로비 서버, 월드 서버, 인스턴스 서버 등 여러 개의 서버를 조합한 분산 아케틱처가 필요함
'내배캠 > Unreal Engine' 카테고리의 다른 글
언리얼 엔진 패키징(데디케이티드 서버 빌드) (0) | 2025.03.21 |
---|---|
채팅으로 하는 숫자 야구 게임 구현(Listen Server) (0) | 2025.03.19 |
온라인 게임과 네트워크 구성 (0) | 2025.03.11 |
네트워크 개념 (0) | 2025.03.10 |
동적 멀티캐스트 델리게이트 (Dynamic Multicast Delegate) (0) | 2025.02.11 |