프로젝트/CCFF

미친 선인장과 분노의 버섯 - #11 싱글 플레이 -> 멀티 플레이 전환 (네트워크 기반 설계)

2h1824 2025. 4. 9. 21:02

멀티 플레이 환경에 맞게 함수 리팩토링

공격 가능한지 판단할 변수 추가

BaseCharacter.h
{
	UPROPERTY(ReplicatedUsing=OnRep_CanAttack)
	uint8 bCanAttack : 1;
   	UFUNCTION()
	void OnRep_CanAttack();
}

 

BaseCharacter.cpp
ABaseCharacter::ABaseCharacter()
{
	bCanAttack=true;
}

void ABaseCharacter::OnRep_CanAttack()
{
	if (bCanAttack==true)
	{
		GetCharacterMovement()->SetMovementMode(MOVE_Walking);
	}
	else
	{
		GetCharacterMovement()->SetMovementMode(MOVE_None);
	}
}
void ABaseCharacter::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	...
	DOREPLIFETIME(ThisClass,bCanAttack);
}
  • 멀티플레이 환경에서 Replicated 되는 변수의 경우 네트워크 부담을 줄이는 것이 중요
  • bool값은 언리얼에서 크기가 명확하지 않는 자료형
  • 따라서 uint8, uint16 과 같이 명확하게 비트 수를 지정할 수 있는 비트 필드를 사용하는 것이 최적화 이점이 있음
  • 특히나 네트워크 환경에서는 이런 불필요한 비트 낭비가 쌓이다 보면 치명적

Server, MulticastRPC

BaseCharacter.h
#pragma region AttackFunctions
	UFUNCTION(Server,Reliable,WithValidation)
	void ServerRPCAttack(const int32 Num);
	UFUNCTION(NetMulticast,Reliable)
	void MulticastRPCAttack(const int32 Num);
#pragma endregion
BaseCharacter.cpp
void ABaseCharacter::ServerRPCAttack_Implementation(const int32 Num)
{
	UE_LOG(LogTemp,Warning,TEXT("ServerRPC Called with %d !!"),Num);
	MulticastRPCAttack(Num);
}

bool ABaseCharacter::ServerRPCAttack_Validate(const int32 Num)
{
	return true;
}
void ABaseCharacter::MulticastRPCAttack_Implementation(const int32 Num)
{
	if (HasAuthority()==true)
	{
		const float MontagePlayTime=Anim.AttackMontage[Num]->GetPlayLength();
		bCanAttack=false;
		OnRep_CanAttack();
		FTimerHandle Handle;
		GetWorld()->GetTimerManager().SetTimer(Handle,FTimerDelegate::CreateLambda([&]()
		{
			bCanAttack=true;
			OnRep_CanAttack();
		}),
		MontagePlayTime,false);
	}
			
	PlayAttackMontage(Num);
}
  • ServerRPC가 요청되면 Multicast로 서버, 모든 클라이언트들에게 다시 RPC를 쏘는 형태로 구성
  • MulticastRPC는 HasAuthority() 함수로 서버라면 공격 가능 상태 판별 변수 관련 로직 실행
    ㅡ> 몽타주 실행시간으로 타이머를 설정해 적절한 시점에 공격 불가능에서 가능으로 전환
  • 서버, 모든 클라이언트에서 몽타주 실행

주요 함수의 실행 위치 지정

void ABaseCharacter::AttackNotify(const FName NotifyName, const FBranchingPointNotifyPayload& Payload)
{
	if (!HasAuthority()) //Client Logic
	{
		// Particle, Effect 
		return;
	}
	//Server Logic
	// Determin the type of attack by name
	FString NotifyNameString = NotifyName.ToString();

	if (!NotifyName.IsValid()||AttackCollisions.IsEmpty()) return;
	// Hitbox 
	if(NotifyNameString.Contains(TEXT("Hitbox")))
	{
		TCHAR LastChar = NotifyNameString[NotifyNameString.Len() - 1];
		int32 AttackNumber = FCString::Atoi(&LastChar)-1;
		//UE_LOG(LogTemp, Log, TEXT("Parsed Attack Number: %d"), AttackNumber);	
		// Activate Collision in proper location
		if (AttackCollisions[AttackNumber])
		{
			// Deactivate Collision
			if (NotifyNameString.Contains(TEXT("End")))
			{
				CurrentActivatedCollision=-1;
				DeactivateAttackCollision(AttackNumber);
				UE_LOG(LogTemp,Warning,TEXT("Deactivate Collision! (Index: %d)"),AttackNumber);
			}
			else // Activate Collision
			{
				CurrentActivatedCollision=AttackNumber;
				DrawDebugBox(GetWorld(),AttackCollisions[AttackNumber]->GetComponentLocation(),AttackCollisions[AttackNumber]->GetScaledBoxExtent(),AttackCollisions[AttackNumber]->GetComponentQuat(),FColor::Red,false,2.0f);
				AttackCollisions[AttackNumber]->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
				UE_LOG(LogTemp,Warning,TEXT("Activate Collision! (Index: %d)"),AttackNumber);
			}
		}
		else
		{
			UE_LOG(LogTemp,Warning,TEXT("Activate Failed!"));
		}
		return;
	}
}
  • 몽타주에서 Notify를 감지했을 때 작동하는 함수
  • HasAuthority()를 통해 클라이언트 판별하여 해당 구간에서는 파티클, 이펙트 관련 로직 작성 예정
  • 그 외 콜리전 활성화 관련 코드들은 모두 서버에서만 작동
  • 결과적으로 콜리전 활성화가 서버에서만 이뤄지니 클라에서는 몽타주만 실행
  • 서버에서는 콜리전이 활성화되었으니 오버랩 감지하여 이벤트 호출, 데미지 및 피격 효과 적용하는 흐름

고찰

void ABaseCharacter::PlayAttackMontage(const int32& Num)
{
    //if (!GetMesh()) return;
    UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    //둘다 유효
    if (AnimInstance&&Anim.AttackMontage[Num])
    {
       //몽타주 실행
       AnimInstance->Montage_Play(Anim.AttackMontage[Num]);
       CurrentCharacterState = ECharacterState::Attack;
    }
}
  • 처음에는 같은 코드에서 주석 처리된 Mesh 유효성 관련 조건문으로 인해 애니메이션이 아예 작동하지 않았음
  • 알아보니 데디 서버에서는 렌더링 관련 정보들이 존재 X
  • 결과적으로 애니메이션 Tick을 돌리는 과정과 각 Bone들의 실제 Transform을 업데이트 하는 과정은 분리된 형태
  • VisibilityBasedAnimTickOption라는 이름의 옵션 설정을 통해 위에서 말한 과정에 대해 설정 가능
  • 위 설정의 기본값 Always Tick Pose ㅡ> Bone의 Transfrom 업데이트 X, PostAnimEvaluation 함수 제대로 실행 X
  • PostAnimEvaluation이 Refresh Bone 이후에 호출되므로 서버에서는 AnimNotify 실행 보장 X
    ㅡ> 서버 랙으로 인해 프레임 건너뛰기, 화면에 메쉬 출력되지 않음 등으로 인해 Notify 호출 보장하지 못함
  • 해결 방법
    1. 엔진 코드 수정 (PostAnimEvaluation)
    2. Always Tick PoseAlways Tick Pose and Refresh Bone으로 변경
    3. 애니메이션 몽타주에서 Montage Tick TypeBranching Point로 변경
      ㅡ> Branching Point로 하면 해당 프레임의 애니메이션 Tick에 도달하면 바로 Notify 실행
             Queued일 경우 Notify 실행이 End of Evaluation Phase에서 이루어져 실행이 지연될 수 있음
  • 추가로 현재의 구조는 몽타주를 서버에서도 실행하게 되는 구조 ㅡ> 서버 부하 증가
  • 클라에서 애니메이션 실행 및 Notify를 통해 콜리전 관련 오버랩 감지
  • 이후 데미지를 적용하기 위한 로직만 서버에서 담당하게 한다면 서버에서는 몽타주를 재생할 필요 X
    ㅡ> 결과적으로 서버의 부하를 낮출 수 있음
  • 동기화 관련 문제가 어떤 식으로 발생할지 고려해 봐야함

참고자료

https://www.inflearn.com/community/questions/950767/uint8-%EB%B3%80%EC%88%98-1-%EB%B9%84%ED%8A%B8-%ED%95%84%EB%93%9C-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4?srsltid=AfmBOorniv6N8BbF5szWhTkGsTz04OdS5zIfn_JWfWsBPHEY-8vuUvXK

 

uint8 변수:1; 비트 필드 사용 시 초기화 질문입니다. - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com

https://velog.io/@hon454/bool-%EB%8C%80%EC%8B%A0%EC%97%90-uint8uint16uint32-%EB%B3%80%EC%88%98%EB%AA%851%EC%9D%84-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0

 

bool 대신에 uint8 <변수명>:1을 쓰는 이유

CPP 프로그래밍을 하다보면 종종 아래와 같은 코드를 보게 된다. 변수명만 보아도 이것은 코딩스탠다드도 bool 변수와 동일하고 실제 사용법도 bool 변수와 사실상 동일하게 사용된다.

velog.io

https://velog.io/@jellypower/UE5-Server%EC%97%90%EC%84%9C-AnimNotify%EA%B0%80-%EC%A0%9C%EB%8C%80%EB%A1%9C-Trigger%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EA%B2%BD%EC%9A%B0

 

UE5) Server에서 AnimNotify가 제대로 Trigger되지 않는 경우

데디케이티드, 리슨 서버에서 AnimNotfy로 설정한 Trigger가 제대로 동작하지 않는 경우가 있다.이에 대한 이유는 서버에서의 애니메이션 설정이 다른것 + 언리얼 엔진 자체가 애니메이션을 제대로

velog.io