Unityでオンラインゲームを個人で作るなら外部のサービスなどを使って実装すると思うが、その中でもよく耳にするのが、「Photon」だ。PhotonはドイツのExitGame社が提供しているサービスで、PhotonServerなるものを使ってオンラインゲームで必要なあれこれを作り手側に提供してくれる。巷にはPhotonを解説してくれる参考記事がたくさんあったが、自分が望んでいるものがなかったので、それをまとめたい。
今回、Photonを使って、実際にマッチングしたらシーン移動をするようなものを作成する。また、対戦相手を待たずに先にシーン移動してしまうのではなく、対戦相手を見つけた後に移動するようにしたい。YouTubeで海外の方が行っているデモをもとに作成するので、そちらも見ておくとわかりやすいかも。
ちなみに最近、マルチプレイを助けてくれる「NetCode for GameObjects」なるものがUnityから出された。まだまだBeta版みたいだが、公式から出ているのが一番良いところだろう。ざっと見るとPhotonと似ているのでPhotonを抑えて置けば、すぐに対応できるのではないだろうか?もし余裕があればそっちの記事も出していきたい。
始める前にPhotonを使うなら必読するべき参考記事を上に貼っておく。無料でここまで纏めてくれているので始める前と後どちらでもいいので、必ず読もう。正直、難しくて自分もまだまだ読んでもわからん部分は多いが、これである程度の知識はつけることができると思う。
PUN2をアセットストアから手に入れる
まず、適当にUnityプロジェクトを作成したら、PUN2をアセットストアから入手しよう。このSDKをゲットすることで、Photonが提供しているサービスのあれこれを使うことができるようになるぞ。作成するプロジェクトは、3Dでも2Dでもお好きな方でどうぞ。
アセットストアでPhoton等と検索すればいくつかPhotonに関係したアセットが表示されると思う。その中から、「PUN2 FREE」を選択しよう。PUNとは、Photon Unity Networkingの略でUnity用のSDKだ。2が最新なのでこっちをゲットしよう。
実際にストアなどにリリース予定の場合は有料版を購入しないといけないらしいが、今回はテストなので無料版で。

アセットをゲットしたらパッケージマネージャーを開いて、PUN2をインポートしよう。

デフォルトで全部にチェックがついていると思うが、ついていなかったらALLを選択してからインポートしよう。

インポートが終わると、PUN Setup画面が表示されるはずだ。表示されなかった場合はツールバーのWindowからPhotonUnityNetworking→PUN Wizard → PUN Setupを選択して表示させよう。

ちなみにPUN2のインポートが完了していると、Assetsフォルダ以下にPhotonのフォルダが生成されている。中にDemoフォルダなどもあるので、詳しくしりたければ見てみるとよいかも。



Photonのダッシュボードからアプリを作成する
上のような画面になったらPhotonの公式ページへ飛んでいって設定を行おう。後から設定もできるが、先にやってしまおう。まだ、登録していない人は新規登録しておこう。
登録が済んだらログインできるので、そこからダッシュボードに移動してアプリを作成する。作成したら、IDが発行されるのでそのIDをコピーして上のSetup画面に貼り付けよう。あとは、参考記事のように設定する。


新しくアプリを作成したら、Photonの種別や、アプリケーション名を選択しよう。



Setup画面に戻って上のIDをペーストしよう。

Setupが完了したら、PhotonServerSettingsが表示される。ここのAppIdPUNの項目に上のIDがセットされているはずだ。ここまできたらPhotonのAPIが使えるようになるぞ。

ちなみに、Regionをjp等に設定した。これは参考記事を元にしている。AppVersionの項目はアプリをリリースする際に実際のバーションと同じバージョンに合わせておくことで、バージョン違いでの通信が起きないようにするものだ。とりあえず、テストなので設定する必要は特にないが一応。

ちなみに、PhotonSeverSettingsはPhotonの中のResourcesの中にあるので覚えておくといいかも。上述したら、WindowからPhotonUnityNetworkingを選択してもアクセスできる。

UIを作成する
次に、ランダムマッチングする用のボタンを作っておく。このボタンを押したらPhotonのサーバーにアクセスして対戦相手が見つかったら別のシーンに移動するようにする。また、「対戦相手を待っています」的な表示用のテキストも作る。下を参考に自分好みに作って欲しい。


移動するシーンの作成
次はマッチングして、移動した後のシーンをあらかじめ作っておこう。
今回はフィールドだけ作成しておいた。よく見られるチュートリアルでは、自分のキャラクターをフィールドの適当な場所に配置して戦えるようにするやつだ。


スクリプトの作成
さて、いよいよメインに入っていく。
Photonの接続部分を書いていくスクリプトを作成する。PhotonMasterとでも名付けて以下のコードを記述した。パッと見難しいが、意外とシンプル。上から順番通りではないが、コードを詳しく見ていこう。
ちなみにPhotonのAPIリファレンスはこちらから。(英語)
自分が使っているバージョンかどうかちゃんと確認しよう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;
public class PhotonMaster : MonoBehaviourPunCallbacks
{
public Text statusText;
private const int MaxPlayerPerRoom = 2;
private void Awake()
{
PhotonNetwork.AutomaticallySyncScene = true;
}
// Start is called before the first frame update
private void Start()
{
PhotonNetwork.ConnectUsingSettings();
}
private void OnGUI()
{
GUILayout.Label(PhotonNetwork.NetworkClientState.ToString());
}
//これをボタンにつける
public void FindOponent()
{
if (PhotonNetwork.IsConnected)
{
PhotonNetwork.JoinRandomRoom();
}
}
//Photonのコールバック
public override void OnConnectedToMaster()
{
Debug.Log("マスターに繋ぎました。");
}
public override void OnDisconnected(DisconnectCause cause)
{
Debug.Log($"{cause}の理由で繋げませんでした。");
}
public override void OnJoinRandomFailed(short returnCode, string message)
{
Debug.Log("ルームを作成します。");
PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = MaxPlayerPerRoom });
}
public override void OnJoinedRoom()
{
Debug.Log("ルームに参加しました");
int playerCount = PhotonNetwork.CurrentRoom.PlayerCount;
if (playerCount != MaxPlayerPerRoom)
{
statusText.text = "対戦相手を待っています。";
}
else
{
statusText.text = "対戦相手が揃いました。バトルシーンに移動します。";
}
}
public override void OnPlayerEnteredRoom(Player newPlayer)
{
if (PhotonNetwork.IsMasterClient)
{
if (PhotonNetwork.CurrentRoom.PlayerCount == MaxPlayerPerRoom)
{
PhotonNetwork.CurrentRoom.IsOpen = false;
statusText.text = "対戦相手が揃いました。バトルシーンに移動します。";
PhotonNetwork.LoadLevel("BattleScene");
}
}
}
}
名前空間と継承を変更する
まずは名前空間の部分。とりあえず、この2つは書いておこう。大体は使ってるからPhotonのコードを使う時には記入しておこう。逆にこれがないと使えないぞ。
using Photon.Pun;
using Photon.Realtime;
次に、MonoBehaviourをMonoBehaviourPunCallbacksに変更する。上の2つこれはとりあえず必須の呪文とでも覚えておこう。
public class PhotonMaster : MonoBehaviourPunCallbacks
Photonに接続する
Awake部分は置いといて、Start関数の中でConnectUsingSettings();
を読んでいる。まずこれで、Photonにアクセスする。何をするにしてもまずこれは絶対必要だ。Photonの詳しい概念は参考資料を見て把握してほしい。
private void Start()
{
PhotonNetwork.ConnectUsingSettings();
}
上の関数を呼んで成功すると、OnConnectedToMaster()
が呼ばれ、失敗するとOnDisconnected(DisconnectCause cause)
が呼ばれる。
public override void OnConnectedToMaster()
{
Debug.Log("マスターに繋ぎました。");
}
成功時呼ばれる。
public override void OnDisconnected(DisconnectCause cause)
{
Debug.Log($"{cause}の理由で繋げませんでした。");
}
失敗時呼ばれる。
これは一般にコールバックと呼ばれるもので、Photonでは、大体(というかすべてかな?)On~
という形で接頭辞にOnがくっ付く。コールバック関数は〇〇したら、呼ばれる関数のことで元々は電話をかけ直す的なところから来てるらしい。とりあえず、何かしたら呼ばれるものだと覚えておこう。
Photonではこのコールバック関数が非常に重要で豊富である。このコールバックをどれだか抑えておけるかが、きっとPhotonの理解に繋がっていくのではないだろうか。
上で記述したMonoBehaviourPunCallbacks
これをただのMonoBehaviour
にするとOn~
系の関数に全て赤線がついて使えなくなってしまう。MonoBehaviourPunCallbacks
はPhotonのコールバック関数を使うためには必須だ。

部屋を作成する
次にシーンで作成したボタンを押したら発動する関数が下だ。
//これをボタンにつける
public void FindOponent()
{
if (PhotonNetwork.IsConnected)
{
PhotonNetwork.JoinRandomRoom();
}
}
PhotonNetwork.IsConnected
の部分はもしPhotonのサーバーに繋がっているならtrueが返される。OnConnectedMasterが呼ばれたならtrueになるはずだ。

サーバーに接続してボタンを押したら、PhotonNetwork.JoinRandomRoom();
を呼ぶ。これは、ランダムな部屋があればランダムな部屋に入室する関数だ。最初の段階では、もちろんルームは存在しないので、OnJoinRandomFailed();
コールバック関数が呼ばれる。
そのコールバック関数の中でCreateRoom
を呼んでいる。
public override void OnJoinRandomFailed(short returnCode, string message)
{
Debug.Log("ルームを作成します。");
PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = MaxPlayerPerRoom });
}
CreateRoom
の第一引数にnullを入れているのは、これを入れることでランダムな名前の部屋を作成してくれるからだ。逆にフレンドと対戦したい部屋を作るなら、ルーム名をテキストオブジェクトなどから取得して、その名前のルームを作成するとよいだろう。

またCreateRoom
の引数は4つほどオプションがあって、今回はRoomOptionsの中で、部屋の入れる人数を指定した。
最初の方でprivate const int MaxPlayerPerRoom = 2;
という定数を宣言していたと思うが、これをMaxPlayersに設定した。これでこの部屋には2人しか入らないような設定がされた。
正直ここらへんの部屋作成や参加の仕方は人それぞれやり方あるので、とりあえず真似した後に自分なりのやり方にしてもよいと思う。
部屋の作成に成功した場合は自動的に部屋に参加する。部屋に参加した後は、OnJoinedRoom()
コールバック関数が呼ばれるぞ。
ちなみに部屋の作成に成功or失敗するとOnCreatedRoom()
やOnCreateRoomFailed
が呼ばれるが今回それは省いている。
部屋に参加したら
部屋に参加して、対戦相手が希望する人数に満たない場合は「対戦相手を待っています」と表示させている。
PhotonNetwork.CurrentRoom.PlayerCount;
で現在の部屋の人数がゲットできるので、その数と希望人数を比較している。
自身が後から参加する場合は部屋の人数に達するので、「対戦相手が揃いました。バトルシーンに移動します。」と表示されるはずだ。
public override void OnJoinedRoom()
{
Debug.Log("ルームに参加しました");
int playerCount = PhotonNetwork.CurrentRoom.PlayerCount;
if (playerCount != MaxPlayerPerRoom)
{
statusText.text = "対戦相手を待っています。";
}
else
{
statusText.text = "対戦相手が揃いました。バトルシーンに移動します。";
}
}
シーンを移動する
いよいよ、シーン移動部分のスクリプトだ。相手を待たずにシーン移動するのであれば上のOnJoinedRoom()
の中でシーン移動させればよいが今回は対戦相手を待ちたいので、OnPlayerEnteredRoom
コールバック関数の中でシーンの移動をしている。
この関数はプレイヤーが部屋に入ってきた時に呼ばれる関数だ。自身がルームに入ってきた時は呼ばれない。この関数の中で他のプレイヤーが入ってくることによって人数が満たされ(今回は2人に設定している。)シーンに移動している。
public override void OnPlayerEnteredRoom(Player newPlayer)
{
if (PhotonNetwork.IsMasterClient)
{
if (PhotonNetwork.CurrentRoom.PlayerCount == MaxPlayerPerRoom)
{
PhotonNetwork.CurrentRoom.IsOpen = false;
statusText.text = "対戦相手が揃いました。バトルシーンに移動します。";
PhotonNetwork.LoadLevel("BattleScene");
}
}
}
シーンの移動をしている部分は、PhotonNetwork.LoadLevel("BattleScene");
で上で作成した別のシーンを読みこんでロードしてる。
このPhotonNetwork.LoadLevel();
はマスタークライアントからしか呼んではいけないので、自身がマスターなのかどうかをif文でチェックしている。部屋を作成すれば、自動的にマスターが割り振られるはずなのでわざわざチェックする必要はないのかもしれないが念の為。
このPhotonNetwork.LoadLevel();
を呼ぶことで、マスター以外はマスターに合わせてシーンが移動させる。ただ、その為にはPhotonNetwork.AutomaticallySyncScene
をtrueにしておく必要がある。このスクリプトの中ではAwakeの中でtrueに設定してある。
private void Awake()
{
PhotonNetwork.AutomaticallySyncScene = true;
}
これをtrueにしているのでマスターのシーン移動のタイミングで他のプレイヤーもシーンを移動するぞ。
また、この部屋にもう他のプレイヤーが入ってこないようにPhotonNetwork.CurrentRoom.IsOpen
をfalseにしてある。これでこの部屋はランダムマッチに引っかからないはずだ。
PhotonNetwork.CurrentRoom.IsOpen = false;
ログを表示させる
Photonは簡単に自身の状態を表示させることができる機能もついてる。GUILayout.Label(PhotonNetwork.NetworkClientState.ToString());
これを書くことで画面の左上(デフォなのかな?)で自身の状態。つまり、「今Photonのマスターサーバーにつながっているのか」や「部屋に入っているかな」などをチェックできる。
コンソール画面で出してもいいが、画面上にでるのはビルドした時にも便利だと思うのでテスト段階ではおすすめだ。
private void OnGUI()
{
GUILayout.Label(PhotonNetwork.NetworkClientState.ToString());
}

空のコンポーネントにスクリプトをくっ付ける
一通り、スクリプトを読み終えたら空のGameObjectsを作成してこれにPhotonMasterをくっつけておく。また、Publicで状態を表示する用のテキストを設定しているので、StatusTextオブジェクトを入れておいてあげよう



ビルドして実際に確かめる
できたら、シーンをビルドして実際の動きを確かめる。
実際にシーンが追加されているか確かめたらビルドしよう。

画面を並べて動きを確かめよう。うまいこと動けば成功だ。

まとめ
ここまでできたら、後はオブジェクトを同期させるなどの動きがメインになってくるだろうか。ユニティちゃんを使ったり、動きを同期させる系のチュートリアルなどは結構溢れているので色々調べてもらうといい。逆にターンベース形式の記事があまりないので、時間があればそっちもまとめたい。というか誰かまとめてほしい。記事が少ない、、
Photonはコールバック関数が肝だと思う。どの関数を読んだらどれが呼ばれるのかを抑えておくことで扱えるようになっていくのではないだろうか。ざっくりと理解していただければ幸いです。わりと公式が読みにくいので色々な記事を参考にしてみてください。