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를 통해 모든 클라이언트에게 보이도록 해준다.