Unreal Engine
Unreal Engine - 데디케이티드 서버 4 (RPC)
gbleem
2025. 4. 7. 00:04
1. RPC 개념
https://gbleem.tistory.com/140#6.%20RPC-1-5
Unreal Engine - 데디케이티드 서버 개념 및 실습
1. 서버의 종류P2P각 컴퓨터가 서버랑 클라이언트를 모두 수행하는 방식리슨 서버HOST 역할을 하는 서버용 컴퓨터가 존재 (클라이언트 역할도 수행)GUEST 역할을 하는 클라이언트용 컴퓨터가 존재
gbleem.tistory.com
자세한 내용은 위의 글 참고
(리마인드)
RPC는 함수를 호출하는 PC와 해당 함수의 로직이 실행되는 PC를 다르게 하기 위해서 사용하는 통신 기법
RPC의 용도
- 액터의 기능에는 큰 영향을 미치지 않는 일시적인 효과에 주로 사용된다.
- 게임 이벤트나 사운드, 파티클 재생
- 중요한 로직은 프로퍼티 리플리케이션 써야한다.
RPC에서 중요한 것 중 하나가 "Own"
- 넷 커넥션에 의해 소유되고 있는지에 대한 내용
- 이것을 확인하기 위한 언리얼 엔진의 함수들
- AController::IsLocalController()
- APawn::IsLocallyControlled()
- 주의) 생성자에서 이 함수를 호출하지 말기
- 그 이유는 폰의 컨트롤러가 생성되어 있다는 것을 보장할 수 없기 때문이다.
2. RPC 실습 1 (server RPC)
2 - 1. 추가된 기능
- Spawn이 가능한 Mine 액터를 추가 (DXLandMine)
- F 키를 바인딩해서 스폰하도록 구현
- 바인딩한 함수
- 주요한 부분은 SetOwner를 사용해서 Owner를 지정한 부분
//character.h
#pragma region LandMine
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<AActor> LandMineClass;
#pragma endregion
//character.cpp
void ADXPlayerCharacter::HandleLandMineInput(const FInputActionValue& InValue)
{
if (IsValid(LandMineClass))
{
FVector SpawnedLocation = (GetActorLocation() + GetActorForwardVector() * 300.f) - FVector(0.f, 0.f, 90.f);
ADXLandMine* SpawnedLandMine = GetWorld()->SpawnActor<ADXLandMine>(LandMineClass, SpawnedLocation, FRotator::ZeroRotator);
SpawnedLandMine->SetOwner(GetController());
}
}
현재까지는 아래 사진처럼 스폰한 쪽에서만 액터가 보인다
- 그 이유는 키 입력은 로컬에서만 이루어져있기 때문에, 바딩딩된 함수는 서버에서 호출되지 않는다.
if (!HasAuthority() && IsLocallyControlled())
{
APlayerController* PC = Cast<APlayerController>(GetController());
UEnhancedInputLocalPlayerSubsystem* EILPS = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer());
EILPS->AddMappingContext(InputMappingContext, 0);
}
- 해결하기 위해서 ServerRPC를 사용할 것이다.
2 - 2. 문제 해결 (using ServerRPC)
//character.h
UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCSpawnLandMine();
//character.cpp
void ADXPlayerCharacter::HandleLandMineInput(const FInputActionValue& InValue)
{
if (!HasAuthority() && IsLocallyControlled())
{
ServerRPCSpawnLandMine();
}
}
void ADXPlayerCharacter::ServerRPCSpawnLandMine_Implementation()
{
if (IsValid(LandMineClass))
{
FVector SpawnedLocation = (GetActorLocation() + GetActorForwardVector() * 300.f) - FVector(0.f, 0.f, 90.f);
ADXLandMine* SpawnedLandMine = GetWorld()->SpawnActor<ADXLandMine>(LandMineClass, SpawnedLocation, FRotator::ZeroRotator);
}
}
bool ADXPlayerCharacter::ServerRPCSpawnLandMine_Validate()
{
return true;
}
그러나 이렇게 구현한 경우 클라이언트에서 보이지 않게 된다.
- 그 이유는 Mine Actor를 리플리케이트 설정을 하지 않았기 때문이다.
- replicates 속성을 true로 지정해주어야 한다.
ADXLandMine::ADXLandMine()
{
...
bReplicates = true;
}
이제 아래와 같이 두 클라이언트에게 보이는 것을 확인할 수 있다.
2 - 3. 정리
흐름 정리
- F키를 누르면
- 해당 키를 누른 클라이언트에서 바인딩된 함수 실행
- 이때, 서버로 보내는 패킷에 server RPC 함수의 정보도 보냄
- 서버에서는 패킷을 받은 후 받아온 server RPC 함수를 수행
- 함수 로직 수행
- 서버에서 mine 액터 스폰
- mine 액터는 replicated = true 이므로, 클라이언트에게도 스폰된 정보를 복제
주의점
- BeginPlay(), Tick(), EndPlay() 함수를 사용할 때 항상 주의해야 한다.
- 위의 함수들은 멀티플레이 환경에서 여러 번 호출될 수 있다.
- 디버깅을 위해 UKismetSystemLibrary::PrintString() 함수를 사용해야 한다.
코드로 알아보기
- 캐릭터의 serverRPC 함수에서 SetOwner()를 호출
- SetOwner에 GetController를 넣는 것이 아니라 this(Character)를 넣은 이유는 controller의 경우 상대(다른 클라이언트)의 여부를 알 수 없기 때문이다.
- 그렇기 때문에 this를 넣어서 구별을 할 수 있도록 하였다. (Controller는 서버랑 소유하는 클라이언트에만 존재하기 때문)
void ADXPlayerCharacter::ServerRPCSpawnLandMine_Implementation()
{
if (IsValid(LandMineClass))
{
FVector SpawnedLocation = (GetActorLocation() + GetActorForwardVector() * 300.f) - FVector(0.f, 0.f, 90.f);
ADXLandMine* SpawnedLandMine = GetWorld()->SpawnActor<ADXLandMine>(LandMineClass, SpawnedLocation, FRotator::ZeroRotator);
SpawnedLandMine->SetOwner(this);
}
}
- Mine에서 owner를 구별하여 출력하는 코드
void ADXLandMine::BeginPlay()
{
Super::BeginPlay();
if (HasAuthority())
{
UKismetSystemLibrary::PrintString(this, FString::Printf(TEXT("Run on server")), true, true, FLinearColor::Green, 5.f);
}
else
{
APawn* OwnerPawn = Cast<APawn>(GetOwner());
if (IsValid(OwnerPawn))
{
if (OwnerPawn->IsLocallyControlled())
{
UKismetSystemLibrary::PrintString(this, FString::Printf(TEXT("Run on owning client")), true, true, FLinearColor::Green, 5.f);
}
else
{
UKismetSystemLibrary::PrintString(this, FString::Printf(TEXT("Run on other client")), true, true, FLinearColor::Green, 5.f);
}
}
}
}
실행 결과
3. RPC 실습 2 (Client RPC, Multicast)
3 - 1. overview
실습 1에서는 "서버 RPC"를 통해 클라이언트에서 호출된 함수가 서버에서 실행되도록 구성
"클라이언트 RPC" 는 그 반대로 서버에서 호출된 함수가 클라이언트에서 실행되도록 구성하는것이다.
- 예를 들어, 캐릭터가 죽었을 때 UI를 띄울 때 사용한다.
"NetMulticast" 의 경우 서버에서 호출하고 모든 클라이언트에서 수행되도록 한다.
- 멀티캐스트의 경우 서버에서만 함수를 호출할 수 있다.
이번 구현에서는 Mine 액터가 폭발한 후 이펙트를 구현할 것이다.
- 이 부분을 서버에서 구현하게되는 이유는 폭발 후 데미지 처리 로직등은 게임에서 매우 중요한 부분이기 때문에 서버에서 구현해주어야 하기 때문이다.
- 그래서 서버에서 호출한 후, 모든 나머지 클라이언트에서 실행되도록 한다.
3 - 2. 코드
//mine.h
private:
UFUNCTION()
void OnLandMineBeginOverlap(AActor* OverlappedActor, AActor* OtherActor);
UFUNCTION(NetMulticast, Unreliable)
void MulticastRPCSpawnEffect();
private:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (AllowPrivateAccess))
TObjectPtr<UParticleSystemComponent> Particle;
bool bIsExploded;
//mine.cpp
ADXLandMine::ADXLandMine()
{
...
Particle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Particle"));
Particle->SetupAttachment(SceneRoot);
Particle->SetAutoActivate(false);
}
void ADXLandMine::BeginPlay()
{
...
if (!OnActorBeginOverlap.IsAlreadyBound(this, &ThisClass::OnLandMineBeginOverlap))
{
OnActorBeginOverlap.AddDynamic(this, &ThisClass::OnLandMineBeginOverlap);
}
}
void ADXLandMine::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
...
if (!OnActorBeginOverlap.IsAlreadyBound(this, &ThisClass::OnLandMineBeginOverlap))
{
OnActorBeginOverlap.RemoveDynamic(this, &ThisClass::OnLandMineBeginOverlap);
}
}
void ADXLandMine::OnLandMineBeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
//if server
if (HasAuthority())
{
MulticastRPCSpawnEffect();
}
}
void ADXLandMine::MulticastRPCSpawnEffect_Implementation()
{
if (HasAuthority())
{
return;
}
//already exploded
if (bIsExploded)
{
return;
}
Particle->Activate(true);
bIsExploded = true;
}
실행 모습
- 각 플레이어가 스폰한 액터에 충돌이 발생하면 Owner를 체크하여 출력해준다.
- 파티클의 경우 Multicast를 통해 모든 클라이언트에게 보이도록 해준다.