🔰

【Photon Fusion】共有モードのチュートリアル

2023/08/16に公開11

セッションに参加しよう

まずプレイヤーがネットワーク上の他のプレイヤーと通信するためには、セッション(Session)に参加する必要があります。セッションは部屋のようなもので、同じセッションに参加しているプレイヤー同士でのみ、データを送受信してゲームを同期させることができます。

NetworkRunnerのプレハブの作成

NetworkRunnerは、Fusionのネットワークを管理するための最重要コンポーネントになります。以下の画像を参考にNetworkRunnerのプレハブを作成してください。


NetworkRunnerインスタンスの生成

シーン上に配置した空のゲームオブジェクトに新規のスクリプト(GameLauncher)を追加して、NetworkRunnerのプレハブをアタッチしましょう。このスクリプトからNetworkRunnerインスタンスを生成して、StartGame()を呼び出すことでセッションに参加できます。


GameLauncher.cs
using Fusion;
using UnityEngine;

public class GameLauncher : MonoBehaviour
{
    [SerializeField]
    private NetworkRunner networkRunnerPrefab;

    private NetworkRunner networkRunner;

    private void Start() {
        // NetworkRunnerを生成する
        networkRunner = Instantiate(networkRunnerPrefab);
        // StartGameArgsに渡した設定で、セッションに参加する
        networkRunner.StartGame(new StartGameArgs {
            GameMode = GameMode.Shared,
            SceneManager = networkRunner.GetComponent<NetworkSceneManagerDefault>()
        });
    }
}

正常にセッションに参加できているか確認するために、コンソールにログを出してみましょう。StartGame()は非同期メソッドになっていて、タスク完了後に戻り値(StartGameResult型)から実行結果を取得して、成功時と失敗時の処理を分岐させることができます。

GameLauncher.cs
+ private async void Start() {
      networkRunner = Instantiate(networkRunnerPrefab);
+     var result = await networkRunner.StartGame(new StartGameArgs {
          GameMode = GameMode.Shared,
          SceneManager = networkRunner.GetComponent<NetworkSceneManagerDefault>()
      });
  
+     if (result.Ok) {
+         Debug.Log("成功!");
+     } else {
+         Debug.Log("失敗!");
+     }
  }


セッションへの参加が成功した時のログ

プレイヤーのアバターを表示しよう

プレイヤーのアバターをネットワークオブジェクトとして生成して、セッションへ参加しているプレイヤーの人数の分だけ、ゲーム画面上にアバターが表示されるようにしてみましょう。

共有モードでは、クライアントが自由にネットワークオブジェクトを生成することができます。各プレイヤーはそれぞれセッションへ参加した時に、自分自身のアバターを生成するようにしましょう。

アバターのプレハブの作成

以下の画像を参考にアバターのプレハブを作成してください。アバターをネットワークオブジェクトにするために、NetworkObjectコンポーネントを追加するのを忘れないようにしてください。





アバターのプレハブが作成できたら、プレハブの参照をGameLauncherにアタッチしましょう。

GameLauncher.cs
    [SerializeField]
    private NetworkRunner networkRunnerPrefab;
+   [SerializeField]
+   private NetworkPrefabRef playerAvatarPrefab;


GameLauncherにアバターのプレハブをアタッチする

アバターの生成と破棄

ネットワークオブジェクトは、Unity標準のInstantiate()ではなくNetworkRunner.Spawn()から生成します。セッションへの参加が成功したら、プレイヤー自身のアバターを生成しましょう。

GameLauncher.cs
    private async void Start() {
        networkRunner = Instantiate(networkRunnerPrefab);
        var result = await networkRunner.StartGame(new StartGameArgs {
            GameMode = GameMode.Shared,
            SceneManager = networkRunner.GetComponent<NetworkSceneManagerDefault>()
        });

+       if (result.Ok) {
+           // ランダムな生成位置(半径5の円の内部)を取得する
+           var randomValue = Random.insideUnitCircle * 5f;
+           var spawnPosition = new Vector3(randomValue.x, 5f, randomValue.y);
+           // プレイヤー自身のアバターを生成する
+           networkRunner.Spawn(playerAvatarPrefab, spawnPosition, Quaternion.identity, networkRunner.LocalPlayer);
+       }
    }

ネットワークオブジェクトの破棄は、Unity標準のDestroy()ではなくNetworkRunner.Despawn()で行います。共有モードでのみ、プレイヤーがセッションから退出した時に、そのプレイヤーが状態権限を持つネットワークオブジェクト(ここではプレイヤー自身のアバター)を自動的に破棄するプロパティを利用することができます。


ネットワークオブジェクトの自動的な破棄を有効にする

アバターの位置の同期

ここでUnityのプロジェクトをビルドして、動作確認してみましょう。生成されたアバターが画面に表示されるようにはなりましたが、アバターの位置は同期されていないことがわかります。

Fusionには、ネットワークオブジェクトの位置を同期するコンポーネントがいくつか用意されているので、今回はNetworkCharacterControllerPrototypeコンポーネントを使ってみましょう。アバターのプレハブにこのコンポーネントを追加すれば、アバターの位置が自動的に同期されるようになります。


NetworkCharacterControllerPrototypeを追加する(CharacterControllerは自動で追加される)

コンポーネントのInterpolation Targetプロパティを設定すると、位置をスムーズに補間して表示できるようになるので、子要素のゲームオブジェクトをアタッチしておきましょう。


NetworkCharacterControllerPrototypeInterpolation Targetを設定する

Unityのプロジェクトをビルドして、複数のアバターの位置が正しく同期されて表示されるかを確認してみましょう。

プレイヤー自身のアバターを操作できるようにしよう

アバターが表示できたので、ここから自分自身のアバターを操作できるようにしてみましょう。

共有モードでは、各プレイヤーがネットワークオブジェクトの状態権限を持つことができるため、状態権限を持つネットワークオブジェクト(ここでは自分自身のアバター)を直接操作することができます。

アバターの移動

ネットワーク上で同期する処理は、UnityのUpdate()内ではなくFixedUpdateNetwork()内で実行する必要があります。新規スクリプトでPlayerAvatarを追加して、FixedUpdateNetwork()の中で取得した入力に応じて、アバターを移動させる処理を実行しましょう。


アバターのプレハブにPlayerAvatarを追加する

PlayerAvatar.cs
using Fusion;
using UnityEngine;

public class PlayerAvatar : NetworkBehaviour
{
    private NetworkCharacterControllerPrototype characterController;

    private void Awake() {
        characterController = GetComponent<NetworkCharacterControllerPrototype>();
    }

    public override void FixedUpdateNetwork() {
        if (Object.HasStateAuthority) {
            var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")).normalized;
            // 入力方向を移動方向としてそのまま渡す
            characterController.Move(inputDirection);
        }
    }
}

アバターのジャンプ

ジャンプボタンの押下を判定して、アバターのジャンプ処理を実行しましょう。

PlayerAvatar.cs
    public override void FixedUpdateNetwork() {
        if (Object.HasStateAuthority) {
            var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")).normalized;
+           var isJumpButtonDown = Input.GetKey(KeyCode.Space);

            characterController.Move(inputDirection);
+           if (isJumpButtonDown) {
+               characterController.Jump();
+           }
        }
    }

カメラの制御

今回はカメラの制御に、Unity公式パッケージのCinemachineを利用します。以下の画像を参考にして、Cinemachine Free Look Cameraのセットアップを行ってください。


カメラにCinemachineBrainを追加する


ヒエラルキーのメニューから「Cinemachine > FreeLook Camera」を選択する



CinemachineFreeLookの設定を調整する

CinemachineFreeLookのプロパティ設定は自由に変えてもらっても構いません。オススメとして、Lens Vertical FOVTopRigMiddleRigBottomRigあたりは、デフォルトの値から変更すると画面が見やすくなると思います。

次に、PlayerAvatarViewスクリプトを新規で作成して、アバターにカメラを追従させる処理を実装しましょう。スクリプトは、アバターの3Dモデル部分のゲームオブジェクトに追加します。

PlayerAvatarView.cs
using Cinemachine;
using UnityEngine;

public class PlayerAvatarView : MonoBehaviour
{
    public void SetCameraTarget() {
        var freeLookCamera = FindObjectOfType<CinemachineFreeLook>();
        freeLookCamera.LookAt = transform;
        freeLookCamera.Follow = transform;
    }
}


アバターの3Dモデル部分にPlayerAvatarViewを追加する

NetworkBehaviourを継承しているスクリプト(PlayerAvatar)は、ネットワークオブジェクトが生成された時にSpawned()が呼ばれます。そこで自分自身のアバターかどうか(自分自身が入力権限を持つネットワークオブジェクトかどうか)を調べて、自分自身のアバターならカメラを追従させる処理を実行しましょう。


PlayerAvatarPlayerAvatarViewをアタッチする

PlayerAvatar.cs
  public class PlayerAvatar : NetworkBehaviour
  {
+     [SerializeField]
+     private PlayerAvatarView view;

      private NetworkCharacterControllerPrototype characterController;
  
      private void Awake() {
          characterController = GetComponent<NetworkCharacterControllerPrototype>();
      }

+     public override void Spawned() {
+         // 自分自身のアバターにカメラを追従させる
+         if (Object.HasStateAuthority) {
+             view.SetCameraTarget();
+         }
+     }

      public override void FixedUpdateNetwork() {
          if (Object.HasStateAuthority) {
              var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")).normalized;
              var isJumpButtonDown = Input.GetKey(KeyCode.Space);

              characterController.Move(inputDirection);
              if (isJumpButtonDown) {
                  characterController.Jump();
              }
          }
      }
  }

ここでひとまずUnityのエディター上で再生してみて、自分自身のアバターにカメラが追従しているか?を確認してみましょう。

カメラが操作できるようになると、入力の方向をそのまま(ワールド座標系の方向として)渡すだけでは操作性に問題が発生することがわかるかと思います。そのため、入力の方向をカメラの正面向きを基準とした方向(ビュー座標系の方向)として、ビュー座標系からワールド座標系に変換する処理を追加する必要があります。

PlayerAvatar.cs
    public override void FixedUpdateNetwork() {
        if (Object.HasStateAuthority) {
-           var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")).normalized;
+           // 入力の方向をカメラの向きを基準にした方向に変換する
+           var cameraRotation = Quaternion.Euler(0f, Camera.main.transform.rotation.eulerAngles.y, 0f);
+           var inputDirection = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical"));
+           inputDirection = (cameraRotation * inputDirection).normalized;
            var isJumpButtonDown = Input.GetKey(KeyCode.Space);

            characterController.Move(inputDirection);
            if (isJumpButtonDown) {
                characterController.Jump();
            }
        }
    }

プレイヤー名を表示しよう

アバターを自由に動かせるようになりましたが、このままではどれが誰のアバターなのか判別がつかないので、アバターの頭上にプレイヤーの名前を表示させてみましょう。

プレイヤー名の保存

事前にプレイヤー名を文字列で保存・取得できるようにしておきましょう。新規スクリプトで、以下のような静的クラスのPlayerDataを作成してください。

PlayerData.cs
using UnityEngine;

public static class PlayerData
{
    public static string NickName {
        get => PlayerPrefs.GetString("NickName", "No Name");
        set => PlayerPrefs.SetString("NickName", value);
    }
}

このチュートリアルでは、ランダムなプレイヤー名を表示することにします。セッションに参加する前に、プレイヤー名を設定する処理を追加しておきましょう。

GameLauncher.cs
    private async void Start() {
+       // ランダムなプレイヤー名を設定する
+       PlayerData.NickName = $"Player{Random.Range(0, 10000)}";

        networkRunner = Instantiate(networkRunnerPrefab);
        var result = await networkRunner.StartGame(new StartGameArgs {
            GameMode = GameMode.Shared,
            SceneManager = networkRunner.GetComponent<NetworkSceneManagerDefault>()
        });

        if (result.Ok) {
            var randomValue = Random.insideUnitCircle * 5f;
            var spawnPosition = new Vector3(randomValue.x, 5f, randomValue.y);
            networkRunner.Spawn(playerAvatarPrefab, spawnPosition, Quaternion.identity, networkRunner.LocalPlayer);
        }
    }

プレイヤー名のテキスト作成

以下の画像を参考にして、アバターの頭上にテキストを追加してください。テキストは、Unity公式パッケージのTextMesh Proを使用します。


ヒエラルキーのメニューから「3D Object > Text - TextMeshPro」を選択する



PlayerAvatarViewにテキストの参照(TextMeshPro)をアタッチして、プレイヤー名をテキストに設定する処理を追加しましょう。また、テキストは常にカメラの正面向きで表示させたいので、LateUpdate()のタイミングでビルボードの処理を実行しましょう。

PlayerAvatarView.cs
  using Cinemachine;
+ using TMPro;
  using UnityEngine;
  
  public class PlayerAvatarView : MonoBehaviour
  {
+     [SerializeField]
+     private TextMeshPro nameLabel;
  
      public void SetCameraTarget() {
          var freeLookCamera = FindObjectOfType<CinemachineFreeLook>();
          freeLookCamera.LookAt = transform;
          freeLookCamera.Follow = transform;
      }

+     // プレイヤー名をテキストに設定する
+     public void SetNickName(string nickName) {
+         nameLabel.text = nickName;
+     }
  
+     private void LateUpdate() {
+         // プレイヤー名のテキストを、常にカメラの正面向きにする
+         nameLabel.transform.rotation = Camera.main.transform.rotation;
+     }
  }

プレイヤー名をネットワークプロパティで定義します。NetworkRunner.Spawn()の引数のOnBeforeSpawned()からネットワークプロパティを初期化するメソッドが渡せるので、そこでプレイヤー名を設定して、アバターが生成された時にテキストに反映させましょう。

PlayerAvatar.cs
+   // プレイヤー名のネットワークプロパティを定義する
+   [Networked]
+   public NetworkString<_16> NickName { get; set; }

    [SerializeField]
    private PlayerAvatarView view;
GameLauncher.cs
    private async void Start() {
        PlayerData.NickName = $"Player{Random.Range(0, 10000)}";

        networkRunner = Instantiate(networkRunnerPrefab);
        var result = await networkRunner.StartGame(new StartGameArgs {
            GameMode = GameMode.Shared,
            SceneManager = networkRunner.GetComponent<NetworkSceneManagerDefault>()
        });

        if (result.Ok) {
            var randomValue = Random.insideUnitCircle * 5f;
            var spawnPosition = new Vector3(randomValue.x, 5f, randomValue.y);
+           networkRunner.Spawn(playerAvatarPrefab, spawnPosition, Quaternion.identity, networkRunner.LocalPlayer, (_, networkObject) => {
+               // プレイヤー名のネットワークプロパティの初期値を設定する
+               networkObject.GetComponent<PlayerAvatar>().NickName = PlayerData.NickName;
+           });
        }
    }
PlayerAvatar.cs
    public override void Spawned() {
        if (Object.HasStateAuthority) {
            view.SetCameraTarget();
        }
+       // プレイヤー名をテキストに反映する
+       view.SetNickName(NickName.Value);
    }

Unityのプロジェクトをビルドして、プレイヤー名が表示されることを確認してみましょう。

Photon運営事務局TechBlog

Discussion

炉田謙 KenRoda炉田謙 KenRoda

良質なチュートリアルをありがとうございます。

質問です。
SessionNameを指定していないように見えるのですが、ここのチュートリアルではデフォルトのルームに入室しているイメージでしょうか。また、ルーム(セッション)名を指定したい場合はどうすれば良いのでしょうか?よろしくお願いいたします。

o8queo8que

StartGameArgsで、

  • SessionNameを設定すると、指定したセッション名のセッションの作成/参加を試みる
  • SessionNameを設定しないと、ランダムなセッションの作成/参加を試みる

という感じです。この記事では説明の簡単のために省略しているので、詳しくは以下のドキュメントが参考になるかと思います!

https://doc.photonengine.com/ja-jp/fusion/current/manual/matchmaking

炉田謙 KenRoda炉田謙 KenRoda

前回の回答を参考に進めていますが、補完とPhotonに関して質問がございます。

位置情報のアップロード・ダウンロードのFrequencyについて教えていただきたいです。また、これがアプリのFPSやディスプレイのHZとどのように関連しているのか知りたいです。
補完のFrequencyに関する情報も教えていただきたいです。これもアプリのFPSやディスプレイのHZとの関係を知りたいと思っています。
それぞれ、可能であれば変更方法を知りたいです。

Meta Quest2での実装の結果、フレームレートが60HZのように感じます。しかし、Quest2の120HZのディスプレイでは表示が不自然に見えることがあります。それに対して、60HZのエディター上では補完がしっかりと行われているようです。
よろしくお願いいたします。

o8queo8que

Fusionのシミュレーションの頻度(ティックレート)は、NetworkProjectConfigの「Simulation > TickRate」から設定できます。ティックレートとフレームレートは独立していて、スナップショット補間はフレーム単位で行われるので、頻度はフレームレートと同じという感じです。

ティックレートのデフォルトは60Hzなので、フレームレートが60Hzの場合に正常・それ以外の場合に不自然なら、そもそも補間が機能していない可能性があるので、まず補間の設定が正しく出来ているか見直してみると良さそうです。

あと、記事の内容に直接関係のない個別の質問等は、公式のお問い合わせフォームから聞いてもらえると助かります!

N4N4

「2プレイヤー間での、変数の共有」を、このサンプルを参考にして試行錯誤しています。

最もシンプルな実験として
・String型変数testを、2プレイヤーのどちらでも参照、編集する
ことを目指しています。
つまり
・変数testは、最初は空文字列
・どちらのプレイヤーも、一回画面クリックすると、testに"added"を追加する
・よって、各プレイヤーが1回ずつ画面をクリックすると、testは"addedadded"になるようにしたい
というものです。

今やってみたのは、サンプルのPlayer_Avatorスクリプトに変数testを追加し
public class Player_Avator : NetworkBehaviour
{

public string test = "";

GameLauncher.csに
//Spawn結果をavaに格納
public NetworkObject ava;
private async void Start()
{

ava = networkRunner.Spawn(playerAvatarPrefab, spawnPosition, Quaternion.identity, networkRunner.LocalPlayer);

}
public void add()
{
ava.GetComponent<Player_Avator>().test += "Added";
}

とやってみましたが、各プレイヤーが画面クリックし上記add()を1回ずつコールしても
Debug.Log(glauncher.GetComponent<GameLauncher>().ava.GetComponent<Player_Avator>().test);
として内容を確認すると、"added"は一個しか追加されていません=変数testの内容は、2プレイヤー間で共有されているのでなく、プレイヤーごとに独立しています。

test変数を、Player_Avatorクラスのstatic変数にしても同じ結果でした。

もう少し変えれば、うまくいくのでしょうか?

それとも、そもそも、この記事の「NETWORK OBJECT」の用途は、上記のような「変数の共用」という目的とは全然違うもので、まったく別の勉強が必要なのでしょうか?

o8queo8que

まず、ネットワーク上で同期したい変数はネットワークプロパティ、関数はRPCで定義する必要があります。(NetworkBehaviourに追加した変数やメソッドが、自動的にいい感じに同期される訳ではないことに注意してください)
また、上記のコードでは、ネットワークオブジェクトのインスタンスはプレイヤー毎に生成されているので、変数の値の更新やメソッドの実行は各インスタンスに対して行われます。プレイヤー間で変数を共有をしたいなら、「共有用のネットワークオブジェクトをシーン上に一つだけ生成する」等の実装も必要になるかと思います。

実装例については、以下の記事も参考にしてみてください!
https://zenn.dev/o8que/articles/afe59cfb76404b

あと、他の方の質問へのコメントでもお願いしているのですが、記事の内容に直接関係のない個別の質問等は、公式のお問い合わせフォームから聞いてもらえると助かります!

N4N4

回答ありがとうございます。

すみません、実は「記事の内容に直接関係のない」かすら、あまり把握していなかったのですが、やはりこの記事のアバター同期とはかなり別の技術のようですね。

公式のお問い合わせフォーム
たびたびお手数ですが、こちらの場所はどちらになるでしょうか?

https://zenn.dev/p/photon_japan

こちらのトップページには、リンクが見つからなかったので。

o8queo8que

技術ブログのトップページのヘッダーにPhoton公式サイトへのリンクが、Photon公式サイトの下部にお問い合わせへのリンクがあります。

N4N4

ありがとうございます。見つかりました。