멀티 플레이 환경에 맞게 함수 리팩토링
공격 가능한지 판단할 변수 추가
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 호출 보장하지 못함 - 해결 방법
- 엔진 코드 수정 (PostAnimEvaluation)
- Always Tick Pose를 Always Tick Pose and Refresh Bone으로 변경
- 애니메이션 몽타주에서 Montage Tick Type을 Branching Point로 변경
ㅡ> Branching Point로 하면 해당 프레임의 애니메이션 Tick에 도달하면 바로 Notify 실행
Queued일 경우 Notify 실행이 End of Evaluation Phase에서 이루어져 실행이 지연될 수 있음
- 추가로 현재의 구조는 몽타주를 서버에서도 실행하게 되는 구조 ㅡ> 서버 부하 증가
- 클라에서 애니메이션 실행 및 Notify를 통해 콜리전 관련 오버랩 감지
- 이후 데미지를 적용하기 위한 로직만 서버에서 담당하게 한다면 서버에서는 몽타주를 재생할 필요 X
ㅡ> 결과적으로 서버의 부하를 낮출 수 있음 - 동기화 관련 문제가 어떤 식으로 발생할지 고려해 봐야함
참고자료
uint8 변수:1; 비트 필드 사용 시 초기화 질문입니다. - 인프런 | 커뮤니티 질문&답변
누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.
www.inflearn.com
bool 대신에 uint8 <변수명>:1을 쓰는 이유
CPP 프로그래밍을 하다보면 종종 아래와 같은 코드를 보게 된다. 변수명만 보아도 이것은 코딩스탠다드도 bool 변수와 동일하고 실제 사용법도 bool 변수와 사실상 동일하게 사용된다.
velog.io
UE5) Server에서 AnimNotify가 제대로 Trigger되지 않는 경우
데디케이티드, 리슨 서버에서 AnimNotfy로 설정한 Trigger가 제대로 동작하지 않는 경우가 있다.이에 대한 이유는 서버에서의 애니메이션 설정이 다른것 + 언리얼 엔진 자체가 애니메이션을 제대로
velog.io
'언리얼엔진(UE) > 프로젝트' 카테고리의 다른 글
미친 선인장과 분노의 버섯 - #13 카메라 회전 트러블 슈팅 + 클라이언트 UI 상태 변화에 따른 업데이트 (0) | 2025.04.11 |
---|---|
미친 선인장과 분노의 버섯 - #12 네트워크 환경에서의 동기화 및 UX(사용자 경험) 개선 (0) | 2025.04.10 |
미친 선인장과 분노의 버섯 - #10 동작에 알맞은 콜리전, Hit 관련 정보 매핑 구조 설계 (0) | 2025.04.08 |
미친 선인장과 분노의 버섯 - #9 격투게임을 위한 사용자 정의 ApplyDamage(Helper class), TakeDamage(Interface) 구현 (0) | 2025.04.07 |
[Project] 미친 선인장과 분노의 버섯 - #8 애니메이션을 이용한 공격 판정 로직 (0) | 2025.04.04 |