3 - 移動とカメラ
概要
ここでは、プレイヤーの入力に基づくプレイヤーの移動と、適切なプレイヤーオブジェクトに追従する一人称視点のカメラを、前章のシーンに追加します。
プレイヤーの移動
Fusionでは、毎ティック更新するゲームプレイのコード(プレイヤーの移動など)をUpdateやFixedUpdateで実行すべきではありません。かわりにFixedUpdateNetworkを使用すべきです。このメソッドによって、全てのクライアント上で移動がスムーズに正しく補間されるようになります。
新規でスクリプトを作成して、名前をPlayerMovementにし、以下のコードを追加してください。
C#
using Fusion;
using UnityEngine;
public class PlayerMovement : NetworkBehaviour
{
    private CharacterController _controller;
    public float PlayerSpeed = 2f;
    private void Awake()
    {
        _controller = GetComponent<CharacterController>();
    }
    public override void FixedUpdateNetwork()
    {
        // FixedUpdateNetwork is only executed on the StateAuthority
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * Runner.DeltaTime * PlayerSpeed;
        _controller.Move(move);
        if (move != Vector3.zero)
        {
            gameObject.transform.forward = move;
        }
    }
}
MonoBehaviourのかわりにNetworkBehaviourを継承することで、FixedUpdateNetworkとネットワークプロパティ(詳細は後述)が使用できるようになります。
ボタン押下の処理
GetButtonDownなどはUnityのUpdateで1フレームのみ反応します。Fusionではゲームプレイのコード(プレイヤーの移動など)をFixedUpdateNetworkで実行するため、ボタン押下状態を正確に取得するための特別な処理が必要になります。
FixedUpdateでも発生する問題と同様です。FixedUpdateNetworkで正確な入力を取得するには、複数のアプローチがあります。
- Updateでボタン押下状態を取得、結果を- boolで保存し、ゲームロジックに使用する。その後の- FixedUpdateNetworkの最後に取得結果をクリアする。
- 新しいUnityのInput Systemを使用して、Update ModeをManual Updateに設定、FixedUpdateNetworkでInputSystem.Updateを呼び出す。
- Fusionのネットワーク入力で、NetworkButtonsを使用する。
ジャンプ
ボタン押下を処理する例として、プレイヤーのジャンプを実装してみましょう。今回は、前節のリストのオプション1を使用します。
PlayerMovenetのコードを、以下のように書き換えてください。
C#
using Fusion;
using UnityEngine;
public class PlayerMovement : NetworkBehaviour
{
    private Vector3 _velocity;
    private bool _jumpPressed;
    
    private CharacterController _controller;
    public float PlayerSpeed = 2f;
    
    public float JumpForce = 5f;
    public float GravityValue = -9.81f;
    private void Awake()
    {
        _controller = GetComponent<CharacterController>();
    }
    
    void Update()
    {
        if (Input.GetButtonDown("Jump"))
        {
            _jumpPressed = true;
        }
    }
    public override void FixedUpdateNetwork()
    {
        // FixedUpdateNetwork is only executed on the StateAuthority
        
        if (_controller.isGrounded)
        {
            _velocity = new Vector3(0, -1, 0);
        }
        
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * Runner.DeltaTime * PlayerSpeed;
        _velocity.y += GravityValue * Runner.DeltaTime;
        if (_jumpPressed && _controller.isGrounded)
        {
            _velocity.y += JumpForce;
        }
        _controller.Move(move + _velocity * Runner.DeltaTime);
        if (move != Vector3.zero)
        {
            gameObject.transform.forward = move;
        } 
        
        _jumpPressed = false;
    }
}
このコードでは、ジャンプ処理と、KCC(キネマティックキャラクターコントローラー)に渡す速度と重力の計算を追加しています。
_jumpPressed変数はボタン押下状態を保存するために使用されます。ボタン押下状態は、判定ミスが起こらないようにUpdateでポーリングされ、FixedUpdateNetworkの最後にリセットされます。
C#
if (_controller.isGrounded)
{
    _velocity = new Vector3(0, -1, 0);
}
上記のコードで、小さな斜面上を歩いている時でも地面に着くようにすることで、ジャンプ中のみプレイヤーが地面から離れるようにします。
テストの実行
PlayerMovementが完成したら、Unityエディターに戻り、PlayerCharacterプレハブにPlayerMovementコンポーネントを追加します。
プレイヤーの移動がテストできるようになったので、ビルドして二つのクライアントを実行してみましょう。
File > Build Settingsで現在のシーンをリストに追加して、ウインドウ左下のPlayer Settingsボタンを押してください。
Player Settingsでは、Resolution and PresentationをFullscreen Modeに設定して、Run In Backgroundにチェックを入れてください。
 
Build Settingsに戻り、Build and Runを押してビルドを作成します。ビルドが完了したら、Unityエディター上でもゲームを実行すると、二つのクライアントが立ち上がることになります。
両方のクライアントでStart Shared Clientボタンを押します。プレイヤーが二人表示され、それぞれが各クライアントで操作できます。歩き回ったりジャンプしたりできるか確認してみてください。
カメラ
プレイヤーのセットアップを完成させるため、カメラを追加しましょう。
カメラ設定には何通りかの方法(三人称、一人称など)があり、実装方法も異なります。マルチプレイゲームでは、ローカルプレイヤーオブジェクトに追従するカメラが一つあることが望ましい場合が多いです。一般的な実装は、以下の二つです。
- ローカルプレイヤーオブジェクトがスポーンされたら、カメラのインスタンスを生成する
- あらかじめカメラをシーン上に置いておき、ローカルプレイヤーオブジェクトがスポーンされたら、そのオブジェクトをカメラのターゲットにする
このチュートリアルでは2のアプローチを使用します。
新規スクリプトを作成して、名前をFirstPersonCameraにし、以下のコードを追加してください。
C#
using UnityEngine;
public class FirstPersonCamera : MonoBehaviour
{
    public Transform Target;
    public float MouseSensitivity = 10f;
    private float verticalRotation;
    private float horizontalRotation;
    
    void LateUpdate()
    {
        if (Target == null)
        {
            return;
        }
        transform.position = Target.position;
        
        float mouseX = Input.GetAxis("Mouse X");
        float mouseY = Input.GetAxis("Mouse Y");
        verticalRotation -= mouseY * MouseSensitivity;
        verticalRotation = Mathf.Clamp(verticalRotation, -70f, 70f);
        horizontalRotation += mouseX * MouseSensitivity;
        transform.rotation = Quaternion.Euler(verticalRotation, horizontalRotation, 0);
    }
}
これは、非常にシンプルな一人称カメラの実装です。このコードにはマルチプレイの要素が含まれていないことに注意してください。このカメラは、オブジェクトをターゲットにして動作する他のカメラ(Cinemachineなど)に置き換えることも可能です。
シーンのMainCameraにFirstPersonCameraを追加します。ターゲットは実行時にコードから設定されるので、ターゲットオブジェクトは空のままで問題ありません。
 
カメラのターゲットを設定するために、PlayerMovementスクリプトを開いてCamera変数を追加します。
C#
public Camera Camera;
ローカルプレイヤーオブジェクトがスポーンされたら、カメラの参照を取得してターゲットを設定します。
C#
public override void Spawned()
{
    if (HasStateAuthority)
    {
        Camera = Camera.main;
        Camera.GetComponent<FirstPersonCamera>().Target = transform;
    }
}
NetworkObjectを初期化する時は、必ずSpawnedを使用するようにしてください。AwakeやStartのタイミングでは、NetworkObjectがまだ正常にアクセスできるようになっていない可能性があります。
HasStateAuthorityはプレイヤーが操作するオブジェクト(ローカルプレイヤーオブジェクト)でのみtrueになります。最後に一人称視点のゲームでは、プレイヤーキャラクターは視線方向基準で移動するので、少し調整が必要です。move変数の計算を以下のコードに置き換えてください。
C#
Quaternion cameraRotationY = Quaternion.Euler(0, Camera.transform.rotation.eulerAngles.y, 0);
Vector3 move = cameraRotationY * new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * Runner.DeltaTime * PlayerSpeed;