
マイケル
どうもみなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
クマ〜〜〜

マイケル
今日はこれまで作っていた3Dゲームに
ネットワーク機能をつけてマルチプレイできるようにしようとしていたのですが・・・
ネットワーク機能をつけてマルチプレイできるようにしようとしていたのですが・・・
※過去記事はこちら!
【Unity】オンライン3Dアクションゲームの作り方をまとめる! (前編)

Unityゲーム開発 オンライン3Dアクションゲームの作り方

マイケル
参考にして進めていた書籍の情報が古すぎて全く使えませんでした・・・

エレキベア
2014年に書かれた本だから仕方ないクマ・・・

マイケル
そんなわけで現在はどのように実装するのがいいのか調べてみたところ、
PhotonUnityNetworking(以下、PUN)というサービスを使うのがよさそうだということが分かりました!
今回はPUNの導入とルーム管理機能について調べてまとめます!
PhotonUnityNetworking(以下、PUN)というサービスを使うのがよさそうだということが分かりました!
今回はPUNの導入とルーム管理機能について調べてまとめます!

エレキベア
PUNって何なんクマ?

マイケル
いい質問だね!
それじゃまずはPUNの概要についてみていこう!
それじゃまずはPUNの概要についてみていこう!
PUNとは

マイケル
まずはPUNとは何か、なぜ選んだかについて!
〜PUNとは〜
Photon Unity Networking の略で、
マスターサーバやルーム管理などマルチプレイ実装に必要な機能を
提供してくれるUnityパッケージである。
・・・ Photon公式サイトはこちら

エレキベア
これを使うとマルチプレイ機能が簡単に実装できるというわけクマね

マイケル
そういうことさ!
こういったサービスを使わなかったら、サーバの準備などハード面の問題やNATパンチスルーなど通信問題等、考慮して組み込まないといけないんだけどその辺の面倒くさい機能を一律提供してくれるんだ!
こういったサービスを使わなかったら、サーバの準備などハード面の問題やNATパンチスルーなど通信問題等、考慮して組み込まないといけないんだけどその辺の面倒くさい機能を一律提供してくれるんだ!

マイケル
ただ、20人までの接続は無料だけど、それ以上は有料プランになってしまうので注意です

エレキベア
すごいクマ〜〜〜!
PUNの他にも色々あるクマか??
PUNの他にも色々あるクマか??

マイケル
まずUnityに標準で組み込まれているUNetというのがあったんだけど、これはUnity2019で廃止されたみたいだ。
Photonと同じようなサービスとしては他にMUN、GameLift、Strix Unity SDKというのもあるみたいだよ!
Photonと同じようなサービスとしては他にMUN、GameLift、Strix Unity SDKというのもあるみたいだよ!

マイケル
今回はその中でも導入例が多くて機能数も多いPUNを使用したというわけさ!

エレキベア
そんなにたくさんあるクマか
導入方法

マイケル
導入方法は以下の手順になります!
1. Photon公式サイトでアカウント登録をし、アプリIDを取得する。
Photon公式サイト

2. AssetStoreから「PUN2」をインポートする。

3. Window > Photon Unity Networking > PUN Wizard > SetUp ProjectでアプリIDを設定する。
※再起動しないとメニューに出てこない場合があります。
4. Assets/Photon/PhotonNetworking/Resources配下にあるPhotonServerSettingsの「Fixed Region」に「jp」と入力する。


マイケル
以上で導入は完了です!

エレキベア
かんたん4ステップクマ
実装方法について

マイケル
それでは実装に移っていきます

マイケル
まず、Photonのルーム管理までの流れは下記の図のようになります!

エレキベア
Photonサーバの中でロビーやルームを管理してくれてるクマね

マイケル
このイラストの矢印部分の
1. Photonサーバ接続・切断
2. ロビー入退室
3. ルーム作成・入退室
に沿って実装方法を見ていくよ!
1. Photonサーバ接続・切断
2. ロビー入退室
3. ルーム作成・入退室
に沿って実装方法を見ていくよ!
Photonサーバ接続・切断
// コールバック機能を使うためMonoBehaviourPunCallbacksを継承
public class PhotonManager : MonoBehaviourPunCallbacks
{
・・・略・・・
// Photonサーバ接続処理
public void ConnectPhoton(bool boolOffline)
{
if (boolOffline)
{
// オフラインモードを設定
PhotonNetwork.OfflineMode = true; // OnConnectedToMaster()が呼ばれる
return;
}
// Photonサーバに接続する
PhotonNetwork.ConnectUsingSettings();
}
// Photonサーバ切断処理
public void DisConnectPhoton()
{
PhotonNetwork.Disconnect();
}
// コールバック:Photonサーバ接続完了
public override void OnConnectedToMaster()
{
base.OnConnectedToMaster();
}
}
// コールバック:Photonサーバ接続失敗
public override void OnDisconnected(DisconnectCause cause)
{
base.OnDisconnected(cause);
}
・・・略・・・
}

マイケル
Photonサーバへの接続はConnectUsingSettingメソッド、
切断時はPhotonNetwork.Disconnectメソッドを使用します。
切断時はPhotonNetwork.Disconnectメソッドを使用します。
使用するとコールバックで
OnConnectedToMasterメソッド(接続完了時)
OnDisconnectedメソッド(失敗時)
が呼び出されるので接続結果に対しての処理はこちらに記述しましょう!

マイケル
コールバック関数を定義するため、継承クラスはMonoBehaviourPunCallbacksに変更するのも忘れずに行って下さい

エレキベア
さっき設定したPhotonServerSettingsを使って接続するクマね

マイケル
飲み込みが早いですね

マイケル
また、オフラインで接続したい場合には、PhotonNetwork.OfflineModeをtrueに設定します。
その場合、trueにした時点でコールバック処理が呼び出されるため注意しましょう!
その場合、trueにした時点でコールバック処理が呼び出されるため注意しましょう!

エレキベア
数行かけば済むから簡単クマね〜〜
ロビー入退室
// コールバック:Photonサーバ接続完了
public override void OnConnectedToMaster()
{
・・・略・・・
// ロビーに接続
PhotonNetwork.JoinLobby();
}
// コールバック:ロビー接続完了
public override void OnJoinedLobby()
{
base.OnJoinedLobby();
}
// コールバック:ルーム一覧更新処理
// (ロビーに入室した時、他のプレイヤーが更新した時のみ)
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
base.OnRoomListUpdate(roomList);
}

マイケル
ロビーへの入室はJoinLobbyメソッドを使用します!
こちらも同じく入室完了後にコールバックとしてOnJoinedLobbyメソッドが呼び出されます。

エレキベア
Photonサーバ接続と同じような感じクマね

マイケル
また、ロビー入室時に上記OnRoomListUpdateメソッドが呼び出されて
ロビー内のルーム一覧が取得できるのですが、これが中々曲者で・・・
ロビー内のルーム一覧が取得できるのですが、これが中々曲者で・・・

マイケル
コールバックされるタイミングは
・ロビー入室時
・他のプレイヤーがルームを更新した時
だけで、ルーム更新時には更新したルームリストしか引数で渡されてこないんです。
・ロビー入室時
・他のプレイヤーがルームを更新した時
だけで、ルーム更新時には更新したルームリストしか引数で渡されてこないんです。
// コールバック:ルーム一覧更新処理
// (ロビーに入室した時、他のプレイヤーが更新した時のみ)
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
base.OnRoomListUpdate(roomList);
// ルーム一覧更新
foreach (var info in roomList)
{
if (!info.RemovedFromList)
{
// 更新データが削除でない場合
roomDispList.Add(info);
}
else
{
// 更新データが削除の場合
roomDispList.Remove(info);
}
}
}

マイケル
そのため上記のように渡されてきたルームが削除かどうかを調べて表示するリストを更新しなければならないようです。

マイケル
その他にも自動入室するAuto-Join Lobyや、ルーム一覧を取得するgetRoomListなんて機能もあったらしいですが、どうやらPUN2で廃止されたらしく、自分で実装するしかなさそうです・・・
リンク先(参考):https://doc.photonengine.com/en-us/pun/v2/getting-started/migration-notes

エレキベア
カナシマシマシクマ・・・・
ルーム作成・入退室
// ルーム作成・入室処理
public void CreateRoom(string roomName)
{
PhotonNetwork.CreateRoom(roomName);
}
// ルーム入室処理
public void ConnectToRoom(string roomName)
{
PhotonNetwork.JoinRoom(roomName);
}
// コールバック:ルーム作成完了
public override void OnCreatedRoom()
{
base.OnCreatedRoom();
}
// コールバック:ルーム作成失敗
public override void OnCreateRoomFailed(short returnCode, string message)
{
base.OnCreateRoomFailed(returnCode, message);
}
// コールバック:ルームに入室した時
public override void OnJoinedRoom()
{
base.OnJoinedRoom();
}

マイケル
ルームの作成はCreateRoomメソッド、
ルームの入室はJoinRoomメソッドを使用します。
ルームの入室はJoinRoomメソッドを使用します。
こちらも他のメソッド と同じくコールバックとして
OnCreatedRoomメソッド (ルーム作成完了時)
OnCreateRoomFailedメソッド (ルーム作成失敗時)
OnJoinedRoomメソッド (ルーム入室時)
がそれぞれよばれます!

マイケル
ただ一点注意なのは、ルームの作成時には自動で入室処理が行われるということです。
まあルームにはホストが必要なので当たり前といえばそうですが・・・
まあルームにはホストが必要なので当たり前といえばそうですが・・・

エレキベア
これでルーム入室まで出来たクマね

マイケル
とりあえずここまでの機能で接続は試せそうだね。
早速これらを使って接続を試していこう!
早速これらを使って接続を試していこう!
接続テストプログラムの作成

マイケル
接続やメソッド呼び出しを確認するため、テスト用の画面を作りました!
作った画面は下記になります。
作った画面は下記になります。


マイケル
モード選択でONLINEModeを選ぶとPhotonサーバと接続すると・・・


マイケル
ルーム作成とロビーにあるルーム一覧が表示される!
確認する機能だけに絞った画面になります
確認する機能だけに絞った画面になります

マイケル
下記にGUI表示も含めたソースは全部下に貼っておくので、
少し長いですが必要に応じて参考にしてください!
少し長いですが必要に応じて参考にしてください!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
public class PhotonManager : MonoBehaviourPunCallbacks
{
string mode; // モード(ONLINE, OFFLINE)
string dispStatus; // 画面項目:状態
string dispMessage; // 画面項目:メッセージ
string dispRoomName; // 画面項目:ルーム名
List<RoomInfo> roomDispList; // 画面項目:ルーム一覧
// 状態
public enum Status
{
ONLINE, // オンライン
OFFLINE, // オフライン
};
private void Start()
{
initParam();
}
// 変数初期化処理
private void initParam()
{
dispRoomName = "";
dispMessage = "";
dispStatus = Status.OFFLINE.ToString();
roomDispList = new List<RoomInfo>();
}
// Photonサーバ接続処理
public void ConnectPhoton(bool boolOffline)
{
if (boolOffline)
{
// オフラインモードを設定
mode = Status.OFFLINE.ToString();
PhotonNetwork.OfflineMode = true; // OnConnectedToMaster()が呼ばれる
dispMessage = "OFFLINEモードで起動しました。";
return;
}
// Photonサーバに接続する
mode = Status.ONLINE.ToString();
PhotonNetwork.OfflineMode = false;
PhotonNetwork.ConnectUsingSettings();
}
// Photonサーバ切断処理
public void DisConnectPhoton()
{
PhotonNetwork.Disconnect();
// 変数初期化
initParam();
}
// コールバック:Photonサーバ接続完了
public override void OnConnectedToMaster()
{
base.OnConnectedToMaster();
if (Status.ONLINE.ToString().Equals(mode))
{
dispStatus = Status.ONLINE.ToString();
dispMessage = "サーバに接続しました。";
// ロビーに接続
PhotonNetwork.JoinLobby();
}
}
// コールバック:Photonサーバ接続失敗
public override void OnDisconnected(DisconnectCause cause)
{
base.OnDisconnected(cause);
dispMessage = "サーバから切断しました。";
dispStatus = Status.OFFLINE.ToString();
}
// コールバック:ロビー入室完了
public override void OnJoinedLobby()
{
base.OnJoinedLobby();
}
// ルーム一覧更新処理
// (ロビーに入室した時、他のプレイヤーが更新した時のみ)
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
base.OnRoomListUpdate(roomList);
// ルーム一覧更新
foreach (var info in roomList)
{
if (!info.RemovedFromList)
{
// 更新データが削除でない場合
roomDispList.Add(info);
}
else
{
// 更新データが削除の場合
roomDispList.Remove(info);
}
}
}
// ルーム作成処理
public void CreateRoom(string roomName)
{
PhotonNetwork.CreateRoom(roomName);
}
// ルーム入室処理
public void ConnectToRoom(string roomName)
{
PhotonNetwork.JoinRoom(roomName);
}
// コールバック:ルーム作成完了
public override void OnCreatedRoom()
{
base.OnCreatedRoom();
dispMessage = "ルームを作成しました。";
}
// コールバック:ルーム作成失敗
public override void OnCreateRoomFailed(short returnCode, string message)
{
base.OnCreateRoomFailed(returnCode, message);
dispMessage = "ルーム作成に失敗しました。";
}
// コールバック:ルームに入室した時
public override void OnJoinedRoom()
{
base.OnJoinedRoom();
// 表示ルームリストに追加する
roomDispList.Add(PhotonNetwork.CurrentRoom);
dispMessage = "【" + PhotonNetwork.CurrentRoom.Name + "】" + "に入室しました。";
}
// ---------- 設定GUI ----------
void OnGUI()
{
float scale = Screen.height / 480.0f;
GUI.matrix = Matrix4x4.TRS(new Vector3(
Screen.width * 0.5f, Screen.height * 0.5f, 0),
Quaternion.identity,
new Vector3(scale, scale, 1.0f));
GUI.Window(0, new Rect(-200, -200, 400, 400),
NetworkSettingWindow, "Photon接続テスト");
}
Vector2 scrollPosition;
void NetworkSettingWindow(int windowID)
{
// ステータス, メッセージの表示
GUILayout.BeginHorizontal();
GUILayout.Label("状態: " + dispStatus, GUILayout.Width(100));
GUILayout.FlexibleSpace();
if (Status.ONLINE.ToString().Equals(dispStatus))
{
// サーバ接続時のみ表示
if (GUILayout.Button("切断"))
DisConnectPhoton();
}
GUILayout.EndHorizontal();
GUILayout.Label(dispMessage);
GUILayout.Space(20);
if (!Status.ONLINE.ToString().Equals(dispStatus))
{
// --- 初期表示時、OFFLINEモードのみ表示
// マスターサーバに接続する
GUILayout.Label("【モード選択】");
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("ONLINE Mode"))
ConnectPhoton(false);
if (GUILayout.Button("OFFLINE Mode"))
ConnectPhoton(true);
GUILayout.EndHorizontal();
}
else if (Status.ONLINE.ToString().Equals(dispStatus))
{
// --- ONLINEモードのみ表示
if (!(PhotonNetwork.CurrentRoom != null))
{
// ルーム作成
GUILayout.Label("【ルーム作成】");
GUILayout.BeginHorizontal();
GUILayout.Label(" ルーム名: ");
dispRoomName = GUILayout.TextField(dispRoomName, GUILayout.Width(150));
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
// 作成ボタン
GUILayout.BeginHorizontal();
if (GUILayout.Button("作成 & 入室"))
{
CreateRoom(dispRoomName);
}
GUILayout.EndHorizontal();
GUILayout.Space(20);
// ルーム一覧
GUILayout.Label("【ルーム一覧 (クリックで入室)】");
// 一覧表示
scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Width(380), GUILayout.Height(100));
if (roomDispList != null && roomDispList.Count > 0)
{
// 更新ボタン
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("更新"))
{
// ロビーに入り直す
roomDispList = new List<RoomInfo>();
PhotonNetwork.LeaveLobby();
PhotonNetwork.JoinLobby();
}
// ルーム一覧
GUILayout.EndHorizontal();
foreach (RoomInfo roomInfo in roomDispList)
if (GUILayout.Button(roomInfo.Name, GUI.skin.box, GUILayout.Width(360)))
ConnectToRoom(roomInfo.Name);
}
GUILayout.EndScrollView();
}
}
}
}
接続テスト結果

マイケル
それじゃ作ったテストプログラムで接続を確認していくよ!

エレキベア
楽しみクマね〜〜

マイケル
ネットワークテストで実行アプリが2つ以上必要だから、
ビルドして書き出したAPPとUnityシミュレータでの操作とビルドファイルでの操作が同期されていることを確認します!
ビルドして書き出したAPPとUnityシミュレータでの操作とビルドファイルでの操作が同期されていることを確認します!



マイケル
まずUnityシミュレータ側でルームを作成して、、


マイケル
ビルドしたAPPで接続するとUnityシミュレータ側で作ったルームが無事に一覧に表示されています!
どうやらうまく同期できたみたいです!
どうやらうまく同期できたみたいです!

エレキベア
おめでとうクマ〜〜〜!!

マイケル
ここまで確認出来ればあとはマルチプレイ機能をガンガン実装していくだけだ!

エレキベア
大きな一歩を踏み出せたクマね
まとめ

マイケル
というわけで今回はPhotonを使用したルーム作成の確認でした!

マイケル
ネット上も古い情報が多すぎて
これだけ調べるのでも丸一日かかってつかれたぜ・・・
これだけ調べるのでも丸一日かかってつかれたぜ・・・

エレキベア
おつかれさまクマ・・・

エレキベア
でも同期できた時は楽しかったクマね〜〜

マイケル
そうだね!
これからゲームに組み込んでいくのが楽しみだ!
これからゲームに組み込んでいくのが楽しみだ!

エレキベア
一緒にがんばっていくクマ〜〜〜!

マイケル
心強いぜエレキベア!
それじゃ今回はこの辺で!アデュー!
それじゃ今回はこの辺で!アデュー!

エレキベア
クマ〜〜〜
【PUN2】ネットワーク管理にPhotonを使ってみる 〜導入からルーム・ロビー管理まで〜 〜完〜
コメント
コメント失礼いたします。
public override void OnCreatedRoom()
{
base.OnCreatedRoom();
dispMessage = “ルームを作成しました。”;
}
とありますがこれにボタンの割り当てなどは必要ないのでしょうか?
私も現在pun2でロビーを作成しているのですがルーム作成ボタンを押してもなにも起こらないのです。。。
コメントありがとうございます!
「OnCreatedRoom()」メソッドは「PhotonNetwork.CreateRoom(roomName);」の処理でルームに入室後、
コールバックとして呼び出されるので、ボタンへの割り当ては不要です。
しかし、「OnCreatedRoom()」メソッドの直後に「OnJoinedRoom()」も呼ばれるため、
メッセージが上書きされ画面上での確認はできないかと思います・・・。
もし呼ばれているのを確認したければ、お手数ですがデバッグログを入れて動かしてみてください!
そもそもロビー入室処理すら呼ばれないのであれば、考えられる原因としては
・MonoBehaviourPunCallbacks を継承していない
・メソッド名が誤っている
あたりでしょうか・・・。
僕もかなり記憶が曖昧ですが、お力になれれば幸いです!1