Unreal Engine
Unreal Engine - 데디케이티드 서버 6 (동기화)
gbleem
2025. 4. 11. 01:21
1. 캐릭터 걷기 및 점프 동기화
코드
더보기
//animinstance.h
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "DXAnimInstanceBase.generated.h"
class UCharacterMovementComponent;
UCLASS()
class SCC_DEDICATEDX_API UDXAnimInstanceBase : public UAnimInstance
{
GENERATED_BODY()
public:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
protected:
UPROPERTY()
TObjectPtr<ACharacter> OwnerCharacter;
UPROPERTY()
TObjectPtr<UCharacterMovementComponent> OwnerCharacterMovementComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
FVector Velocity;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
float GroundSpeed;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
uint8 bShouldMove : 1;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
uint8 bIsFalling : 1;
};
//animinstance.cpp
#include "Animation/DXAnimInstanceBase.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
void UDXAnimInstanceBase::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
OwnerCharacter = Cast<ACharacter>(GetOwningActor());
if (IsValid(OwnerCharacter))
{
OwnerCharacterMovementComponent = OwnerCharacter->GetCharacterMovement();
}
}
void UDXAnimInstanceBase::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
if (!IsValid(OwnerCharacter) || !IsValid(OwnerCharacterMovementComponent))
{
return;
}
Velocity = OwnerCharacterMovementComponent->Velocity;
GroundSpeed = FVector(Velocity.X, Velocity.Y, 0.f).Size();
bShouldMove = (!(OwnerCharacterMovementComponent->GetCurrentAcceleration().IsNearlyZero()) && (3.f < GroundSpeed));
bIsFalling = OwnerCharacterMovementComponent->IsFalling();
}
단순히 코드 작성 후 Anim BP를 만들어주면 애니메이션 연동이 되는데 그 이유는 캐릭터의 default 세팅에 replicate movement 가 true로 되어있기 때문이다.
2. 공격 동기화
2 - 1. 공격 로직 추가
공격을 하는 클라이언트의 동작을 모두 서버를 거친 후 수행하도록 설계하면 아래와 같이 설계할 수 있다.
- 이 방식의 문제는 서버쪽 로직이 너무 과부화되어 통신 부하가 발생하는 경우 사용자가 불편함을 느낄 수 있다.
더보기
//character.h
#pragma region Attack
public:
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
void CheckMeleeAttackHit();
private:
UFUNCTION(NetMulticast, Reliable)
void MulticastDrawDebugMeleeAttack(const FColor& DrawColor, FVector TraceStart, FVector TraceEnd, FVector Forward);
UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCMeleeAttack();
UFUNCTION(NetMulticast, Reliable)
void MulticastRPCMeleeAttack();
UFUNCTION()
void OnRep_CanAttack();
void PlayMeleeAttackMontage();
protected:
UPROPERTY(ReplicatedUsing = OnRep_CanAttack)
uint8 bCanAttack : 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TObjectPtr<UAnimMontage> MeleeAttackMontage;
float MeleeAttackMontagePlayTime;
#pragma endregion
void ADXPlayerCharacter::HandleMeleeAttackInput(const FInputActionValue& InValue)
{
if (bCanAttack && !GetCharacterMovement()->IsFalling())
{
ServerRPCMeleeAttack();
}
}
float ADXPlayerCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("TakeDamage: %f"), DamageAmount), true, true, FLinearColor::Red, 5.f);
return Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
}
void ADXPlayerCharacter::CheckMeleeAttackHit()
{
//server check
if (HasAuthority())
{
TArray<FHitResult> OutHitResults;
TSet<ACharacter*> DamagedCharacters;
FCollisionQueryParams Params(NAME_None, false, this);
const float MeleeAttackRange = 50.f;
const float MeleeAttackRadius = 50.f;
const float MeleeAttackDamage = 10.f;
const FVector Forward = GetActorForwardVector();
const FVector Start = GetActorLocation() + Forward * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * MeleeAttackRange;
bool bIsHitDetected = GetWorld()->SweepMultiByChannel(OutHitResults, Start, End, FQuat::Identity, ECC_Camera, FCollisionShape::MakeSphere(MeleeAttackRadius), Params);
if (bIsHitDetected)
{
for (auto const& OutHitResult : OutHitResults)
{
ACharacter* DamagedCharacter = Cast<ACharacter>(OutHitResult.GetActor());
if (IsValid(DamagedCharacter))
{
DamagedCharacters.Add(DamagedCharacter);
}
}
FDamageEvent DamageEvent;
for (auto const& DamagedCharacter : DamagedCharacters)
{
DamagedCharacter->TakeDamage(MeleeAttackDamage, DamageEvent, GetController(), this);
}
}
FColor DrawColor = bIsHitDetected ? FColor::Green : FColor::Red;
MulticastDrawDebugMeleeAttack(DrawColor, Start, End, Forward);
}
}
void ADXPlayerCharacter::MulticastDrawDebugMeleeAttack_Implementation(const FColor& DrawColor, FVector TraceStart, FVector TraceEnd, FVector Forward)
{
const float MeleeAttackRange = 50.f;
const float MeleeAttackRadius = 50.f;
FVector CapsuleOrigin = TraceStart + (TraceEnd - TraceStart) * 0.5f;
float CapsuleHalfHeight = MeleeAttackRange * 0.5f;
DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, MeleeAttackRadius, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor, false, 5.f);
}
void ADXPlayerCharacter::ServerRPCMeleeAttack_Implementation()
{
MulticastRPCMeleeAttack();
}
bool ADXPlayerCharacter::ServerRPCMeleeAttack_Validate()
{
return true;
}
void ADXPlayerCharacter::MulticastRPCMeleeAttack_Implementation()
{
if (HasAuthority())
{
bCanAttack = false;
OnRep_CanAttack();
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda
([&]() -> void
{
bCanAttack = true;
OnRep_CanAttack();
}), MeleeAttackMontagePlayTime, false
);
}
PlayMeleeAttackMontage();
}
void ADXPlayerCharacter::OnRep_CanAttack()
{
if (bCanAttack)
{
GetCharacterMovement()->SetMovementMode(MOVE_Walking);
}
else
{
GetCharacterMovement()->SetMovementMode(MOVE_None);
}
}
void ADXPlayerCharacter::PlayMeleeAttackMontage()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (IsValid(AnimInstance))
{
AnimInstance->StopAllMontages(0.f);
AnimInstance->Montage_Play(MeleeAttackMontage);
}
}
추가) 노티파이 두번 호출되는 것 방지하기 위한 세팅
2 - 2. 공격 로직 개선
서버의 과부화를 덜어주기 위해 클라이언트쪽에서 처리할 수 있는 로직을 최대한 클라이언트에서 처리할 수 있도록 코드를 수정하여 더 좋은 게임 환경을 만들어 보자 (+ WithValidation 키워드 활용)
- 서버에서 체크하는 "공격 가능" 처리를 delay time을 이용해서 클라이언트에서 처리하는 것이다.
- 공격 가능 시간 = 몽타주 재생시간 - 서버 딜레이(MeleeAttackTimeDifference)
- 서버 딜레이 시간 = 현재 서버시간 - 공격입력을 했을때의 서버 시간
- 공격 가능 시간 = 몽타주 재생시간 - 서버 딜레이(MeleeAttackTimeDifference)
- 공격가능 시간이 0보다 큰 경우 공격을 수행하고, 만약 서버 딜레이가 너무 커버린 경우는 아무일도 일어나지 않게한 후 CanAttack 변수를 그래도 true로 두어 다음 공격은 바로 실행될 수 있도록 한다.
- Validate 함수의 경우는
- InStartMEleeAttackTime - LastStartMeleeAttackTime을 통해서 이전 공격 기준 얼마나 빠른 시점에 공격했는지를 체크 후
- 너무짧은 경우 블락처리를 해서 안정성을 높이는 처리를 진행
더보기
//character.h
UFUNCTION(Server, Reliable, WithValidation)
void ServerMeleeAttack(float InStartMeleeAttackTime);
//void ServerRPCMeleeAttack();
//UFUNCTION(NetMulticast, Reliable)
UFUNCTION(NetMulticast, UnReliable)
void MulticastRPCMeleeAttack();
float LastStartMeleeAttackTime;
float MeleeAttackTimeDifference;
void ADXPlayerCharacter::HandleMeleeAttackInput(const FInputActionValue& InValue)
{
if (bCanAttack && !GetCharacterMovement()->IsFalling())
{
//ServerRPCMeleeAttack();
ServerMeleeAttack(GetWorld()->GetGameState()->GetServerWorldTimeSeconds());
if (!HasAuthority() && IsLocallyControlled())
{
PlayMeleeAttackMontage();
}
}
}
void ADXPlayerCharacter::ServerMeleeAttack_Implementation(float InStartMeleeAttackTime)
{
MeleeAttackTimeDifference = GetWorld()->GetTimeSeconds() - InStartMeleeAttackTime;
MeleeAttackTimeDifference = FMath::Clamp(MeleeAttackTimeDifference, 0.f, MeleeAttackMontagePlayTime);
if (KINDA_SMALL_NUMBER < MeleeAttackMontagePlayTime - MeleeAttackTimeDifference)
{
bCanAttack = false;
OnRep_CanAttack();
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda
([&]() -> void
{
bCanAttack = true;
OnRep_CanAttack();
}), MeleeAttackMontagePlayTime - MeleeAttackTimeDifference, false, -1.f
);
}
LastStartMeleeAttackTime = InStartMeleeAttackTime;
PlayMeleeAttackMontage();
MulticastRPCMeleeAttack();
}
bool ADXPlayerCharacter::ServerMeleeAttack_Validate(float InStartMeleeAttackTime)
{
if (LastStartMeleeAttackTime == 0.f)
{
return true;
}
return (MeleeAttackMontagePlayTime - 0.1f) < (InStartMeleeAttackTime - LastStartMeleeAttackTime);
}
void ADXPlayerCharacter::MulticastRPCMeleeAttack_Implementation()
{
if (!HasAuthority() && !IsLocallyControlled())
{
PlayMeleeAttackMontage();
}
}