FlyingSystemの実装

phi16です。飛び入り参加しました[^a]。Boze Advent Calendar 22日目です。

[^a]: きっかけは[こちら](https://note.com/takaomi_crisis/n/n1f4602c66e93)

三日坊主さんに話を伺って実装した **FlyingSystem** について書きます。
![VRChat_1920x1080_2020-12-18_18-36-29.367.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/adb53a3a-53c3-9aaa-84b3-96346f93d4aa.png)
9割の人は **[これ](https://github.com/phi16/VRC_storage#flyingsystem)** が欲しい情報だと思います。

&nbsp;
以下、1割くらいの人に向けて書きます。

## きっかけ

三日坊主さんが片手操作ができる飛行システムを求めていたので、作りました。

もともと三日坊主さんの創ってらっしゃるワールドは**悉くデカい**ので、「普通」の移動方法が空間的にふさわしくない、と思われる状況です。

ということで、ギミックというよりも「移動の一形態」として (自然に溶け込んだ機能として) 存在させています。

## 方針

片手なので出来ることは`PickupUseDown`と`PickupUseUp`くらいしかありません。

片手移動という点で近いのは [CyanClimbing prefab](https://twitter.com/cyanlaser/status/1150642852520529921) ですが、プレイヤーの移動量を実際の手の移動差分から導出するとあの広大な世界ではどうしようもないことになります。

なので**移動差分を速度に割り当てる**のはとても自然な解釈だと思います。

というわけで、「`PickupUseDown`したローカル位置を保存」「現在のローカル移動量をプレイヤーの速度として適用」というのが基本的な流れです。

> ちなみに似たような思想で[泳ぐ機構](https://twitter.com/phi16_/status/1152938847266754561)を作ったこともありました。これは「`PickupUseUp`した瞬間にローカル移動量を逆向きの速度として適用」する装置です。
>
> [N-FlightSystem](https://namanonamako.booth.pm/items/1564685) も移動差分を速度にするという点では共通ですね。まぁこれらは速度というよりも微小移動、ですけど。

また、「空中に留まる」という願いもあると思ったので、掴んでる最中はずっと浮いてるようにすると丁度良さそうという感じ。「掴んでいる最中は3次元の自由移動が出来る」という解釈にもなって自然ですね。

あ、あとDesktopはどうしようもないのでUnityのWASDQE移動を連れてきました。慣れると便利です。

## 実装

やるだけ…に見えるんですが、今回**ローカル空間**というのがちょっと厄介です。ローカルって何？という話。

最も望ましいのは**トラッキング空間**なんです。移動差分としてプレイヤーが認識している以上、その過程は連続で、例えば Comfort Turn による不連続な回転による影響は望ましくないと言えるでしょう。

しかしトラッキング空間を得る方法はなんとVRChatにはありません。

> Climbing prefab での locomotion system 的には TeleportTo の特殊な (？) 挙動を使うと[トラッキング空間が得られる](http://vrchat.wikidot.com/worlds:guides:player-tracking#toc3)ことが知られているんですが、これはハックみたいなもんなのであまり使いたくない。

諦めてプレイヤー空間を使うのが限度なんですが、そうすると頭を回した時に同時にプレイヤー空間も回るので移動方向が変わってしまうのですよね。実際そうなっちゃってます。

まぁ毎回`PickupUseUp`すれば良いというのもあるし、移動中そんなに見回すこともないだろう、という判断です。「トリガー外せば静止できる」というのはそれらに対して良い安心感になっているかもしれませんね。

というわけで。

```cs
/* 
Hierarchy
- This Udon
  - origin
  - target
*/
public void Pickup() {
    hovering = true;
}
public void Drop() {
    hovering = false;
}
public void PickupUseDown() {
    flying = true;
    Vector3 pos = player.GetPosition();
    Quaternion rot = player.GetRotation();
    Vector3 targetWorldPos = target.position;
    transform.position = pos;
    transform.rotation = rot;
    originPos = transform.InverseTransformPoint(targetWorldPos);
    origin.position = target.position = targetWorldPos;
}
public void PickupUseUp() {
    flying = false;
}
void Update() {
    if(player == null || !player.IsUserInVR()) return;
    if(!hovering) {
        VRCPlayerApi.TrackingData head = player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
        target.position = head.position + head.rotation * Vector3.back * 0.1f; // 10cm back
        return;
    }
    Vector3 velocity = player.GetVelocity();

    if(flying) {
        Vector3 pos = player.GetPosition();
        Quaternion rot = player.GetRotation();
        Vector3 targetWorldPos = target.position;
        transform.position = pos;
        transform.rotation = rot;
        target.position = targetWorldPos;
        Vector3 vel = target.position - origin.position;
        velocity = Vector3.Lerp(velocity, vel * speed, flyAtten);
    } else {
        velocity = Vector3.Lerp(velocity, Vector3.zero, hoverAtten);
    }
    velocity -= player.GetGravityStrength() * Time.deltaTime * Physics.gravity;
    player.SetVelocity(velocity);
}
```

Udon自体をプレイヤー空間として、その子として「始点 (`origin`)」と「終点 (`target`)」があります。

トリガーを引いた瞬間に始点の位置を記録、あとは終点との差分 `vel` に比例した値が速度になります。

直接 `velocity` に代入していないのは**常にスムーズに動いてほしかった**から。今回の移動形式はとてもゆるやかで (目的が移動及び観賞ですから)、移動速度も連続であってほしかった。

`flyAtten`と`hoverAtten`は「どれくらいの速さで目的速度に収束するか」のパラメータです。`flyAtten`は`0.2`、`hoverAtten`は`0.01`になってるので (坊主さんが変更してなければ)、飛び回る時はわりかし手の移動差分量に追従するが、トリガーを離すとふわーっと速度が遅くなっていく、ようになってます。

気持ち的には「トリガーを押す」のは**強い意思**を持つので迅速に従い、「トリガーを離す」のは**穏やかな意思**なので現状維持しつつ落ち着いていく動きをする、ということです。

もしも急速に止まりたいのならそれは強い意思であり、実際「トリガーを離して**押して**離す」ことで達成することができます。適切なUIだと思います。

&nbsp;
ちなみにホバリング中に普通にスティック移動すると普通に移動できるのは偶然の産物です。まぁ確かにそうなる。すごい使いやすくていいんですよね。

## 酔いについて

特にそんなつもりは無かったんですが、酔わないらしいです。

一般に酔いに関する話として、軸にあるのは**動きがメンタルモデルと一致しているかどうか**だと思っています。

- 単純な操作であればあるほど自分の操作がどう影響するかを理解しやすい
- 操作とそれに伴う挙動が「概念的に近い」程、直感的
- 予想が現実と一致するほど違和が少ない
- 未来が (無意識的に) 理解できるほど、その操作は自然に溶け込むことができる

そう思っています。これは私の中の仮説。

まぁ「思うように動く」「動かしたように動く」ということ。
そして「思わないように動かない」「動かしてないようには動かない」ということ。

ギミックを掴んでるだけだと安定。トリガーを引いただけでも安定。**自分が手を動かした分だけ、実際に速度が出る。**自身の労力がそのまま力になってる。

そして速度に直結しているから、「あっちへ行きたい」みたいなことを考えたときに行うべき行動というのも単純。制御が直接的ということですね。

あと動きが常に連続性を保っている、ということが多少寄与してたらいいなぁ、とはちょっと思います。トリガーを引いた瞬間は**速度が0なので連続**。離した瞬間は**速度を維持してちょっと進むので連続**です。

加えてさっき書いたように「意思が強ければ強い程、強く制御が行われる」というのも一つあるといいなと思ってます。

&nbsp;
まぁここまでは設計的な話ですが、実際のところそれよりも寄与が大きそうなのは、**回らないこと**ですね。やっぱり。

あと坊主さんのワールドは**広い**ので、飛んでいる間というのは「壁から遠い」ことが多いとおもいます。そうすると「常に遠景を見ている」ようになる。環境の変化が激しくない、というのも1つの要因かな、ということを思いました。

どちらとも、**急激に視界が変化しない**ことを意味します。穏やかなのは良いことですね。

## 小さい話

- FlyingSystemっていう名前は何も考えてなかっただけで、習慣的に付いちゃったやつ。
- 配布する理由もしない理由も特に無かったんですけど、今回はついでというやつです。
    - 価値を丸投げする行為がどうか、みたいな話はあると思って静観はしていたんですが、場は結局作られなかったようなのでこれでいいんだと思います。
    - ここから場が生まれるのも尚良しではありますね。
- そういや重力キャンセルの仕組みが相変わらず雑なので飛びながらメニュー開くと吹っ飛びます。
    - まぁある程度で収束するので大きな問題にはなってないです。
- [Cocoon-05](https://vrchat.com/home/world/wrld_c3319ee2-02f1-4e0a-b0dc-8e11a297eb65)に導入してもらった際に**飛ぶと激重になる問題**があって… まったく原因がわからなかったんですけど。
  - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/da8041dd-cfee-1a5a-256e-27b4b7600d27.png)
  - 恐らくこの知見を使うことになるのは三日坊主さんだけな気がしますが、興味深い現象でした。

## おわり

以上です。これからも坊主さんが作るワールドを楽しみにしています。

坊主さんに限らず「自由な空間移動を前提としたワールド」はいろいろ面白そうなのでわくわくですね。

何かのきっかけになったらそれは良い話、かもしれません。

ありがとうございました。