Unreal Hub/Unreal Lab
[SeSAC, Unreal Network] 2025. 04. 07
likepint
2025. 4. 7. 11:01
Listen Server (리슨서버) Connection 이해
UCLASS(config=Game)
class ASeSAC_NetworkCharacter : public ACharacter
{
GENERATED_BODY()
public:
virtual void Tick(float DeltaSeconds) override;
void PrintNetLog();
};
void ASeSAC_NetworkCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
PrintNetLog();
}
void ASeSAC_NetworkCharacter::PrintNetLog()
{
const FString validConnection = GetNetConnection() != nullptr ? TEXT("Valid Connection") : TEXT("Invalid Connection");
const FString ownerName = GetOwner() != nullptr ? GetOwner()->GetName() : TEXT("No Owner");
const FString logStr = FString::Printf(TEXT("Connection : %s\n Owner Name : %s"), *validConnection, *ownerName);
DrawDebugString(GetWorld(), GetActorLocation() + GetActorUpVector() * 100, *logStr, nullptr, FColor::White, 0, true, 1);
}
왼쪽은 호스트이자 플레이어(Controller 0)의 화면으로 연결이 필요 없으므로, Invalid Connection이 뜨고,
오른쪽은 플레이어(Controller 1)의 화면으로 서버에 연결이 되어야 하므로, Valid Connection이 뜨는 모습
권한 (Authority)
#define LOCAL_ROLE UEnum::GetValueAsString<ENetRole>(GetLocalRole())
#define REMOTE_ROLE UEnum::GetValueAsString<ENetRole>(GetRemoteRole())
void ASeSAC_NetworkCharacter::PrintNetLog()
{
const FString validConnection = GetNetConnection() != nullptr ? TEXT("Valid Connection") : TEXT("Invalid Connection");
const FString ownerName = GetOwner() != nullptr ? GetOwner()->GetName() : TEXT("No Owner");
const FString logStr = FString::Printf(TEXT("Connection : %s\n Owner Name : %s\n Local Role : %s\n Remote Role : %s"), *validConnection, *ownerName, *LOCAL_ROLE, *REMOTE_ROLE);
DrawDebugString(GetWorld(), GetActorLocation() + GetActorUpVector() * 100, *logStr, nullptr, FColor::White, 0, true, 1);
// 권한(Authority)
// ROLE_Authority : 모든 권한을 다 가지고 있음 (로직 실행 가능)
// HasAuthority()
//
// ROLE_AutonomouseProxy : 제어(Input)만 가능
// IsLocallyControlled() : 제어권을 갖고 있는지
//
// ROLE_SimulatedProxy : 시뮬레이션만 가능
}
Owner가 없는 월드상에 배치된 액터의 권한
UCLASS()
class SESAC_NETWORK_API ACNetActor : public AActor
{
GENERATED_BODY()
public:
ACNetActor();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
public:
UPROPERTY(VisibleAnywhere)
class UStaticMeshComponent* MeshComp;
void PrintLog();
}
void ACNetActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
PrintLog();
}
void ACNetActor::PrintLog()
{
const FString validConnection = GetNetConnection() != nullptr ? TEXT("Valid Connection") : TEXT("Invalid Connection");
const FString ownerName = GetOwner() != nullptr ? GetOwner()->GetName() : TEXT("No Owner");
const FString logStr = FString::Printf(TEXT("Connection : %s\n Owner Name : %s\n Local Role : %s\n Remote Role : %s"), *validConnection, *ownerName, *LOCAL_ROLE, *REMOTE_ROLE);
DrawDebugString(GetWorld(), GetActorLocation() + GetActorUpVector() * 100, *logStr, nullptr, FColor::Red, 0, true, 1);
}
일정 범위내 플레이어를 찾아서 Owner 설정
UCLASS()
class SESAC_NETWORK_API ACNetActor : public AActor
{
GENERATED_BODY()
public:
// Owner 검출 영역
UPROPERTY(EditAnywhere)
float SearchDist = 200;
};
void ACNetActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (HasAuthority())
{
AActor* newOwner = nullptr;
float minDist = SearchDist;
for (TActorIterator<ASeSAC_NetworkCharacter> it(GetWorld()); it; ++it)
{
AActor* other = *it;
float dist = GetDistanceTo(other);
if (dist < minDist)
{
minDist = dist;
newOwner = other;
}
}
// Owner 설정
if (GetOwner() != newOwner)
SetOwner(newOwner);
} // if HasAuthority
DrawDebugSphere(GetWorld(), GetActorLocation(), SearchDist, 30, FColor::Yellow, false, 0, 0, 1);
PrintLog();
}
권한이 있는 플레이어의 경우
HasAuthority()가 있을 경우이므로, 서버에서는 권한이 있으므로 Owner로 설정이 가능
권한이 없는 플레이어의 경우
권한이 없으므로, Owner로 설정이 불가능하여 Owner 설정이 바뀌지 않는 모습
RPC (Remote Procedure Call) 활성화
로컬에서 호출되지만 (호출하는 머신과는) 다른 머신에서 원격 실행되는 함수
ACNetActor::ACNetActor()
{
// Replicate 활성화
bReplicates = true;
}
서버와 클라이언트간의 RPC활성화로 기존에 서버에서만 바뀌던 Owner를 클라이언트에도 바뀌도록 동기화
서버와 클라이언트 간의 회전값 동기화
UCLASS()
class SESAC_NETWORK_API ACNetActor : public AActor
{
GENERATED_BODY()
public:
// 회전 값 동기화 변수
UPROPERTY(Replicated)
float RotYaw = 0;
};
void ACNetActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FindOwner();
PrintLog();
// Server일 경우
if (HasAuthority())
{
// 서버 영역
AddActorLocalRotation(FRotator(0, 50 * DeltaTime, 0));
RotYaw = GetActorRotation().Yaw;
}
else
{
// 클라이언트 영역
FRotator newRot = GetActorRotation();
newRot.Yaw = RotYaw;
SetActorRotation(newRot);
}
}
void ACNetActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ACNetActor, RotYaw);
}
UCLASS()
class SESAC_NETWORK_API ACNetActor : public AActor
{
GENERATED_BODY()
public:
// 회전 값 동기화 변수
//UPROPERTY(Replicated)
UPROPERTY(ReplicatedUsing = "OnRep_RotYaw")
float RotYaw = 0;
UFUNCTION()
void OnRep_RotYaw();
};
void ACNetActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FindOwner();
PrintLog();
// Server일 경우
if (HasAuthority())
{
// 서버 영역
AddActorLocalRotation(FRotator(0, 50 * DeltaTime, 0));
RotYaw = GetActorRotation().Yaw;
}
else
{
//// 클라이언트 영역
//FRotator newRot = GetActorRotation();
//newRot.Yaw = RotYaw;
//SetActorRotation(newRot);
}
}
void ACNetActor::OnRep_RotYaw()
{
// 클라이언트 영역
FRotator newRot = GetActorRotation();
newRot.Yaw = RotYaw;
SetActorRotation(newRot);
}
Tick에서 호출하지 않지만, ReplicatedUsing을 사용하기때문에 자동으로 동기화
동기화 대역폭 조정
ACNetActor::ACNetActor()
{
bReplicates = true;
// 대역폭 조정
NetUpdateFrequency = 1;
}
멀리 있는 캐릭터의 경우 대역폭을 조정하는 방법으로 최적화를 할 수도 있다.
보간
UCLASS()
class SESAC_NETWORK_API ACNetActor : public AActor
{
GENERATED_BODY()
public:
float CurrentTime = 0;
float LastTime = 0;
};
void ACNetActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FindOwner();
PrintLog();
// Server일 경우
if (HasAuthority())
{
// 서버 영역
AddActorLocalRotation(FRotator(0, 50 * DeltaTime, 0));
RotYaw = GetActorRotation().Yaw;
}
else
{
// 클라이언트 자체 보간
// 경과시간 증가
CurrentTime += DeltaTime;
// 0으로 나눠지지 않도록 LastTime 값 체크
if (LastTime < KINDA_SMALL_NUMBER)
return;
// 이전 경과시간과 현재 경과시간의 비율계산
float lerpRatio = CurrentTime / LastTime;
// 이전 경과시간만큼 회전할 것으로 새로운 회전값 계산
float newYaw = RotYaw + 50 * LastTime;
// 예측되는 값으로 진행된 시간만큼 보간 처리
float lerpYaw = FMath::Lerp(RotYaw, newYaw, lerpRatio);
// 최종 적용
FRotator curRot = GetActorRotation();
curRot.Yaw = lerpYaw;
SetActorRotation(curRot);
}
}
void ACNetActor::OnRep_RotYaw()
{
// 클라이언트 영역
FRotator newRot = GetActorRotation();
newRot.Yaw = RotYaw;
SetActorRotation(newRot);
// 업데이트된 경과 시간 저장
LastTime = CurrentTime;
// 경과 시간 초기화
CurrentTime = 0;
}
색상 동기화
UCLASS()
class SESAC_NETWORK_API ACNetActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY()
class UMaterialInstanceDynamic* Mat;
// 재질에 동기화될 색상
UPROPERTY(ReplicatedUsing = "OnRep_ChangeMatColor")
FLinearColor MatColor;
UFUNCTION()
void OnRep_ChangeMatColor();
};
void ACNetActor::BeginPlay()
{
Super::BeginPlay();
Mat = MeshComp->CreateDynamicMaterialInstance(0);
if (HasAuthority())
{
FTimerHandle handle;
auto lambda = [&]()
{
MatColor = FLinearColor::MakeRandomColor();
OnRep_ChangeMatColor();
};
GetWorld()->GetTimerManager().SetTimer(handle, lambda, 1, true);
}
}
// 색상동기화
void ACNetActor::OnRep_ChangeMatColor()
{
if (Mat)
Mat->SetVectorParameterValue(TEXT("FloorColor"), MatColor);
}
void ACNetActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ACNetActor, RotYaw);
DOREPLIFETIME(ACNetActor, MatColor);
}
대역폭 수정
ACNetActor::ACNetActor()
{
bReplicates = true;
// 대역폭 조정
NetUpdateFrequency = 100;
}
이전보다 훨씬 비슷한 시점에 동기화되는 모습