
2019.4.10更新
私はunityを始めて8ヶ月になります。
まだ日が浅いのでわからない事が沢山ありますが、いくらかわかってきたところもあります。
今回は、ブラウザやアプリケーションでどうやってオンラインで対戦するの?
というところを中心に、私がどのようにUnityとPhotonでオンライン対戦を実現したか、についてこのブログに書こうと思います。
目次
最初のPhotonの実装の仕方入門編

まず、Photon Engineとはrealtimeオンライン対戦を可能にするためのサービスです。 詳細の説明と設定の仕方はこちらを参照するととても詳しく書いてあります。
要するにserver周りの事を気にせずに、割と簡単にマルチプレイヤーのオンライン対戦が実現できますということです。
UnityにPhotonを入れることで開発が格段に楽になります。ユーザーにもとても分かり易いはずです。
cloudってよくわからないし、サーバーサイドの技術もないよーという人も安心です。
sdkやapiがどうのというような難しいことは全く分からなくて大丈夫です。
オンラインでアカウントを作り登録し、Photonのアセット(部品)をダウンロードしてUnityにインポートする(取り込む)と使えるようになります。
Unityのアセットストア(部品を買えるとところ)から探すと下記のようなものが提供されています。
Photon Unity Network のVersionについて

幾つか種類があるのですが私が使っているものは無料の同時通信が20人までのものです。
有料にするともっと多い人数で同時通信接続できるものにアップグレードしたりできます。
私が使っているのはPhotonUnityNetworking(PUN1)ですが、もしかしたら、これから始める方はPUN2のほうが良いかもしれません。
Photon Unity Networking 2 PUN2
PUN1のサポートもこれまで通り行うと書いてありますが、新しいバージョンは下記のPUN2になりますので新規で使う用ならこちらです。
どちらも無料でダウンロードできますが、PUN1とPUN2の互換性はおそらくないと思います。動作確認はしていませんが。

私が最初の設定をしたphotonengineの実例は2つありますので、以下のリンクからどうぞ。
Photonで何ができるのかを知るためには、Unityのアセットに組み込まれている、デモを見てみましょう! (これにまさる説明はありません。)
Photonはデモをとにかく最初に絶対に見よう
このデモ(デモンストレーション)には色々な種類があり、サンプルとしてPhotonでできる基本操作がほぼほぼ網羅されています。
UnityのPhotonデモの始め方はこちらです。(現在リンク先が閉鎖されていますので下記に転記します。)
Asset StoreのPUNパッケージには、複数のデモが含まれています。 いずれも本格的なゲームではありませんが、それぞれのデモではマルチプレイヤーゲームの機能を構成するシーンやプレハブの実践的な設定手順を示しています。 以下で、PUN FreeとPUN+ packageパッケージのデモを数点紹介します。
この時、見落としがちなのはこの下の引用している文章です。
インポートしただけではデモは実行されませんので、必ずこれを用いてやってください。
読み込まれない場合には、Unityの以下のメニューを使用してください:「Window」 -> 「Photon Unity Networking」 -> 「Configure Demos (build setup)」同梱されているデモはすべて、この「Hubシーン」から起動できます。 AppIDをセットアップして、ビルドおよび実行してください。
要するにHubシーン(下の状態にして)実行またはビルド(ゲームを梱包)するということです。


(ちなみにUnityで実行とはこの左の再生ボタンを押すことを言います)
このデモンストレーションがすごいんです!! 英語ですが直感的でとってもわかりやすいです。何ができるのか、遊びながら理解できます。
まずはこの英語でごちゃごちゃ書いてあるのは読まなくていいので、左のボタンをどれか押してみましょう。ゲームが始まります。
上から順に全部見ましょう。これで下記のPhotonの機能は完全に網羅できます。
しかも、プロジェクトビューのアセットフォルダにソースコードが入っているので、読める時期が来たらぜひ参考にしてみましょう。(最初はわからないのでスルーです)
Photon Serverを使ってまずは部屋に直接入ろう
まず一番簡単な対戦をするためには、マルコポーロデモを作ってみましょう。Unity とPhotonで一番簡単なゲームになります。
設定通りにできるとモンスターが出てきて、十字キーに対応して床の上を歩きまわれます。

そして、同じビルド(別に梱包)した同じプログラムを起動すると、同じ部屋に接続して入って一緒に徘徊できます。
なんと、チャットも可能です。
オンラインのUnity対戦がこんなにすぐに可能になるなんて! とビックリするはずです。
この時同じ部屋に入るためには、管理画面(PhotonServerSettings)のAutoJoinLobbyにチェックが入っていることと、RegionがJP(japan)で同じになっていること、アカウントを取得登録した時のappIDが同じことを確認しましょう。

これは、上のリンク先のドキュメントにも書いてありますが、忘れやすいのでダブルチェックが必要です。
fpsのチュートリアルを見てPhoton unity network によるオブジェクトの初期配置をしよう
マルコポーロデモが出来たら今度はFPSゲームに挑戦してみましょう。
このデモをやるタイミングとしては、いくつかUnityでチュートリアルのゲームを作れるようになってからが良いと思います。
(FPSとは銃を構えて動き回り相手を撃つタイプのゲームです)
英語ですが、やってることだけ見れば何となくプレイヤーの初期配置の仕方や同期の取り方(後述)等を説明していることがわかります。
オブジェクトの初期配置するには、まず、PhotonViewというスクリプトをコンポーネントとして付加してあげることが必要になります。
PhotonViewとはデフォルトのPhotonAssetにデフォルトで入っているC#スクリプトの一つです。

オンライン対戦ではPhotonViewがついていないオブジェクトは、対戦相手のウィンドウには表示されません。
オブジェクトの初期配置をするには、オブジェクトにPhotonViewをつけて、ResourceフォルダにPrefabとしてセーブしてあげる必要があります。

PhotonNetwork.Instantiate("Prefab名", 座標情報, 回転情報, 0) で呼び出せば大丈夫です。例えば、「犬猫将棋」では下記のように書いています。
Vector3 temp = new Vector3(4.3f, -1.5f, -1.0f); GameObject cardCopy = (GameObject)PhotonNetwork.Instantiate("matatabi", temp, Quaternion.identity, 0);
これはFPSのチュートリアルに出てくるFirstPersonCharactorもそうですし、対戦カードゲームのカードでも同様です。
よって、FPSを学ぶことで、カードの配り方の参考にすることもできます。
具体的な使用例は以下のカードの配り方の参考記事をご参照下さい。
最初はわからなくても、やっているうちにわかるようになるので、大丈夫です!
Photon Unity Network における自駒の判断
初期配置したオブジェクトが自分のものかどうか判断するためには、PhotonView.isMineというbool値(trueかfalseかの値)が使えます。
これがtrue かfalseかによって自分の駒かどうかの判断ができます。
具体例は下記のリンクを参考にしてみてください。
Photon network の同期の取り方
それから、前章のFPSゲームのチュートリアルによって、どのように二つのプログラムがお互いに同期を取れるかがわかります。
同期とは、、、
2台のパソコン、あるいはパソコンとスマートフォンやサーバーなどに同じデータがあるとき、データの変更点を双方同時に抽出し、共通の状態に書き換えることです。 パソコンで困ったときに開く本
要するに、自分の画面で見えている現象を、相手のPCにも同じように表示するためにはデータを送らなければいけないということです。
そして、その逆の場合、データを受信してあげる必要があるということです。
PhotonView.csをコンポーネントとしてつけてあげるだけでは、初期表示はされますが、同期はとってくれません。

マルコポーロチュートリアルでお互いのモンスターが動いて見えるのは、同期が取れているからです。
同期が取れていなければ、相手のモンスターは自分のウィンドウでは動いても、相手のウィンドウでは初期の場所から動かないはずです。
この同期が取れると、カードや、コンポーネント等の共有オブジェクトを、ゲームの中で動かしたりできるようになります
同期の取り方には色々ありますが、ここからは突っ込んだ話になってくるので、ゲームを作り始める段階では斜め読みで大丈夫です。
PhotonView による同期の取り方
まず、PhotonViewの監視対象にTransformを入れてあげるという方法があります。
PhotonViewのコンポーネントの中にTransformコンポーネントをドラッグ&ドロップしてあげます。すると、カクカクの動きですが、同期をとってくれます。

具体的な使用例は下記のリンクを参考にしてみてください。
スクリプトによる同期の取り方
次に、PhotonVIewの監視対象に、スクリプトを ドラッグ&ドロップして 入れてあげるという方法があります。

この場合、 OnPhotonSerializeView() という関数を使う必要があります。具体的には実例を参考にしてみてください。
実は、 これについては私はまだあまりよく理解できていないまま使っています。分からなくなったら下記のリンク先に詳細を解説している方がいるので是非是非、参考にしてみてください。
すごく深い考察をしているので、いつもお世話になっています。
うら干物書き:【Unity】僕もPhotonを使いたい #07 位置の同期
RPCメソッドによる同期の取り方
RPCメソッドとはリモート・プロシージャ・コールの略で、ネットワーク上の同じルーム(部屋)にいる人全員に呼ばれる関数です。
- PhotonTargets.All = 部屋にいる全員に呼ぶ
- PhotonTargets.Others = 部屋にいる自分以外の人全員に呼ぶ
- PhotonTargets.MasterClient = 部屋を作った人だけに呼ぶ

これを使い分ければ、自分の以外のプレイヤーにもメソッドを実行することができます。裏を返せば、オンライン対戦において普通のメソッドは自分にしか有効ではありません。
下記は私の経験則ですが、
これを使用するにはまず、PhotonViewを関数を書いているスクリプト自体に付けてあげる必要があります。
そして、Photon.Monobehaviorを継承します。
PhotonView photonView; と GetComponent<PhotonView>();をしておくことが事前準備として必要です。
メソッド本体には [PunRPC] と文頭に付けたうえで関数を命名します。
呼び出す場合は、Targets.Allの場合、
photonView.RPC("関数名", PhotonTargets.All);
といった具合になります。具体例は下記のリンクを参考にしてみてください。
また、詳しい原理が知りたい方は下記リンクより深い考察をしている方がいらしゃるのでご参考まで。
うら干物書き: 【Unity】僕もPhotonを使いたい #08 RPC() PhotonTargets編
自分のオブジェクトと相手のオブジェクト
所有権の移譲
PhotonViewを付けているオブジェクトはネットワーク上でどのプレイヤーも見れるのですが、実は所有権というコンセプトがあるんです。
自分のものと他人のものの区別ということですね。
例えば自分のカードはクリックされても、他人に見られなかったり、勝手に人のオブジェクトをDestroyできないような仕組みが最初からついています。

例えば「犬猫将棋」の場合、角をシーンビューでクリックすると、OwnerIDが確認できます。これはPhotonNetwork.player.ID = 1の人がコントロールしているという意味です。
よって、player2が駒を取る際にDestroyをしようとしても他人のオブジェクトなのでできません。
そこで、所有権の移譲が必要になります。
以下のように書くと対象のプレイヤー番号に所有権が移ります。 GetComponent<PhotonView>().TransferOwnership(プレイヤー番号)
その際は、PhotonVIewのOwnerをTakeOverにしておきましょう。

具体例は下記のリンクを参考にしてみてください。
他にもいろいろな所有権の移譲の仕方がありますが、私が理解しているのはこの一つだけです。
Photon におけるカメラの考え方
対戦型ボードゲームでは盤面を挟んでボードの向きを逆にしたい時があると思います。
これは単純にカメラを反転させるだけでいいのですが、その時にはまりやすい罠があるので、こちらで確認しておきましょう。
点数計算などのデータの保有
ROOM CUSTOM PROPERTY
それから、点数や、役の判定等の場合に重宝するのが、 ROOMCUSTOMPROPERTY です。
これは部屋固有の値を覚えておける機能になります。
例えば、各プレイヤーの点数の計算結果や、手役の判定となる値の結果などを保持することができます。
送信するときは下記のように書きます。hogeは任意の言葉です。
var properties = new ExitGames.Client.Photon.Hashtable(); string hoge = "データ名" + データ番号; properties.Add(hoge, データ); PhotonNetwork.room.SetCustomProperties(properties);
例えばデータ型がintの時にjusinに受信値を代入するときは下記のように書きます。
int jusin = (int)PhotonNetwork.room.customProperties["データ名" + データ番号];
意外と簡単ですよね。これは実例がいっぱいあるので、下記を参考にして書いてみてくださいね。
ターンの実装方法
それから、アナログゲームといえば、ターンですね!

実は、Photonにはターンを実装するためのTurnManagerというものが組み込まれています。
タイマー付きのターンをカウントしてくれるシステムが使えます。
これを使うことで、「犬猫将棋」のようなターン制のゲームも作れるようになります。
それではタイマーの中身を細かく見ていきましょう。
まずは冒頭の部分です。PunBehaviour、IPunTurnManagerCallbacksを継承する必要があります。using Photon;も加えておきましょう。
using System; // 注意 using System.Collections; using Photon; // 注意 using UnityEngine; using UnityEngine.UI; //注意 public class INSCore : PunBehaviour, IPunTurnManagerCallbacks
忘れがちなターンマネージャーを使いますよーという文章も必要です。
private PunTurnManager turnManager;
それから、turnManagerをコンポーネントに追加したり、ターンの長さを決めたり、PhotonViewをGetComponentしたりしておきます。
public void Awake()// StartをAwakeにする。 { this.turnManager = this.gameObject.AddComponent<PunTurnManager>();//PunTurnManagerをコンポーネントに追加 this.turnManager.TurnManagerListener = this;//リスナー? this.turnManager.TurnDuration = 5f;//ターンは5秒にする PhotonView = GameObject.Find("scripts").GetComponent<PhotonView>();//scriptsにphotonviewを付けておくのを忘れずに。 }
IPunTurnManagerCallbacksを継承した時点で下記の5行は必ず入れます。
それぞれの使い方で分かっていることを下に書いておきます。
public void OnPlayerFinished(PhotonPlayer photonPlayer, int turn, object move)//1 { //今のところ使っていない機能です。使い方が分かったら教えてください。 } public void OnPlayerMove(PhotonPlayer photonPlayer, int turn, object move)//2 { //今のところ使っていない機能です。使い方が分かったら教えてください。 } public void OnTurnBegins(int turn)//3 { //ターンが開始した場合に呼びたい処理を書きます。 } public void OnTurnCompleted(int obj)//4 { //ターンのムーブ(後述)が全プレイヤー終了した場合に呼びたい処理を書きます。 this.StartTurn();//ここで次のターンを開始しています(後述)。 } public void OnTurnTimeEnds(int turn)//5 タイマーが終了した場合 { //タイマーが終了した場合に呼びたい処理を書きます。 this.StartTurn();//ここで次のターンを開始しています(後述)。 }
この時、プレイヤーのムーブとは、 this.turnManager.SendMove() をしてあげるとムーブが完了したとみなされます。
タイマーが終了するか、プレイヤーが全員ムーブを終えると、次のターンが開始されるようになっています。
public void StartTurn()//ターン開始メソッド(シーン開始時にRPCから呼ばれる呼ばれるようにしてあります。) { if (PhotonNetwork.isMasterClient) { this.turnManager.BeginTurn();//turnmanagerに新しいターンを始めさせる } }
ターンを開始するのはマスタークライアントだけで大丈夫なようです。
this.turnManager.BeginTurn();でターンマネージャーが自動的にターンを開始します。
これがザックリとしたターンの流れになります。
私がどのようにこのターンの仕組みにたどり着いたかについて知りたい方は下記の記事も参考にしてみてください。
タイマーを任意のタイミングで次に回す
それから、タイマーをオンライン対戦で任意のタイミングで次のプレイヤーに回したい場合は下記の記事を参考にしてみてくださいね。

片方のプレイヤーがボタンをクリックすると次のプレイヤーにターンが回るようなシステムを組んでみました。
これは交代制の将棋のようなゲームにはうってつけになります。
交代制の場合、2回のうち1回だけ自分のターンを回したいので、自分のターン以外は無条件でターンを終わる命令を書いてあげました。
アクションが必要なターンのみアクションするかの選択をさせ、それ以外はアクションする選択の余地を奪っています。
PhotonNetwor.player.ID(マスタークライアントが1、その他が2にこの場合なります)と、ターン数を2で割った余り+1が等しい時にアクションを行うというシステムです。
こちらの方は交代制ターンの解説になるので、より、実践的な内容になっています。
ターンの応用

ターンの応用としては、自分の駒を動かしたときにターンを回すというようなことができます。
駒が動いたということを検知した時に turnManager.SendMove を行うことで、自分のターンの終了を検知するシステムになっています。
こちらもかなり実践的で突っ込んだ内容になっています。
下記リンクからどうぞ。
ターンの間の対戦相手の拘束

交代制のターンゲームでは、アクティブプレイヤー以外のプレイヤーが手を行えると、問題になってしまいますね。
ターンの最中に自分しか動けないようにする必要があります。 その実装方法はこちらに解説があります。
簡単に説明すると駒についているMouse.csスクリプトをDestroyして、ターン終了時にAddComponentし直すというようなことをしています。
下記のリンクを是非参考にしてみてください。
ロビーと部屋の作り方

それから、たくさんの人にプレイしてもらうためには、同時に部屋をいくつも立てる必要があります。
部屋の一覧から部屋を選択してそのルームに入るというような事が出来たらとても良いですよね。
そのためにはロビーとルームの作成方法が役に立つと思います。
これについては私はあまりよく理解できないまま、チュートリアルをひたすら写し続けることで実装することができました。
難しい説明はスキップしてしまっているので、ちょっとわかりにくいかもしれません。
私がいかにしてロビーとルームの作成を行ったかは下記を参考にしてみてください。
まとめ
最後に、他にもいろいろな機能がPhotonにはあると思うのですが、私が知っているのはこれくらいです。
簡単にリアルタイムの対戦ゲームが作れそうですよね!
もう少しうまく使えるようになったら、アプリ化してみたいと思っています。
一緒にphotonを使い倒して、新たな機能が見つかったら是非教えてくださいね!
以上、まとめに戻る↓