오늘은 Listen Server 환경에서 캐릭터 공격이 서버와 클라이언트 간에 어떻게 동기화 되는지 구현해보고자 합니다.
멀티플레이 환경에서는 각 플레이어의 행동이 다른 플레이어에게도 동일하게 보이도록 동기화 과정이 필요합니다.
특히 공격처럼 애니메이션, 판정, 데미지 처리가 함께 이루어지는 기능은 서버와 클라이언트 간 역할을 명확히 나누는 것이
중요합니다.

캐릭터 공격 처리 순서

#1. 클라이언트가 공격을 입력한 경우
- 클라이언트는 먼저 자신의 공격 애니메이션을 즉시 재생하고, 동시에 Server RPC를 통해 서버에 공격 요청을 전달합니다.


void AABCharacterPlayer::AttackHitCheck()
{
// 공격 판정은 중요한 로직이기 때문에 서버에서 처리.
//if(HasAuthority())
if(IsLocallyControlled())
{
// 로그 출력.
AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
FHitResult OutHitResult;
FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);
const float AttackRange = Stat->GetTotalStat().AttackRange;
const float AttackRadius = Stat->GetAttackRadius();
const float AttackDamage = Stat->GetTotalStat().Attack;
const FVector Forward = GetActorForwardVector();
const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * AttackRange;
bool HitDetected = GetWorld()->SweepSingleByChannel(
OutHitResult,
Start,
End,
FQuat::Identity,
CCHANNEL_ABACTION,
FCollisionShape::MakeSphere(AttackRadius), Params);
// 서버 기준 현재 시간.
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());
}
}
}
}
#2 서버가 공격 요청을 받은 경우
- 서버는 공격 가능 여부를 검증한 뒤 공격 상태를 갱신하고, 다른 클라이언트에게 공격 애니메이션이 재생되도록 동기화 합니다.
MulticastRPC 사용 개선
멀티플레이 환경에서 모든 상황에 Muticast RPC를 사용하는 것은 네트워크 데이터 전송량을 불필요하게 증가시킬 수 있습니다.
예를 들어 클라이언트가 2명 이상 존재하는 상황을 생각해보겠습니다.
Client1이 공격 입력을 수행하면, 먼저 Server RPC를 통해 서버에게 자신의 행동을 전달합니다.
이후 서버는 해당 요청을 처리한 뒤 Multicast RPC를 통해 모든 클라이언트에게 공격 애니메이션 또는 상태 정보를 전달합니다.
이때 Client2 입장에서는 Client1 의 행동을 처음 전달받는 것이므로 필요한 데이터입니다.
하지만 Client1은 이미 Local에서 공격 애니메이션을 재생하고 상태를 처리한 상태이기 때문에, 다시 같은 정보를 전달받으면 중복 처리가 발생할 수 있습니다.
또한 Listen Server 환경에서는 서버 측에서도 이미 공격 요청을 처리했기 때문에, Multicast를 통해 서버와 요청 클라이언트까지 다시 동일한 정보를 받게 되면 불필요한 네트워크 전송과 중복 로직이 발생할 수 있습니다.
따라서 모든 대상에게 무조건 Multicast RPC를 보내기보다는, 이미 처리가 완료된 서버와 요청 클라이언트는 제외하고, 해당 정보를 처음 받아야 하는 다른 클라이언트들에게만 전달하는 방식으로 개선할 수 있습니다.
이를 통해 중복된 데이터 전송을 줄이고, 네트워크 사용량을 보다 효율적으로 관리할 수 있습니다.

// 이 함수는 서버에서 실행됨.
void AABCharacterPlayer::ServerRPCAttack_Implementation(float AttackStartTime)
{
// 공격 시작 처리.
bCanAttack = false;
OnRep_CanAttack();
// 서버에 전달된 시간 차를 확인.
// 서버 기준 현재 시간에서 클라이언트가 전달한 시간을 뺌.
AttackTimeDifference = GetWorld()->GetRealTimeSeconds() - AttackStartTime;
// 시간 차가 어느정도인지 로그 출력.
AB_LOG(LogABNetwork, Log, TEXT("LagTime: %f"), AttackTimeDifference);
// 타이머설정할때 0또는 음수로 설정하면 동작 안함.
// 타이머가 동작은 하도록 값 보정.
AttackTimeDifference = FMath::Clamp(AttackTimeDifference, 0.0f, AttackTime - 0.01f);
// 타이머 설정.
// 공격 종료 시간을 계산할 때,
// 애니메이션 재생시간에서 클라에서 서버까지 메시지가 전달되는데까지
// 걸린시간을 고려해서 설정.
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(
Handle,
FTimerDelegate::CreateLambda(
[&]()
{
// 공격 종료 처리.
bCanAttack = true;
OnRep_CanAttack();
}
), AttackTime - AttackTimeDifference, false
);
// 클라이언트가 공격을 시작한 시간을 저장 (기록)
LastAttackStartTime = AttackStartTime;
// 서버에서도 애니메이션 재생.
PlayAttackAnimation();
// 멀티캐스트 RPC 호출
// MulticastRPCAttack();
// 위에 멀티캐스트 RPC 대신 필요한 클라이언트에만 ClientRPC를 호출.
for(APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
{
// 2개 필터링.
// 필터링 대상#1: 요청한 클라이언트.
// 필터링 대상#2: 서버에 있는 PlayerController.
// #1: 요청한 클라이언트의 PlayerController 필터링.
// GetController()는 클라이언트가 공격 입력을 눌러서 서버 RPC를 호출했을 때
// 서버에서 실행되는 함수.
// 서버에 있는 클라이언트 A의 캐릭터 복제 원본에서
// ServerRPCAttack_Implementation() 실행
if (PlayerController && PlayerController != GetController())
{
// #2: 추가로 필터링 서버에서 제어하는 PlayerController 필터링
if(!PlayerController->IsLocalController())
{
// 해당 클라이언트한테 애니메이션 재생 전달.
AABCharacterPlayer* OtherPlayer = Cast<AABCharacterPlayer>(PlayerController->GetPawn());
if(OtherPlayer)
{
// ClientRpc를 통해서 아래 로직을 수행 요청.
// OtherPlayer->PlayerAttackAnimation()
OtherPlayer->ClientRPCPlayAnimation(this);
}
}
}
}
}
#3. 공격 판정 처리
- 공격 판정은 최종적으로 서버에서 검증하며, 유효한 공격으로 판단되면 데미지 처리를 수행합니다.
'Unreal Project > Unreal Study' 카테고리의 다른 글
| Unreal Engine Listen Server _ 04 ( 커스텀 이동 구현(Teleport) ) (0) | 2026.06.10 |
|---|---|
| Unreal StateTree로 NPC 대기와 순찰 AI 구현하기 (0) | 2026.06.08 |
| 언리얼 팀프로젝트 회고 (0) | 2026.06.07 |
| Data_Asset 및 Quater View, Shoulder View 구현 (0) | 2026.02.26 |
| Unreal Collision에 관하여 (0) | 2025.10.31 |