TIL

[Unreal Engine 멀티플레이]전투·애니메이션·컴포넌트 동기화

think95592 2026. 8. 5. 17:22

핵심 원칙

전투 기능은 입력, 서버 판정, 애니메이션, 피해 상태, UI를 분리한다.

클라이언트 입력
→ Server RPC
→ 서버 검증·명중 판정·피해 적용
→ 지속 상태 복제
→ 각 클라이언트 UI 갱신

클라이언트는 공격 의도를 요청하고 화면을 표현한다. 서버는 공격 가능 여부, 명중 대상, 피해량, 사망을 최종 결정한다.

RPC와 Property Replication 구분

  • Server RPC: 클라이언트가 서버에 공격을 요청할 때 사용한다.
  • NetMulticast RPC: 공격 몽타주, 타격 이펙트, 사운드처럼 한 번 재생하면 끝나는 표현에 사용한다.
  • Property Replication: 체력, 사망, 보호막처럼 늦게 접속한 플레이어도 현재값을 알아야 하는 상태에 사용한다.

예를 들어 공격 이펙트는 늦게 접속한 플레이어가 과거분까지 볼 필요가 없지만, 체력 70과 사망 여부는 반드시 현재 상태로 받아야 한다.

서버 권위 공격 설계

클라이언트는 다음처럼 최소 정보만 보낸다.

ServerRequestAttack();

클라이언트가 대상, 피해량, 치명타 여부를 보내면 조작할 수 있으므로 서버가 직접 결정한다.

서버 검증 항목:

  • 살아 있는가
  • 현재 공격 가능한 상태인가
  • 쿨다운이 끝났는가
  • 무기 상태가 올바른가
  • 서버 기준 공격 범위 안에 대상이 있는가
  • 서버 Trace 또는 Overlap 결과로 실제 명중했는가

Trace와 충돌 옵션

LineTraceSingleByChannel은 서버가 공격자 앞에 있는 대상을 찾는 가상 선 충돌 검사다.

FCollisionQueryParams Params(
    SCENE_QUERY_STAT(MeleeAttack),
    false,
    this);

GetWorld()->LineTraceSingleByChannel(
    Hit, Start, End, ECC_Pawn, Params);

Params를 먼저 만드는 이유는 충돌 검사 규칙을 설정하기 위해서다.

  • this: 공격자 자신은 충돌 검사에서 제외한다.
  • false: 단순 충돌을 사용한다.
  • ECC_Pawn: Pawn 충돌 채널을 검사한다.

자기 자신을 제외하지 않으면 공격 시작점 근처의 공격자 캡슐을 먼저 맞힐 수 있다.

공격 상태와 중복 피해 방어

서버는 공격 상태를 소유한다.

Idle → Windup → Active → Recovery → Idle

Idle 상태일 때만 새 공격 요청을 승인한다. 따라서 클라이언트가 공격 요청을 여러 번 보내도 첫 요청 뒤에는 이미 Windup 상태이므로 나머지 요청을 거부할 수 있다.

같은 휘두름 중 같은 대상에게 여러 프레임 피해가 들어가는 문제는 서버 전용 피격 대상 집합으로 막는다.

TSet<TObjectPtr<AActor>> HitActorsThisAttack;

한 공격에서 이미 맞힌 대상이면 다시 피해를 적용하지 않는다. 이 목록은 UI나 늦은 접속자에게 필요하지 않은 서버 내부 처리이므로 복제하지 않는다.

애니메이션과 피해 판정

공격 몽타주는 표현이고, 피해 판정은 게임 규칙이다.

  • 몽타주가 보인다고 명중한 것은 아니다.
  • 클라이언트 AnimNotify로 피해를 확정하면 안 된다.
  • 서버가 공격 시작 후 정해진 공격 창에서 Trace/Overlap을 실행해 명중을 판정한다.
  • 긴 공격·차징처럼 진행 시점이 중요한 몽타주는 서버 시작 시각 또는 공격 상태를 함께 동기화할 수 있다.

AimOffset 동기화

내 캐릭터는 입력 직후 ControlRotation을 바꾸고 즉시 AimOffset을 계산해 표현한다. 다른 클라이언트는 서버가 복제한 캐릭터 회전과 필요한 조준 원본을 받아 자기 AnimInstance에서 AimOffset을 계산한다.

내 캐릭터: 로컬 입력 → 즉시 표현 → 서버 검증
남의 캐릭터: 서버 복제본 수신 → 로컬 AnimInstance 표현

애니메이션 파라미터인 AimYaw, AimPitch는 보통 복제하지 않고, 각 클라이언트가 회전 원본으로 계산한다.

HealthComponent와 UI

체력과 사망 상태는 UHealthComponent에 둔다.

서버 HealthComponent가 원본 변경
→ CurrentHealth / bIsDead 복제
→ 클라이언트 OnRep 실행
→ UI 체력바 갱신

UI는 체력을 직접 바꾸거나 판정하지 않는다. 복제된 현재값을 읽어 그리기만 한다.

또한 UI가 복제 알림보다 늦게 생성될 수 있으므로, 이벤트 구독 후 현재 체력값으로 한 번 초기화해야 한다.

디버깅 기준

“공격 애니메이션은 보이는데 체력이 안 깎인다”면 다음 순서로 확인한다.

  1. 서버가 ServerRequestAttack을 승인했는가?
  2. 서버의 공격 상태가 올바르게 전환됐는가?
  3. 서버 HitWindow가 시작됐는가?
  4. 서버 Trace의 Start, End, 충돌 채널, 자기 자신 제외 설정이 맞는가?
  5. HitActor가 실제 대상인가?
  6. 서버가 ApplyDamageOnServer를 호출했는가?
  7. CurrentHealth 복제와 클라이언트 OnRep가 실행됐는가?

HitActor=None이면 애니메이션 재생 여부와 별개로 서버 명중 판정이 실패한 것이므로 체력이 줄지 않는 것이 정상이다.