다양한 기록

[ChronoSpace] ** Multiplay With GAS Setting (and Widget) ** 본문

언리얼 엔진/ChronoSpace

[ChronoSpace] ** Multiplay With GAS Setting (and Widget) **

라구넹 2025. 1. 30. 09:35

GAS나 Multiplay 같이 있는 예시가 그렇게 많진 않아 세팅하기 좀 어렵다

Lyra는 코드 자체가 좀 너무 많아 참고하려면 시간이 많이 걸릴 것이다

그래서 여기저기 뒤져보면서 코드 정리해서 만들어놨다

 

1. 기본 설정 (일반 멀티플레이 설정이랑 같고, GAS랑 관계 없이 기본 설정)

bReplicates = true 및 컴포넌트들에 리플리케이션 설정

이것만 해놔도 어느 정도는 리플리케이션이 된다

(ex. 스태틱 메시 컴포넌트 리플리케이트 설정 안하면 클라이언트에서 안보임)

 

2. Player Ability System 설정

// player cpp
void ACSCharacterPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	//UE_LOG(LogCS, Log, TEXT("[NetMode: %d] PossessedBy"), GetWorld()->GetNetMode());
	SetASC();
	SetGASAbilities();
}

PossessedBy는 서버에서만 실행됨 → 서버 ASC 설정

GiveAbility는 서버에서만 가능함, 서버 ASC 설정 이후에 진행

GiveAbility를 클라이언트에서 하다가 오류 났다

 

void ACSCharacterPlayer::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();
	// UE_LOG(LogCS, Log, TEXT("*** [NetMode : %d] OnRep_PlayerState, %s, %s"), GetWorld()->GetNetMode(), *GetName(), *GetPlayerState()->GetName());

	SetASC();
	EnergyBar->ActivateGAS();
	// UE_LOG(LogCS, Log, TEXT("*** [NetMode : %d] OnRep_PlayerState, %s, %s"), GetWorld()->GetNetMode(), *GetName(), *GetPlayerState()->GetName());
}

클라이언트의 플레이어도 ASC를 가지긴 해야 함 (어트리뷰트 리플리케이션과 위젯 때문)

정확히는, ASC가 리플리케이션 되는데 그걸 가져와야 함

 

이 경우 OnRep_PlayerState에서 플레이어 스테이트가 리플리케이션 되면 어빌리티 시스템 설정을 해주고, 그 다음에 위젯의 필요 설정들을 해주어야 함 (서버의 위젯 설정은 BeginPlay에서 해줘야 함)

 

3. Widget 설정

// 위젯 cpp

#include "UI/CSGASUserWidget.h"
#include "AbilitySystemBlueprintLibrary.h"

void UCSGASUserWidget::SetOwner(AActor* InOwner)
{
	if (IsValid(InOwner))
	{
		Owner = InOwner;
	}
}

void UCSGASUserWidget::SetAbilitySystemComponent(AActor* InOwner)
{
	if (IsValid(InOwner))
	{
		ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(InOwner);
	}
}

UAbilitySystemComponent* UCSGASUserWidget::GetAbilitySystemComponent() const
{
	return ASC;
}

서버와 클라이언트 플레이어의 ASC 설정 시점이 많이 다름

SwtOwner와 SetAbilitySystemComponent는 분리되어야 함

 

// 위젯 컴포넌트 cpp

void UCSGASWidgetComponent::InitWidget()
{
	Super::InitWidget();
	UE_LOG(LogCS, Log, TEXT("InitWidget IS Called"));
	UCSGASUserWidget* GASUserWidget = Cast<UCSGASUserWidget>(GetWidget());
	if (GASUserWidget)
	{
		GASUserWidget->SetOwner(GetOwner());
	}
	else
	{
		UE_LOG(LogCS, Log, TEXT("InitWidget Failed - No GASUserWidget"));
	}
}

void UCSGASWidgetComponent::ActivateGAS()
{
	UCSGASUserWidget* GASUserWidget = Cast<UCSGASUserWidget>(GetWidget());
	if (GASUserWidget)
	{
		GASUserWidget->SetAbilitySystemComponent(GetOwner());
	}
	else
	{
		UE_LOG(LogCS, Log, TEXT("ActivateGAS Failed - No GASUserWidget"));
	}
}

InitWidget의 발동 시점

리슨 서버: PossessedBy → InitWidget → BeginPlay

클라이언트: InitWidget → BeginPlay → PossessedBy

 

InitWIdget에서 어빌리티 컴포넌트를 설정하게 되면 서버나 클라이언트에서 위젯 ASC 연결 문제가 생김

위에서 함수 분리해 뒀던 이유가 이것

 

하위 위젯 컴포넌트는 내버려둬도 되는데, 이유는 GAS의 어트리뷰트가 Effect로 변경되면 알아서 리플리케이션 되기 때문

 

void ACSCharacterPlayer::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	//UE_LOG(LogCS, Log, TEXT("[NetMode: %d] PossessedBy"), GetWorld()->GetNetMode());
	SetASC();
	SetGASAbilities();
}

void ACSCharacterPlayer::BeginPlay()
{
	Super::BeginPlay();
	UE_LOG(LogCS, Log, TEXT("[NetMode: %d] BeginPlay"), GetWorld()->GetNetMode());

	if ( HasAuthority() )
	{
		EnergyBar->ActivateGAS();
	}
	
	if (!IsLocallyControlled())
	{
		//EnergyBar->SetVisibility(false);
		return;
	}

	// 이 밑으론 로컬 컨트롤러만 진입 가능
	APlayerController* PlayerController = CastChecked<APlayerController>(GetController()); 
	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer())) 
	{
		Subsystem->AddMappingContext(MappingContext, 0);
	}

	//UE_LOG(LogCS, Log, TEXT("[NetMode: %d] BeginPlay"), GetWorld()->GetNetMode());
}

ActivateGAS의 실행 시점

서버: PossessedBy

클라이언트: BeginPlay

 

4. 어빌리티 설정

- 어빌리티 발동 RPC 및 설정

클라이언트는 입력을 받기만 하고, 실제 실행은 서버에서 되어야 함

근데 클라이언트도 GiveAbility만 안되는 거지 ActivateAbility 되는데, 이에 문제가 있음

UCSGA_WhiteHall::UCSGA_WhiteHall()
{
	NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly;
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

NetExecutionPolicy를 서버 온리로 설정해주지 않으면 서버와 클라이언트 둘 다 실행되버린다

그러다보니, 액터를 스폰하는 어빌리티의 경우 액터가 총 두 개 생성된다

저걸 해줘야 서버에서만 실행된다

 

// h
// Input Pressed RPC **************************************
void GASInputPressed(int32 InputId);

UFUNCTION(Server, Reliable)
void ServerGASInputPressed(int32 InputId);

void HandleGASInputPressed(int32 InputId);
// ********************************************************

// cpp
void ACSCharacterPlayer::GASInputPressed(int32 InputId)
{
	
	if ( HasAuthority() )
	{
		//UE_LOG(LogCS, Log, TEXT("[NetMode : %d], GASInputPressed"), GetWorld()->GetNetMode());
		HandleGASInputPressed(InputId);
	}
	else
	{
		//UE_LOG(LogCS, Log, TEXT("[NetMode : %d], GASInputPressed"), GetWorld()->GetNetMode());
		ServerGASInputPressed(InputId);
	}
}

void ACSCharacterPlayer::ServerGASInputPressed_Implementation(int32 InputId)
{
	//UE_LOG(LogCS, Log, TEXT("[NetMode : %d], ServerGASInputPressed_Implementation"), GetWorld()->GetNetMode());
	if ( HasAuthority() )
	{
		HandleGASInputPressed(InputId);
	}
}

void ACSCharacterPlayer::HandleGASInputPressed(int32 InputId)
{
	if ( !ASC )
	{
		return;
	}

	FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
	if (Spec)
	{
		if (Spec->InputPressed) return;
		UE_LOG(LogCS, Log, TEXT("[NetMode : %d], HandleGASInputPressed"), GetWorld()->GetNetMode());
		Spec->InputPressed = true;
		if (Spec->IsActive())
		{
			ASC->AbilitySpecInputPressed(*Spec);
		}
		else
		{
			ASC->TryActivateAbility(Spec->Handle);
		}
	}
}

서버는 그냥 실행하고 클라이언트는 서버로 RPC를 날리는 형태의 코드

클라이언트는 딱 입력만 받는다

(사실 클라이언트에서 그냥 어빌리티 Activate하는 식으로 구현해도 되긴 하는데,

가능하면 클라이언트는 입력 받기 + 렌더링만 하도록)

 

- 타겟 액터 RPC

타겟 액터에서 RPC를 써줘야 하는 경우가 있다

메테리얼 채도 조정은 RPC를 써줘야 적용된다

 

void UCSAT_WeakenGravityBox::SpawnAndInitializeTargetActor()
{
	SpawnedTargetActor = Cast<ACSTA_WeakenGravityBox>(GetWorld()->SpawnActorDeferred<ACSTA_WeakenGravityBox>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
	if (SpawnedTargetActor)
	{
		SpawnedTargetActor->SetOwner(GetOwnerActor());
		SpawnedTargetActor->OnComplete.AddDynamic(this, &UCSAT_WeakenGravityBox::OnTargetActorReadyCallback);
		SpawnedTargetActor->SetGravityCoef(GravityCoef);
	}
}

타겟 액터는 기본적으로 오너가 없어서 RPC가 제대로 안써진다

어빌리티 태스크에서 SetOwner 해줘야 한다

 

// h
UFUNCTION(NetMulticast, Reliable)
void NetMulticastSaturationSetting(float InGravityCoef);

void SaturationSetting();
void HandleSaturationSetting(float InGravityCoef);

// cpp
void ACSTA_WeakenGravityBox::NetMulticastSaturationSetting_Implementation(float InGravityCoef)
{
    HandleSaturationSetting(InGravityCoef);
}

void ACSTA_WeakenGravityBox::SaturationSetting()
{
    if ( HasAuthority() )
    {
        NetMulticastSaturationSetting(GravityCoef);
    }
}

void ACSTA_WeakenGravityBox::HandleSaturationSetting(float InGravityCoef)
{
    UE_LOG(LogCS, Log, TEXT("[NetMode : %d] HandleSaturationSetting, %f"), GetWorld()->GetNetMode(), GravityCoef);
    UMaterialInstanceDynamic* DynMaterial = Cast<UMaterialInstanceDynamic>(StaticMeshComp->GetMaterial(0));
    FLinearColor OrgColor;
    DynMaterial->GetVectorParameterValue(FName(TEXT("Color")), OrgColor);

    // r:h g:s b:v a:a
    FLinearColor HSVColor = OrgColor.LinearRGBToHSV();
    HSVColor.G *= InGravityCoef;

    DynMaterial->SetVectorParameterValue(FName(TEXT("Color")), HSVColor.HSVToLinearRGB());
}

NetMulticast 쓰면 클라이언트 전부에 날아간다

 

NetMulticast 쓰면 리슨 서버랑 클라이언트 전부에 날아간다

즉, 서버에서 저 함수를 사용해도 서버 본인에서도 실행된다

 

단, GravityCoef는 에디터에서 변경한 값인데, 클라이언트는 에디터에서 변경한 값을 읽지 못하는데 의도된 사항인지 버그인지는 모르겠고 RPC 인자에 추가해줘서 해결함


결과

 

 

https://forums.unrealengine.com/t/widget-component-set-to-screen-space-does-not-render-for-listen-server-but-renders-for-clients/2156399/6

 

Widget Component set to screen space does not render for Listen Server but renders for Clients.

doesn’t work for me. the widget component in the child actor is invisible when playing

forums.unrealengine.com

서버에서 위젯이 둘 다 안보이는지 이유

언리얼 5.5.1 버그였다

나랑 똑같은 문제를 겪은 사람들이 많았다