【技術解説】UnityとPhotonを使ったマルチプレイ開発
こんにちは!デザイニウムのウィリアム チェンです。
今回は、2022年7月に株式会社LIFULLさんが提供を開始したAndroidアプリ「空飛ぶホームズくんBETA」のマルチプレイ開発ついてお話します。デザイニウムは、ホロラボと共に本アプリの開発を担当させて頂きました。
「空飛ぶホームズくんBETA」とは
「空飛ぶホームズくんBETA」は、最大8名のプレイヤーが同時にデジタルツインの世界中に飛び回ることができます。この記事では、使っているマルチプレイに関しての技術と課題点について解説していきます。
1.マルチプレイの実装方法
「空飛ぶホームズくんBETA」では入退室管理、プレイヤーの位置管理、情報の通信、そして音声通話も必要です。一から作るにはコストが高いので、Photon、MLAPI、Mirrorなど色々なサービスを比較しました。開発難度や対応機能などを考えて、最終的にPhotonに決めました。
UnityとPhotonは相性が良く、RPCなどの機能も対応できます。またPhoton Voiceというマルチプレイ音声通話もありますので、今回の案件にはピッタリだと考えました。
2.ユーザー編
「空飛ぶホームズくんBETA」ではユーザーをホストユーザーとゲストユーザーに分けています。ホストユーザーは部屋を作って、ゲストユーザーがURL経由でその部屋に参加する仕組みです。その流れとしては、
ホスト側:
ホストプレイヤーがチーム名を決定
ホストがキャラクターを作成
ホスト側がRoomIDを生成し、サーバ上に新しい部屋を作成
共有できるURLを発行(パラメータもURLに入れる)
ゲスト側:
URLをロードする
ディープリンク経由で直接インストールしたアプリを開く
ディープリンクからRoomIDなどパラメータを取得
ゲストがキャラクターを作成
ホストが作った部屋に参加
ディープリンクの実装
ディープリンクを使えば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と名前などのパラメータの処理と自動的アプリを起動する動作を行います。
<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にスクリプトを追加すると、そのオブジェクトのスクリプト内の変数などのデータも共有できるので、とても便利です。
スクリプト上では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タイプ。
Unityタイプは基本的にUnityのMicrophoneクラスの設定を使っていますが、UnityのMicrophoneクラスは制限が多くて使いにくい場合があります。スピーカーを使用する時にハウリングが発生しやすく、多くのデバイス上でハウリングしないための調整はとても時間がかかりました。
Photonタイプは自由度は高いですが、デバイスによって発生する問題が多いです。開発時、イヤホンが使えない問題が発生して、最終的にUnityタイプに戻ることになりました。なので、両方とも実機で試して、プロジェクトによって最適のタイプを選択するのがおすすめです。
3.位置共有編
位置共有の基本
「空飛ぶホームズくんBETA」では、プレイヤー以外に物件施設と3D街などのオブジェクトの位置合わせをしなければなりません。3D街はGoogle Maps Platformが管理しています。初期位置の経緯度を設定し、プレイヤーが一定距離以上移動すれば、次の区画の3D街が生成されます。物件と施設のアイコンは3D街が基準なので、プレイヤーの位置が正しければ見ている景色は同じはずです。
Photon Transform Viewにプレイヤーオブジェクトを追加すれば、部屋に反映されるオブジェクトのTransformの変化がPhoton View経由で他のプレイヤーに伝えられます。選択できるのはPosition、Rotation、そしてScaleです。今回選択していたのはPositionとRotationのみです。つまり自分のキャラクターがUnityのWorld上の(100,100,100)の位置へ移動すると、Photonが他のプレイヤーのWorld上に生成された自分のアバターも(100,100,100)の位置へ移動できます。
プレイヤー間移動
プレイヤーが他のプレイヤーの位置まで移動する方法は4つあります。
3D街上Joystickで移動
同じ物件を内覧
プレイヤーリストを使ってテレポート
地図、キーワード検索での移動
上記の移動は、基本的にUnity World上相手プレイヤーの位置を参考にして、自分のアバターをその位置に合わせます。しかし、移動の距離が長すぎると、Floating Originの中心が変更され、位置がずれることになります。なので、マルチプレイする時には参加者全員もホストのFloating Originの中心を合わせることが必要です。(詳しい方法は次のパートで説明します)
4.RPC通信編
RPCとは、Remote Procedure Call:リモートプロシージャコールの略称です。RPCは通信回線やコンピュータネットワークを通じて別のコンピュータ上で動作するソフトウェアへ処理を依頼したり、結果を返したりするための規約です。Photonではこのような通信方法が使えます。プレイヤー間の情報共有やイベントのトリガーとして使うにはとても便利です。
PhotonでRPCの使い方
RPC通信を実行するにはPhotonViewクラスのRPCを使う必要があります。
"対象が実行すべき関数"は指定した対象がRPCを受けた後、実行する関数です。
”対象の指定”は部屋内全員や、ホスト以外、自分以外のプレイヤーなどを指定できます。
”追加パラメータ”は対象まで送りたい情報をパラメータとして追加できます。
対象の指定は基本的以下となります:
以下は、ホストがホスト以外の全員にホストが使っている経緯度を伝えるサンプルコードです。
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