캐릭터 무브먼트 컴포넌트

Character Movement Component, 캐릭터 무브먼트 컴포넌트에 대한 상세 설명입니다.

Choose your operating system:

Windows

macOS

Linux

CharacterMovementComponent 를 사용하는 캐릭터는 자동으로 클라이언트-서버 네트워킹이 탑재됩니다. CharacterMovementComponent 를 통한 네트워크 게임에서의 매 프레임 플레이어 이동 예측, 리플리케이션, 보정 작동 방식은 이렇습니다:

  • TickComponent 함수가 호출됩니다.

  • 프레임에 대한 가속도 및 회전 변화를 계산합니다.

  • (로컬 제어 캐릭터의 경우) PerformMovement 또는 (네트워크 클라이언트의 경우) ReplicateMoveToServer 를 호출합니다.

ReplicateMoveToServer 는 (PendingMove 목록에) 이동을 저장하고 PerformMovement 를 호출한 뒤, 리플리케이트되는 함수 ServerMove 에 무브먼트 파라미터, 클라이언트의 결과 위치, 타임스탬프를 전달하여 호출하는 것으로 이동을 서버에 리플리케이트합니다. 이는 서버에서 실행되며, 여기서 무브먼트 파라미터를 디코딩한 다음 적합한 이동이 발생하도록 합니다. 그런 다음 결과 위치를 살펴보고, 지난 클라이언트 응답 이후의 시간을 알아보고, 클라이언트에서 말하는 위치와 서버에서 결정된 위치 사이의 차이를 알아봅니다. 차이가 충분이 큰 경우, 서버는 ClientAdjustPosition 을 호출하고, 이는 클라이언트에 리플리케이트되면서 보정된 위치도 함께 전달합니다.

클라이언트에서 TickComponent 가 다시 호출되고, 서버에서의 보정을 수신한 경우, 클라이언트는 ClientUpdatePosition 을 호출한 뒤 PerformMovement 를 호출합니다. 이 프로세스는 서버가 조정한 이동의 타임스탬프 이후 발생한 이동 대기 목록의 모든 이동을 재생합니다.

캐릭터 무브먼트 및 시뮬레이티드 프록시

지금까지 설명한 CharacterMovementComponent 의 접근법은 권위적 서버에 접속된 클라이언트 하나에 대해서만 자세히 다루고 있습니다. AI 조종 캐릭터나 원격 컴퓨터의 플레이어는 "시뮬레이티드 프록시" 로 간주되며, 약간 다른 코드 패스를 통합니다.

원격 제어 캐릭터의 이동은 보통 일반적인 PerformMovement 코드를 사용하여 (오소리티가 있는) 서버에서 업데이트됩니다. 위치, 회전, 속도와 같은 액터 상태는 물론 기타 (점프나 웅크리기같은) 캐릭터 전용 정보도 일반 리플리케이션 메커니즘을 통해 다른 머신으로 리플리케이트됩니다. 즉 로컬 조종 캐릭터처럼 매 프레임 업데이트를 받을 필요가 일반적으로는 없다는 뜻입니다. 그러한 캐릭터가 더욱 부드러워 보이게 하기 위해서, 클라이언트 머신은 서버에서 새로운 오소리티가 있는 데이터가 도착할 때까지 시뮬레이티드 프록시에 대해 매 프레임 시뮬레이션 업데이트를 실행합니다. 반면 AI 조종 캐릭터는 보통 서버에서 바로 실행되므로, 원격 플레이어는 서버에 자체 로컬 업데이트 정보를 보내고, 그 후 그 플레이어에 대한 무브먼트 업데이트를 완벽히 한 다음, 주기적으로 그 데이터를 다른 모든 플레이어에게 리플리케이트합니다.

리플리케이션 상태에 따른 예상 이동 결과 시뮬레이션은 다음 서버 오소리티가 있는 업데이트까지 이성적인 예측으로 "간극을 메우기" 위한 것입니다. 다음 업데이트가 도착하면, 실제적으로 로컬 시뮬레이션을 리셋시킨 다음 최신 정보를 기반으로 새로 시작시킵니다.

시뮬레이티드 프록시에 대한 무브먼트 업데이트 다수는 UCharacterMovementComponent::SimulateMovement 에서, 그 이후 MoveSmooth 에서 차례로 이루어집니다. MoveSmooth 는 다양한 (보행, 비행 등의) 이동 모드에 대한 풀버전 이동 모드 업데이트를 간소화시킨 버전으로, 실행 비용도 싸고 의도상 복잡도도 덜합니다.

시뮬레이티드 프록시 스무딩

캐릭터가 단순히 전방 이동하는 경우, 시뮬레이션된 업데이트는 다음 리플리케이션 업데이트에 매우 근접할 확률이 높습니다. 직선 운동은 예측이 꽤나 간단하기 때문입니다. 월드에 고정된 벽에 달려가는 경우에도, 그 편차나 잇따른 업데이트 역시 높은 정확도로 시뮬레이션 가능할 것입니다.

하지만 기존 리플리케이트된 상태의 스냅샷 기반 로컬 시뮬레이션이 실제 올바른 위치에서 벗어나게 되는 경우가 몇 가지 있습니다. 특히나 사람이 조종하는 캐릭터가 그렇습니다. 어떤 속도로 움직이는 캐릭터가 있다고 리플리케이트된 상태를 가정해 봅시다. 다음 업데이트를 기다리는 동안, 시뮬레이티드 프록시는 계속해서 그 속도로 움직일 것입니다. 하지만 원격 캐릭터가 속도 업데이트를 보낸 직후 이동을 멈출 수도 있습니다. 로컬 시뮬레이션에서는 이를 알 방도가 없기에, 다음 서버 업데이트가 도착할 때까지 잘못된 예측을 하게 됩니다.

서버 업데이트를 받았을 때 시뮬레이티드 프록시의 위치가 시각적으로 "튀어" 보이는 것을 방지하기 위해, UCharacterMovementComponent::SmoothClientPosition 함수를 사용하여 캐릭터의 시각적 표현 위치에 스무딩 작업을 해 줍니다. 이는 기본적으로 (클라이언트의 네트워크 데이터 상의 "SmoothNetUpdateTime" 으로 설정된) 특정 기간 내 대상 위치에 도달하도록 하는 단순한 스무딩을 적용하는 것입니다.

CharacterMovementComponent 네트워킹 디버깅

캐릭터 네트워킹의 디버깅 및 분석을 위한 유용한 툴이 몇 가지 있습니다. 무언가 잘못되어 보이는 클라이언트의 콘솔에서 보통 먼저 해 보면 좋은 것은, "p.NetShowCorrections 1" 를 입력하는 것입니다 (Shipping 빌드가 아닌 경우에만 작동합니다). 이 옵션을 서버에서 켜는 것도 좋을 수 있습니다. 이를 통해 출력 콘솔의 로그는 물론 콜리전 모양의 (초록색) "올바른" 위치 및 (빨강색) "잘못된" 위치를 그려주는 것으로 클라이언트에서 수신된 (또는 서버에서 전송된) 네트워크 보정 정보를 확인할 수 있습니다. 클라이언트에서 "올바른" 위치는 서버에서 보정을 위해 전송된 것인 반면, "잘못된" 위치는 서버에서 오차 허용 범위를 벗어난 것으로 판단된 로컬 위치입니다. 서버에서도 그 개념은 비슷합니다. "올바른" 서버 위치는 초록색으로 그려지는 반면, "잘못된" 클라이언트 수신 위치는 빨강으로 그려집니다. "p.NetCorrectionLifetime" 변수는 월드에서 디버그 시각화가 사라지기 전까지 유지되는 기간을 초 단위로 나타냅니다.

문제 진단에 도움이 되는 또 한가지 방법은, CharacterMovement 네트워크 무브먼트 함수에 의해 전송된 데이터 일부의 로그를 켜는 것입니다. "log LogNetPlayerMovement Verbose" 콘솔 명령으로 위치, 회전, 가속도를 포함한 캐릭터 무브먼트 데이터의 송수신 내역 각각에 대한 로그를 켭니다. 위치 변화가 서버에 리플리케이트되지 않는 방식으로 클라이언트에서 이루어져 발생한 오차 보정에 대한 설명이 될 수 있습니다.

이동 스피드 핵 방지 (네트워크 게임)

네트워크 게임은 보통 부정 행위를 통해 부당한 이득을 보려는 사용자들의 대상이 됩니다. 한 가지 일반적으로 게임에서 사용되는 부정 행위는, 치트 소프트웨어를 사용하여 게임 클라이언트에서 시간이 빨리 가도록 하는 방법입니다. 수정되지 않은 CharacterMovementComponent 함수성을 사용하는 게임은 이러한 부정 행위에 취약하여, 캐릭터가 정상보다 빠른 속도로 이동하는 것을 막을 수 없습니다. 이러한 부정 행위 방지를 위하여, CharacterMovementComponent 에 시간적 차이를 감지하여 해결하는 기능을 추가했습니다.

감지

시간차 감지 기법은, 클라이언트에서 온 ServerMove RPC 와 서버에 전달된 시간 중 알려진 (믿을만한) 것에 대해 서버가 비교하도록 시키는 것입니다. 이 차이는 누계 방식으로 기록되며, 그 누계가 사용자가 정의한 한계치를 초과하는 경우, 해법 조치를 취합니다.

해법

이용되는 해결 방법은 클라이언트에서 앞으로 오는 ServerMove RPC 의 타임스탬프가 서버 시간에 일치하도록 덮어쓰는 것입니다. 그 후 RPC 사이 캐릭터 (위치, 회전, 가속도 등)이동 데이터의 차이 일정 부분을 빼서, 클라이언트와 서버가 다시 동기화될 때까지 시간 차를 "보상"합니다.

환경설정

기본적으로 시간 차 감지 및 해법은 꺼져있습니다. GameNetworkManager 에서 환경설정 가능한 변수는 다음과 같으며, 그 기본값은 BaseGame.ini 에서 찾을 수 있습니다. 이 값은 프로젝트 전용 게임 환경설정 파일에서 덮어쓸 수 있습니다.

변수 이름

효과

bMovementTimeDiscrepancyDetection

감지를 켭니다. 감지시, 경고 로그가 발동됩니다. 해법이 켜진 경우, 같이 적용됩니다.

bMovementTimeDiscrepancyResolution

해법을 켭니다. 충분한 차이가 감지된 경우 클라이언트를 보정하고 시간을 돌려주도록 합니다.

MovementTimeDiscrepancyMaxTimeMargin

클라이언트의 시간이 서버 예상 게임 시간보다 이 이상 앞서면 감지/해법이 발동됩니다.

MovementTimeDiscrepancyMinTimeMargin

클라이언트의 시간이 서버 예상 게임 시간보다 이 이상 뒤쳐지면 감지/해법이 발동됩니다.

MovementTimeDiscrepancyResolutionRate

해법 도중 시간을 되돌려주는 속도입니다. 기본값은 100%, 즉 시간 빚을 다 갚을 때까지 클라이언트는 움직일 수 없다는 뜻입니다.

MovementTimeDiscrepancyDriftAllowance

클라이언트와 서버 사이에 허용되는 클락 변동 추이를 초당 백분율로 받습니다. 급작스런 패킷 손실이나 퍼포먼스 버벅임으로 인한 잘못된 긍정을 방지하는 데 도움이 됩니다.

bMovementTimeDiscrepancyForceCorrectionsDuringResolution

해법 도중 클라이언트 업데이트를 강제시킬지 여부입니다. 이동 오류 허용치가 관대하거나 ClientAuthorativePosition 옵션이 켜진 프로젝트에 필요합니다.

이 세팅들은 특정 프로젝트에 맞게끔 미세조정해야 할 필요가 있습니다. 기본적인 테스팅은 클라이언트에서 slomo 치트를 사용해서 할 수 있습니다. "p.DebugTimeDiscrepancy" 변수로 서버의 시간차 로깅을 켤 수 있습니다.

고급 토픽: 캐릭터 무브먼트에 새로운 무브먼트 능력 추가하기

캐릭터에 새로운 이동 능력을 추가하기 위한 방법은 여러가지 있습니다. 예를 들어, 캐릭터에 텔레포트 능력을 부여할 수 있습니다. 이 능력으로 T 키를 누르면 캐릭터 전방 10 미터 위치에 장애물이 없는 경우 텔레포트한다고 칩시다. 추가로 이 능력이 네트워크 게임에서도 작동해야 한다고 칩시다.

접근법 1: 클라이언트에서만 실행

한 가지 접근법은 장애물을 검사한 뒤, 없는 경우 캐릭터를 전방으로 10 미터 직접 이동시킵니다. 여기에는 네트워크를 고려하지 않으며, 단순히 클라이언트에서 실행합니다.

결과 : 네트워크 게임에서 실패합니다. 로컬 클라이언트에서는 잠깐 텔레포트했다가 시작 위치로 빠르게 되돌아옵니다.

분석 : 이는 단지 우리 코드가 이렇게 실행된다 하는 기준점입니다. 네트워크 게임이 아니라면 괜찮을 것입니다. 하지만 네트워크를 고려하지 않기에, 네트워크 게임에서는 실패할 것입니다. 서버는 이 스킬을 이해하지 못하므로, 클라이언트 캐릭터의 위치, 이동, 가속도를 사용하여 클라이언트의 최종 위치를 알아냅니다. 서버 입장에서 보면, 캐릭터의 예상 위치와 클라이언트에서 보내온 위치 차이가 10 미터 난다는 것은, 클라이언트 쪽의 오류입니다. 서버는 오류를 교정하고, 클라이언트는 로컬서에 서버의 교정을 적용하여, 텔레포트는 취소됩니다.

접근법 2: 서버만 RPC

네트워크 게임에서 가장 단순하게 통하는 접근법은, 서버가 그 능력을 처리한 뒤 클라이언트에게 결과를 알려주는 것입니다. 그렇게 하기 위해 Reliable Server 태그가 붙은 UFUNCTION 을 구성하여 클라이언트에서 발동되면 텔레포트 코드를 실행시키는 것입니다.

결과 : 네트워크 게임에서 통합니다. 하지만 그 작동 방식에서 몇 가지 문제점이 있습니다. 클라이언트에서는 스킬 사용시 지연시간이 눈에 띌 것이고, 순간적인 위치 이동이 아니라 부드러운 이동처럼 보일 것이라는 점입니다.

먼저, 플레이어가 능력을 사용했을 때 랙이 눈에 띌 텐데, 클라이언트에서의 명령이 서버에서 전송되고 거기서 실행된 뒤 전송되어 돌아오는 동안 클라이언트는 실제로 그런 일이 벌어졌는지도 모르는 것입니다. 둘째, 스무딩 코드로 인해 텔레포트가 부드러운 움직임처럼 보입니다. 위치가 즉시 바뀌어야 하는 느낌인데 말이지요. 셋째, 서버의 플레이어 이동은 보정으로 인식되는데, 실제 그렇지도 않은 데다 게임의 네트워크 플레이를 디버깅하려 할 때 실제 보정 마스킹도 걸러질 수가 있습니다.

분석 : 통하기는 하지만 이상적이지는 않습니다. 플레이어가 T 키를 눌러 텔레포트하면, 그 명령만 서버로 전송할 뿐, 로컬에서 벌어지는 일은 없습니다. 이로 인해 플레이어는 조작감이 떨어진다고 느낄 것이고, 플레이어는 텔레포트 명령이 통했는지 아닌지 바로 알 수가 없을 것입니다. 서버가 명령을 받으면 캐릭터는 움직이지만, 여전히 클라이언트에게 텔레포트이라고 알려주지는 않습니다. 다음 번 클라이언트가 업데이트를 전송할 때, 서버는 클라이언트가 보고한 위치가 10 미터 떨어져 있으니, 보정 전송합니다. 이 보정은 클라이언트가 서버의 예상 위치로 (부드럽게) 이동하게 만드는데, 실질적으로 텔레포트는 성공한 듯 해도 뭔가 틀려 보입니다. "p.NetShowCorrections" 를 1 로 설정했다면, 네트워크 보정이라는 알림이 뜰 것입니다. 추가로, 파티클이나 사운드 이펙트같은 것으로 능력을 다듬을래도, 실제 능력이 실행되는 곳은 서버이기에 제대로 표시되지도 않을 것입니다. 클라이언트가 언제 능력을 시도했는지 (그래서 명령을 서버로 전송했는지) 알고는 있지만, 성공했는지 실패했는지 보고도 없이 그냥 잠시 후의 보정만 표시되는 것입니다.

접근법 3: 서버 RPC 및 로컬 트리거

이 접근법에서, 클라이언트는 로컬에서 텔레포트를 실행한 뒤, 서버의 텔레포트 RPC 도 호출합니다.

결과 : 네트워크 게임에서 작동은 하며, 희박하지만 심각한 문제가 있을 수 있습니다.

분석 : 이 접근법은 기존 접근법의 단점, 말하자면 로컬에서의 이동 지연과, 이동이 능력으로 인한 것이 아닌 서버 보정으로 인해 발생했다는 사실을 보완하기 위한 것입니다. 사운드나 파티클 이펙트같은 텔레포트의 함수성도 전부 가능한데, 능력이 이제 클라이언트에서 실행되기 때문입니다. 이는 기존 방법보다 훨씬 잘 통하기는 하는데, 네트워크 환경에서는 깨질 수가 있다는 중대한 주의사항이 있습니다. 주요 문제점은 클라이언트가 텔레포트 발동 시점 이전으로 보정된 경우, 클라이언트가 이동 대기 목록을 반복하면서 텔레포트를 재발동시킬지 여부를 알 수가 없어, 클라이언트측에서 텔레포트가 손실된 것으로 보인다는 점입니다. 이로 인해 특정 상황에서, 특히나 네트워크 조건이 안좋을 경우, 게임 반응 속도가 떨어진다는 느낌을 줄 수 있습니다.

접근법 4: CharacterMovementComponent 능력 구현

이 접근법에서는, 텔레포트 능력에 대한 인지를 CharacterMovementComponent 자손 클래스에 추가합니다. 이 버전은 네트워크에서도 안정적으로 작동할 것입니다.

결과 : 네트워크 게임에서 작동하며, 구현시 몇 가지 주의하기만 하면 됩니다.

분석 : 여기에는 약간의 배경지식이 필요합니다: 앞서 언급했듯이, CharacterMovementComponent 를 사용하는 캐릭터는 이동 대기 목록같은 것에 입력 결과를 큐 형태로 저장합니다. 목록의 각 이동은 한 프레임에서 이동 시작시의 상태, 이를테면 (보통 플레이어 입력의 결과인) 위치, 회전, 가속도, 점프 상태 등을 기록합니다. 이동이 클라이언트에서 서버로 전송되면서, 그 이동을 유지하다가 서버가 그 완료를 승인하면서 이전 것들을 제거해 나갑니다. 서버 보정의 경우, 클라이언트는 그 보정이 언제 발생했는지 알고 있고, 해당 시점 이후 발생한 모든 이동을 "다시 재생"할 수 있습니다. 이것이 좋은 점이라면, 보정 시점 이후의 캐릭터 이동은 보정 이후 다시 적용될 것이기에, 플레이어는 보정이 있었는지 눈치채지도 못한다는 점입니다. 보정 이후 발동된 능력이 다시 적용된다는 뜻이기도 합니다. 이러한 경우 UCharacterMovementComponent::DoJump 함수에서 보듯이 bReplayingMoves 파라미터를 검사하여 특수효과 중복 재생을 방지할 수 있습니다.

텔레포트 능력 자체는 CharacterMovementComponent 의 점프 능력과 비슷한 방식으로 추가될 것입니다. 이 능력이 언제 발동되었는지를 표시하고, 서버에서 제대로 처리해 주도록 할 필요가 있습니다. 게임 반응 속도를 좋게 하기 위해 여전히 로컬에서 처리해 줘야 할 것입니다. 클라이언트와 서버 사이의 데이터 송수신은 기존 네트워크 처리의 일환으로 자동 처리되기에, 그냥 데이터 패킹과 언패킹만 해 주면 됩니다. UCharacterMovementComponent 에서 자손 클래스를 생성하고 나서는, AllocateNewMove 를 덮어써서 FSavedMove_Character 자체 버전을 생성할텐데, 여기에 이동 대기 목록을 저장할 것입니다. FSavedMove_Character 자체 버전에는 덮어쓰여진 메서드가 몇 개 있을 것입니다. GetCompressedFlags 는 텔레포트 능력 발동을 알리기 위한 플래그를 새로 추가하여 덮어씁니다. UpdateFromCompressedFlags 역시 새로운 텔레포트 플래그를 언패킹하고 서버측에서 능력을 발동시키도록 업데이트했습니다. 이 메서드로 네트워크 게임에 안정적인 탄탄한 이동 능력을 만들 수 있을 것입니다.

언리얼 엔진 문서의 미래를 함께 만들어주세요! 더 나은 서비스를 제공할 수 있도록 문서 사용에 대한 피드백을 주세요.
설문조사에 참여해 주세요
건너뛰기