This document is about: FUSION 2
SWITCH TO

네트워킹 컨트롤러 코드

개요

Fusion은 자체적으로 FixedUpdateNetwork()라는 시뮬레이션 타이밍 세그먼트를 가지고 있는데, 이는 실제 시간이 충분히 지나면 앞으로 실행되는 고정된 인터벌 타이밍 세그먼트이고, 시뮬레이션을 나타낸다는 점에서 유니티의 FixedUpdate()와 근본적으로 매우 유사합니다.

그러나 Fusion에서 FixedUpdateNetwork()를 호출하고 관리합니다. FixedUpdateNetwork()FixedUpdate()의 가장 큰 차이점은 서버/클라이언트 토폴로지(공유 모드가 아님)에서 Fusion은 서버 업데이트가 도착하면 클라이언트의 상태(네트워크 된 속성)를 정기적으로 재설정한 후 원격 서버 틱 상태에서 현재 로컬 틱으로 재시뮬레이션합니다. 참조 클라이언트 측 예측

또한 모든 토폴로지에서 Fusion은 틱에 대해 모든 FixedUpdateNetwork() 콜백이 실행된 직후 상태를 캡처합니다. 따라서 FixedUpdateNetwork()는 모든 시뮬레이션 코드의 정확한 위치가 됩니다. FixedUpdateNetwork()와 시뮬레이션은 본질적으로 동의어이기 때문입니다.

반면에 FixedUpdate()FixedUpdateNetwork()와 완전히 분리되어 있으므로 일반적으로 피해야 합니다.

서버 클라이언트 모드에서의 컨트롤러 코드

서버/클라이언트 토폴로지(전용 서버/호스트/싱글)를 사용하려면 FixedUpdateNetwork() 내부에서 모든 컨트롤러 코드를 시뮬레이션하는 것이 중요합니다. 일반적으로 Update()FixedUpdate() 타이밍 세그먼트를 사용하므로 에셋 스토어에서 코드를 가져오거나 자신의 오래된 코드를 재사용할 때 기억하는 것이 중요합니다.

FixedUpdateNetwork()는 충분한 게임 시간(누적)이 지났을 때 앞으로 증가하는 고정 인터벌 타이밍 세그먼트라는 점에서 유니티의 FixedUpdate()와 매우 유사합니다. 그러나 가장 큰 차이점은 서버 모드의 FixedUpdateNetwork()가 정기적으로 틱을 다시 실행한다는 것입니다.

이를 제외한 모든 시뮬레이션 코드는 재시뮬레이션을 인식하지 못하고 재시뮬레이션을 실행하지 않으므로 클라이언트 동기화가 지속됩니다.

클라이언트 측 예측 참조

FixedUpdateNetwork 네트워크 요구 사항에 대한 예외

Update() 또는 FixedUpdate() 내부의 시뮬레이션 코드는 다음과 같은 경우 허용 가능한 결과를 제공할 수 있습니다:

  • 객체에 대해 어떤 종류의 클라이언트 예측을 사용하지 않고(클라이언트의 원격 시간대에 전적으로 존재), 서버만 객체 상태에 변경사항을 적용하고 있는 경우, 그리고
  • 클라이언트는 해당 객체에 대한 변경사항을 원활하게 보간할 필요가 없습니다.

이 경우

공유 모드에서의 컨트롤러 코드

공유 모드는 서버 클라이언트 모드(서버/호스트/싱글)와 같은 고정 틱에서 시뮬레이션을 합니다. 그러나 개발자는 다음과 같은 이유로 FixedUpdateNetwork()가 아닌 유니티에서 일반적으로 사용되는 Update()의 입력을 적용하기를 원할 수 있습니다;

  • 가장 빠른 사용자 입력 응답 결과가 나타납니다.
  • Update()를 사용하여 기존 에셋을 코딩할 수 있습니다.
  • 다른 컴포넌트 시스템 및 에셋은 Update()에서 변경될 것으로 예상할 수 있습니다

이렇게 하면 제어되는 객체가 로컬 렌더링 기간(마지막 두 시뮬레이션된 틱 사이의 간격)보다 앞서게 되고, 객체가 실제 로컬 플레이어 시간에서 그리고 렌더링을 시뮬레이션하게 됩니다.

그러나 Update()에서 제어되는 객체를 이동하는 것이 Fusion의 고정 시뮬레이션과 완전히 호환되는 것은 아닙니다. 변환 값은 FixedUpdateNetwork() 이후에 캡처되고 Update() 이후에는 캡처되지 않으므로 Update()에서 변경된 내용은 다음 시뮬레이션(향후 유니티 업데이트)까지 캡처되지 않습니다. 이는 캡처와 Update()의 임의 deltaTime 기반 이동 사이의 앨리어싱으로 인해 마이크로 히치 및/또는 잠복을 생성할 수 있습니다.

공유 모드에서 앨리어싱

공유 모드는 FixedUpdateNetwork()에서 시뮬레이션할 컨트롤러 코드를 엄격하게 요구하지 않기 때문에 사용자가 Update()에서만 시뮬레이션을 선택할 경우 앨리어싱 오류가 발생할 가능성이 있습니다. 아래 그림은 문제를 보여줍니다. Fusion은 Update()에서 컨트롤러 코드가 이동하거나 상태를 변경하는 경우에도 FixedUpdateNetwork() 이후의 상태를 캡처합니다. 이러한 고정 간격 네트워크 값은 값의 변경이 발생한 실시간에 대한 고려 없이 다른 클라이언트의 보간에 의해 사용되므로 보간 된 결과에 히치 및/또는 잠복이 발생할 수 있습니다.

Update() vs FixedUpdateNetwork()의 시뮬레이션으로 인해 발생하는 앨리어싱
Update() vs FixedUpdateNetwork()의 시뮬레이션으로 인해 발생하는 앨리어싱

앨리어싱 대응전략

이 앨리어싱이 문제이거나 눈에 띄는 경우 이 문제를 해결하기 위한 몇 가지 옵션이 있습니다.

노트: 이러한 앨리어싱은 시뮬레이션되는 값의 특성과 상대적인 프레임 레이트 및 틱 레이트 값에 따라 우려가 될 만큼 크지 않을 수 있습니다. 매우 높은 틱 레이트 및 프레임 레이트에서는 앨리어싱이 플레이어에게 눈에 띄지 않을 수 있습니다. 또한 프레임 레이트가 틱 레이트보다 상당히 높은 경우에는 우려가 덜할 수 있습니다.

1) 아무것도 하지 마세요

게임의 특정 사용 사례를 위해 앨리어싱이 충분히 경미하고 사용자에게 눈에 띄지 않을 경우 당연히 무시하고 Update()에서 시뮬레이션만 하면 됩니다.

2) FixedUpdateNetwork()로 이동하여 상태 권한에서 인터폴레이션

하나는 공유 모드를 서버 모드처럼 처리하고 모든 컨트롤러 코드를 시뮬레이션 기반으로 만들어 FixedUpdateNetwork()에서만 실행하도록 하는 것입니다. 그런 다음 인터폴레이션을 사용하여 최신 두 시뮬레이션 결과를 Render()로 표시합니다. 단점: 이는 적은 양의 입력 지연을 유발합니다.

3) FixedUpdateNetwork() 및 Update()로 모두 이동

또 다른 방법은 FixedUpdateNetwork() 그리고 Update()를 혼합하여 시뮬레이션하는 것입니다. FixedUpdateNetwork()에서 이동하면 객체가 틱 기반 캡처 및 복제에 대해 균일하고 올바르게 이동하여 원격 클라이언트에서 원활한 결과를 얻을 수 있습니다. 추가적으로 Update()에서 이동하면 마지막 틱 이후 시간이 경과한 것을 고려할 수 있으므로 보간 없이 객체를 로컬 시간에 맞게 올바른 위치로 이동할 수 있습니다.

C#


using Fusion;
using UnityEngine;

/// <summary>
/// The objective of this basic controller code is to simulate 
/// Object movement both in FixedUpdateNetwork() AND in Update().
/// It is important to move the Object in FixedUpdateNetwork() 
/// to produce evenly spaced results, as these snapshots are used 
/// to produce smooth interpolation on remote clients.
/// However, if it very common with Client Authority topologies 
/// to apply inputs in Update(), as to apply player input 
/// immediately to the rendered Object, rather than interpolating 
/// between previous simulated states. So it becomes necessary 
/// to simulate in both FUN() and Update() to satisfy both 
/// Simulation and Interpolation requirements.
/// Simulation in Update() to produce smooth local movement, 
// and Simulation in FUN() to capture even networked tick results.
/// </summary>
public class HybridSharedController : NetworkBehaviour, IBeforeUpdate
{
  [SerializeField, Unit(Units.PerSecond)]
  public float Speed = 5f;
  
  float   _lastSimulationTime;
  Vector3 _currentInput;

  public override void Spawned() 
  {
    // Capture the starting time so our 
    // first simulation has a correct delta value.
    _lastSimulationTime = Runner.SimulationTime;
  }

  public override void FixedUpdateNetwork() 
  {
    // Only move the State Authority (there is no prediction in Shared Mode)
    if (HasStateAuthority == false) return;
    
    // Calculate the amount of time that has passed since the last 
    // FixedUpdateNetwork or Update - as this is our real delta
    float timeSinceLastSim = Runner.SimulationTime - _lastSimulationTime;
    _lastSimulationTime = Runner.SimulationTime;
    
    transform.position += _currentInput * timeSinceLastSim;
  }

  // The BeforeUpdate() callback is the same as Update(), 
  // but it fires BEFORE all Fusion work is done.
  // Update() would also work here, but input used in FUN() 
  // would always be from the previous Update().
  void IBeforeUpdate.BeforeUpdate()
  {
    // Only move the State Authority (no prediction in Shared Mode)
    if (HasStateAuthority == false) return;

    // Collect input every Update(), and store it -
    // as we will also use this input in FixedUpdateNetwork()
    Vector3 input = default;

    if (Input.GetKey(KeyCode.W)) { input += Vector3.forward * Speed; }
    if (Input.GetKey(KeyCode.S)) { input += Vector3.back    * Speed; }
    if (Input.GetKey(KeyCode.A)) { input += Vector3.left    * Speed; }
    if (Input.GetKey(KeyCode.D)) { input += Vector3.right   * Speed; }

    _currentInput = input;

    // Calculate the amount of time that has passed since last simulation
    // (FixedUpdateNetwork or Update), as this is our real delta time
    float realtime = Runner.LocalRenderTime + Runner.DeltaTime;
    float timeSinceLastSim = realtime - _lastSimulationTime;
    _lastSimulationTime = realtime;
    
    transform.position += input * timeSinceLastSim;
  }
}

Back to top