수정중인 페이지 입니다.

Fusion 103 - 예측

개요

Fusion 103은 예측과 권위 있는 서버 네트워크 게임에서 클라이언트에 대한 빠른 피드백을 제공하는 데 어떻게 사용되는지 설명할 것입니다.

이 섹션이 끝날 때, 프로젝트는 플레이어가 예측된 kinematic 볼을 생성할 수 있도록 합니다.

이 주제에 관련된 상세 설명은 매뉴얼을 참고하세요

메인 화면으로

Kinematic 객체

아무 객체나 스폰 하기 위해서, 프리팹이 있어야 합니다.

  1. 유니티 에디터에서 새로운 빈 GameObject를 생성합니다
  2. Ball로 이름을 부여합니다
  3. 여기에 NetworkTransform 컴포넌트를 추가합니다.
  4. Fusion will show a warning about a missing NetworkObject 컴포넌트가 빠져 있다고 Fusion이 경고를 나타내므로, Add Network Object를 누릅니다.
  5. Interpolation Data Source 애서 Predicted로 변경하고 World Space로 설정합니다.
  6. Sphere child to the Ball에 구체 자식을 추가합니다.
  7. 모든 방향에서 스케일을 0.2로 낮춥니다.
  8. 상위 개체에 있는 NetworkTransform 컴포넌트의 InterpolationTarget으로 하위 항목을 드래그합니다. 이를 통해 NetworkTransform은 주 네트워크 객체 자체(네트워크 상태로 스냅될)에서 매끄럽게 보간되는 시각적(하위 객체)을 분리할 수 있습니다..
  9. 자식 구체에서 콜라이더를 제거합니다
  10. 대신 상위 객체에 새 구체 콜라이더를 만들고 하위 객체의 시각적 표현을 완전히 덮을 수 있도록 반지름을 0.1로 지정합니다.
  11. 새로운 게임 객체에 새로운 스크립트를 추가하고 Ball.cs로 이름을 변경합니다.
  12. 마지막으로 전체 Ball 객체를 프로젝트 폴더로 끌어 프리팹을 만듭니다.
  13. 씬을 저장하여 네트워크 개체를 베이크하고 씬에서 프리팹 인스턴스를 삭제합니다.

Ball Prefab
Ball 프리팹

메인 화면으로

예측된 이동

목표는 Ball 의 인스턴스가 모든 피어에서 동시에 동일하게 동작하도록 하는 것입니다.

여기서 "동시적"은 "동일한 시뮬레이션 틱에서"를 의미하며, 동일한 실제 시간이 아닙니다. 이 작업을 수행하는 방법은 다음과 같습니다.

  1. 서버는 일정한 간격으로 특정 틱에서 시뮬레이션을 실행하고 각 틱에서 FixedUpdateNetwork()를 호출합니다. 서버는 한 틱에서 다음 틱까지 항상 앞으로 이동합니다. 이는 로컬 물리 시뮬레이션의 규칙적인 유니티 동작에 대한 FixedUpdate()와 정확히 같습니다. 각 시뮬레이션 체크 표시 후 서버는 이전 체크 표시에 대한 네트워크 상태의 변화를 계산, 압축 및 브로드캐스트합니다.
  2. 클라이언트는 이러한 스냅샷을 정기적으로 수신하지만 항상 서버보다 뒤처집니다. 스냅샷이 수신되면 클라이언트는 내부 상태를 해당 스냅샷의 틱으로 다시 설정한 다음 자체 시뮬레이션을 실행하여 수신된 스냅샷과 클라이언트 현재 틱 사이의 모든 틱을 즉시 다시 시뮬레이션합니다.
  3. 클라이언트의 현재 틱은 항상 서버보다 훨씬 앞서며, 서버가 지정된 틱에 도달하기 전에 사용자로부터 수집한 입력을 서버로 보낼 수 있으며 시뮬레이션을 실행하기 위한 입력이 필요합니다.

여기에는 다음과 같은 여러 가지 의미가 있습니다:

  1. 클라이언트는 프레임당 FixedUpdateNetwork()를 여러 번 실행하고 업데이트된 스냅샷을 수신할 때 동일한 체크 표시를 여러 번 시뮬레이션합니다. Fusion이 FixedUpdateNetwork()를 호출하기 전에 적절한 체크 표시로 재설정하기 때문에 네트워크 상태에 대해 작동하지만, 네트워크 상태가 아닌 경우에는 그렇지 않으므로 FixedUpdateNetwork()에서 로컬 상태를 사용하는 방법에 매우 주의해야 합니다.
  2. 각 피어는 알려진 이전 위치, 속도, 가속도 및 기타 결정론적 특성을 기반으로 객체의 예측된 미래 상태를 시뮬레이션할 수 있습니다. 한 가지 예측할 수 없는 것은 다른 참가자들의 입력이기 때문에 예측은 실패할 것입니다.
  3. 로컬 입력은 즉각적인 피드백을 위해 클라이언트에게 즉시 적용되지만, 신뢰할 수 있는 것은 아닙니다. 여전히 서버에서 생성된 스냅샷은 결국 입력의 틱 로컬 애플리케이션이 예측에 불과하다는 것을 정의합니다.

이러한 점을 염두에 두고 Ball 스크립트를 열고 기본 클래스를 NetworkBehaviour로 변경하여 Fusions 시뮬레이션 루프에 포함시키고 사전 생성된 boilerplate 코드를 Fusions FixedUpdateNetwork()의 오버라이드로 바꿉니다.

이 간단한 예에서는 공이 5초 동안 일정한 속도로 전진하다가 사라집니다. 다음과 같이 객체 트랜스폼에 간단한 선형 동작을 추가합니다:

using Fusion;

public class Ball : NetworkBehaviour
{
  public override void FixedUpdateNetwork()
  {
    transform.position += 5 * transform.forward * Runner.DeltaTime;
  }
}

시간 단계가 Time.deltaTime이 아니라 Runner.DeltaTime이라는 점을 제외하고는 일반 비네트워크 유니티 개체를 이동하는 데 사용되는 코드와 거의 동일합니다. 유니티 트랜스폼과 같이 겉보기에는 지역적인 특성으로 네트워크 전반에 걸쳐 작동하는 비결은 물론 앞서 추가한 NetworkTransform 컴포넌트이기도 합니다. NetworkTransform은 트랜스폼 속성이 네트워크 상태의 일부임을 확인하는 편리한 방법입니다.

코드는 설정된 시간이 경과한 후에도 물체를 사라지게 해야 하므로 무한대로 날아가지 않고 결국 플레이어의 목을 가격합니다. 퓨전은 타이머에게 편리한 헬퍼 타입을 제공하며, 적절한 이름은 TickTimer입니다. 현재 남은 시간을 저장하는 대신 종료 시간을 틱 단위로 저장합니다. 즉, 타이머를 모든 틱에서 동기화할 필요가 없으며 타이머가 생성될 때 한 번만 동기화하면 됩니다.

게임 네트워크 상태에 TickTimer를 추가하려면 TickTimer 유형의 life 속성을 볼에 추가하고, Getter와 Setter에 빈 스텁을 제공하고, [Networked] 속성으로 표시합니다.

[Networked] private TickTimer life { get; set; }

객체가 생성되기 전에 타이머를 설정해야 하며, 로컬 인스턴스가 생성된 후에만 Spawned()가 호출되므로 네트워크 상태를 초기화하는 데 사용하지 않아야 합니다.

대신 플레이어에서 호출할 수 있는 Init() 메소드를 만들고 이 메소드를 사용하여 수명 속성을 향후 5초로 설정하십시오. 이는 TickTimer 자체에서 정적 헬퍼 메소드 CreateFromSeconds()를 사용하는 것이 가장 좋습니다.

public void Init()
{
  life = TickTimer.CreateFromSeconds(Runner, 5.0f);
}

마지막으로, FixedUpdateNetwork()는 타이머가 만료되었는지 확인하고, 만료되면 볼의 없애주어야 합니다:

if(life.Expired(Runner))
  Runner.Despawn(Object);

전반적으로 Ball 클래스는 이제 다음과 같이 보여야 합니다:

using Fusion;

public class Ball : NetworkBehaviour
{
  [Networked] private TickTimer life { get; set; }

  public void Init()
  {
    life = TickTimer.CreateFromSeconds(Runner, 5.0f);
  }

  public override void FixedUpdateNetwork()
  {
    if(life.Expired(Runner))
      Runner.Despawn(Object);
    else
      transform.position += 5 * transform.forward * Runner.DeltaTime;
  }
}

메인 화면으로

프리팹 스폰 하기

프리팹을 스폰 하는 것은 플레이어 아바타를 생성하는 것과 동일하게 작동하지만, 플레이어가 네트워크 이벤트(게임 세션에 참여하는 플레이어)에 의해 생성된 경우 사용자의 입력에 따라 공이 생성됩니다.

이것이 작동하기 위해서는 입력 데이터 구조체를 추가 데이터로 증강해야 합니다. 이 작업은 이동과 동일한 패턴을 따르며 다음 세 단계가 필요합니다:

  1. 입력 구조체에 데이터를 추가합니다.
  2. 유니티의 입력에서 데이터를 수집합니다.
  3. 플레이어 FixedUpdateNetwork() 구현에서 입력을 적용합니다.

NetworkInputData 를 열고 buttons이라는 새 바이트 필드를 추가하고 첫 번째 마우스 단추에 대한 상수를 정의합니다:

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
  public const byte MOUSEBUTTON1 = 0x01;

  public byte buttons;
  public Vector3 direction;
}

BasicSpawner를 열고 OnInput() 메소드로 이동하여 기본 마우스 버튼에 대한 확인을 추가한 다음 buttons 필드의 첫 번째 비트를 설정합니다. 빠른 탭이 누락되지 않도록 마우스 버튼이 Update()에서 샘플링되고 입력 구조체에 기록되면 재설정됩니다.

private bool _mouseButton0;
private void Update()
{
  _mouseButton0 = _mouseButton0 | Input.GetMouseButton(0);
}

public void OnInput(NetworkRunner runner, NetworkInput input)
{
  var data = new NetworkInputData();

  if (Input.GetKey(KeyCode.W))
    data.direction += Vector3.forward;

  if (Input.GetKey(KeyCode.S))
    data.direction += Vector3.back;

  if (Input.GetKey(KeyCode.A))
    data.direction += Vector3.left;

  if (Input.GetKey(KeyCode.D))
    data.direction += Vector3.right;

  if (_mouseButton0)
    data.buttons |= NetworkInputData.MOUSEBUTTON1;
  _mouseButton0 = false;

  input.Set(data);
}

Player 클래스를 열고 GetInput() 체크 내 첫 번째 비트가 설정되면 버튼 비트를 가져오고 프리팹을 생성합니다. 프리팹에는 일반 유니티 [SerializeField] 멤버가 제공되며 이 멤버는 유니티 인스펙터에서 할당할 수 있습니다. 다른 방향으로 산란을 할 수 있도록 마지막 이동 방향을 저장할 멤버 변수도 추가해서 공의 전진 방향으로 사용합니다.

[SerializeField] private Ball _prefabBall;
private Vector3 _forward;
...
if (GetInput(out NetworkInputData data))
{
  ...
  if (data.direction.sqrMagnitude > 0)
    _forward = data.direction;
  if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
  {
      Runner.Spawn(_prefabBall, 
      transform.position+_forward, Quaternion.LookRotation(_forward), 
      Object.InputAuthority);
  }
  ...
}
...

생성 빈도를 제한하려면 각 생성 사이에 만료되어야 하는 네트워크 타이머에서 생성할 호출을 래핑 합니다. 버튼 누름이 감지될 때만 타이머를 재설정합니다:

[Networked] private TickTimer delay { get; set; }
...
if (delay.ExpiredOrNotRunning(Runner))
{
  if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
  {
    delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
    Runner.Spawn(_prefabBall, 
    transform.position+_forward, Quaternion.LookRotation(_forward), 
    Object.InputAuthority);
...

볼이 동기화되기 전에 추가적인 초기화가 필요하기 때문에 Spawn에 대한 실제 호출은 약간 수정이 필요합니다. 특히, 틱 타이머가 올바르게 설정되었는지 확인하려면 앞에서 추가한 Init() 메소드를 호출해야 합니다.

이를 위해 Fusion을 사용하면 프리팹을 인스턴스화한 후 호출될 Spawn()에 콜백을 제공할 수 있지만 동기화되기 전에 콜백을 할 수 있습니다.

요약하면 클래스는 다음과 같아야 합니다:

using Fusion;
using UnityEngine;

public class Player : NetworkBehaviour
{
  [SerializeField] private Ball _prefabBall;

  [Networked] private TickTimer delay { get; set; }

  private NetworkCharacterController _cc;
  private Vector3 _forward;

  private void Awake()
  {
    _cc = GetComponent<NetworkCharacterController>();
    _forward = transform.forward;
  }

  public override void FixedUpdateNetwork()
  {
    if (GetInput(out NetworkInputData data))
    {
      data.direction.Normalize();
      _cc.Move(5*data.direction*Runner.DeltaTime);

      if (data.direction.sqrMagnitude > 0)
        _forward = data.direction;

      if (delay.ExpiredOrNotRunning(Runner))
      {
        if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
        {
          delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
            Runner.Spawn(_prefabBall, 
            transform.position+_forward, Quaternion.LookRotation(_forward), 
            Object.InputAuthority, (runner, o) =>
            {
              // Initialize the Ball before synchronizing it
              o.GetComponent<Ball>().Init();
            });
        }
      }
    }
  }
}

테스트 전 마지막 단계는 Player 프리팹의 _prefabBall 필드에 프리팹을 할당하는 것입니다. 프로젝트에서 PlayerPrefab을 선택한 다음 Ball 프리팹을 Prefab Ball 필드에 끌어다 놓습니다.

기술문서 TOP으로 돌아가기