現在、Photon Fusionはv2がリリースされていますが、記事はPhoton Fusion v1に準拠して作成されていますのでご注意下さい。
これは記事の作成中に前触れもなく大型アップデートとなるv2が発表されたため、時間的に確認や対応ができなかったためです。
本記事のv2対応やv2そのものの情報については今後調査していく予定です。
はじめに
Photon Fusionアンバサダーのニム式です。
2Dアクションゲームにおいて、小さなステージのスタートからゴールへ移動するのを目的とするルールは非常に多く存在します。
その時間を競うオンラインレース要素、さらに各プレイヤーが任意の場所にオブジェクト生成するギミックを追加したものも存在します。
Photon Fusionを使うことで、こういったリアルタイムオンライン対戦ゲーム特有のギミックを追加することができます。
以前の記事では公式サンプルを元に、ステージの改造やオブジェクトを生成する方法、その生成ルールの追加の仕方を紹介しました。
今回は、オブジェクトの生成をUI(uGUI)と連携する方法について紹介します。
前提記事
Photon Fusionの基本的な解説は以下の記事で行っていますので、そちらを参照下さい。
動作確認環境
1.1.6 Fusion Razor Madness 1.1.6 Build 162
Windows 11 Home 22H2
Unity 2020.3.48f1
Fusion SDK 1.1.6 F Build 162
UI作成の要件
今回は前項の動画に出てきている、リスポーン中に表示されるUIを作ります。UIはその中の一つをクリックすると対応するオブジェクトが生成され、マップ中に配置できるようにします。
なお、ネットワーク・トポロジーはクライアントホスト型を基準に実装しています。
重要な注意点
Photon Fusionでは同期する同期しないで処理が変わるため、何を同期するかについてきちんと考えておく必要があります。
基本的にUIの表示内容は同期しませんが、その操作については同期することが多くなります。
今回は「UIの表示・非表示は同期しない、UI要素の選択は同期する」必要があります。
それを実装するに当たって重要なポイントがいくつかあります。
ButtonのOnclickedコールバックの結果を同期することができない
今回の仕様ではUIは各々で表示内容が異なる必要があるため、同期しないオブジェクトとして各プレイヤーキャラクターに対してCanvasを用意しています。
通常であれば、対応するButtonをクリックすればOnclickedコールバックが呼ばれ、設定された処理が行われます。
しかし、Photon FusionではOnclickedコールバックは同期してくれません。
クライアント側で対象のUIをクリックした場合でもその結果がホスト側には伝わらず、なかったことにされ同期しないという結果になります。
クライアント側で実行はされるため当然Debug.Logは出力されますが、結果は反映されないといった状態になり現状把握をややこしくさせるため注意が必要です。
マウスの位置とマウスのどのボタンを押下したかという情報はインプットとして同期が可能です。
今回は、事前にUI要素を別の同期オブジェクトに登録しておき、クリックしたマウスの位置の座標と対象のUI要素の重なりを判定することで、onClickコールバックのような動作を実現しています。
生成時のSetparentが無効になる
今回のUIは表示非表示の切り替えごとに中身の生成破棄を行います。そのため生成ごとにSetparentをする必要があります。
また各々のプレイヤーで違う内容を表示したいため1プレイヤーにつき1つUIを持っていますが、他のプレイヤーのUIは非表示にしておく必要があります。普通はStartやSpawnedのタイミングで(もしくはprefabで)SetActive(false)しておけば済みます。
しかし、これは仕様かはわかりませんが、そのタイミングでGameobjectが非アクティブ(親だけ非アクティブでも同じ)になっているとSetparentが失敗してしまいます。
そのため、オブジェクトではなく大本のCanvas自体をオンオフする必要があります。
本題からは外れますが、同期オブジェクト(NetworkObjectがついたもの)でSetparentする場合は子にNetwork Transform Anchorコンポーネントをアタッチしておく必要があります。
実装
以上の内容を踏まえた、UI周りの実装について紹介していきます。
それ以外については、以前の記事とスライドをご参照下さい。
UIの設定
まずはUIを初期設定する処理です。
TrapUIManager.cs
のFixedUpdateNetwork
ではゲームのステートを監視し、状態が切り替わったタイミングでUIの設定を行います。
ResetTrapScreen
では不要になったUIオブジェクトの破棄を行います。
SpawnRandomUIItem
ではobjectToSpawnList
に登録されたUIオブジェクトの生成が行われます。
生成したオブジェクトはクリック判定のときに参照することになるためspawnedObjectList
に登録しますが、NetworkLinkedList
を使い同期できるようにします。
[Networked, Capacity(6)]
NetworkLinkedList<NetworkObject> spawnedObjectList { get; }
public override void FixedUpdateNetwork()
{
currentValue = _behaviour.isRespawning() == false && _levelBehaviour.StartTimerIsExpired(Object.Runner);
if (previousValue != currentValue)
{
if(currentValue)
{
ResetTrapScreen();
}
else
{
SpawnRandomUIItem();
}
previousValue = currentValue;
}
//略
}
private void ResetTrapScreen()
{
foreach (var target in spawnedObjectList)
{
Runner.Despawn(target);
}
spawnedObjectList.Clear();
trapSpawned = false;
}
private void SpawnRandomUIItem()
{
for (int i = 0; i < uiCount; i++)
{
var obj = objectToSpawnList[Random.Range(0,objectToSpawnList.Count)];
if (obj != null)
{
Runner.Spawn(obj,obj.transform.position,Quaternion.identity,Object.InputAuthority,Init);
void Init(NetworkRunner runner, NetworkObject spawnedObject)
{
spawnedObject.transform.SetParent(_trapIconArea);
spawnedObjectList.Add(spawnedObject);
}
}
}
}
UIの表示コントロール
次はUIの表示機能です。UIは各プレイヤー毎に表示を変化させる、つまり表示を同期しない必要があります。
そのため表示非表示を反映する機能については前述のとおり、同期を行わないオブジェクトである大本のCanvasをオンオフすることで実現します。
表示非表示の切り替えは以下のようにトラップの生成破棄フラグtrapSpawned
で行います。フラグは生成のCheckOverlap
と破棄のResetTrapScreen
で更新します。
また自分以外のプレイヤーの場合は表示する必要がないので、Object.HasInputAuthority
で先に判定します。今回はプレイヤーオブジェクトの子にこのTrapUIManager.cs
がついているので、自分の場合はtrue、自分以外の場合はfalseになります。
[SerializeField] private Canvas _trapUICanvas;
public override void FixedUpdateNetwork()
{
//略
if (Object.HasInputAuthority)
{
if (trapSpawned)
{
_trapUICanvas.enabled = false;
}
else
{
_trapUICanvas.enabled =
!(_behaviour.isRespawning() == false && _levelBehaviour.StartTimerIsExpired(Object.Runner));
}
}
else
{
_trapUICanvas.enabled = false;
}
}
トラップオブジェクトの生成
次はUIをクリックしてトラップを生成する処理です。
オフラインゲームの場合であればonClickコールバックを起点にする形で問題ありませんが、前述の通りその実行と結果は同期されません。
そこで代替手段として、ObjectSpawner.cs
でゲームのステートとクリック判定を取得、TrapUIManager.cs
のCheckOverlap
でその位置にあるUI子要素探し、対応するトラップを返して生成します。
public override void Spawned()
{
_behaviour = GetBehaviour<PlayerBehaviour>();
_inputController = GetBehaviour<InputController>();
_levelBehaviour = FindObjectOfType<LevelBehaviour>();
_trapUIManager = FindObjectOfType<TrapUIManager>();
}
public override void FixedUpdateNetwork()
{
if (GetInput<InputData>(out var input))
{
if (input.GetButtonPressed(_inputController.PrevButtons).IsSet(InputButton.LCLICK))
{
if (_behaviour.isRespawning() || _levelBehaviour.StartTimerIsExpired(Runner) == false)
{
var target = _trapUIManager.CheckOverlap(input.MousePosition);
if (target != null)
{
Runner.Spawn(target, input.MousePosition, Quaternion.identity, Object.InputAuthority);
}
}
}
}
}
public NetworkObject CheckOverlap(Vector3 mousePosition)
{
NetworkObject result = null;
if (spawnedObjectList.Count == 0 || trapSpawned) return null;
var pos = Camera.main.WorldToScreenPoint(mousePosition);
foreach (var value in spawnedObjectList)
{
if (RectTransformUtility.RectangleContainsScreenPoint(value.GetComponent<RectTransform>(), pos))
{
result = value.GetComponent<TrapUIItem>().GetTrapObject();
}
}
if (result != null)trapSpawned = true;
return result;
}
//個別のトラップにアタッチ
//trapObjectは生成したいPrefabを設定
public NetworkObject GetTrapObject()
{
return trapObject;
}
トラップの設置
最後にトラップの設置機能です。
前項で生成したトラップは、マウスへの追従と設置機能を持ちます。また設置できるかどうかと設置済みかどうかをビジュアル化するため、SpriteRenderer.colorで色を変化させています。
public override void Spawned()
{
isFollowing = true;
_collider = GetComponent<Collider2D>();
_collider.enabled = false;
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_tilemap = FindObjectsOfType<Tilemap>().First(x => x.name == "Background");
_inputController = Runner.GetPlayerObject(Object.InputAuthority).GetBehaviour<PlayerData>().Instance.GetBehaviour<InputController>();
}
public override void FixedUpdateNetwork()
{
if (GetInput<InputData>(out var input))
{
if (isFollowing)
{
//マウスに追従&マスにスナップ
var cellPosition = _tilemap.WorldToCell(input.MousePosition);
transform.position = cellPosition + new Vector3(0.5f,0.5f,0);
//設置不可の範囲では色を変える
if (_tilemap.GetTile(cellPosition) == null)
{
_spriteRenderer.color = Color.red;
}
else
{
_spriteRenderer.color = Color.blue;
//クリックしたら配置
//設置不可領域でクリックしたら無効
if (input.GetButtonPressed(_inputController.PrevButtons).IsSet(InputButton.LCLICK))
{
isFollowing = false;
_collider.enabled = true;
_spriteRenderer.color = Color.white;
}
}
}
}
}
まとめ
UI操作の結果を同期するという、やってそうでやってなかった、かつよく使われるそうな内容を紹介しました。同期するものとしないものを跨いだ処理のため、非常にややこしくなる可能性があります。(デバッグログは表示されるが同期されない、という状況に非常に頭を悩ませました)
未検証ですが、Canvasを一本化したり同期するのをオブジェクトではなくIDにしたりと、もう少し簡素にする方法があるかもしれません。