1. 호스트의 권한
게임 시작 버튼을 Host만 누를 수 있게 하고 싶어서 아래와 같은 로직을 생각하게 되었다.
- 처음에 시작 버튼을 Hidden 한 상태로 시작한다.
- 해당 UI를 가지고 있는 owning actor를 찾아서
- HasAuthority() 함수를 통해 서버인지 확인하고 서버라면, 버튼이 Visible 한 상태로 바꿔주는 작업을 해준다.
- 또한, 버튼이 눌러지지 않도록 disable 상태로 만들어 준다.
다음으로는 게임모드가 가진 상태에 따라서 UI의 실행 상태를 정해주는 로직을 구성했다.
- 리슨 서버이기 때문에 HasAuthority 체크 후 UGameplayStatics::GetGameMode를 통해서 게임모드를 가져온 후
- 해당 변수르 Tick에서 체크하여 true인 경우, 버튼을 활성화 시켜준다.
궁금증
- UI와 게임 모드와의 연결은 어떤 방식으로 하는 것이 좋은가
- 게임 모드와 UI의 직접적인 연동은 좋지 않은 방식?
//헤더 파일
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "StartGameButton.generated.h"
class UButton;
UCLASS()
class FOODRUMBLE_API UStartGameButton : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
UFUNCTION()
void OnButtonClicked();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidget))
TObjectPtr<UButton> StartGameButton;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<AActor> OwningActor;
};
//cpp 파일
void UStartGameButton::NativeConstruct()
{
Super::NativeConstruct();
if (IsValid(StartGameButton))
{
StartGameButton->SetVisibility(ESlateVisibility::Hidden);
OwningActor = GetOwningPlayer();
if (IsValid(OwningActor) && OwningActor->HasAuthority())
{
StartGameButton->SetVisibility(ESlateVisibility::Visible);
StartGameButton->SetIsEnabled(false);
StartGameButton->OnClicked.AddDynamic(this, &ThisClass::OnButtonClicked);
}
}
}
void UStartGameButton::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
if (IsValid(OwningActor) && OwningActor->HasAuthority())
{
AGameModeBase* GM = UGameplayStatics::GetGameMode(GetWorld());
if (IsValid(GM))
{
ANewGM* NewGM = Cast<ANewGM>(GM);
if (IsValid(NewGM))
{
if (NewGM->GetIsReady())
{
StartGameButton->SetIsEnabled(true);
}
}
}
}
}
void UStartGameButton::OnButtonClicked()
{
AGameModeBase* GM = UGameplayStatics::GetGameMode(GetWorld());
if (IsValid(GM))
{
ANewGM* NewGM = Cast<ANewGM>(GM);
if (IsValid(NewGM))
{
RemoveFromParent();
NewGM->CanStartGame();
}
}
}
- 왼쪽 호스트의 화면에서만 start 버튼이 보인다.

2. 플레이어 넘버 띄우기
각 플레이어에게 들어온 순서에 맞게 번호를 띄워주는 위젯을 통해 각 플레이어의 번호를 알 수 있도록 하는 것이 목표이다.
플레이어가 들어오는 것을 확인하는 것은 게임 모드의 PostLogin 함수 사용
void ANewGM::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
ANewPlayerController* NewPC = Cast<ANewPlayerController>(NewPlayer);
if (IsValid(NewPC))
{
TotalPlayerControllers.Add(NewPC);
ANewPlayerState* NewPS = NewPC->GetPlayerState<ANewPlayerState>();
if (IsValid(NewPS))
{
NewPS->PlayerIndex = TotalPlayerControllers.Num();
}
}
}
각 플레이어의 넘버는 "PlayerState"에 저장된 PlayerIndex 사용
- 해당 변수는 리플리케이티드 된 변수이다.
PlayerNumberText라는 위젯 클래스를 만들어서, 캐릭터가 해당 위젯 클래스를 가지고 있도록 구현
//character.h
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI")
TObjectPtr<UWidgetComponent> PlayerNumberTextWidget;
//character.cpp 생성자
PlayerNumberTextWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("PlayerNumberText"));
PlayerNumberTextWidget->SetupAttachment(GetMesh());
PlayerNumberTextWidget->SetWidgetSpace(EWidgetSpace::World);
- Tick 함수를 돌면서 항상 해당 UI가 정면을 보도록 구현
void ANewCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (IsValid(PlayerNumberTextWidget))
{
FVector WidgetComponentLocation = PlayerNumberTextWidget->GetComponentLocation();
FVector LocalPlayerCameraLocation = UGameplayStatics::GetPlayerCameraManager(this, 0)->GetCameraLocation();
PlayerNumberTextWidget->SetWorldRotation(UKismetMathLibrary::FindLookAtRotation(WidgetComponentLocation, LocalPlayerCameraLocation));
}
}
- MulticastRPC를 통해 Index를 매개변수로 받아 Widget을 업데이트 하는 로직을 수행한다
void ANewCharacter::MulticastRPCUpdateWidget_Implementation(int32 InIndex)
{
UPlayerNumberText* PNT = Cast<UPlayerNumberText>(PlayerNumberTextWidget->GetWidget());
if (IsValid(PNT))
{
PNT->SetPlayerNumber(InIndex);
}
}
게임 모드(서버) 에서는 모든 캐릭터들이 게임 시작 준비 완료가 된 경우
character의 멀티캐스트 함수를 실행하고, 매개변수로는 PlayerState의 PlayerIndex를 가져와서 넣어준다.
void ANewGM::OnMainTimerElapsed()
{
ANewGS* NewGS = GetGameState<ANewGS>();
if (!IsValid(NewGS))
{
return;
}
switch (NewGS->MatchState)
{
case EMatchState::None:
break;
case EMatchState::Waiting:
{
...
else if(bCanStartGame)
{
RemainWaitingTimeForPlaying--;
for (auto PC : TotalPlayerControllers)
{
ANewPlayerState* NewPS = PC->GetPlayerState<ANewPlayerState>();
ANewCharacter* NewPC = Cast<ANewCharacter>(PC->GetPawn());
if (IsValid(NewPS) && IsValid(NewPC))
{
NewPC->MulticastRPCUpdateWidget(NewPS->GetPlayerIndex());
}
}
}
}
}
}
기억할 점 (이슈)
- 처음에는 OnRep 함수에서 바로 Multicast RPC를 통해 값을 업데이트 하려고 했지만, 각 로컬 플레이어에게만 적용되는 문제가 있었다
- 문제의 원인은 OnRep 함수는 클라이언트에서만 돌아가는 함수라는 점과
- MulticastRPC를 클라이언트에서 사용하면 로컬에서만 실행된다는 점이다. (서버에서 실행하면, 서버 및 모든 클라이언트에서 실행)
- OnRep 함수가 서버에서 값이 변경되어 리플리케이션 될 때 호출되는 함수이기 때문에 클라이언트(로컬)에서 실행되는 함수이다.
- 그렇기 때문에, OnRep 함수는 각 클라이언트의 화면에서만 보이는 UI를 업데이트 할 때 쓰기에 적합하다는 생각이 들었다.
- 지금 구현하고자 하는 것은 모든 플레이어의 번호를 모든 클라이언트들이 공유해야 하기 때문에 적합하지 않았다.
플레이어 넘버가 동기화 되어 보이는 모습

3. 움직임 동기화
기본적으로 언리얼 엔진에서는 movement replication이라는 옵션이 존재하여, 움직임에 대한 동기화를 해준다.
추가적으로 애님 몽타주를 통해 방어 자세를 하여 공격을 막는 로직을 구현하는 로직을 정리해 볼 것이다.
애님 몽타주 사용
- 헤더 파일에서는 입력이 시작된 순간에 대한 함수와 입력이 끝난 순간 실행될 함수를 각각 serverRPC와 MulticastRPC로 만들어준다.
- 추가로 몽타주를 실행하는 함수를 구현해준다. (PlayGuardMontage)
#pragma region Guard
public:
UFUNCTION(Server, Reliable)
void ServerRPCGuard();
UFUNCTION(Server, Reliable)
void ServerRPCGuardEnd();
UFUNCTION(NetMulticast, Reliable)
void MulticastRPCGuard();
UFUNCTION(NetMulticast, Reliable)
void MulticastRPCGuardEnd();
UFUNCTION()
void OnRep_IsInvincible();
void PlayGuardMontage();
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UAnimMontage> GuardMontage;
UPROPERTY(ReplicatedUsing = OnRep_IsInvincible)
bool bIsInvincible;
#pragma endregion
- 입력 바인딩 & 변수 리플리케이트
void ANewCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
...
EIC->BindAction(GuardAction, ETriggerEvent::Triggered, this, &ThisClass::HandleGuardInputStart);
EIC->BindAction(GuardAction, ETriggerEvent::Completed, this, &ThisClass::HandleGuardInputEnd);
}
void ANewCharacter::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
...
DOREPLIFETIME(ThisClass, bIsInvincible);
}
- RPC 함수
- 기본 로직이 서버를 거친 후, 중요한 변수는 서버에서 처리를 하도록 구현하였다.
- bIsInvincible은 방어상태일 때 캐릭터가 공격을 받지 않는 처리를 해야하므로, serverRPC에서 값을 바꿔주었고,
- MulticastRPC를 통해 애니메이션 몽타주를 실행하였다.
void ANewCharacter::ServerRPCGuard_Implementation()
{
bIsInvincible = true;
MulticastRPCGuard();
}
void ANewCharacter::ServerRPCGuardEnd_Implementation()
{
bIsInvincible = false;
MulticastRPCGuardEnd();
}
void ANewCharacter::MulticastRPCGuard_Implementation()
{
PlayGuardMontage();
}
void ANewCharacter::MulticastRPCGuardEnd_Implementation()
{
StopGuardMontage();
}
- 추가적으로 방어를 했을 때 캐릭터의 움직임을 막기 위해 움직임 input 바인딩 함수에 아래와 같은 처리를 해 주었다.
void ANewCharacter::HandleMoveInput(const FInputActionValue& InValue)
{
...
if (bIsInvincible)
{
return;
}
...
}
- 실행 모습
4. Respawn 시스템
특정 액터에 트리거박스를 통해 캐릭터가 떨어지면, 다시 원점이 되는 위치에 캐릭터를 생성시켜주는 액터 구현하기
- OnOverlap 함수를 바인딩하여 캐릭터가 해당 액터의 콜리젼에 닿는 순간을 체크한다.
- 이후 로직은 게임내에서 중요한 로직이기 때문에 server에서 처리할 수 있도록, HasAuthority로 서버 체크를 해준다.
AKillZoneActor::AKillZoneActor()
{
PrimaryActorTick.bCanEverTick = false;
SceneComp = CreateDefaultSubobject<USceneComponent>(TEXT("Scene Component"));
RootComponent = SceneComp;
CollisionComp = CreateDefaultSubobject<UBoxComponent>(TEXT("Box Collision"));
CollisionComp->SetupAttachment(RootComponent);
CollisionComp->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Static Mesh"));
StaticMeshComp->SetupAttachment(CollisionComp);
StaticMeshComp->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
CollisionComp->OnComponentBeginOverlap.AddDynamic(this, &AKillZoneActor::OnOverlap);
}
void AKillZoneActor::OnOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (IsValid(OtherActor))
{
ANewCharacter* NewCharacter = Cast<ANewCharacter>(OtherActor);
if (IsValid(NewCharacter) && HasAuthority())
{
NewCharacter->OnDeath();
//UE_LOG(LogTemp, Warning, TEXT("Player Death"));
}
}
}
- OnDeath 함수의 경우 추가적인 서버에서의 처리 (예를 들어, 게임 모드가 가지고 있는 죽은 플레이어 리스트에 해당 캐릭터 컨트롤러를 넣기 등) 를 위하여, 플레이어 컨트롤러의 OnCharacterDeath 함수를 call 한다.
- 현재 기능상으로는 추가적인 기능은 없고, 캐릭터를 맵 원점에 리스폰 해줘야 하므로, 멀티캐스트를 통해 진행해 준다.
void ANewCharacter::OnDeath()
{
ANewPlayerController* NewPC = GetController<ANewPlayerController>();
if (IsValid(NewPC) && HasAuthority())
{
NewPC->OnCharacterDead(); //현재는 비어있는 함수
MulticastRPCRespawnCharacter();
}
}
void ANewCharacter::MulticastRPCRespawnCharacter_Implementation()
{
SetActorLocation(FVector(0.f, 0.f, 20.f));
}
궁금증
- 이런식의 로직이 잘짜여진 로직인지 궁금
- 죽는 것에 대한 처리를 캐릭터의 authority 체크로 한 것
- multicast를 통해 respawn 시킨 것
'Unreal Engine' 카테고리의 다른 글
| Unreal Engine - 멀티플레이 네트워크 최적화 2 (0) | 2025.04.23 |
|---|---|
| Unreal Engine - 멀티플레이 네트워크 최적화 1 (0) | 2025.04.22 |
| Unreal Engine - 멀티캐스트 델리게이트 (0) | 2025.04.13 |
| Unreal Engine - 데디케이티드 서버 7 (동기화 2) (0) | 2025.04.12 |
| Unreal Engine - 데디케이티드 서버 6 (동기화) (0) | 2025.04.11 |