Unreal Multiplayer ActorComponent 애매모호성. +LockonSystemComponent
전에 포스팅한 글과 같이 ActorComponent라고 생각하면 당연히 각 클라이언트들의 스폰 폰(Spawn Pawn)이 생성 될테고 해당 폰은 각 AutoAuthority, Proxy의 역할로 움직이는 역할에 그러면 ActorComonent도 사용하기 위해서는 Replicated를 사용하여 공유를 하고 변환을 받는다는 얘기 일거다. 하지만 각자에게 부여된 컴포넌트와는 다르게 값이 공유되지만 값은 공유되지 않는 동시성을 가지고 있다.
그게 잘 보여주는게 LockonSystem을 만들었을때이다.
예로 만약 아래와 같이 코드를 적용했다고 하면. (완전한 코드가 아니기 때문에 아래에 있는대로 변경하기 바람.)
그냥 Replicated를 이용해 코드를 짰을때.
.h
class SOULNETWORKPROJECT_API ULockonSystemComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
ULockonSystemComponent();
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
public:
UFUNCTION(BlueprintCallable)
void CheckEnemy();
UFUNCTION(BlueprintCallable)
void SwitchLockOn(AActor* HitActor);
protected:
UFUNCTION(BlueprintGetter)
bool GetLockOn() { return bLockOn; }
private:
UFUNCTION()
void UpdateLockOnMode(float InDeltaTime);
UFUNCTION(Client, Reliable, WithValidation)
void Client_UpdateLockOnMode(float InDeltaTime);
UFUNCTION(Client, Reliable, WithValidation)
void Client_SetValue(class ACharacterBase* Target);
UFUNCTION(Server, Reliable, WithValidation)
void Server_SetValue(class ACharacterBase* Target);
UFUNCTION()
void AddIgnoreActors(AActor* InActors) { IgnoreActors.Add(InActors); }
UFUNCTION(NetMulticast, Reliable, WithValidation)
void Multicast_FalseControllerYaw();
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Info")
float CheckLength = 10000.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Info")
float InterpSpeed = 5.f;
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated)
bool bLockOn = false;
private:
UPROPERTY(Replicated)
class ACharacterBase* LockChr;
UPROPERTY(EditAnywhere)
float MaxDistance = 500.f;
UPROPERTY(VisibleAnywhere)
float MaxBlindTime = 5.f;
UPROPERTY(VisibleAnywhere)
float CurrentBlineTime = 0.f;
TArray<AActor*> IgnoreActors;
class APlayerCharacter* Owner;
};
.cpp
ULockonSystemComponent::ULockonSystemComponent()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = true;
// ...
SetIsReplicated(true);
}
// Called when the game starts
void ULockonSystemComponent::BeginPlay()
{
Super::BeginPlay();
// ...
Owner = Cast<APlayerCharacter>(GetOwner());
if(!Owner) return;
IgnoreActors.Add(Owner);
//SwitchLockOn(nullptr);
}
// Called every frame
void ULockonSystemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// ...
if (GetOwnerRole() == ROLE_Authority)
{
UpdateLockOnMode(DeltaTime);
}
else
{
Client_UpdateLockOnMode(DeltaTime);
}
}
void ULockonSystemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ULockonSystemComponent, bLockOn);
DOREPLIFETIME(ULockonSystemComponent, LockChr);
}
void ULockonSystemComponent::CheckEnemy()
{
UE_LOG(LogTemp, Warning, TEXT("Call Lockon"));
if (!bLockOn)
{
FVector Start = Owner->GetActorLocation();
FVector End = Start + Owner->GetCamera()->GetForwardVector() * CheckLength;
FHitResult OutHit;
bool bHit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), Start, End, 30.f, TraceTypeQuery3, false, IgnoreActors, EDrawDebugTrace::ForOneFrame, OutHit, false);
if (bHit)
{
SwitchLockOn(OutHit.GetActor());
}
else
{
SwitchLockOn(nullptr);
}
}
else
{
SwitchLockOn(nullptr);
}
}
void ULockonSystemComponent::SwitchLockOn(AActor* HitActor)
{
/*
*/
if (HitActor)
{
if (ACharacterBase* Chr = Cast<ACharacterBase>(HitActor))
{
//UE_LOG(LogTemp, Warning, TEXT("%d"), (int8)Chr->GetFaction());
if (Owner->GetFaction() != Chr->GetFaction())
{
Client_SetValue(Chr);
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(false);
LockChr->LockOnWidget->SetHiddenInGame(false);
}
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
Client_SetValue(nullptr);
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
Client_SetValue(nullptr);
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
Client_SetValue(nullptr);
}
}
void ULockonSystemComponent::Client_SetValue_Implementation(ACharacterBase* Target)
{
Server_SetValue(Target);
}
bool ULockonSystemComponent::Client_SetValue_Validate(ACharacterBase* Target)
{
return true;
}
void ULockonSystemComponent::Server_SetValue_Implementation(ACharacterBase* Target)
{
if (!Target)
{
bLockOn = false;
//LockChr = nullptr;
Multicast_FalseControllerYaw();
}
else
{
bLockOn = true;
LockChr = Target;
}
}
bool ULockonSystemComponent::Server_SetValue_Validate(ACharacterBase* Target)
{
return true;
}
void ULockonSystemComponent::UpdateLockOnMode(float InDeltaTime)
{
if(!bLockOn || !LockChr)
{
return;
}
else if (bLockOn)
{
FVector StartLoc = Owner->GetActorLocation();
FVector TargetLoc = FVector(LockChr->GetActorLocation().X, LockChr->GetActorLocation().Y, LockChr->GetActorLocation().Z - 100.f);
FRotator TargetRot = UKismetMathLibrary::FindLookAtRotation(StartLoc, TargetLoc);
FRotator ConRot = Owner->GetController()->GetControlRotation();
FRotator InterpRot = UKismetMathLibrary::RInterpTo(ConRot, TargetRot, InDeltaTime, InterpSpeed);
FRotator NewRot = FRotator(InterpRot.Pitch, InterpRot.Yaw, ConRot.Roll);
if (!Owner->GetHasCrowdControl())
{
Owner->GetController()->SetControlRotation(NewRot);
Owner->bUseControllerRotationYaw = Owner->GetCharacterMovement()->MaxWalkSpeed > Owner->GetWalkSpeed() + 1.f ? false : true;
}
FVector Start = Owner->GetActorLocation();
FVector End = LockChr->GetActorLocation();
FHitResult OutHit;
bool bHit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), Start, End, 30.f, TraceTypeQuery1, false, IgnoreActors, EDrawDebugTrace::ForOneFrame, OutHit, false);
if (bHit)
{
if (ACharacterBase* Chr = Cast<ACharacterBase>(OutHit.GetActor()))
{
if (Owner->IsHostile(Chr)) CurrentBlineTime = 0.f;
}
else
{
CurrentBlineTime += InDeltaTime;
if (CurrentBlineTime >= MaxBlindTime)
{
SwitchLockOn(nullptr);
return;
}
}
}
}
}
void ULockonSystemComponent::Multicast_FalseControllerYaw_Implementation()
{
Owner->bUseControllerRotationYaw = false;
}
bool ULockonSystemComponent::Multicast_FalseControllerYaw_Validate()
{
return true;
}
void ULockonSystemComponent::Client_UpdateLockOnMode_Implementation(float InDeltaTime)
{
if (!bLockOn || !LockChr)
{
return;
}
else if (bLockOn)
{
FVector StartLoc = Owner->GetActorLocation();
FVector TargetLoc = FVector(LockChr->GetActorLocation().X, LockChr->GetActorLocation().Y, LockChr->GetActorLocation().Z - 100.f);
FRotator TargetRot = UKismetMathLibrary::FindLookAtRotation(StartLoc, TargetLoc);
FRotator ConRot = Owner->GetController()->GetControlRotation();
FRotator InterpRot = UKismetMathLibrary::RInterpTo(ConRot, TargetRot, InDeltaTime, InterpSpeed);
FRotator NewRot = FRotator(InterpRot.Pitch, InterpRot.Yaw, ConRot.Roll);
if (!Owner->GetHasCrowdControl())
{
Owner->GetController()->SetControlRotation(InterpRot);
Owner->bUseControllerRotationYaw = Owner->GetCharacterMovement()->MaxWalkSpeed > Owner->GetSprintSpeed() - 1.f ? false : true;
}
FVector Start = Owner->GetActorLocation();
FVector End = LockChr->GetActorLocation();
FHitResult OutHit;
bool bHit = UKismetSystemLibrary::SphereTraceSingle(GetWorld(), Start, End, 30.f, TraceTypeQuery1, false, IgnoreActors, EDrawDebugTrace::ForOneFrame, OutHit, false);
if (bHit)
{
if (ACharacterBase* Chr = Cast<ACharacterBase>(OutHit.GetActor()))
{
if (Owner->IsHostile(Chr)) CurrentBlineTime = 0.f;
}
else
{
CurrentBlineTime += InDeltaTime;
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Yellow, FString::Printf(TEXT("CurrentBlindTime : %1f"), CurrentBlineTime));
if (CurrentBlineTime >= MaxBlindTime)
{
SwitchLockOn(nullptr);
return;
}
}
}
}
}
bool ULockonSystemComponent::Client_UpdateLockOnMode_Validate(float InDeltaTime)
{
return true;
}
코드를 간략하게 설명하면 당연히 캐릭터가 해당 폰으로 회전하기 위해서는 Tick함수를 통해서 계속 업데이트를 시켜주면서 회전을 시켜야 하지만 일반 함수로는 적용이 되지 않기 때문에 RemoteRole를 통해서Autority 일경우에는 일반 함수를 실행하고 그게 아닐경우에는 Client를 적용하여 적용되는지를 각 클라이언트에서 움직이게 알려준다.
그리고 나머지는 Replicated를 시키고 그 값들이 Server에서 변경해야 되는걸 알수 있을거다.
하지만, 여기서 문제점은 값이 공유가 되는데 그 값이 완전히 공유를 못한다는거다. 뭔 소리인가 하면 값이 Replicated되어 공유를 하지만 값이 변경됨에 따라 값이 당연히 스위칭에 맞게 변경되어야하지만 락온을 해제하고 다시 읽어드림에 있어 클라이언트에서는 값 지정이 안된다. 정확한 원인은 모르겠으나 어림짐작으로는 다른 클라이언트에도 값이 전달이 안되는 오류 같은거 같다. (정확히 말하면 락온을 적용하고 해제하고에 있어 해당 함수들이 연동이 되며 값이 전달되지만 그 과정에서 오류가 생기는듯하다.)
그래서 사용한것이 DOREPLIFETIME_CONDITION 매크로이다.
조건식 프로퍼티 리플리케이션
조건에 따라 액터 프로퍼티를 리플리케이트하는 법에 대한 상세 정보입니다.
docs.unrealengine.com
해당 매크로는 Replicated된 변수들을 어떻게 처리한걸지 조건을 걸고 실행하는거다. 그래서 주로 사용할거는 COND_OwnerOnly라는 조건 매크로인데 이 매크로는 오직 Actor오너에게만 값을 받아 다른 오너들에게 값전달이 안되게 막는거다. (그러면 값을 공유하는 의미가 없는거 아니냐는 말도 있을수도 있지만 해당 회전을 하기위해서는 각자의 클라이언트에서 실행을 해야하고 Client내에서 값을 알기 위해서는 Server측에 전달을 해서 변경을 해야 값이 변경되기 때문에 아래의 코드 두개로 Replicated와 그렇지 않았을때 차이를 올려놨습니다.)
//Replicated를 안 했을 경우.//
void ULockonSystemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//DOREPLIFETIME_CONDITION(ULockonSystemComponent, bLockOn, COND_OwnerOnly);
//DOREPLIFETIME_CONDITION(ULockonSystemComponent, LockChr, COND_OwnerOnly);
}
void ULockonSystemComponent::SwitchLockOn(AActor* HitActor)
{
/*
*/
if (HitActor)
{
if (ACharacterBase* Chr = Cast<ACharacterBase>(HitActor))
{
//UE_LOG(LogTemp, Warning, TEXT("%d"), (int8)Chr->GetFaction());
if (Owner->GetFaction() != Chr->GetFaction())
{
//Client_SetValue(Chr);
bLockOn = true;
LockChr = Chr;
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(false);
LockChr->LockOnWidget->SetHiddenInGame(false);
}
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
bLockOn = false;
LockChr = nullptr;
//Client_SetValue(nullptr);
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
bLockOn = false;
LockChr = nullptr;
//Client_SetValue(nullptr);
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
bLockOn = false;
LockChr = nullptr;
//Client_SetValue(nullptr);
}
}
//Replicated를 적용할 경우.//
void ULockonSystemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION(ULockonSystemComponent, bLockOn, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(ULockonSystemComponent, LockChr, COND_OwnerOnly);
}
void ULockonSystemComponent::SwitchLockOn(AActor* HitActor)
{
/*
*/
if (HitActor)
{
if (ACharacterBase* Chr = Cast<ACharacterBase>(HitActor))
{
//UE_LOG(LogTemp, Warning, TEXT("%d"), (int8)Chr->GetFaction());
if (Owner->GetFaction() != Chr->GetFaction())
{
Client_SetValue(Chr);
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(false);
LockChr->LockOnWidget->SetHiddenInGame(false);
}
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
Client_SetValue(nullptr);
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
Client_SetValue(nullptr);
}
}
else
{
if (GetOwnerRole() == ROLE_Authority)
{
if (LockChr)
{
LockChr->HealthWidget->SetHiddenInGame(true);
LockChr->LockOnWidget->SetHiddenInGame(true);
}
}
Client_SetValue(nullptr);
}
}
(Widget이 뜨는것은 ReplicatedUsing을 이용하여 값이 변경될때마다 위젯이 나오나 안나오게 설정하면 조건에 따라 Owner에게만 적용된다.)