본문 바로가기
Unreal 게임 개발/Unreal 강의 개인 정리

네트워크 공격 기능 개선 & 최적화 - Unreal Network MultiPlayer Framework

by daisy0461 2025. 3. 11.

이전 글 처럼 공격 로직을 만들면 별 문제가 없어 보이고 에디터에서 정상적으로 동작했다.

하지만 통신 부하가 발생하는 경우에는 문제가 발생할 수 있다.

 

통신 렉이 심한 경우 공격 모션이 늦게 재생되거나 시각적인 타이밍과 클라이언트와 서버가 일치하지 않을 수 있다.

이를 확인하기 위해 DefaultEngine.ini에 다음과 같이 추가해준다.

이렇게 작성하면 500ms. 즉, 0.5초 딜레이를 가지고 패킷을 전송한다.

이동만 해도 다음처럼 이동에 사용되는 패킷이 유효하지 않다는 경고 로그를 찍고 한박자 느리게 움직인다.

Attack을 해봐도 서버보다 클라이언에서 한박자 느리게 움직인다.

이렇게 타이밍이 맞지 않으면 플레이 경험적으로 좋지 않다.

 

이러한 현상이 발생하는 원인의 순서는 다음과 같다.

  1. 클라이언트에서 Attack or Move 입력 명령
  2. 입력이 서버로 전달되는 과정에서 시간이 걸림
  3. 서버에서 ServerRPC_Implemetation을 실행하고 NetMulticast RPC를 호출함.
  4. NetMulticast RPC에서 다시 클라이언트까지 전달되는 과정에서 시간이 걸림.
  5. 클라이언트에 명령이 오면 이제 Attack Animation 실행
  6. Attack의 Hit 판정을 Attack Animation의 Notify로 했다면 서버의 공격 판정도 늦게 판정된다.
  7. 전체적인 공격 Animation과 판정이 다 느리게 적용된다.

이렇게 모든 것을 서버에서 처리하는 방식이 데이터 관리 측면에선 안전하지만

통신 상태가 나쁘다면 사용자는 공정한 게임플레이를 하지 못하고 정확한 판정 또한 낼 수 없다.

 

앞에서 했던 코드에서 수정할 리스트이다.

  1. 클라이언트에서 처리할 수 있는 기능은 최대한 클라이언트에서 직접 처리
  2. 최정 판정은 서버에서 하지만 다양한 로직을 활용해 자세하게 검증한다.
  3. 네트워크 데이터 전송은 최소화한다.

이제 순서를 바꿔보자. (렉이 있다는 가정)

  1. 입력 명령과 동시에 ServerRPC를 호출하고 Animation을 재생한다.
    ServerRPC에서 현재 클라이언트의 시간 정보를 포함해서 보낸다.
  2. 서버에서 패킷이 왔을 때 Animation을 재생하면 클라이언트와 서버 간에 Animation 재생 타이밍이 다르다.
    이때 서버에서 패킷을 받을 때 시간정보를 비교해서 얼마나 렉이 걸렸는지 확인한다.
    해당 시간 정보를 활용해 중요한 정보는 동기화되도록 한다.
  3. 공격의 판정도 1차적으론 서버에서 하지 않고 클라이언트에서 판정한다.
    현재 판정이 재생하는 Animation에 따라 결정이 되는데 (Anim Notify 사용 시)
    서버와 클라이언트는 렉 때문에 서로 다른 타이밍에 Animation을 재생한다.
    그렇기 때문에 서버에서 판정하지 않고 클라이언트에서 판정한다.
  4. 하지만 클라이언트에서 판정을 클라이언트가 악의적으로 사용가능하기에
    판정 결과를 서버로 보내고 판정 결과를 서버가 해당 결과를 수용할지 검증한다.
  5. 클라이언트가 보낸 판정결과를 수용하면 Actor에게 데미지를 전달한다.

이제 코드로 예시를 보자

일단 h파일이다.

PlayAttackAnimaion()을 통해 바로 Animaion을 재생할 수 있도록 추가해준다.

ServerRPC에 위에서 설명과 같이 float를 파라미터로 추가해서 넘겨줄 수 있도록 변경한다.

마지막에 공격한 시간을 LastAttackTime, 서버와 클라이언트 간 시간 차이를 기록하는 AttackTimeDifference를 추가한다.

MulticastRPCAttack도 Reliable에서 Unreliable로 바꿨다. 특히 모든 Multicast는 Unreliable로 설정하는 것이 성능적으로 좋다.

이제 Attack에서 클라이언트일 경우 Attack을 하도록 제작한다.

클라이언트의 타이머를 돌리고 클라이언트에서 AttackAnimation을 재생하도록 한다.

이때 ServerRPC에 시간을 보내기 위해서 Attack 함수 가장 아래에서 Server시간을 받아와 ServerRPC를 넣는다.

OnRep_CanAttack이 빠졌는데 서버에서 Movement를 조절하면 또 딜레이 될 수 있기 때문에 여기서 동일하게 처리한다.

이제 ServerRPC에서 AttackDifference를 현재 시간 - Attack이 입력된 시간을 한다.

이것을 Timer에 넣어 클라이언트의 Attack과 시간 차이가 나지 않도록 한다.
그리고 위 Attack에선 없던 OnRep_CanAttack이 있다. 이것도 그냥 위처럼 대체해도 괜찮다.

그리고 서버에 있는 캐릭터도 Attack Montage를 Play시켜준다.

그렇기에 서버에 있는 캐릭터와 클라이언트에 있는 캐릭터의 Animation 재생 시점이 다르다.

이건 주석에 나와있는 내용 그대로이다.

서버에서 실행하면 MulticastRPC를 통해 다른 클라이언트에게 Animation을 재생하도록 한다.

 

이제 판정부분을 수정하면 된다.

판정에 사용될 ServerRPC를 2개 추가한다.

Hit을 체크하는 곳에 서버인지 클라이언트인지 구분하고 클라이언트에서 HitDetected 됐다면

서버에서 검증하도록 하고 서버의 캐릭터가 HitDeteced됐다면 바로 검증만 해준다.

Server에서 ServerRPC를 수행한다.

HitResult의 Hit Actor를 들고와서 Actor를 감싸는 Box(BoundingBox)의 Center를 구한다.

그리고 그 Center와 HitLocation과의 거리가 일정 거리 이하면 서버에서 Hit검증을 한번 더 해준다.

 

이렇게 되면 렉이 있어도 이전보다는 더 부드럽고 정확하게 Hit과 관련된 로직을 만들 수 있다.

 

 

추가적인 최적화

다음과 같이 일반 FVector를 사용하는 것 보다 FVector_NetQuantize를 사용하는 것이 용량이 더 작아서

네트워크 데이터 전송량을 최적화 할 수 있으며
TraceDir과 같은 노말Vector는 FVector_NetQuantizeNormal로 더 적은 용량으로 보낼 수 있다.

하지만 데이터가 작아지기에 데이터 로스가 일어날 수 있다.

 

그리고 ServerRPCAttack_Implementation이다.

원래 ServerRPCAttack_Implementation에선 마지막에 Multicast를 사용해서 Animation을 Play했다.

이때 Multicast는 서버와 모든 클라이언트에게 전송이 되는데

서버는 ServerRPC에서 로컬은 Attack에서 이미 Animation을 진행중이다.

이러한 데이터도 줄이고 싶다면 다음과 같이 작성하면 된다.

이렇게 하면 Server도 제외되고 로컬 PlayerController도 제외가 되고 다른 나머지 PlayerController에

ClientRPC 명령을 보내서 Animation을 재생하도록 한다. 해당 RPC는 간단하게 Animation만 재생한다.

 

다음과 같이 연관성을 고려하지 않으면 Multicast를 사용하는 것보다 Client를 사용하는 것이 통신 횟수도 줄이고 더 빠르게 전송할 수 있다.