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)
      • 서버 딜레이 시간 = 현재 서버시간 - 공격입력을 했을때의 서버 시간
  • 공격가능 시간이 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();
	}
}