This document is about: FUSION 2
SWITCH TO

共有モードのプレイヤー入力

Topology
SHARED AUTHORITY

はじめに

共有モードでは、InputAuthorityを持つクライアントがStateAuthorityを持ちます。そのため、入力をリモートへ送信する必要はなく、複雑な予測や再シミュレーションのロジックも不要です。

共有モードの入力処理はホストモードより単純ですが、タイミング関連の注意点は存在します。このドキュメントでは、これらの注意点と、ネットワークステート・瞬間的な入力・物理演算を扱うためのベストプラクティスを提供します。

備考: サンプルコードはUnity Input System (Package)の'Dynamic Update'モードをデフォルトで使用しています。サンプルはInput Actionsではなく、KeyboardMouseを直接使用しています。Legacy Input Systemを使用する場合、構文は違いますがロジックは同じです。


ネットワークステートを変更する入力

FixedUpdateNetworkは固定のティックレートで呼び出されます。呼び出しごとに特定ティックにおける新しいネットワークステートを生成し、Fusionはこれらのステートを補間してワールドをスムーズに表現します。これ以外(Updateなど)で入力を処理すると、補間された値が「上書き」されるため、ビジュアルのちらつきが発生します。

アドバイス: ネットワークステートを変更する入力は、FixedUpdateNetworkで処理してください。

実装例:

C#

[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : NetworkBehaviour
{
    private CharacterController _controller;

    private void Awake()
    {
        _controller = GetComponent<CharacterController>();
    }

    public override void FixedUpdateNetwork()
    {
        // 大抵の入力はFixedUpdateNetwork内で
        // Unity Input Systemから直接取得できる
        var keyboard = Keyboard.current;

        var moveDirection = Vector2.zero;

        if (keyboard.wKey.isPressed) { moveDirection += Vector2.up; }
        if (keyboard.sKey.isPressed) { moveDirection += Vector2.down; }
        if (keyboard.aKey.isPressed) { moveDirection += Vector2.left; }
        if (keyboard.dKey.isPressed) { moveDirection += Vector2.right; }
                
        Move(moveDirection.normalized);
    }

    void Move(Vector2 inputDirection)
    {
        // inputDirectionを使用して_controllerを移動させて、
        // オブジェクトの向きを調整する
    }
}

入力失敗の回避

短時間しか確認できない入力値もあります。例えば、毎フレームリセットされるマウス移動量や、数フレームだけ押されたボタンなどです。持続的に押される移動キーとは異なり、これらは適切なタイミングでキャプチャしないと失われる可能性があります。

これは、Unity Input SystemがUpdateで更新される一方、FixedUpdateNetworkは固定のタイムステップで実行されるためです。Updateの1フレーム間に、FixedUpdateNetworkの呼び出しが0回/1回/複数回含まれることがあるため、短い入力はUpdateで蓄積して見逃されないようにする必要があります。

1. ボタンの「タップ」

プレイヤーが非常に素早い「タップ」操作を行い、FixedUpdateNetworkが特定フレーム間で実行されなかった場合、ボタン押下状態を完全に見逃す可能性があります。

アドバイス: ボタン押下を絶対に見逃されないようにするには、Updateで押下をキャプチャしFusionのFixedUpdateNetworkで消費した後にクリアしましょう。

実装例:

C#

public class PlayerMovement : NetworkBehaviour
{
    bool _jumpPressed;

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
          return;

        // "WasPressed"の状態は一時的なものなので、
        // Update内でキャプチャして、
        // 次のFixedUpdateNetwork実行時に見逃されないようにする
        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
          _jumpPressed = true;
        }
    }

    public override void FixedUpdateNetwork()
    {
        if(_jumpPressed)
        {
            DoJump();
        }
        _jumpPressed = false;
    }

    void DoJump()
    {
      // ネットワーク同期するジャンプロジックを記述する
    }
}

2. マウス移動量

UnityはUpdate毎にマウス移動量をリセットします。FixedUpdateNetwork内でのみ値を確認すると、精度が失われ、回転に「カクつき」が生じます。これは、FixedUpdateNetwork呼び出し直前1フレームの移動量しか認識されていないためです。

アドバイス Update内で移動量を蓄積し、FixedUpdateNetwork内でその値を処理/リセットしてください。

実装例:

C#

public class PlayerMovement : NetworkBehaviour
{
    Vector2 _accumulatedMouseDelta;

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
          return;

        // マウス移動量は一時的なものなので、
        // Update内で蓄積して、
        // 次のFixedUpdateNetwork実行時に見逃されないようにする
        _accumulatedMouseDelta += Mouse.current.delta.ReadValue();
    }

    public override void FixedUpdateNetwork()
    {
        AdjustAim(_accumulatedMouseDelta);

        _accumulatedMouseDelta = Vector2.zero;
    }

    void AdjustAim(Vector2 delta)
    {
      // ここでtransformにエイム調整を適用する
    }
}

Unityで補間されるRigidbodyを変更する入力

ほとんどのゲームは上記のルールで十分ですが、例外的にネットワークステートの補間にFusionを使用しないケースがあります。その一例がForecast Rigidbodyで、Unity物理のFixedUpdate呼び出し間の補間を使用します。Fusionと物理の連携についての詳細は、物理をご覧ください。

これに該当するオブジェクトは、Unityの入力をFixedUpdateで読み取る必要があります。繰り返しになりますが、FixedUpdateが呼び出される頻度はUpdateとは異なるため、瞬間的な入力はUpdateでキャプチャしFixedUpdateで適用する必要があることに注意してください。

C#

[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : NetworkBehaviour
{
    Rigidbody _rb;
    bool _jumpPressed;

    void Awake()
    {
        _rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
            return;
        
        // WasPressedイベントは一時的なものであるため、
        // 次のFixedUpdate実行時に見逃されないように、キャプチャする必要がある
        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
          _jumpPressed = true;
        }
    }

    void FixedUpdate()
    {
        var keyboard = Keyboard.current;
        var moveDirection = Vector2.zero;

        if (keyboard.wKey.isPressed) { moveDirection += Vector2.up; }
        if (keyboard.sKey.isPressed) { moveDirection += Vector2.down; }
        if (keyboard.aKey.isPressed) { moveDirection += Vector2.left; }
        if (keyboard.dKey.isPressed) { moveDirection += Vector2.right; }

        Move(moveDirection.normalized, _jumpPressed);  

        // キャプチャした入力をリセットする
        _jumpPressed = false;
    }

    void Move(Vector2 inputDirection, bool didJump) 
    {
        // inputDirectionとjumpフラグをforceに変換し、
        // それらを_rbに適用する
    }
}

ヒント: Forecast Physicsで最良の結果を得るには、物理に影響する入力をネットワーク同期して適用することを推奨します。詳細はFusionと物理の連携例をご覧ください。


ネットワークステートに影響しない入力

ネットワークステートに影響しない入力は、Unityの標準的なベストプラクティスに準拠できます。一般的に可能な限り低レイテンシを実現するため、入力をUpdateLateUpdateで処理します。

ネットワークステートに影響しない可能性がある入力の例:

  1. カメラの回転: これは通常、低レイテンシの入力とスムーズな動作が求められます。ネットワークステートに直接的な影響を受けないため、入力はLateUpdateで処理できます。
  2. UIのナビゲーション: これらイベント処理はUnityのベストプラクティスに従うことができます。

実践的な入力

上記のパターンの組み合わせが必要な場合もあります。例えば、一人称視点のシューティングゲームでは、マウス入力はキャラクターの向き(ネットワーク同期している)と、カメラの向き(ネットワーク同期していない)の両方に影響します。

そしてカメラは、次のFixedUpdateNetworkを待たずに「即時」に動作させることが望ましいです。

これを実現するには、Updateで入力を蓄積し、FixedUpdateNetworkでネットワーク同期するtransformに適用します。その後、LateUpdateで補間されたtransformと入力を組み合わせてカメラを回転させます。

実装例:

C#

[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : NetworkBehaviour
{
    // Yawはネットワーク同期するtransformとカメラに影響する
    float _yaw;

    // Pitchは純粋なビジュアル用でカメラのみに影響する
    float _pitch;

    void Update()
    {
        if (Object == null || !Object.HasStateAuthority)
            return;

        // 一時的な入力を蓄積する
        // (Update内)
        Vector2 mouseDelta = Mouse.current.delta.ReadValue();

        const float _sensitivity = 0.1f;
        _yaw += mouseDelta.x * _sensitivity;

        // ここでカメラのpitchも蓄積して
        // LateUpdateで適用できるようにしておく
        _pitch += mouseDelta.y * -_sensitivity;
    }

    public override void FixedUpdateNetwork()
    {
        // yaw値をこのプレイヤーのtransform.rotationに適用する
        // これはNetworkTransformによってネットワーク同期される
        transform.rotation *= Quaternion.Euler(0f, _yaw, 0f);

        // 蓄積した入力をリセットする
        _yaw = 0;
    }

    void LateUpdate()
    {
        if (Object == null || !Object.HasStateAuthority)
            return;

        Camera.main.transform.position = transform.position;

        // Fusionで補間されたtransformのyawに、蓄積されたyawを加算する
        // これによって、低レイテンシかつスムーズなカメラの回転になる
        Camera.main.transform.rotation = Quaternion.Euler(_pitch, transform.eulerAngles.y + _yaw, 0f);
    }
}

ヒント: このアプローチはForecast Rigidbodyを使用した物理ベースの移動でも動作します。補間処理はFusionではなくUnity内部で行われるため、Yaw値はFixedUpdateNetworkではなくFixedUpdate内でRigidbodyに適用してください。

Back to top