VCIアイテムに対する操作の取得とフィードバックのバリエーション

# はじめに
こんにちは、OIEです。
この記事では、スクリプトを含んだVCIアイテムを開発するうえで、**利用者による操作へのフィードバックをどのように利用者自身に伝えるか**、そのノウハウを共有したいと思います。VRアイテムを作ったことがない状態からいくつか開発した中で得られた、限定的な知識のメモ書きとなりますが、実装例も交えて紹介しますので、参考になれば幸いです！

本記事は以下のような情報を扱います。

* 利用者の操作を取得する方法まとめ（イベント関数とその他）
* 明示的なフィードバックが必要な場面とは
* 五感に基づくフィードバックのバリエーション


# 前提
以下の環境を想定しています。

* VirtualCast（安定版） Ver.1.9.4g, 1.9.4h, 1.9.5d
* VirtualCast（ベータ版）　Ver.2.0.0a
* UniVCI 0.29

執筆している現在のバーチャルキャストは、ちょうど1.9系から2系への過渡期であり、スタジオに代わる「ルーム」がベータ版でのみ利用できる状況です。UIをめぐる環境も近々に大きく変わるかもしれないため、利用できる関数などは随時[公式のリファレンス](https://virtualcast.jp/wiki/doku.php?id=vci:script:reference)をご確認ください。
また、この先バーチャルキャストのバージョンが進んだり、将来的にVCIスクリプトがバーチャルキャスト以外でもサポートされるようになった場合、手がコライダーに触れた際などの挙動が変わり、同じVCIスクリプトでも意図したとおりのフィードバックが実現できないことも予想されますので、実際のターゲット環境に合わせた開発を行っていただければと思います。


# この記事を書いたきっかけ
スクリプトを含むVCIでは、アクションを開始したり挙動を変える契機として、利用者からなんらかの操作を受け付けるものが多いかと思います。このとき、受け付けるためのインターフェイスとしてどのようなものがあるか、フィードバックはどんな方法があるか、どんな場合に必要かといった情報があれば、利用者にとって使いやすいアイテムを検討する手助けになってくれるはずです。しかし実際には、これらが網羅的に集められ参照できるようなサイトが現状見つからず、（私をはじめ）各開発者が手探りで体得している状況ではないでしょうか。
今回この記事を書いたのは、自分が考えたことをかたちにしてまとめたかったこと、これを読んだ方に役立ったらうれしいと思ったことはもちろんですが、上記のような情報の共有や議論のネタの一つになったらなぁ・・という思いもあったりします。

# 利用者の操作を取得する方法まとめ
それではさっそく、フィードバックを考える前段として、利用者の操作を受け付ける方法について確認しておきましょう。
VCIで利用者からの操作を受け付ける方法はいくつも考えられますが、**VCIのイベント関数を用いる方法**と、イベント関数を利用せずに**独自に取得手段を実装する方法**の２つに大別してみます。

## A. VCIのイベント関数を用いる方法
[イベント関数](https://virtualcast.jp/wiki/doku.php?id=vci:script:reference:eventfunction)は、ある条件を満たしたときに実行される関数であり、利用者の操作を取得できる関数としては以下のものがあります。

* コライダーとの接触 - *onCollisionEnter, onCollisionExit*
* トリガーとの接触 - *onTriggerEnter, onTriggerExit*
* グリップ操作（使う） - *onUse, onUnuse*
* グラブ操作（掴む） - *onGrab, onUngrab*

VCIを直接操作する場面は、上記の関数で大半のケースをカバーでき、あらゆる操作の起点として大いに役立ちます。
**グリップ**と**グラブ**はリアルでのコントローラー操作を取得するものですが、VR空間内においても**コライダー**と**トリガー**を用いれば、手（HandPointMarker）との接触を検知するかたちで、直感的な操作を受け付けることができます。

## B. 独自に取得手段を実装する方法
Aのイベント関数を用いなくても、利用者の状態やアクションを検知する仕組みを実装することで、操作を受け付けることが可能です。[ExportAvatar](https://virtualcast.jp/wiki/doku.php?id=vci:script:reference:exportavatar)で**利用者自身と各ボーンの位置・方向**を取得できますし、それらの変移パターンを**アクション・ジェスチャー**として判定することで操作手段に組み込むこともできそうです。
また、利用者の情報を直接取得しなくとも、手に持ったり放り投げたりしたサブアイテムの位置や衝突対象を取得するといった、**サブアイテムを介した間接的な操作取得**も考えられます。


# 明示的なフィードバックが必要な場面とは
利用者の操作を無事取得できたうえで、フィードバックが必要となるのは、どのような場面でしょうか。
まず、明示的なフィードバックが**不要**なケースから考えてみます。

例えば、あるVCIアイテムを読み込んだら、電球の形をしたサブアイテムが出現したとします。手で触ってみると、暗い状態ではすぐに明るくなり、逆に明るい場合は暗くなりました。

こんな場合、目の前で起こった結果以外のフィードバックが不要であるのは自明・・とは思うのですが、改めて考えてみると、不要になるために以下のような条件を満たす必要がありそうです。

1. 操作したあと、時間の隔たりなく結果が返ってくること
1. 結果を何らかの感覚で認識できること
1. 常に予想した通りの結果を得られること

もしこれらの条件が満たせない場合・・・例えば、操作した瞬間になんの反応も感じられなかったり、簡単には理解できないことが起きてしまうような場合、利用者は、自分が操作を誤ったのか、スクリプトやPCに不具合が発生しているのか、まさかコントローラーの調子が悪いのか・・と混乱してしまいます。このため、目の前の結果で上記条件を満たせないときには、**「ちゃんと操作を正しく受けつけたよ」**というメッセージを与える意味で、利用者に対して明示的なフィードバックを別途提示してあげるのがよいと思います。

# 五感に基づくフィードバックのバリエーション
さて、明示的なフィードバックが必要だと判断できたら、どんなかたちで実現するかを考えなければなりません。バリエーションをたくさん思いつけるように、手始めとして**五感**のどれに訴えるかを想像してみることにしました。
現状、五感のうち**嗅覚**、**味覚**は（少なくとも一般消費者レベルでは）VRでの自由な再現が実用化されていないため、ここでは**触覚**、**聴覚**、**視覚**で提示できるフィードバックのバリエーションをそれぞれ考えてみます。

## 触覚
バーチャルキャストでは、手とサブアイテムのあいだで「コライダーの接触」「トリガーの接触」が発生するとコントローラーが振動し、疑似的に触った感覚を味わうことができます。

また、[HapticPulseOnGrabbingController](https://virtualcast.jp/wiki/doku.php?id=vci:script:reference:exportassets#hapticpulseongrabbingcontroller), HapticPulseOnTouchingController を用いることで、特定のサブアイテムを掴んでいる or 触れている状態でコントローラーを振動させることができます。ゲームへの応用がすぐに思いつきますが、フィードバックとしても、振動の強さと再生時間を指定できるため、短い振動を処理開始の合図としたり、強くて長い振動によって処理継続中や処理が重いステータス、あるいは「拒否」のニュアンスを伝えたり・・意外と応用範囲が広そうです。
（2020/12/11訂正：本記事投稿時に「プログラム側で能動的に振動を発生させる手段は現状ない」と記載していましたが、実際には上記の通り関数として存在していたため、修正・追記いたしました。）

## 聴覚
音の再生は、[ExportAudio](https://virtualcast.jp/wiki/doku.php?id=vci:script:reference:exportassets:audio)で積極的に利用することができます。音楽や爆発音といった、結果に付随する音を直接再生するだけでなく、手元のボタンを操作した際に「カチッ」といった音が出ることでも、フィードバックの一つになるかと思います。

ただ、オーディオの再生環境や再生音量の設定、聴覚の感度などが、利用者によってバラつきがある点は、考慮したほうがいいかもしれません。何度も再生される音の音量はどれくらいがよいか、減衰はどう設定するのがよいか。。オーディオ周りは、視覚面に比べると、利用の仕方にまだ手探り感が強いように思えます。現状では、聴覚のフィードバックについては、別のかたちのフィードバックで補強してあげるのがよい気がしています。

## 視覚
多くの場合メインとなるフィードバック経路です。見えることが重要なので、操作対象が見にくい場合や、操作取得でBの独自実装を行う場合は、ボタンやインジゲーターといった別のサブアイテムで変化を提示することも検討したいですね。

表現のバリエーションが多数あるため、ここではフィードバックのために変更する対象を示す**「パターン」**と、変更が片道か往復かを表す**「反復性」**に分けて考えます。それぞれのパターンに対して反復性を使い分けることができるので、全体のバリエーションとしては、パターン数 * 反復性の掛け算で考えられそうですね。例えば**「色が変わる」×「2つの状態を往復して遷移し続ける」**であれば、色の点滅というかたちでフィードバックを伝えられます。
また、ここで挙げているパターンは、どれも関数が1つくらいで再現できるものばかりで単純なものですが、パターン同士を組み合わせることでよりリッチな表現を実現することも可能です。アイテムの特性次第で、装飾性に富んだフィードバックを返すようなVCIがあったって、いいはず！

* パターン
 * 色が変わる
 * 発光する、消灯する [^1]
 * 文字が変わる
 * 別のものに差し替わる
 * サイズが変わる
 * 場所が移る、回転する
 * 消える、現れる [^2]
 * 凹む、戻る [^3]
 * 動きが変わる

[^1]: 執筆時点では、光量をコントロールする関数は公開されていないため、現状ではサブアイテムを差し替えるかたちでしか実装できず、「別のものに差し替わる」と被ってます。
[^2]: サブアイテムを消す方法については、[VCImeetup](https://virtualcast.jp/blog/2020/07/vcimeetup001/)でらーめんさんが講演された内容が参考になります（左記リンク先の「③らーめんさん「サブアイテムを疑似的に消す方法」」参照）。本記事で掲載している実装例のコードでも、サブアイテムを疑似的に消すために、サイズを1/1000倍に縮小しています。
[^3]: 実装としては、「サイズが変わる」や「場所が移る、回転する」と被る場面が多いと思いますが、「押しボタンを連想させて理解してもらいやすい」という点で特徴的なため、別枠としました。

* 反復性
 * 別の状態に一方向に遷移する
 * 2つの状態を往復して遷移し続ける [^4]

[^4]: 短時間で往復が終了すれば「操作完了」、往復がずっと続けば「処理中」「操作受付中」として認識されやすいようです。

### 視覚フィードバックの実装例
ここでは、今までに作成したVCIのなかから実装例を紹介したいと思います。コードについては説明上不要な部分はカットして掲載している点、ご了承ください。

#### 「消える、現れる」×「2つの状態を往復して遷移し続ける」
VCI「[カメラのズームリモコン](https://seed.online/products/09e18f1ef75b22ccb7e2470aa9adfbceccec253879583a5369fa66e854bfe3c3)」で、現在アクティブなカメラのアイコン周辺を点滅させている処理です。ここでは、アイコン直下に配置したハイライト用サブアイテム（CAMERA_HIGHLIGHT）のサイズを一定時間ごとに縮小・拡大させることで、ハイライト状態のOFF/ONを再現しています。

![cube_qiita02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918056/9e675468-d84b-3022-844a-597d22a3dc3f.gif)


```lua:main.lua
function update()

    -- このあたりで、現在時刻を表す数値を、変数 timeCodeNow に代入する処理

    -- 以下、時間経過で明滅フラグを切り替える処理
    if timeCodeNow - timeCodeBlink > BLINK_TIME then -- 最終フラグ切り替え時刻から一定時間（BLINK_TIME）経過したら
        if blink_hide == false then
            blink_hide = true -- 明滅フラグ切り替え
        else
            blink_hide = false -- 明滅フラグ切り替え
        end
        timeCodeBlink = timeCodeNow -- 最終フラグ切り替え時刻を保存
    end

    -- 以下、明滅フラグを反映させる処理。本来は毎フレーム実行する必要はないので、フラグ切り替え後だけに発動するよう要改良
    if blink_hide == true then -- 明滅フラグが true なら
        CAMERA_HIGHLIGHT.SetLocalScale(Vector3.__new(0.001, 0.001, 0.001)) -- めちゃくちゃ小さくして隠す
    else -- false なら
        CAMERA_HIGHLIGHT.SetLocalScale(Vector3.one) -- 元のサイズに戻して表示する
    end

end
```

このコードのなかで、サイズを縮小・拡大している個所を別の処理に置き換えれば、その処理で「2つの状態を往復して遷移し続ける」反復性を再現できます。別のパターンに置き換えてもいいし、もっと凝った処理に置き換えてもいいですね。

#### 「色が変わる」×「2つの状態を往復して遷移し続ける」
VCI「[変形インテリア「CUBE」](https://seed.online/products/c557cdf658fc3f297aa919e614f3d4ada6272496f05a84c907f97566d35a9cbd)」で、台座の一部が白黒に点滅する処理です。グリップ操作を行うことでテーマ切替間隔（≒アニメーションが再生される時間）を3段階に変えられるようにしており、台座がそのボタンとインジゲーターの役割を担っています。 [^5] ここでは点滅自体に加え、点滅のスピードを都度変更することで、現在のテーマ切替間隔が3段階のどれに当たるかを把握できるようにしています。 [^6]

![cube_qiita03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918056/df1771fc-a7d4-cd4e-91a6-2272cf8b23fb.gif)

[^5]: テーマ切替間隔は、設定変更がうまくいったかどうかを結果だけから判断しようとすると、実際に切り替わるまで待たないと分かりません（最長30分待ちです！）。つまり、フィードバックが不要な条件のひとつ「操作したあと、時間の隔たりなく結果が返ってくること」を満たせてないわけですね。このため、安定して目に入りやすい台座部分でフィードバックを即座に返しています。
[^6]: ボタンや文字などで、現在の設定内容を表示する手もあるのですが、本VCIでは（分かりにくくならない範囲で）極力ボタンやインジゲーターを排除したかったため、このような実装にしています。



# おわりに
本記事では、操作の取得方法からフィードバックのバリエーションまでを概観しました。私自身がVCI開発の中で気づいた範囲をメモ的に残したものなので、決して網羅的ではないと思いますが、もし利用者へのフィードバック方法を検討する中で参考になる点があれば幸いです。また、「こんなフィードバック方法もあるよ」といった情報を紹介いただければうれしいです！