일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 112일선
- 전주비빔 라이스 버거
- 224일선
- 언리얼5
- 이득우의 언리얼 프로그래밍 1
- 리팩터링 3장
- shader
- 언리얼 5
- URP
- 2023 게이밍
- 주식
- 구글 컨퍼런스
- 이득우의 언리얼 프로그래밍1
- 산토리 하이볼
- 2023 게이밍 인 구글 클라우드
- GenAI
- 스즈메의 문단속
- 2023 구글 클라우드
- 448일선
- 스포일러 주의
- 작계훈련
- 공부
- 주식단테
- 리팩터링
- 2023 Gaming
- unity
- 1일차
- 상계9동
- JavaScript
- 리팩터링 4장
- Today
- Total
개발 이야기 안하는 개발자
언리얼5_3_3 : RPC의 이해와 활용 본문
RPC(remote Procedure Call)
원격 프로시저(함수) 호출의 약자로 우너격 컴퓨터에 있는 함수를 호출할 수 있도록 만든 통신 프로토콜
서버와 클라이언트간에 빠르게 행동을 명령하고 정보를 주고받는데 사용된다.
언리얼 엔진에선 클라가 서버로 통신하는 유일한 수단이다.
Client RPC
서버에서 클라로 호출하는 RPC
특정 클라에게만 명령을 보낼 수 있다.
서버에서 명령을 보낼 클라의 커넥션을 소유한 액터를 사용해야 한다 (AActor::GetNetConnection)
Server RPC
클라에서 서버로 호출하는 RPC
언리얼에서 유일하게 클라가 서버의 함수를 호출 할 수 있음.
서버쪽에서 클라의 명령을 검증할 수 있는 함수를 구현할 수 있다.
Client RPC와 동일하게 서버와의 커넥션을 소유한 액터를 사용해야 한다.
NetMulticast RPC
서버를 포함해 모든 플레이어에게 명령을 보내는 RPC
프로퍼티 리플리케이션과 유사하지만 다른 용도로 사용한다
RPC 선언에 관련된 키워드
Unreliable : RPC호출을 보장하지 않는 옵션. 빠름
Reliable : RPC호출을 보장해주는 추가 옵션. 정말 필요한 때만 호출
WithValidation : 서버에서 검증 로직을 추가로 구현할 때 추가하는 옵션
사용했던 프로퍼티 리플리케이트를 이번엔 RPC로 구현해 본다.
색상이 바뀌는 분수대를 제작해본다.
UFUNCTION(NetMulticast, Unreliable)
void MulticastRPCChangeLightColor(const FLinearColor& NewLightColor);
제작한 함수를 정의할땐 뒤에 _Implementation을 붙여야 한다.
void AABFountain::MulticastRPCChangeLightColor_Implementation(const FLinearColor& NewLightColor)
{
AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *NewLightColor.ToString());
UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
if (PointLight)
{
PointLight->SetLightColor(NewLightColor);
}
}
시작부분에서 리플리케이션에서 했던것 처럼 타이머를 실행 시켜서 1초마다 RPC를 호출해준다.
void AABFountain::BeginPlay()
{
...
const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
MulticastRPCChangeLightColor(NewLightColor);
...
}
이렇게 하면 클라이언트의 분수 색이 바뀌는 것을 볼 수 있는데,
이때 클라이언트도 분수로 부터 멀어지면 색상이 바뀌지않는다.
RPC로 연관성에 영향을 받기 때문이다.
이번엔 클라가 서버에게 보내주는 서버RPC를 제작해본다.
UFUNCTION(Server, Unreliable)
void ServerRPCChangeLightColor();
void AABFountain::ServerRPCChangeLightColor_Implementation()
{
const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
MulticastRPCChangeLightColor(NewLightColor);
}
순서는 클라이언트가 서버에게 메세지를 보내면 서버가 MulticastRPC를 쏘도록 하는 로직이다.
FTimerHandle Handle2;
GetWorld()->GetTimerManager().SetTimer(Handle2, FTimerDelegate::CreateLambda([&]
{
for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
{
APlayerController* PlayerController = Iterator->Get();
if (PlayerController && !PlayerController->IsLocalPlayerController())
{
SetOwner(PlayerController);
break;
}
}
//위 for문과 동일한 기능
//for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
//{
// if (PlayerController && !PlayerController->IsLocalPlayerController())
// {
// SetOwner(PlayerController);
// break;
// }
//}
}
), 10.0f, false, -1.0f);
RPC를 쏘기위해 서버가 타이머를 통해서 컨트롤러에게 오너쉽을 제공한다.
타이머를 10초로 설정하고 10초안에 컨트롤러가 들어오면 해당 컨트롤러에게 분수대 오너를 제공해서 RPC를 받아 실행하도록 한다.
이런식으로 GetNetConnection을 활용해서 Connection이 있는 경우를 체크해 볼 수 있다.
...beginPlay 안되는 코드
else
{
SetOwner(GetWorld()->GetFirstPlayerController());
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
{
ServerRPCChangeLightColor();
}
), 1.0f, true, 0.0f);
}
위 코드처럼 BeginPlay에서 진행하면 실행되지 않는다.
서버가 아닌 클라에서 보내야 하기 때문에 hasAuthority() 가 false인 else문에서 클라이언트가 호출해줘야 한다.
분수대는 오너가 없는 액터임으로 오너를 삼아야지만 해당 RPC를 쏠 수 있다.
FirstPlayerController는 자기 자신의 컨트롤러이기 때문에 가져와서 오너를 가져오면된다.
하지만 위 코드에서 클라이언트가 오너를 가져오는 것은 의미가 없다.
클라이언트는 서버의 컨트롤러의 복제품이기 때문에 여기서 오너를 지정하는 것은 서버에게 영향을 주지 못한다.
즉, 서버에게 오너가 설정되어야 하기때문이다.
이번에 헤더에 WithValidation를 추가해서 검증 기능을 추가해본다.
UFUNCTION(Server, Unreliable, WithValidation)
void ServerRPCChangeLightColor();
bool AABFountain::ServerRPCChangeLightColor_Validate()
{
return true;
}
위 메소드를 추가하면 실행이 가능하다.
검증을 진행해야 한다면 위와 같은 Validate를 추가해야 한다.
이번엔 ClientRPC로 제작해본다.
UFUNCTION(Client, Unreliable)
void ClientRPCChangeLightColor(const FLinearColor& NewLightColor);
void AABFountain::ClientRPCChangeLightColor_Implementation(const FLinearColor& NewLightColor)
{
AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *NewLightColor.ToString());
UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
if (PointLight)
{
PointLight->SetLightColor(NewLightColor);
}
}
begin
타이머
...
const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
//MulticastRPCChangeLightColor(NewLightColor);
ClientRPCChangeLightColor(NewLightColor);
...
서버 Client와 마찬가지로 Owner를 가지고 있어야 하기 때문에 이를 추가해 주어야 한다.
위 코드를 실행해보면 서버가 처음 10초는 색상이 변경이 된다.
이후 10초뒤에 서버는 타이머에 맞춰 클라이언트에게 Owner를 준다.
이때 부터 서버는 색상이 변경되지 않고, 클라이언트만 색상이 변하는 것을 볼 수 있는데,
즉 이렇게 해당 액터의 Owner를 가진 특정 클라이언트에게만 실행시키게 되는 것이 ClientRPC이다
RPC 주의사항
각 RPC 종류마다 올바르게 사용할 것.
- Client, NetMulticast는 서버에서만 호출
- Server는 클라에서만 호출, 플레이어 참여하는 리슨서버의 경우 호출 가능.
- Client, Server는 오너십을 가지고 있는 액터에서 호출
- 컨트롤러 : IsLocalController()
- 폰 : IsLocallyControlled()
틱 및 빈번하게 호출되는 함수에서 Reliable RPC를 사용하지 말것.
NetMulticast RPC의 잦은 사용은 네트워크 부하를 가중시키니 신중하게 사용할 것.
게임 플레이 및 액터 상태에 영향을 미치는 경우 RPC보다 프로퍼티 리플리케이션을 사용할 것.
프로퍼티 리플리케이션과 넷멀티캐스트 RPC
유사점
- 서버와 모든 클라이언트의 지정한 함수를 호출할 수 있음.
- 지정한 데이터 전송을 보장할 수 있음.
- 액터의 오너십과 무관하게 연관성으로 동작함.
차이점
- Property Replication 으로 설정한 데이터는 클라이언트에 반드시 동기화됨.
- NetMulitcast RPC를 호출한 타이밍에 클라이언트가 없으면 해당 데이터를 받을 길이 없음
정리
- PropertyReplication은 게임에 영향을 미치는 데이터에 사용함(Gameplay Property)
- NetMulicast RPC는 게임과 무관한 휘발성 데이터에 사용함(Cosmetic)
캐릭터 공격 플로우
기존에 있던 공격 방식
몽타주를 활용해서 Notify를 통해 공격 판정을 제어했다.
클라이언트에서 입력을 하는 부분을 확장한다.
서버로 데이터를 보내고, 서버가 공격을 진행하고 공격 판정을 진행한다.
UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCAttack();
UFUNCTION(NetMulticast, Reliable)
void MulticastRPCAttack();
UPROPERTY(ReplicatedUsing = OnRep_CanAttack)
uint8 bCanAttack : 1;
UFUNCTION()
void OnRep_CanAttack();
void AABCharacterPlayer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AABCharacterPlayer, bCanAttack);
}
클라이언트에게 입력을 받으면 ServerRPCAttack이 호출된다.
이로 서버는 MulticastRPCAttack()을 모두에게 호출한다.
서버가 공격 타이머를 호추하고, 이 순간에 공격 판정을 진행한다.
프로퍼티 만들었으니까 등록한다.
void AABCharacterPlayer::Attack()
{
//ProcessComboCommand();
if (bCanAttack)
{
ServerRPCAttack();
}
}
공격 키를 눌렀을때 서버에게 받은 bCatAttack이 가능상태일때 서버로 공격 메소드를 보낸다.
bool AABCharacterPlayer::ServerRPCAttack_Validate()
{
return true;
}
void AABCharacterPlayer::ServerRPCAttack_Implementation()
{
AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
MulticastRPCAttack();
}
받은 서버는 순서대로 Validate를 체크해서 유효한지 확인한다.
이후 Implementation을 호출하는데 MulticastRPCAttack()을 통해 모든 클라이언트 에게 해당 액터가 공격을 진행하라고 보낸다.
void AABCharacterPlayer::MulticastRPCAttack_Implementation()
{
AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
if (HasAuthority())
{
bCanAttack = false;
OnRep_CanAttack();
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
{
bCanAttack = true;
OnRep_CanAttack();
}
), AttackTime, false, -1.0f);
}
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(ComboActionMontage);
}
해당 메소드를 받은 다른 클라이언트들은 애니메이션을 진행하는데, 이때 HasAuthority() (서버인가) 를 확인하는데, 서버만 공격 판정을 위해 타이머로 공격 가능상태, 불가능 상태를 호출하게 된다.
다른 클라이언트가 보기엔 해당 액터는 공격 애니메이션만 진행하는 중인것이다.
물론 당사자도 애니메이션만 진행하게 된다.
void AABCharacterPlayer::OnRep_CanAttack()
{
if (!bCanAttack)
{
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
}
else
{
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}
}
상태에 맞춰 움직임 상태를 수정할 수 있는데, Property Replicated 인데 이렇게 호출하는 이유는 서버는 이 상태를 가지지 못하기 때문에 서버만 따로 OnRep_CanAttack()을 호출하고 클라이언트는 CallBack을 통해 호출받게 된다.
void AABCharacterPlayer::AttackHitCheck()
{
if (HasAuthority())
{
...
이제 AttackHitCheck는 서버만 진행하도록 로직을 수정한다.
Notify가 호출하는 공격 체크는 이제 서버만 진행한다.
액터 컴포넌트 리플리케이션
언리얼에서 리플리케이션의 주체는 액터이다.
액터가 소유하는 언리얼 오브젝트에 대해 리플리케이션 진행이 가능하다.
- 이를 통틀어 서브오브젝트(subobject)라고도 한다.
스탯을 관리하는 액터 컴포넌트의 리플리케이션 설정
- 리플리케이션 지정 : SetIsReplicated(true)
- 리플리케이션이 준비되면 호출되는 이벤트 함수 : ReadyForReplication
액터가 리플리케이션을 활성화할때 진행했떤 PostInitializeComponent와 BeginPlay 사이의 PostNetInit()과 유사하다.
CharacterStatComponent에 있는 CurrentHP를 동기화 할 것이다.
protected:
UPROPERTY(ReplicatedUsing = OnRep_CurrentHp, Transient, VisibleInstanceOnly, Category = Stat)
float CurrentHp;
...
protected:
...
virtual void ReadyForReplication() override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UFUNCTION()
void OnRep_CurrentHp();
};
SetIsReplicated(true);
...
void UABCharacterStatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UABCharacterStatComponent, CurrentHp);
}
생성자에선 SetIsReplicated를 넣어준다.
사용할 CurrentHP를 등록한다.
void UABCharacterStatComponent::ReadyForReplication()
{
AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
Super::ReadyForReplication();
}
여기서 보면 로그를 AB_SUBLOG 로 변경했는데 그 이유는 Define되어있는 부분에서 GetLocalRole()과 GetRemoteRole()을 사용했다.
이 두 함수는 액터에 대한 메소드이고 ActorComponent의 메소드가 아니다.
따라서 오너를 가져와서 오너의 롤을 확인해야한다.
#define LOG_LOCALROLEINFO *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetLocalRole()))
#define LOG_REMOTEROLEINFO *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetRemoteRole()))
#define LOG_SUBLOCALROLEINFO *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetOwner()->GetLocalRole()))
#define LOG_SUBREMOTEROLEINFO *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetOwner()->GetRemoteRole()))
#define AB_LOG(LogCat, Verbosity, Format, ...) UE_LOG(LogCat, Verbosity, TEXT("[%s][%s/%s] %s %s"), LOG_NETMODEINFO, LOG_LOCALROLEINFO, LOG_REMOTEROLEINFO, LOG_CALLINFO, *FString::Printf(Format, ##__VA_ARGS__))
#define AB_SUBLOG(LogCat, Verbosity, Format, ...) UE_LOG(LogCat, Verbosity, TEXT("[%s][%s/%s] %s %s"), LOG_NETMODEINFO, LOG_SUBLOCALROLEINFO, LOG_SUBREMOTEROLEINFO, LOG_CALLINFO, *FString::Printf(Format, ##__VA_ARGS__))
void UABCharacterStatComponent::OnRep_CurrentHp()
{
AB_SUBLOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
OnHpChanged.Broadcast(CurrentHp);
if (CurrentHp <= KINDA_SMALL_NUMBER)
{
OnHpZero.Broadcast();
}
}
체력이 변경되면 호출되는 CallBack 메소드 이다.
캐릭터의 공격 구현 문제점
클라이언트의 모든 행동은 서버를 거친 후에 수행되도록 설계되어 있음.
통신 부하가 발생하는 경우 사용자 경험이 나빠짐.
의도적으로 패킷 랙을 발생시킨 후 이의 결과를 확인.
DefaultEngine.ini
[PacketSimulationSettings]
PktLag=500
이렇게 랙을 설정하고 실행하면 이동이나 공격, 데미지 연산등에 딜레이가 생겨서 나쁜 경험을 준다.
이를 위해 개선해야 한다.
- 클라이언트에서 처리할 수 있는 기능은 최댛나 클라이언트에서 직접 처리하여 반응성을 높인다.
- 최종 판정은 서버에서 진행하되 다양한 로직을 활용해 자세하게 검증한다.
- 네트워크 데이터 전송을 최소화 한다.
개선된 서버 통신
클라에서의 입력을 서버로 도달하는데에 랙이 걸리기 때문에 둘 사이의 애니메이션 차이는 분명히 존재한다.
이때 타이머를 제작하고 타이머를 활용해 ServerRPC에 담아서 보내고, 이를 서버가 받아 연산해서 얼마나의 딜레이가 있는지를 측정한다.
이 측정값을 활용해서 얼마나 딜레이가 되었는지 측정하고 이를 공격에 영향을 주도록 제작한다.
클라이언트가 데미지를 준것을 스스로 판단하고 해당 결과를 서버에게 보내 이 결과값이 타당한지를 확인하게 한다.
타당하다면 이를 처리해주고 종료하게 된다.
이렇게 함으로써 클라이언트가 행한 행동으로 유저에게 좋은 경험을 선사하고 서버에게 옳은 정보를 보내주게 될 것이다.
characterPlayer.h에 코드를 추가한다.
protected:
...
void PlayAttackAnimation();
virtual void AttackHitCheck() override;
void AttackHitConfirm(AActor* HitActor);
void DrawDebugAttackRange(const FColor& DrawColor, FVector TraceStart, FVector TraceEnd, FVector Forward);
UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCAttack(float AttackStartTime);
UFUNCTION(NetMulticast, Unreliable)
void MulticastRPCAttack();
UFUNCTION(Client, Unreliable)
void ClientRPCPlayAnimation(AABCharacterPlayer* CharacterToPlay);
UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCNotifyHit(const FHitResult& HitResult, float HitCheckTime);
UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCNotifyMiss(FVector_NetQuantize TraceStart, FVector_NetQuantize TraceEnd, FVector_NetQuantizeNormal TraceDir, float HitCheckTime);
UPROPERTY(ReplicatedUsing = OnRep_CanAttack)
uint8 bCanAttack : 1;
UFUNCTION()
void OnRep_CanAttack();
float AttackTime = 1.4667f;
float LastAttackStartTime = 0.0f;
float AttackTimeDifference = 0.0f;
float AcceptCheckDistance = 300.0f;
float AcceptMinCheckTime = 0.15f;
공격과 공격 판정, 공격 애니메이션을 모두 분리했다.
이는 클라이언트가 진행해야 할 부분과 서버가 이를 판단해야 하는 부분을 분리하기 위함이다.
그리고 딜레이 시간이 얼마나 걸렸는지를 판단하기 위해 변수도 추가했다.
void AABCharacterPlayer::Attack()
{
//ProcessComboCommand();
if (bCanAttack)
{
if (!HasAuthority())
{
bCanAttack = false;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
{
bCanAttack = true;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}
), AttackTime, false, -1.0f);
PlayAttackAnimation();
}
ServerRPCAttack(GetWorld()->GetGameState()->GetServerWorldTimeSeconds());
}
}
공격 부분을 서버가 아닌경우에만 공격 모션이 취하도록 수정했다.
이후 공격을 진행했다고 알리고 타이머뒤에 이동 가능 상태로 변경했다.
전송할때 사용되는 GetWorld()->GetGameState()->GetServerWorldTimeSeconds()를 사용해야 서버시간으로 활용이 가능하다.
void AABCharacterPlayer::PlayAttackAnimation()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->StopAllMontages(0.0f);
AnimInstance->Montage_Play(ComboActionMontage);
}
애니메이션이 실행되는 코드이다.
void AABCharacterPlayer::ServerRPCAttack_Implementation(float AttackStartTime)
{
AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
bCanAttack = false;
OnRep_CanAttack();
AttackTimeDifference = GetWorld()->GetTimeSeconds() - AttackStartTime;
AB_LOG(LogABNetwork, Log, TEXT("LagTime : %f"), AttackTimeDifference);
AttackTimeDifference = FMath::Clamp(AttackTimeDifference, 0.0f, AttackTime - 0.01f);
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
{
bCanAttack = true;
OnRep_CanAttack();
}
), AttackTime - AttackTimeDifference, false, -1.0f);
LastAttackStartTime = AttackStartTime;
PlayAttackAnimation();
//MulticastRPCAttack();
for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
{
if (PlayerController && GetController() != PlayerController)
{
if(!PlayerController->IsLocalController())
{
AABCharacterPlayer* OtherPlayer = Cast<AABCharacterPlayer>(PlayerController->GetPawn());
if (OtherPlayer)
{
OtherPlayer->ClientRPCPlayAnimation(this);
}
}
}
}
}
공격 딜레이의 시간을 측정해서 이를 타이머로 활용해서 영향을 주게 한다.
void AABCharacterPlayer::MulticastRPCAttack_Implementation()
{
if (!IsLocallyControlled())
{
PlayAttackAnimation();
}
}
Multicast 는 게임에 영향을 크게 주지않는 효과들을 넣는게 더 안정적이다.
그래서 내 캐릭터가 아닌 캐릭터들에게 애니메이션을 실행하도록 로직을 추가했다.
bool AABCharacterPlayer::ServerRPCAttack_Validate(float AttackStartTime)
{
if (LastAttackStartTime == 0.0f)
{
return true;
}
return (AttackStartTime - LastAttackStartTime) > AttackTime;
}
마지막으로 공격한 시간과 다시 공격한 시간의 차이가 기본으로 설정된 공격 시간보다 작게 되면 뭔가 문제가 있다는 뜻이다. 따라서 이 값을 토대로 return 해주게 된다.
공격 판정 부분은 HasAuthority 부분을 IsLocallyControlled() 로 변경한다.
내가 컨트롤러 일때 이를 구현한다.
void AABCharacterPlayer::AttackHitCheck()
{
if (IsLocallyControlled())
{
...
float HitCheckTime = GetWorld()->GetGameState()->GetServerWorldTimeSeconds();
if (!HasAuthority())
{
if (HitDetected)
{
ServerRPCNotifyHit(OutHitResult, HitCheckTime);
}
else
{
ServerRPCNotifyMiss(Start, End, Forward, HitCheckTime);
}
}
else
{
FColor DebugColor = HitDetected ? FColor::Green : FColor::Red;
DrawDebugAttackRange(DebugColor, Start, End, Forward);
if (HitDetected)
{
AttackHitConfirm(OutHitResult.GetActor());
}
}
}
}
서버가 아니라면 데미지를 입힌경우 서버로 보내고, 아닌경우는 흘린다.
서버의 경우에는 데미지 입는게 판단된다면 서버입장에선 바로 데미지를 주는 로직을 타면 된다.
데미지를 받은게 인정될 경우
bool AABCharacterPlayer::ServerRPCNotifyHit_Validate(const FHitResult& HitResult, float HitCheckTime)
{
return (HitCheckTime - LastAttackStartTime) > AcceptMinCheckTime;
}
void AABCharacterPlayer::ServerRPCNotifyHit_Implementation(const FHitResult& HitResult, float HitCheckTime)
{
AActor* HitActor = HitResult.GetActor();
if (::IsValid(HitActor))
{
const FVector HitLocation = HitResult.Location;
const FBox HitBox = HitActor->GetComponentsBoundingBox();
const FVector ActorBoxCenter = (HitBox.Min + HitBox.Max) * 0.5f;
if (FVector::DistSquared(HitLocation, ActorBoxCenter) <= AcceptCheckDistance * AcceptCheckDistance)
{
AttackHitConfirm(HitActor);
}
else
{
AB_LOG(LogABNetwork, Warning, TEXT("%s"), TEXT("HitTest Rejected!"));
}
#if ENABLE_DRAW_DEBUG
DrawDebugPoint(GetWorld(), ActorBoxCenter, 50.0f, FColor::Cyan, false, 5.0f);
DrawDebugPoint(GetWorld(), HitLocation, 50.0f, FColor::Magenta, false, 5.0f);
#endif
DrawDebugAttackRange(FColor::Green, HitResult.TraceStart, HitResult.TraceEnd, HitActor->GetActorForwardVector());
}
}
서버가 현재 HitActor와의 거리를 연산한다.
이후 해당 거리가 맞을만한 충분한 거리였다고 판단되면 인정을 해준다.
그게 아니라면 로그만 띄운다.
void AABCharacterPlayer::AttackHitConfirm(AActor* HitActor)
{
AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
if (HasAuthority())
{
const float AttackDamage = Stat->GetTotalStat().Attack;
FDamageEvent DamageEvent;
HitActor->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
}
}
맞은 경우를 인정해준 결과 서버인 경우에 데이터를 연산해준다.
공격 범위 디버그로 그리는 메소드를 구현한다.
void AABCharacterPlayer::DrawDebugAttackRange(const FColor& DrawColor, FVector TraceStart, FVector TraceEnd, FVector Forward)
{
#if ENABLE_DRAW_DEBUG
const float AttackRange = Stat->GetTotalStat().AttackRange;
const float AttackRadius = Stat->GetAttackRadius();
FVector CapsuleOrigin = TraceStart + (TraceEnd - TraceStart) * 0.5f;
float CapsuleHalfHeight = AttackRange * 0.5f;
DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor, false, 5.0f);
#endif
}
빗나갔을 경우를 구현한다.
bool AABCharacterPlayer::ServerRPCNotifyMiss_Validate(FVector_NetQuantize TraceStart, FVector_NetQuantize TraceEnd, FVector_NetQuantizeNormal TraceDir, float HitCheckTime)
{
return (HitCheckTime - LastAttackStartTime) > AcceptMinCheckTime;
}
void AABCharacterPlayer::ServerRPCNotifyMiss_Implementation(FVector_NetQuantize TraceStart, FVector_NetQuantize TraceEnd, FVector_NetQuantizeNormal TraceDir, float HitCheckTime)
{
DrawDebugAttackRange(FColor::Red, TraceStart, TraceEnd, TraceDir);
}
ServerRPCAttack_Implementation.cpp
//ServerRPCAttack_Implentation
for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
{
if (PlayerController && GetController() != PlayerController)
{
if(!PlayerController->IsLocalController())
{
AABCharacterPlayer* OtherPlayer = Cast<AABCharacterPlayer>(PlayerController->GetPawn());
if (OtherPlayer)
{
OtherPlayer->ClientRPCPlayAnimation(this);
}
}
}
}
void AABCharacterPlayer::ClientRPCPlayAnimation_Implementation(AABCharacterPlayer* CharacterToPlay)
{
AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
if (CharacterToPlay)
{
CharacterToPlay->PlayAttackAnimation();
}
}
이렇게 딜레이가 생겨서 유저의 경험 악화를 최소화 하는 방법을 사용할 수 있게 되었다.
'Unreal > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
언리얼5_3_5 : PVP 게임 제작 (0) | 2024.04.03 |
---|---|
언리얼5_3_4 : 움직임 리플리케이션 (0) | 2024.04.01 |
언리얼5_3_2 : 액터 리플리케이 (1) | 2024.03.29 |
언리얼5_3_1 : 언리얼 네트워크 멀티플레이어 프레임워크 기초 (2) | 2024.03.29 |
언리얼5_2_5 : 언리얼 게임의 완성 (0) | 2024.03.28 |