【Unity6 × VRoid × Photon】Unity6でマルチプレイを実装する方法

【Unity6 × VRoid × Photon】Unity6でマルチプレイを実装する方法

前回は、VRoidアバターをUnityに取り込み、キーボード操作で動かせるようにしました。

今回はその続きとして、Photonを利用して複数プレイヤーが同時に接続できるように設定していきます。

・Photonを使ったマルチプレイ接続

・アバター生成とカメラ追従

・他プレイヤーとのアニメーション同期

・頭上の名前表示の同期

ツール概要
Photon PUN2Unity Asset Store で無料配布:https://assetstore.unity.com/packages/tools/network/pun-2-free-119922
Photon App IDhttps://dashboard.photonengine.com/ でアカウント作成後、App ID を取得

Photonのアカウント作成とApp ID取得

  1. Photon Cloud にアクセスし、アカウントを作成してサインイン
  2. ダッシュボード画面を開く:https://dashboard.photonengine.com/
  3. 以下の手順でアプリケーションを作成
    • CREATE A NEW APP をクリック
    • Multiplayer Game を選択
    • Select Photon SDK → Pun を選択
    • Application Name → 任意の名前を入力(例:Photon_Test)
    • CREATE をクリック
  4. 作成されたアプリケーションの App ID をコピー

PUN2のインポート

  1. Unityのアセットストアから PUN2 - FREE をインポート
  2. Unityに戻り、Package Manager > My Assets > PUN2 - FREE から Import 
  3. PUN Setup 画面が表示されたら、AppId or Email に先ほどコピーしたApp IDをペースト
  4. Setup Project をクリックし、完了後に Close

サーバーの設定

  1. Project > Assets > Photon > PhotonUnityNetworking > Resources > PhotonServerSettings を開く
  2. Inspector > Server / Cloud SettingsFixed Regionjp に設定(日本サーバ優先)

VRoid Studio でのアバター準備

  1. VRoid Studio から、マルチプレイ用にもう1体のアバターをダウンロード
  2. Unity の Assets > Avatar フォルダにドラッグ&ドロップ
  3. Hierarchy にアバターを配置し、Animator Controller に前回作成した WalkingAnimator を設定

スクリプトの追加

  1. Assets > Scripts 内で右クリック
    → Create > MonoBehaviour Scriptを選択
    → MultiplayerManager , AvatarAnimationSync, AvatarNameLabel を新規追加
  2. それぞれの.cs ファイルをダブルクリックし、スクリプトをコピーして保存
    AvatarMovementController, AvatarCameraFollow の内容をアップデート(プログラムの解説はコメントに記載)
スクリプト名役割
MultiplayerManagerPhoton接続とアバターの生成
AvatarAnimationSync歩行状態の同期
AvatarNameLabel頭上にプレイヤー名を表示
AvatarMovementControllerアバターの移動制御
AvatarCameraFollow自分のアバターにカメラを追従

MultiplayerManager.cs

Photon接続とアバターの生成

using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

// Manages multiplayer connections and avatar instantiation
// マルチプレイヤー接続とアバター生成を管理する
public class MultiplayerManager : MonoBehaviourPunCallbacks
{
    // List of avatar prefabs to instantiate
    // 生成するアバタープレハブのリスト
    public GameObject[] avatarPrefabs;

    void Start()
    {
        // Set the player's nickname (randomized number)
        // プレイヤー名を設定(ランダムな番号を付与)
        PhotonNetwork.NickName = "Player" + Random.Range(1, 999);

        // Connect to the Photon server
        // Photonサーバーに接続
        PhotonNetwork.ConnectUsingSettings();
    }

    // Called when connected to the master server
    // マスターサーバーに接続されたときに呼ばれる
    public override void OnConnectedToMaster()
    {
        // Join or create a room named "Room"
        // "Room" という名前のルームに参加、なければ作成
        PhotonNetwork.JoinOrCreateRoom("Room", new RoomOptions(), TypedLobby.Default);
    }

    // Called when successfully joined a room
    // ルームに参加したときに呼ばれる
    public override void OnJoinedRoom()
    {
        // Choose avatar based on the player's unique ID
        // プレイヤーのIDに基づいてアバターを選択
        int index = (PhotonNetwork.LocalPlayer.ActorNumber - 1) % avatarPrefabs.Length;

        // Randomize the starting position
        // 初期位置をランダムに設定
        Vector3 pos = new Vector3(Random.Range(-3, 3), 0, Random.Range(-3, 3));

        // Instantiate the avatar for this player
        // プレイヤー用のアバターを生成
        PhotonNetwork.Instantiate(avatarPrefabs[index].name, pos, Quaternion.identity);
    }
}

AvatarAnimationSync.cs

歩行状態の同期

using UnityEngine;
using Photon.Pun;

// Synchronizes animation state across the network
// アニメーションの状態をネットワークで同期する
public class AvatarAnimationSync : MonoBehaviourPun, IPunObservable
{
    private Animator animator;
    private bool isWalking;

    void Start()
    {
        // Get Animator component
        // Animatorコンポーネントを取得
        animator = GetComponent<Animator>();
    }

    void Update()
    {
        // Update walking animation state
        // 歩行アニメーションの状態を更新
        animator.SetBool("IsWalking", isWalking);
    }

    public void SetIsWalking(bool walking)
    {
        // Update walking state locally
        // ローカルで歩行状態を更新
        isWalking = walking;
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            // Send walking state to other players
            // 自分の歩行状態を他のプレイヤーに送信
            stream.SendNext(isWalking);
        }
        else
        {
            // Receive walking state from other players
            // 他のプレイヤーから歩行状態を受信
            isWalking = (bool)stream.ReceiveNext();
        }
    }
}

AvatarNameLabel.cs

頭上にプレイヤー名を表示

using UnityEngine;
using Photon.Pun;
using TMPro;

// Displays the player's name above the avatar
// アバターの頭上にプレイヤー名を表示する
public class AvatarNameLabel : MonoBehaviourPun
{
    [SerializeField] private TextMeshPro nameLabel;

    void Start()
    {
        // Set the name to "PlayerX" where X is the player number
        // プレイヤー番号を元に "PlayerX" の名前を表示
        nameLabel.text = $"Player{photonView.OwnerActorNr}";
    }

    void LateUpdate()
    {
        // Make the name label always face the camera
        // 名前ラベルが常にカメラ方向を向くように調整
        nameLabel.transform.rotation = Camera.main.transform.rotation;
    }
}

AvatarMovementController.cs

アバターの移動制御(前回のスクリプトをアップデート)

using UnityEngine;
using Photon.Pun;

// Handles player movement and animation
// プレイヤーの移動とアニメーションの制御を行う
public class AvatarMovementController : MonoBehaviourPun
{
    private Animator animator;
    private AvatarAnimationSync animationSync;

    void Start()
    {
        // Get Animator component attached to the avatar
        // アバターにアタッチされているAnimatorコンポーネントを取得
        animator = GetComponent<Animator>();

        // Get the animation sync script
        // アニメーション同期用スクリプトを取得
        animationSync = GetComponent<AvatarAnimationSync>();
    }

    void Update()
    {
        // Only the local player can control this avatar
        // 自分のアバターのみ操作できるように制限
        if (!photonView.IsMine) return;

        // Get horizontal (left/right) and vertical (forward) input values
        // 水平方向(左右)および垂直方向(前進)の入力を取得
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        // Determine if the avatar is walking
        // 移動キーが押されているかどうかで「歩行中」かを判定
        bool isWalking = Mathf.Abs(h) > 0.1f || Mathf.Abs(v) > 0.1f;

        // Pass the walking state to the Animator
        // 歩行状態をAnimatorに伝える
        animator.SetBool("IsWalking", isWalking);

        // Synchronize walking state over the network
        // ネットワーク同期用スクリプトに歩行状態を送信
        if (animationSync != null)
            animationSync.SetIsWalking(isWalking);

        // Rotate the avatar based on left/right input
        // 左右キーに応じてアバターを回転
        transform.Rotate(0, h * 100f * Time.deltaTime, 0);

        // Move the avatar forward if up key is pressed
        // 上キーが押されたらアバターを前進させる
        if (v > 0) transform.Translate(Vector3.forward * 2f * Time.deltaTime);
    }
}

AvatarCameraFollow.cs

自分のアバターにカメラを追従(前回のスクリプトをアップデート)

using UnityEngine;
using Photon.Pun;

// Controls the camera to follow the local player's avatar
// ローカルプレイヤーのアバターにカメラを追従させる
public class AvatarCameraFollow : MonoBehaviourPun
{
    void Start()
    {
        // Only create a camera for the local player's avatar
        // 自分のアバターにのみカメラを生成する
        if (!photonView.IsMine) return;

        // Remove existing AudioListeners to avoid conflicts
        // 他のAudioListenerを削除(複数あるとエラーになる)
        foreach (var listener in FindObjectsByType<AudioListener>(FindObjectsSortMode.None))
        {
            Destroy(listener);
        }

        // Create a new camera object and set it up
        // 新しいカメラオブジェクトを生成し設定する
        GameObject camObj = new GameObject("PlayerCamera");
        camObj.tag = "MainCamera";
        camObj.AddComponent<Camera>();
        camObj.AddComponent<AudioListener>();

        // Attach the camera to the avatar and position it
        // カメラをアバターにアタッチし、位置を設定
        camObj.transform.SetParent(transform);
        camObj.transform.localPosition = new Vector3(0, 2, -3);
        camObj.transform.localRotation = Quaternion.Euler(13, 0, 0);
    }
}

アタッチ手順

  1. 2体のアバターを選択 → Inspector > Add Component →
    • AvatarMovementController
    • AvatarCameraFollow 
    • AvatarAnimationSync
    • AvatarNameLabelを追加
  2. Hierarchy > 右クリック > Create Empty
    → GameObjectの名前を MultiplayerManager に変更
  3. MultiplayerManagerMultiplayerManager.cs をアタッチ
  4. InspectorAvatar Prefabs の + をクリックし、2体のVRoidアバターを追加

TextMeshProの導入

  1. Hierarchyのアバターを選択した状態で右クリック
    3D Object > Text-TextMeshPro
  2. 「Import TMP Essentials」をクリック(自動的にフォント素材がインポートされる)
  3. 「Import TMP Examples & Extras」もクリック

TextMeshProの配置

  1. 作成した Text (TMP) をアバターの頭上に移動
    • フォントサイズは 「2」 程度に設定
    • Alignment「Center」と「Middle」を「中央寄せ」に設定
    • RectTransform > Pos Y「1.8」程度に設定

テキストの位置は、アバターの頭上に!
サイズや位置はアバターによって変えてください!

名前のリンク付け

  1. Text (TMP) の名前を Text (TMP)_A , Text (TMP)_B(任意)に変更 (アバターを判別できる名前に変更)
  2. Hierarchyから2体のアバターをそれぞれ選択し、
    Inspector からAvatarNameLabel コンポーネントの NameLabel の項目に Text (TMP)_A, B を選択

Photon対応のPrefab設定

  1. Hierarchyの2体のアバターを選択
  2. InspectorAdd Component
    • Photon View
    • Photon Transform View

Prefabとして保存

  1. Hierarchyの2体のアバターを選択
  2. Assets > Photon > PhotonUnityNetworking > Resources フォルダにドラッグ&ドロップ
  3. 「Original Prefab」をクリック
  4. Resources 内に2体のアバターが表示されていることを確認

非表示設定

  1. Hierarchyの2体のアバターを選択
  2. Inspector の上部にあるチェックを外して非表示状態にする

メインカメラの消去

Hierarchy「Main Camera」を消去する

ビルド手順

  1. ▶️ Play を押し、エラーがないか確認
  2. File > Edit > Project Settings... をクリック
  3. Player > Resolution and Presentation > Default Is Native Resolution から FullScreen Window > Windowed に変更(Windowのサイズは任意で変更)

4. Build And Run をクリック

5. test など適当な名前にし、任意の場所に保存

6. これでWindowが立ち上がったら成功です!

ローカルテスト

  1. Build And Run 後、Unityの画面でも「Play」を押す
  2. デスクトップでビルドした実行ファイルを2つ起動する

確認ポイント

チェック項目内容
✅ プレイヤーが2体表示されるエディタとビルドしたゲーム両方に登場
✅ 名前が正しく表示されているPlayer1, Player2 のように表示
✅ キーボード入力で動く各自の画面で別々に動かせる
✅ カメラは個別追従他のプレイヤーのカメラが干渉しない
✅ 歩行アニメーションが同期片方が歩けば、相手にも反映される

全て上手く動作すれば成功です!

今回の手順を通じて、以下のことができるようになりました

・Photonを使ったマルチプレイ接続

・アバター生成とカメラ追従

・他プレイヤーとのアニメーション同期

・頭上の名前表示の同期

ぜひ、これをベースにご自身のプロジェクトに活用してください!

  • この記事を書いた人

kiro

kiroです。 Blender、Unityなどで学んだことを備忘録としてまとめています。