This document is about: FUSION 2
SWITCH TO

3 - 예측

개요

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

이 섹션의 끝에서 프로젝트를 통해 플레이어는 예측된 운동학적 볼을 생성할 수 있습니다.

운동학적 객체

물체를 스폰 하려면 먼저 프리팹이 있어야 합니다.

  1. 유니티 편집기에서 빈 게임 객체를 새로 만듭니다
  2. 이름을 Ball로 바꿉니다
  3. 여기에 새로운 NetworkTransform 컴포넌트를 추가합니다.
  4. Fusion에 NetworkObject 컴포넌트가 누락되었다는 경고가 표시되므로 Add Network Object를 누르십시오.
  5. Ball에 자식 구체 추가
  6. 모든 방향으로 0.2로 축소합니다
  7. 자식 구에서 콜라이더를 분리합니다
  8. 대신 부모 개체에 새 구 콜라이더를 만들고 하위 개체의 시각적 표현을 완전히 포함하도록 반지름을 0.1로 지정합니다.
  9. 게임 객체에 새로운 스크립트를 추가하여 Ball.cs이라고 부릅니다
  10. 마지막으로 전체 Ball 객체를 프로젝트 폴더로 드래그하여 프리팹을 만듭니다
  11. 씬을 저장하여 네트워크 개체를 베이크하고 씬에서 프리팹 인스턴스를 삭제합니다.
Ball 프리팹
Ball 프리팹

예측된 이동

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

이 컨텍스트에서 "동시적"은 "동일한 시뮬레이션 틱 위에"를 의미하는 것이지, 동일한 실제 세계 시간을 의미하는 것이 아닙니다. 이를 달성하는 방법은 다음과 같습니다:

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

이는 다음과 같은 여러 가지 시사점을 제공합니다:

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

이를 염두에 두고 Ball 스크립트를 열고 기본 클래스를 NetworkBehaviour로 변경하여 Fusion의 시뮬레이션 루프에 포함시킨 후 사전 생성된 보일러 플레이트 코드를 Fusion의 FixedUpdateNetwork()의 오버라이드로 교체합니다.

C#

using Fusion;

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

시간 스텝이 Time.deltaTime이 아니라 Tick 간의 시간에 해당하는 Runner.DeltaTime이라는 점을 제외하면 일반적인 비네트워크 객체를 이동하는 데 사용되는 코드와 거의 동일합니다. 유니티 transform처럼 로컬 속성에서 네트워크 전체에서 작동하는 비결은 물론 앞서 추가된 NetworkTransform 컴포넌트입니다. NetworkTransform은 변환 속성을 네트워크 상태에 포함시킬 수 있는 편리한 방법입니다.

정해진 시간이 지난 후에도 스폰 해제해야 무한대로 날아가지 않고 결국엔 빙글빙글 돌며 플레이어의 목을 때립니다. Fusion은 타이머에 편리한 헬퍼 타입, 즉 TickTimer를 제공합니다. 현재 남은 시간을 저장하는 대신 종료 시간을 틱 단위로 저장합니다. 즉, 타이머가 생성될 때마다 한 번만 동기화할 필요가 없습니다.

게임 네트워크 상태에 TickTimer를 추가하려면 TickTimer 타입의 life라는 이름의 속성을 Ball에 추가한 후 getter 및 setter에 빈 스텁을 제공하고 [Networked] 속성으로 표시합니다.

C#

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

[Networked]로 표시된 필드는 Fusion에서 직렬화 코드를 생성할 때 사용하는 속성이고 {get; set;} 스텁이 있어야 합니다. 항상 이 패턴을 따르세요.

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

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

C#

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

마지막으로 FixedUpdateNetwork()를 업데이트하여 타이머가 만료되었는지 확인하고 만약 그렇다면 공을 스폰 해제해야 합니다:

C#

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

전반적으로 Ball 클래스는 다음과 같은 모습을 보여야 합니다:

C#

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라는 새 바이트 필드를 추가하고 첫 번째 마우스 버튼에 대한 상수를 정의합니다:

C#

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
    public const byte MOUSEBUTTON0 = 1;

    public NetworkButtons buttons;
    public Vector3 direction;
}

NetworkButtons 타입은 최적의 대역폭 사용으로 네트워크로 연결된 여러 버튼의 입력 상태를 추적하는 데 도움이 되는 Fusion 타입입니다.

BasicSpawner를 열고 OnInput() 메소드로 이동하여 기본 마우스 버튼을 확인하고 해당 버튼이 다운된 경우 첫 번째 비트를 설정합니다. 빠른 탭을 놓치지 않도록 Update()에서 마우스 버튼을 샘플링하고 입력 구조체에 기록되면 다시 설정합니다:

C#

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;
  
  data.buttons.Set( NetworkInputData.MOUSEBUTTON0, _mouseButton0);
  _mouseButton0 = false;

  input.Set(data);
}

Player 클래스를 열고 GetInput() 확인란 안에서 버튼이 눌렸는지 확인한 후 프리팹을 생성합니다. 프리팹에는 유니티 인스펙터에서 할당할 수 있는 일반 유니티 [SerializeField] 멤버가 제공됩니다. 다른 방향으로 스폰 할 수 있도록 멤버 변수를 추가하여 마지막 이동 방향을 저장하고 이를 공의 앞 방향으로 사용합니다.

C#

[SerializeField] private Ball _prefabBall;

private Vector3 _forward = Vector3.forward;
...
if (GetInput(out NetworkInputData data))
{
  ...
  if (data.direction.sqrMagnitude > 0)
    _forward = data.direction;
  if (data.buttons.IsSet(NetworkInputData.MOUSEBUTTON0))
  {
      Runner.Spawn(_prefabBall, 
      transform.position+_forward, Quaternion.LookRotation(_forward), 
      Object.InputAuthority);
  }
  ...
}
...

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

C#

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

네트워크 객체는 StateAuthority(호스트)만 생성할 수 있기 때문에 StateAuthority에 대한 확인이 필요합니다. 따라서 이들 객체의 생성은 이동과 달리 호스트에서만 실행되고 클라이언트에서는 예측되지 않습니다.

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

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

요약하면, 클래스는 다음과 같이 보여야 합니다:

C#

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 (HasStateAuthority && delay.ExpiredOrNotRunning(Runner))
      {
        if (data.buttons.IsSet(NetworkInputData.MOUSEBUTTON0))
        {
          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 필드로 끕니다.

다음 호스트 모드 기본 4 - 물리

Back to top