見出し画像

【技術解説】UnityとPhotonを使ったマルチプレイ開発

こんにちは!デザイニウムウィリアム チェンです。
今回は、2022年7月に株式会社LIFULLさんが提供を開始したAndroidアプリ「空飛ぶホームズくんBETA」のマルチプレイ開発ついてお話します。デザイニウムは、ホロラボと共に本アプリの開発を担当させて頂きました。

「空飛ぶホームズくんBETA」とは

Androidアプリ「空飛ぶホームズくんBETA」は、3Dの街を自由に飛び回り、シームレスにバーチャル内見ができるVRサービスです。バーチャル内見の3Dモデルは、平面の間取り図から3Dの部屋を生成する技術を用い自動生成しています。2020年秋に発表したプロトタイプ版から、3Dマップの表示領域を日本全国に拡大し、大都市を中心に表示可能な物件数が増加しました。さらに、最大8名で体験を共有することができるマルチプレイヤー機能を追加し、デジタルツインによる広大な空間を舞台にしたVRコミュニケーションが可能になります。

LIFULLが日本初、3Dの地図と物件情報を活用したデジタルツインで新しい住まいの探し方が体験できるAndroidアプリ「空飛ぶホームズくんBETA」を提供開始

「空飛ぶホームズくんBETA」は、最大8名のプレイヤーが同時にデジタルツインの世界中に飛び回ることができます。この記事では、使っているマルチプレイに関しての技術と課題点について解説していきます。


1.マルチプレイの実装方法

「空飛ぶホームズくんBETA」では入退室管理、プレイヤーの位置管理、情報の通信、そして音声通話も必要です。一から作るにはコストが高いので、Photon、MLAPI、Mirrorなど色々なサービスを比較しました。開発難度や対応機能などを考えて、最終的にPhotonに決めました。

Photon Unity Networking(PUN)は、マルチプレイヤーゲーム対応のUnityパッケージです。 柔軟性の高いマッチメイキングによってプレイヤーはルームに入室し、ルーム内のオブジェクトはネットワーク上で同期されます。RPC、カスタムプロパティ、または「低いレベル」のPhotonイベントなどの機能があります。信頼性が高く、(オプションで)高速な通信が専用Photonサーバーによって実現されます。このため、クライアントは1対1で接続する必要はありません。

*Photonのウェブサイトより

UnityとPhotonは相性が良く、RPCなどの機能も対応できます。またPhoton Voiceというマルチプレイ音声通話もありますので、今回の案件にはピッタリだと考えました。

2.ユーザー編

「空飛ぶホームズくんBETA」ではユーザーをホストユーザーとゲストユーザーに分けています。ホストユーザーは部屋を作って、ゲストユーザーがURL経由でその部屋に参加する仕組みです。その流れとしては、

ホスト側:

  1. ホストプレイヤーがチーム名を決定

  2. ホストがキャラクターを作成

  3. ホスト側がRoomIDを生成し、サーバ上に新しい部屋を作成

  4. 共有できるURLを発行(パラメータもURLに入れる)

ゲスト側:

  1. URLをロードする

  2. ディープリンク経由で直接インストールしたアプリを開く

  3. ディープリンクからRoomIDなどパラメータを取得

  4. ゲストがキャラクターを作成

  5. ホストが作った部屋に参加

ディープリンクの実装

ディープリンクを使えばURL(ブラウザ経由)から直接インストールしたアプリを開くことができます。そのために、まずはAndroidManifestにintent-filterを追加するが必要があります。

 <intent-filter>
     <action android:name="android.intent.action.MAIN" />
     <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 <intent-filter>
     <action android:name="android.intent.action.VIEW" />
     <category android:name="android.intent.category.DEFAULT" />
     <category android:name="android.intent.category.BROWSABLE" />
     <data android:scheme="定義したscheme変数" android:host="定義したhost変数" />
 </intent-filter>

そして、ウェブサイト側ではJavascriptで部屋のIDと名前などのパラメータの処理と自動的アプリを起動する動作を行います。

https://www.hostingurl.com/?roomid=”部屋のID”&roomName=”部屋の名前”

生成したURLの構造
空飛ぶホームズくん 招待ページ
<script>
    function getRoomID() {
    const queryString = window.location.search;
    	const urlParams = new URLSearchParams(queryString);
    	const roomid = urlParams.get('roomid');
    	const roomName = urlParams.get('roomName');
    	document.getElementById("link").href = "定義したscheme変数://定義したhost変数?" + roomid + "?" + roomName;
    	window.location.href = "定義したscheme変数://定義したhost変数?" + roomid + "?" + roomName;
    }
    window.onload = getRoomID;
</script>

最後には開いたアプリにディープリンクのパラメータを受け取る動作が必要です。アプリケーションは ”定義したscheme変数://”で始まるすべてのリンクを開き、Application.deepLinkActivated イベントで URL を処理できます。

private void Start()
    {
        if (Instance == null)
        {
            Instance = this;
            Application.deepLinkActivated += onDeepLinkActivated;
            if (!string.IsNullOrEmpty(Application.absoluteURL))
            {
                // Cold start and Application.absoluteURL not null so process Deep Link.
                onDeepLinkActivated(Application.absoluteURL);
            }
            DontDestroyOnLoad(gameObject);
        }
    }

private void onDeepLinkActivated(string url)
   {
       Debug.Log("Deep Link Activation Success");
       // ... //
   }

そうすると、ゲストプレイヤーは受け取った部屋IDと名前でホストが作った部屋に参加できるようになります。

キャラクター作成と共有方法

「空飛ぶホームズくんBETA」では自分のアバターを作成することができます。そのアバターはIDや名前やパーツの番号など色々な情報を持っています。こういった情報はPhoton Viewを使って、自動的に他のプレイヤーに常に共有しています。

アバター作成画面

Photon Viewを持つオブジェクト(例えばプレイヤーアバター)は自分の情報を常時他のプレイヤーと共有しています。Obeserved Componentsにスクリプトを追加すると、そのオブジェクトのスクリプト内の変数などのデータも共有できるので、とても便利です。

Inspector上のPhoton View

スクリプト上ではOnPhotonSerializeViewを使って、変数の変化を常に観察しています。

void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting) //自分のアバターの情報を他のプレイヤーへ転送
        {
            stream.SendNext(skinColor);
            stream.SendNext(hairColor);
            stream.SendNext(clothesColor);
            stream.SendNext(hairType);
            stream.SendNext(isMale);
            stream.SendNext(isAvatarChanged);
        }
        else if(stream.IsReading) //自分のクライアント上他のプレイヤーのアバターの情報を読む
        {
            skinColor = (int[])stream.ReceiveNext();
            hairColor = (int[])stream.ReceiveNext();
            clothesColor = (int[])stream.ReceiveNext();
            hairType = (int)stream.ReceiveNext();
            isMale = (bool)stream.ReceiveNext();
            StartCoroutine(ColorChanging());

            // ... // 
        }
    }

PhotonVoiceの設定と課題

PUNを使えば、そのまま作成した部屋の中のプレイヤーにPhoton Voiceのコンポーネントを追加できます。その中にMicrophone設定があります、Photon Voiceは二種類のMicrophone Typeを設定できます。
:UnityタイプとPhotonタイプ。

Microphone Typeの選択

Unityタイプは基本的にUnityのMicrophoneクラスの設定を使っていますが、UnityのMicrophoneクラスは制限が多くて使いにくい場合があります。スピーカーを使用する時にハウリングが発生しやすく、多くのデバイス上でハウリングしないための調整はとても時間がかかりました。

Photonタイプは自由度は高いですが、デバイスによって発生する問題が多いです。開発時、イヤホンが使えない問題が発生して、最終的にUnityタイプに戻ることになりました。なので、両方とも実機で試して、プロジェクトによって最適のタイプを選択するのがおすすめです。

3.位置共有編

位置共有の基本

「空飛ぶホームズくんBETA」では、プレイヤー以外に物件施設と3D街などのオブジェクトの位置合わせをしなければなりません。3D街はGoogle Maps Platformが管理しています。初期位置の経緯度を設定し、プレイヤーが一定距離以上移動すれば、次の区画の3D街が生成されます。物件と施設のアイコンは3D街が基準なので、プレイヤーの位置が正しければ見ている景色は同じはずです。

Inspector上のPhoton Transform View

Photon Transform Viewにプレイヤーオブジェクトを追加すれば、部屋に反映されるオブジェクトのTransformの変化がPhoton View経由で他のプレイヤーに伝えられます。選択できるのはPosition、Rotation、そしてScaleです。今回選択していたのはPositionとRotationのみです。つまり自分のキャラクターがUnityのWorld上の(100,100,100)の位置へ移動すると、Photonが他のプレイヤーのWorld上に生成された自分のアバターも(100,100,100)の位置へ移動できます。

プレイヤー間移動

プレイヤーが他のプレイヤーの位置まで移動する方法は4つあります。

  1. 3D街上Joystickで移動

  2. 同じ物件を内覧

  3. プレイヤーリストを使ってテレポート

  4. 地図、キーワード検索での移動

プレイヤーリストの他のユーザーを押すとテレポートができます

上記の移動は、基本的にUnity World上相手プレイヤーの位置を参考にして、自分のアバターをその位置に合わせます。しかし、移動の距離が長すぎると、Floating Originの中心が変更され、位置がずれることになります。なので、マルチプレイする時には参加者全員もホストのFloating Originの中心を合わせることが必要です。(詳しい方法は次のパートで説明します)

4.RPC通信編

RPCとは、Remote Procedure Call:リモートプロシージャコールの略称です。RPCは通信回線やコンピュータネットワークを通じて別のコンピュータ上で動作するソフトウェアへ処理を依頼したり、結果を返したりするための規約です。Photonではこのような通信方法が使えます。プレイヤー間の情報共有やイベントのトリガーとして使うにはとても便利です。

PhotonでRPCの使い方

RPC通信を実行するにはPhotonViewクラスのRPCを使う必要があります。

this.GetComponent<PhotonView>().RPC("対象が実行すべき関数", ”対象の指定”, ”追加パラメータ”);

  • "対象が実行すべき関数"は指定した対象がRPCを受けた後、実行する関数です。

  • ”対象の指定”は部屋内全員や、ホスト以外、自分以外のプレイヤーなどを指定できます。

  • ”追加パラメータ”は対象まで送りたい情報をパラメータとして追加できます。

対象の指定は基本的以下となります:

All
RPCを他のプレイヤー全員に送信して、このクライアントで即座に実行します。後で入ってきたプレイヤーはこのRPCを実行しません。

Others
RPCを他のプレイヤー全員に送信します。このクライアントはRPCを実行しません。後で入ってきたプレイヤーはこのRPCを実行しません。

MasterClient
RPCをMasterClientだけに送信します。注記: MasterClientはRPCを実行する前に切断され、RPCが欠損する原因になるかもしれません。

Photon Unity Networking 2 Public API ドキュメント

以下は、ホストがホスト以外の全員にホストが使っている経緯度を伝えるサンプルコードです。

IEnumerator delayCallFarRPC()
    {
        yield return waitTime_5sec;
        float hostLat = floatingOriginUpdater.recordedOriginLat;
        float hostLng = floatingOriginUpdater.recordedOriginLng;
        object[] args = new object[] { hostLat, hostLng };
        if (this.GetComponent<PhotonView>().IsMine)
        {
            Debug.Log("Tell others to teleport, I am host :" + hostLat + "," + hostLng);
            this.GetComponent<PhotonView>().RPC("callFarUIByMaster", RpcTarget.Others, args);
        }
    }

"callFarUIByMaster"は、ゲスト全員が実行すべき関数です。スクリプトの中に[PunRPC]のタグを追加する必要があります。

[PunRPC]
private void callFarUIByMaster(float hostLat, float hostLng)
{
    movementController.floatingOriginUpdater.farHostLat = hostLat;
    movementController.floatingOriginUpdater.farHostLng = hostLng;
    movementController.floatingOriginUpdater.receivedFarFromHost();
}

他のユーザーが内覧中の物件を生成する

他のプレイヤーが内覧する時、もし自分の3D街にその対象物件のアイコンが存在しない場合、RPCイベントが実行された時に相手の物件のデータをもらって、こちらのクライアント上に該当の物件を生成する必要があります。自定義のクラス(以下の例のbeanDataDataというクラス)はそのまま転送することができませんが、まずはクラスをJsonに変更すればデータの転送ができます:

beanData bean = JsonUtility.FromJson<beanData>(enteredJson_r);

そのあとまたbeanDataクラスに戻せば、アイコン生成にデータが使えます:

place.GetComponentInChildren<textureManager>().serializedPropertyBean = JsonUtility.ToJson(bean);

RPCを使ったFloating Origin対策

3.の”プレイヤー間移動”で説明した「参加者全員もホストのFloating Originの中心を合わせることが必要」についてですが、プレイヤー全員にもFloating Origin Updaterという機能が動作しています。何故Floating Origin Updaterが必要かというと、それはカメラとオブジェクトがUnityの中心から離れすぎるとFloating-Points Precision Errorsが発生するからです。

例えば東京から大阪までのような遠距離を移動すると、3Dオブジェクトがシーンの中央から離れすぎてFloating-Points Precision Errorsが発生します。発生すると、3DモデルやUIオブジェクトの見た目がおかしくなります。それを防止するために、GMPのFloating Origin Updaterを使います。しかし、プレイヤー全員が自分の中心を更新すると、位置がずれるので、ホストの中心を合わせるのが一番の対策になります。

[PunRPC]
private void updateFloatingOriginByMaster(float hostLat, float hostLng)
{
    Debug.Log("Check getting _pos or not 2: " + hostLat + " " + hostLng);
    // Move Guest to correct Pos
    if (!joinedMaster)
    {
        joinedMaster = true;
        if (PlayerPrefs.HasKey("skipMultiTut"))
        { // finished Multi Tutor
            movementController.placesAPIManager.requireMinimapUpdate = true;
            movementController.placesAPIManager.tempdisableAllowAPICall();
            movementController.placesAPIManager.removeAllLifull();
            floatingOriginUpdater.googleMapController.externalTeleport(hostLat, hostLng);
            StartCoroutine(delayedMovingOrigin(hostLat, hostLng));
        }
        else
        { // Start teleport after finished multi tutor
            movementController.tutorialcontoller.atLat = hostLat;
            movementController.tutorialcontoller.atLng = hostLng;
            Debug.Log("tutorialcontoller.atLatLng " + hostLat + " " + hostLng);
            movementController.tutorialcontoller.needAfterTele = true;
        }
    }
}

この目的のために特定対象を指定できるRPCは一番の解決案となります。

5.最後に

最近ではVRやMRの体験でもマルチプレイ対応の要求が増えています。今回PUNを使ってマルチプレイ体験を作って感じたのは、やはりPhotonの機能の便利さと実装のしやすさだと思います。まだどうやってマルチプレイを実装するか悩んでいる方は、是非PhotonのPUNを試してみてください!

アプリの開発裏話をLIFULLさんのnoteで公開しているので、ぜひこちらもご覧ください!


The Designium.inc
Official website
Interactive website
Twitter (フォローお待ちしてます😉✨)
Facebook



この記事が気に入ったらサポートをしてみませんか?