핵심 원칙
전투 기능은 입력, 서버 판정, 애니메이션, 피해 상태, 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가 복제 알림보다 늦게 생성될 수 있으므로, 이벤트 구독 후 현재 체력값으로 한 번 초기화해야 한다.
디버깅 기준
“공격 애니메이션은 보이는데 체력이 안 깎인다”면 다음 순서로 확인한다.
- 서버가 ServerRequestAttack을 승인했는가?
- 서버의 공격 상태가 올바르게 전환됐는가?
- 서버 HitWindow가 시작됐는가?
- 서버 Trace의 Start, End, 충돌 채널, 자기 자신 제외 설정이 맞는가?
- HitActor가 실제 대상인가?
- 서버가 ApplyDamageOnServer를 호출했는가?
- CurrentHealth 복제와 클라이언트 OnRep가 실행됐는가?
HitActor=None이면 애니메이션 재생 여부와 별개로 서버 명중 판정이 실패한 것이므로 체력이 줄지 않는 것이 정상이다.
'TIL' 카테고리의 다른 글
| [Unreal Engine 멀티플레이] 패키징과 Dedicated Server 배포 준비 (0) | 2026.08.07 |
|---|---|
| [Unreal Engine 멀티플레이] 게임 사이클과 복제 최적화 (1) | 2026.08.06 |
| [Unreal Engine 멀티플레이] 남은 시간이 아닌 마감 시각을 복제하는 서버 권위 타이머 (0) | 2026.08.04 |
| [Unreal Engine 멀티플레이] 서버 권위 기반 숫자 야구 구현 (0) | 2026.08.03 |
| [Unreal Engine 멀티플레이] NetRole과 RPC 기초, Ownership, 채팅 시스템 흐름 (1) | 2026.07.31 |