【Unity】UnityでVisionProのVolume(Bounded)コンテンツのxボタンは閉じるボタンではなかった件【VisionPro】

Graffity でVisionPro + Unity でコンテンツ開発・技術研究を行っているcovaです。

今回はVisionPro の固有UIである VolumeApp(Bounded Volume) の独特の性質について解説します。


# TL;DR
- 開発用ツールである PlayToDevice 経由だと xボタンでVolumeを非表示にすると Closed ステートになる
- 実機で動かすと Background のステート止まり。Closed ステートにはならない→CloseボタンではなくBackgroundボタン
  - VolumeCameraの OpenWindow/CloseWindow をコールしても何も起きない
  - Background は一旦ホームに戻って再度アプリを開くと自動的にForeground になる
      - 介入不能
- Background ステートやClosed ステートになってもGameObject 自体はHierarchy上に残っているので注意


# 背景

VisionPro には Window モード と Volume モード, Immersive Space モードがあります。

![05_mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/affb676f-1647-d0b8-fd41-5512d72eeaba.png)


そしてWindow/Volume モードのコンテンツはOS側が提供するUIを操作することで現実での表示位置を制御する事ができます。
(UnityでいうところのBounded App）

![06_mode_unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/6714b12d-c06f-361c-94b3-62404d2c8f31.png)


また、xボタンを選択する事で、そのアプリを `閉じるような` 操作ができます。

![IMG_0157.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/6ab8b4cf-3915-0483-2655-466429db062b.png)


:::note alert
後述しますが 一般的なUIのバツボタンと同じ挙動 (Quit) ではないので要注意です
:::

この `閉じるような` 挙動をする xボタンですが、Unityで開発する上ではかなり厄介な特性があったので、注意喚起も兼ねて紹介します

# 検証環境

| 項目 | version等 |
|---|---|
| Unity | Unity6 (Unity6000.0.23f1) |
| PolySpatial | 2.0.4 |
| Xcode | 16.0 |

# xボタンの挙動について
## Volume のState 定義について

[公式Document](https://docs.unity3d.com/Packages/com.unity.polyspatial@2.0/api/Unity.PolySpatial.VolumeCamera.WindowEvent.html) にも記載されている通り、Volume は5つのStateが定義されています。

![State.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/387855ee-09b1-bbae-68da-de7c3f7ac666.png)

基本的には Opened → Resized→　...　→ Backgrounded → Closed という流れだと推測できます。
ただし、後述の通り、xボタンを押しても `必ずしもClosedにはならない` という挙動をするのがvisionOSです。

しかもClosed になったりならなかったりするので開発する上でも非常に扱いが難しいため、実際の挙動について調査してみました。

## 調査方法

Unity のVolumeCamera がついているObjectに対して以下のScriptを貼り付けてLogを調査します。

```WindowEventObserver.cs

using Unity.PolySpatial;
using UnityEngine;

[RequireComponent(typeof(VolumeCamera))]
public class WindowEventObserver : MonoBehaviour
{
    [SerializeField] private VolumeCamera _volumeCamera;

    private void Reset()
    {
        TryGetComponent(out _volumeCamera);
    }

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        _volumeCamera.WindowStateChanged.AddListener(this.OnWindowStateChanged);
    }

    private void OnDestroy()
    {
        _volumeCamera?.WindowStateChanged.RemoveListener(this.OnWindowStateChanged);
    }

    private void OnWindowStateChanged(VolumeCamera cam, VolumeCamera.WindowState state)
    {
        Debug.Log($"OnWindowStateChanged: {state.WindowEvent}");
    }
}

```

## Unityでの利用用途について

Unity での開発では使用する状況によって挙動が異なります
一つ目は想定アプリモードです。

1. Volume App (Bounded Volumeコンテンツ) の場合
1. Immersive App (Unbounded Volumeコンテンツ) 上で表示する Volume(Bounded)コンテンツの場合

また、いわゆるDebug モードなのか、実機なのかでも異なります。
具体的には

1. [PlayToDevice](https://docs.unity3d.com/Packages/com.unity.polyspatial.visionos@2.0/manual/PlayToDevice.html) を用いてUnityEditor とVisionPro 実機を通信させてリアルタイムデバッグする場合
1. VisionPro 実機にアプリを入れて確認する場合

という形です。

つまりは
| ユースケース \ Mode | VolumeApp | Volume on ImmersiveApp |
|---|---|---|
| PlayToDevice | | | 
| 実機にInstall | | |

といった表にまとめられます。

### 1. PlayToDevice で Volume mode 
PlayToDevice ではUnity のConsole Window でLogを確認できるので
Logを確認しました。

![02_Volume_P2D.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/fdeead45-a920-6aba-525a-5c6511bbbf14.png)

上記を見ると、問題なくClosed ステートに切り替わっていることがわかります

### 2. PlayToDevice で Volume on ImmersiveApp mode 

こちらも同様にログを確認します
![01_MR_Volume_P2D.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/dc77c104-295c-2a07-0fdc-f8bb90d58bad.png)

こちらも先ほど同様、問題なくClosed ステートに切り替わっていることがわかります

### 3. 実機にInstall で Volume mode 
今度は実機Install なのでXcode 側に表示されるLog を確認します。
![03_Volume_install.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/a92115c3-fee5-8aa1-b11f-ed0d2f573b73.png)

ちょっと長いので抜粋をすると Opend→Focused→Backgrounded までは表示されていますがClosed 判定がありませんでした。

また、一度Home画面に戻り、再度アプリを立ち上げたところ Focused になり閉じたはずのVolumeが再表示されました。


### 4. 実機にInstall で Volume on ImmersiveApp mode 
こちらも同様にXcode のLogを確認します。
![04_MR_Volume_install.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/fe2a32e6-8be0-5cc0-e0da-1968ccf70027.png)

結果は先ほど同様、やはりClosed のStateは確認されませんでした。
また、Homeに戻る→再度アプリを選択すると Immersive View は再度表示されませんでしたが消したはずのVolumeのみ再度表示されました。

StateもFocusedになることから `実機でのxボタン` は `Closeボタン` ではなく `Backgroundedボタン` ということがわかります。

### ちなみに
#### APIを試してみたところ
VolumeCameraには `OpenWindow()` と `CloseWindow()` というAPIがあります。
![08_api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/e6d2b6af-0669-c01f-af6f-7cb8906fc882.png)

https://docs.unity3d.com/Packages/com.unity.polyspatial@2.0/api/Unity.PolySpatial.VolumeCamera.html#methods

こちらをBackgrounded な状態で呼び出したり、強制的に CloseWindow()→OpenWindow() を呼ぶことで
ImmersiveSpace 上に自動的に再表示させようとしましたがダメでした。

どうやらOS側でBackgroundedになったUIは再度Foregdounded（Focused)になるタイミングで再表示させるようです。

#### Unity 上の挙動

xボタンを押してもOS側で非表示にしているだけで、Hierarchy上にはGameObjectはのこっています。
そのため、xボタンでStateがBackgrounded になったら自前でDestroy()をコールしないと存続しっぱなしになります。

開発者側で無理やり制御したい場合はBackgroundedStateになったらDestroyしてUIObjectがNullなら再度生成というフローを取るしかなさそうです（ImmersiveSpace 上のVolume コンテンツ表示のみ可能）

# まとめ

| ユースケース \ Mode | VolumeApp | Volume on ImmersiveApp |
|---|---|---|
| PlayToDevice | Closeになる | Closeになる | 
| 実機にInstall | Backgrounded で止まる | Backgrounded で止まる  |

ということで結論としては

:::note
xボタンは Closeボタンではなく `Backgroundedボタン`
:::

ということになりそうです。