본문 바로가기

UE5 에러 대처

언리얼 SkeletalMesh 리플리케이트 불가능

개요

MovementComponent에서 WalkSpeed는 리플리케이션 대상이 아니다

그렇습니다. 혹시 달리기 등을 통해 MovementComponent의 WalkSpeed를 변경하는 분들은 다른 방식을 사용해야 합니다.

GameMode는 서버에만 있다

그렇습니다. 아무리 클라이언트가 발광을 해도 얻을 수 없는 그것, 서버의 게임모드. 이 상태에서 만일 게임 모드에서 ServerRPC를 부르는 건 쓸모없는 행위가 되겠죠.

USkeletalMeshComponent에서 SkeletalMesh는 리플리케이트 되지 않는다

// SkeletalMeshComponent.h
private:
	/** The skeletal mesh used by this component. */
	UE_DEPRECATED(5.1, "This property isn't deprecated, but getter and setter must be used at all times to preserve correct operations.")
	UPROPERTY(EditAnywhere, Transient, BlueprintSetter = SetSkeletalMeshAsset, BlueprintGetter = GetSkeletalMeshAsset, Category = Mesh)
	TObjectPtr<class USkeletalMesh> SkeletalMeshAsset;

게임에서 캐릭터 등의 제작을 위해서 USkeletalMeshComponent를 사용하게 되는데 여기서 SkeletalMesh를 변경하는 기능이 있을 때 USkeletalMeshComponent 자체를 리플리케이션 하는 경우가 있습니다. 이 경우 컴포넌트에서 SKeletalMesh는 리플리케이션 대상이 아니기 때문에 메쉬가 변경되지 않습니다.

 

이를 위해서는 SkeletalMeshComponent를 리플리케이션 하기보다는 SkeletalMesh 포인터를 자체적으로 두고 이를 리플리케이션 해 변경될 때 SetSkeletalMesh를 해주는 게 좋습니다.

// Weapon.h
UPROPERTY(ReplicatedUsing = OnRep_WeaponSkeletal)
TObjectPtr<USkeletalMesh> WeaponSkeletal;
UPROPERTY(EditAnywhere, Category = "Weapon Properties")
TObjectPtr<USkeletalMeshComponent> WeaponMesh;
void AWeapon::SetItemPropertyFromDataAsset(const UItemDataAsset* DataAsset)
{
	//... 데이터 에셋에서 무기의 데이터를 가져오는 중 ...
	WeaponSkeletal = WeaponDataAsset->WeaponSkeletalMesh;
    	// 리슨 서버를 위해 OnRep 함수를 바로 불러줍니다.
	OnRep_WeaponSkeletal();
}

void AWeapon::OnRep_WeaponSkeletal()
{
	// 노티파이 리플리케이트에서 SetSkeletalMesh를 합니다.
	WeaponMesh->SetSkeletalMesh(WeaponSkeletal);
}

 

 

AIController는 서버에 올라가 있다

BT를 통해서 AI를 만든 적이 있고 이걸 EnemyController에서 관리하고 있었습니다. 멀티플레이화를 위해서 적 캐릭터가 공격 몽타주를 재생할 필요가 있었는데 'BT Task > EnemyController > EnemyCharacter의 멀티캐스트'의 방식대로 호출하고 있었습니다.

 

 

여기서 '서버 소유 액터, 멀티캐스트' 조합이 되니 모든 클라이언트에서 몽타주 재생을 볼 수 없었습니다.

 

그래서 해결한 방법이 RPC 대신 리플리케이션을 사용하는 것입니다. 리플리케이션의 소유는 서버에 있으니 서버에서 값을 변경하면 각 클라이언트들이 받아 이를 활용하는 것이죠.

void AEnemy::TriggerAttackToTarget()
{
	// 리플리케이션 되는 변수는 트리거 형식으로 사용
	bAttackTrigger = !bAttackTrigger;
	OnRep_AttackToTarget();
}

void AEnemy::OnRep_AttackToTarget()
{
	// 이후 리플리케이트 노티파이에서 몽타주를 재생합니다.
    	// 이렇게 하면 리플리케이트를 받는 순간 모든 클라이언트들이 각자의 몽타주를 재생하게 됩니다.
	PlayNormalAttackMontage();
}

추가적으로 클라이언트에서 이 AIController에 접근하려 하면 nullptr을 반환하게 됩니다.

 

 

이후 진입 유저들에게도 필요한 정보를 준다면 RPC 보다는 리플리케이트

리플리케이트는 이후 유저들에게 정보를 보여줄 수 있습니다. 만일 게임의 아이템이 있고 서버에서 이 아이템에 대한 정보를 입력했다고 합시다. 입력 작업을 RPC로 하게 된다면 그 시점에 있는 클라이언트들은 그 정보를 받을 수 있겠지만 이후 들어오는 클라이언트는 지금까지의 RPC를 받을 수 없습니다. 그런 게 없겠죠. 

 

그래서 이후 유저들에게 변경된 정보를 보여주고 싶다면 리플리케이션을 생각해보는게 좋습니다.

 

 

'가상 함수 RPC'를 원한다면 '가상함수를 호출하는 RPC를 만들어보자

파생 클래스별로 RPC 콜을 할 시 다른 일을 하고 싶다면 RPC 함수 자체를 가상 함수로 만들고 싶은 경우가 있습니다. 그래서 만든 다음 Super::RPC를 호출하게 되면 무한루프에 빠지게 됩니다.

  • 파생 클래스의 RPC 호출
  • 파생 클래스의 RPC_Implementation 호출
  • 파생 클래스의 RPC_Implementation에서 Super::RPC 호출
  • virtual 이니깐 파생 클래스의 RPC_Implementation 호출
  • 파생 클래스는 여전히 Super::RPC 호출중
  • ???

이런 식으로 동작하기 때문에 만일 파생 클래스에서 재정의를 했다면 Super의 _Implementation 버전을 호출해줘야 합니다.

 

개인적으로 이런 제약을 강제로 프로그래머들에게 넣기는 힘들어 보이며 이런 멀티플레이 코드와 게임 로직적인 코드가 나뉘어야 된다고 생각해 가상함수를 호출하는 RPC를 만드는 것을 선호하게 되었습니다. 

void AFACharacter::OverrideableLogic()
{}

void AFACharacter::ServerRPC()
{
	OverrideableLogic();
}

이런 식으로 RPC에서는 가상 함수를 호출하기만 하고 파생 클래스들은 가상함수를 재정의하기만 하면 되는 것이죠.

 

이런 방법은 RPC 뿐 아니라 리플리케이션 콜백 함수에서도 동일하게 적용할 수 있습니다.

 

 

RPC 함수를 인터페이스화 하고 싶다면 인터페이스 함수에서 RPC 함수를 부르자

플레이어 컨트롤러가 소유권이 있다 보니 이에 접근해서 함수들을 사용하는 일이 많이 있습니다. 그런데 전부 컨트롤러의 구체 클래스를 접근하면 의존도가 높아져 컴파일 시간이 점점 길어지는 문제가 생기게 될 것입니다.

 

그래서 이런 의존도를 없애기 위해서 인터페이스에 비가상 RPC를 정의하려고 하면 아래 오류가 나오게 될 것입니다.

Interface functions that are not BlueprintImplementableEvents must be declared 'virtual' [UnrealHeaderTool Error]

그렇다고 가상 RPC를 만들자니 뭔가 잘 동작하지도 않고 이상하기도 하고... 경험의 부족일 수는 있겠으나 여러 문제들이 있어 인터페이스에 재정의 가능한 RPC를 만드는 건 포기했습니다.

 

이후 생각해 낸 방법이 인터페이스 함수를 재정의하는 곳에서 RPC를 호출하자입니다. 

class PROJECTFA_API IItemRPCableController
{
	GENERATED_BODY()

public:
	virtual void OpenLootingBox(ALootingBox* LootingBox){}
	virtual void DropItem(APickupItem* Item){}
};

이렇게 인터페이스를 만든 다음 이후 이를 재정의하는 플레이어 컨트롤러에서는

void APlayableController::OpenLootingBox(ALootingBox* LootingBox)
{
	ServerOpenLootingBox(LootingBox);
}

void APlayableController::DropItem(APickupItem* Item)
{
	ServerDropItem(Item);
}

이런 식으로 RPC들을 부르기만 하는 것이죠. 이후 이를 사용하는 함수에서는 아래처럼 인터페이스에 접근하면 되는 것입니다.

const auto OwnerPawn = Cast<APawn>(GetOwner());
if(OwnerPawn == nullptr)	return;
if(const auto OwnerController = OwnerPawn->GetController<IItemRPCableController>())
{
    OwnerController->DropItem(this);
}

 

 

인터페이스 포인터는 RPC로 넘길 수 없다. 대신 UObject를 사용하게 하자.

만일 함수를 아래처럼 만들었다고 가정해 봅시다.

UFUNCTION(Server, Reliable)
void ServerFunc(IInterfaceClass* Interface);

이렇게 한 경우 제대로 컴파일이 되지 않을 것입니다. 기억상으로 gen 파일에서 문제가 많이 나온 것으로 알고 있습니다. 

 

일단 RPC에 태울 수 있는 포인터의 경우 네트워크 ID 가 있어야 합니다. I 계열의 인터페이스 클래스의 포인터는 이런 부분에 대해 알 리가 없죠.

 

그래서 이 포인터를 넘기고 싶다면 아래처럼 함수를 작성해야 합니다.

void APlayableController::ServerUseItem_Implementation(UObject* Item)
{
	const auto UsableItem = Cast<IInventoryUsable>(Item);
	if(UsableItem == nullptr)	return;
	UsableItem->UseAction();
}

이런 식으로 UObject의 포인터를 받아와서 캐스팅을 한 후 사용하는 게 좋습니다. 그리고 혹시나 TScriptInterface<> 를 사용한다고 해도 위 인터페이스와 동일한 오류가 나올 테니, UObject의 포인터를 넘기는 게 일단 개발에 마음이 편할 겁니다.

출처: https://husk321.tistory.com/429 [껍데기방:티스토리]