FlyingSystemの実装


phi16です。飛び入り参加しました1。Boze Advent Calendar 22日目です。

三日坊主さんに話を伺って実装した FlyingSystem について書きます。

9割の人は これ が欲しい情報だと思います。

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

きっかけ

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

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

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

方針

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

片手移動という点で近いのは CyanClimbing prefab ですが、プレイヤーの移動量を実際の手の移動差分から導出するとあの広大な世界ではどうしようもないことになります。

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

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

ちなみに似たような思想で泳ぐ機構を作ったこともありました。これは「PickupUseUpした瞬間にローカル移動量を逆向きの速度として適用」する装置です。

N-FlightSystem も移動差分を速度にするという点では共通ですね。まぁこれらは速度というよりも微小移動、ですけど。

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

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

実装

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

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

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

Climbing prefab での locomotion system 的には TeleportTo の特殊な (?) 挙動を使うとトラッキング空間が得られることが知られているんですが、これはハックみたいなもんなのであまり使いたくない。

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

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

というわけで。

/* 
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 に代入していないのは常にスムーズに動いてほしかったから。今回の移動形式はとてもゆるやかで (目的が移動及び観賞ですから)、移動速度も連続であってほしかった。

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

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

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

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

酔いについて

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

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

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

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

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

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

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

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

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

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

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

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

小さい話

  • FlyingSystemっていう名前は何も考えてなかっただけで、習慣的に付いちゃったやつ。
  • 配布する理由もしない理由も特に無かったんですけど、今回はついでというやつです。
    • 価値を丸投げする行為がどうか、みたいな話はあると思って静観はしていたんですが、場は結局作られなかったようなのでこれでいいんだと思います。
    • ここから場が生まれるのも尚良しではありますね。
  • そういや重力キャンセルの仕組みが相変わらず雑なので飛びながらメニュー開くと吹っ飛びます。
    • まぁある程度で収束するので大きな問題にはなってないです。
  • Cocoon-05に導入してもらった際に飛ぶと激重になる問題があって… まったく原因がわからなかったんですけど。
    • 恐らくこの知見を使うことになるのは三日坊主さんだけな気がしますが、興味深い現象でした。

おわり

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

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

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

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


  1. きっかけはこちら