[neos(NeosVR)]Protopsychoで実装したサイコキネシスのシステムの解説


Protopsychoでメインシステムとして使われているサイコキネシスの動作がどのように実装されているか、おおまかに解説します。
LogiXの実装や解読に関する十分な知識を保有していることを前提としていますので、補足等はありません。
この記事で触れるのは、サイコキネシスのシステム特有の部分のみです。
文面だけで内容を読み解くのは難しいと思いますので、実際にLogiXを解析しながら補助資料として使っていただくのが良いと思います。
ProtoPsycoなどスペルミスがあるのは、途中で気づいたけど治す時間がなかったからなので気にしないでください。

## 基本的な考え方
　ものすごく大雑把に書くと、動かす対象からCharactarContlollerを取得して
　CharactarContloller経由でベクトル操作を上書きしてサイコキネシスの操作を実現する、というシンプルな発想で実装されています。
　当然そんな簡単にできるわけがないんですが、如何にして実装したかを以下で解説します。

## インストール処理
![スクリーンショット 2022-03-03 193739.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/23cb726a-12d4-176b-a359-cc67b24d8c30.png)

処理自体はProtoPsydoSystem/SystemManage内のProtoPsycoSystem.Installで行われています。
まずプレイヤーとなるユーザーのRootSlotにProtoPsycoコピーをした後、
HandSlot内にあるProtoPsycoLeftHand(RightHand)をユーザーのRootSlotの直下にあるLeftHandとRightHandにコピーしています。

## 対象の取得の仕方（サーチモード）
![スクリーンショット 2022-03-04 174407.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/077aed96-755d-f8f5-7169-1049c0284698.png)


![スクリーンショット 2022-03-04 180055.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/ebe735b7-4f76-86ee-b837-0d6c42273e3b.png)
この処理はProtoPsyco/HandSlot/ProtoPsycoLeftHand(RightHand)/LogiX/CatchAction
で実行されています。

この白い球体と対象を衝突させることで、OnCollisionStart経由で動かす対象を取得します。
その際、判定をstaticTriggerにしないと以下のバグが発生しました。
https://github.com/Neos-Metaverse/NeosPublic/issues/3127
簡単に書くと、本来とは別の物に衝突したという判定になるかなり厄介なバグで
私の場合はミサイルを掴もうとすると何故か勝手に爆発(コンテナや壁に当たった判定)になる怪現象に悩まされ
このバグのせいで2日かそれ以上時間を無駄にしました。

白い球体は、「HandSlot/ProtoPsycoLeftHand(RightHand)/Startpooint」とその子のSlotの位置を結んだ直線上に常にあるように設定されていて、具体的には「Raycaster」で放っているRaycastが衝突した地点か、「StatusSlotの子Slot」内で設定したサイコキネシスの最大距離のいずれかの位置になります。

オブジェクトと白い球体が衝突した際、「Find Character Controller 」で対象にCharacterControllerがあるかどうか(=動かせる対象か)を確認し、存在すればtmplateの下のTargetSlottemplateを「SearchSphere/CatchingSlot」にコピーしてSlot名を変更、そのSlot内に対象となるSlotとCharactarContlollerの値を記録します。(以下CatchingSlotにコピーしたSlotは、説明の便宜上「能力対象Slot」と呼びます。)
![スクリーンショット 2022-03-03 194129.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/f2781ef3-b59c-38ec-4de8-36cb5142e0ee.png)
![スクリーンショット 2022-03-03 194202.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/7842baa6-ceff-6b88-0fa6-47cb3973f9f1.png)

逆に何かしらのオブジェクトが白い球体の範囲から外れた場合、
この「SearchSphere/CatchingSlot」内をループさせて、Controllerの中に範囲から外れたCharacterControllerと同じものをもつSlotが含まれているかどうかを確認しています。含まれていたらCatchingSlot内の該当「能力対象Slot」を削除します。

手のひらをかざしている間、この処理が常に動き続けます。
あくまで衝突の開始と終了時にのみイベントを発生させているだけなのでそこまで負荷は高くないと思います。
tmplateからコピーした「能力対象Slot」を蓄積させてまとめることで、複数の対象に対する値を保持しています。
理屈の上では1個でも100個でもサイコキネシスの対象とすることが出来ます。
また、今回はSlotとCharacterControllerが必要でしたが、記録しておくのが単一の値だけでいいなら、DynamicValuVariableのWriteorCreateを駆使して複数の対象の値を保持する方法もあります。必要な要件に合わせて使い分けると良いと思います。
また、自分自身を掴んでしまわないようにUserRootかそれ以下のSlotの場合は対象外にする処理を入れています。
一応DynamicValueを使ってUserを掴めるか否かの設定を制御していますが、今回はこの設定を変更することはありませんでした。


## 対象の動かし方（グラブモード）
![スクリーンショット 2022-03-04 182901.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/2fd2b273-840c-1e67-8971-7498734a95db.png)
この処理はProtoPsyco/HandSlot/ProtoPsycoLeftHand(RightHand)/LogiX/GrabAction
で実行されています。

コントローラーのグラブを押すと、
その時点で「SearchSphere/CatchingSlot」に入っている対象の「能力対象Slot」に対して、
・重力を0にする。
・「SearchSphere/CatchingSlot」の中の「能力対象Slot」を「GrabSphere/GrabbingSlot」内に移動させる。
・上記を移動させた際、対象のオブジェクトの掴んだ瞬間の座標をとってきて「能力対象Slot」の座標に設定する。
の処理をまず行います。
これによって、対象を掴んだ時の処理を行うための準備をします。

次に、移動したSlotに対してGrabを行っている間以下の処理を適用します。
・対象のオブジェクトを「能力対象Slot」の位置に対して移動させる。移動する速度は「能力対象Slot」の位置と対象オブジェクトとの距離に依存する。(近いほどゆっくりになる)
こうすることで、Grabをしている間に手を動かすとその位置に向かって少し遅れてついてくるような挙動になります。
距離を離すほど早く動くので、基本極端に遅れることはありません。
この処理は毎フレーム行っているため、処理としては重めです。
つまり、掴んだ瞬間の位置を記録して、サイコキネシスの中心部分(掴んでる際は青い球)との相対的な位置を常に保つようにしています。

:::note info
脳内で考えてた段階ではDriveで解決するつもりでしたが繋ぐ手段が浮かばず、かつ実装開始時点ではあらゆるCharactarContlollerを持ったオブジェクトを汎用的に動かせるようにする予定だったのでこのような形になっています。
結局、未解決の事項　に書いてる不具合もあって、ゲーム外でもサイコキネシスを使えるようにする余裕がなく今回はゲームのみの応募となりました。
:::

投げる動作については、
掴んでいる手の動きが一定以上の速さ(0.1秒前の位置との比較で算出)の状態でGrtabを離すと投げる判定になります。
投げる際の軌道は、サイコキネシスの中心部分(掴んでる際は青い球)の動きを参照します。
投げるか否かの判定に手の動きの方を参照しているのは、サイコキネシスの中心部分はその時の位置によって0.1秒当たりの移動量が全く違う為判定としては扱いづらいためです。例えば手から10ｍ離れている時もあれば1m先にある場合もあるので、全くあてになりません。

また、Grabボタンを離した際に投げる動作に該当しない場合は掴んだオブジェクトを離して開放する処理を行います。
・「GrabSphere/GrabbingSlot」内にある「能力対象Slot」を全て削除

:::note info
今回はサイコキネシスの各種動作にクールタイムを設けていないため、
ゲームの攻略上全く利点はないですが同じオブジェクトを掴んだ瞬間に投げることもできますし、投げた瞬間にすぐに掴むという芸当も可能です。
:::


## その他細かい操作について
コントローラーのボタン数の制限もあって、今回はトリガーやメニューボタンのタッチ操作を取り入れています。
ただ、これらは使わなくてもゲーム自体はクリアできるため、細かく位置を制御したりテクニカルなことをしたい方向けの機能となります。
例えばコンテナを掴んで物陰から投げるなどしたい場合は掴んだオブジェクトの位置調整が重要になります。

サーチモード
プライマリトリガーで白い球体が見えなくなります。
仕組みとしては、コントローラー操作に合わせて「MeshRender」のOnOffを切り替えているだけです。
能力を使わない際に白い球体が視覚的に鬱陶しい事と、掴んだ際にプライマリトリガーを触ってると引き寄せられてしまう為
オブジェクトを掴むときはプライマリトリガーを触らない動作にさせたくてこのような形になりました。

グラブモード
プライマリトリガータッチでサイコキネシスの中心部分を手元に引き寄せます。
掴んでるオブジェクトはサイコキネシスの中心部分との相対的な位置を保つので、結果的にオブジェクトも引き寄せられます。
メニューボタンタッチでサイコキネシスの中心部分を遠くに離します。
この引き寄せたり離す動作については、「HandSlot/ProtoPsycoLeftHand(RightHand)/Startpooint」とその子のSlotの位置を結んだ直線上を常に行き来するように設定しています。


## ステータスの切り替え方
![スクリーンショット 2022-03-03 193739.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2308792/c5d51ceb-2361-9a03-d541-7f2d449095a6.png)
まずStatusSlot/StatusTemplateのSlotを直に指定して、次にその子の中に該当する名前のSlotがあるか探します。
見つかった場合、UserRoot/ProtoPsyco/StatusSlotの子を全て削除した上で、上記で見つけたSlotをコピーして移動させます。
処理としてはこれで終わりです。
実際に取り出す側に工夫がされていて、ReadDynamicValueを使う際は、UserRoot/ProtoPsyco/StatusSlotを親にして
GetChildの0番目のSlotを取得するようにしています。
こうすることでStatusSlotの子がどんなSlotでも常に望む結果を得ることが出来るようにしています。

## 未解決の事項
simulatingUserについて
このゲームではCharactarContlollerを使って疑似的な物理演算を行っていますが、
その際動作をさせるにはsimulatingUserを設定する必要があります。
当初はHostUserに設定してプレイヤーと計算を分散させられるようにする予定でしたが
実際に動かすとHostUserに設定されたCharactarContlollerは他のユーザーが飛ばしたパルス経由では上手く動かせないことが判明しました。
恐らく処理の途中で強制的にHostUserがパルスを飛ばすように追記すれば対処は可能と思われますが、
期間中にその動作の実装方法が思いつかなかったのと純粋に開発時間が足りなかったため、
今回はホストとプレイヤーが同じになるように注意分を追加しました。

## 最期に
雑多な文章でお世辞にも読みやすいとは言えないと思いますが、LogiX開発の参考になれば幸いです。
別の記事でステージ毎の遷移やリセット処理のやり方、音の制限などについて書こうと思っています。
サイコキネシスと違って汎用的な物なので、より参考になると思います。