Skip to main content

Robloxの物理、より良いスリープシステムの構築

7月

30, 2020

by kleptonaut


テック

物理エンジンでの「スリープ」について話しましょう。

スリープシステムの目的は、動いていない物体に対する作業をやめて、動くときに作業を開始することです。 大変なことは、これらの状況間の移行において最適な発見的手法を選ぶことです。 他のエンジンによくある物体の速度に関するアプローチは、速度がいくつかのフレームに対して与えられた閾値以下の場合、物体をスリープ状態にすることです。 起きている物体と衝突することによって起きることができ、あるいはいくつかの他のイベントによって起きるかもしれません。 最近まで、Robloxもこの手法のバリエーションを使用していました。

この手法のすでに知られた問題として、物体が数フレームでほぼ速度ゼロかもしれないのに、現在の軌道を終了していない物理交流などがあります。 ボールが傾斜した平面をボールが持つ勢いでやってくるという単純な状況を想定できます。 最終的にボールは運動エネルギーがなくなり、軌道の頂点に達し(勾配で押し上げることによって潜在エネルギーをすべて変換)、また勾配を下りて行きます。 しかし、十分に低速な場合は、ボールはまた転げ落ちる前に勾配の軌道の頂点でただスリープします。

もし、開発者がこのような微妙なケースに遭遇した場合、エンジンはよく代替方法を提供してくれることがあります。 それらは、すべての交流で性能を犠牲にして、何かがスリープすべきときに閾値を調整することができるかもしれません。 または、特定のオブジェクト上でスリープをすべて無効化するかもしれません。 特定の期間、1つのオブジェクトのスリープを無効化したいだけかもしれません。 いずれにせよ、開発者が出くわす微妙なスリープの例を発見して対処するのは開発者次第です。 Robloxでは、開発者の作業量を削減し、このような悪い状況をすべて防ぐ方法を見つけようとしました。 スリープシステムにとっての目的は、開発者が心配する必要がなくなることです。

Robloxエンジンでは、剛体は「アセンブリ」と呼ばれています。 それは、1つかそれ以上の硬く結合した原始的な物体、またはパーツの集まりですが、これは起きているかスリープしているオブジェクトです。

アセンブリは、ヒンジやロープのような結合によって連結することができ、「メカニズム」と呼ばれるものを補います。 これが重要なのは、アセンブリが自分のメカニズム内にある他のアセンブリの状態に基づいてスリープ状態を推測することができるからです。これについては後述します。

他のエンジンのように、Robloxはいつ物体がスリープするかを決定するのに伝統的な発見的手法を使ってきました。 それぞれのアセンブリは、平均値からの逸脱が閾値内である限りは、その過去の位置と配向の「履歴」を保持し、アセンブリは起き続けます。 本質的に、もしも速度がいくつかのフレームに設定済みの値以下だったら、アセンブリはスリープします。 この値は、臨界速度として言及します。

起きているアセンブリは赤で縁取りしてあります。 スリープしているアセンブリは、黒で縁取りしてあります。 静的アセンブリには、縁取りがありません。

上の例は、前に説明したようにボールが傾斜路でつかえているのを図解しています。 ここで、回転中のパーツがシリンダーを坂の上へと押し上げ、しばらくシリンダーを静止させ続けます。 シリンダーの速度は、スリープするのに十分な時間臨界速度以下になります。 これは、低重力下で跳ねるボールや空中でスリープ状態になることや背の高いブロックのその場所への転倒、地面と同じ高さになる前にスリープ状態になることなど、様々な例で図解できます。

スリープすべきかどうかを決定する前のいくつかのフレームでのアセンブリの位置変更よりもいい情報がないかを探すことが目的でした。 最も基本的な力学のうちの一つになぜ従わないのでしょうか? 「物体は力に作用されない限り、休息しつづけるか一定の速度で動き続ける」。 重力はシリンダーに作用するのが分かっていて、それは持続するべきです。 オブジェクトが一定の速度で動いているとき、または、バランスの取れていない力が作用しているときは、起きていて欲しいのです。 もし、どちらかのケースが正しい場合、物体は起きているはずです。 物理ソルバーからアセンブリにある最後のインパルス値をつかむことができ、別のインパルス閾値と照らし合わせます

同じ例を再確認しているとき、シリンダーが正しく勾配を下っていくのを見ました。 ここで、バランスの取れていない力によって実行されていると、アセンブリがスリープするのは不可能です。 アセンブリがスリープするためには、インパルス閾値も臨界速度以下である必要があります。

この同じ論理を原因となるアセンブリを起こすために使おうとすることで、小さな問題に直面します。 何かをスリープさせるということの意味は、それがソルバーにないため、スリープ中のアセンブリの最後のインパルス値をチェックすることはできません。 それには、コンタクトとプロパティ変更イベントに頼る必要がまだあります。 しかし、メカニズム関係は様々なケースで助力となります。 実際のところ、アセンブリの履歴は同じメカニズムにある他のアセンブリがいつ起きるべきかを決めるのに役立ちます。

一つのメカニズムに起きているアセンブリがあるとき、そのアセンブリに直接、隣接するすべてのものが「チェック中」状態(もし、起きていない場合)に置かれます。 「チェック中」状態にあるアセンブリは、まだスリープ状態ですが、フレームごとに起きているアセンブリの履歴をチェックし、もしもその逸脱が他のより大きな閾値以上の場合は、「チェック中」のアセンブリも起きているべきないと決定します。 これは、近隣臨界速度として言及されます。

もし、これらのアセンブリのうちの一つが大幅に動いている場合、もう一つのアセンブリも起きて動いているはずだと想定します。 これは常に正しいわけではないので、結局は必要以上に少し挑戦的になります。 例えば、2つのアセンブリ(回転する羽根車と土台)からなる風車があるとしましょう。 土台は動いていないのでスリープ状態で、回転する羽根車は起きていることが理想的です。 しかし、回転する羽根車が近隣臨界速度以上の速度で動いている限り、土台は誤って起きた状態でいつづけます。

この理想的な動作は、他の種類のメカニズムでは問題を生じる可能性があります。 1つのアセンブリを起きたままでいつづけさせるのに十分に早く動きつつ、近隣のものをスリープチェック中状態にしておくのに十分に低速な状態にできます。 もし、起きているアセンブリがスリープ中の近隣のものも動くことを必須とする点まで動くと、どちらもそこで動けなくなり、結局はスリープします。

こちらは、その場合の実演です。 重力ゼロの状態で2つのブロックがロープに繋がっているとします。 大きなブロックは起きていて動いていますが、固定パーツは「スリープチェック中」です。 ロープがピンと張るくらいロープが遠くに移動するまでは、これは正しいです。

「スリープチェック中」のアセンブリは、オレンジで縁取りしてあります。

スリープチェック中のアセンブリがロープがピンと張っているかどうかを知る方法はなく、それに関しては近隣のものはスリープし続けるのに安全な速度でまだゆっくりと動いています。 動いているブロックはロープを引きますが、それ以上できないので止まり、それからスリープします。

もうすでに予測しているかもしれませんが、スリープしている近隣のものを起こすのに新しい近隣インパルス閾値が使えます。 このロープのケースでは、ロープがピンと張るとすぐ、合力でブロックが大規模な変更を経ることが分かっています。 「チェック中」のアセンブリを起こすイベントとして変更の実施を使います。 その例では、ロープがピンと張ったら、スリープしているパーツが起きます。 スリープしているパーツそのものに対して追加の計算を実行することなしにです。

これは、スリープ中のアセンブリを起こすことに使われるだけでなく、もっと長くスリープさせておくのにも使えます。 起きている周囲のアセンブリがかなり早く動いているというだけで、アセンブリを起こす必要があると推測する必要はもうないのです。

最終的に、小さな変更を数回加えただけでエンジンのスリープシステムを大幅に改善しました。 注意深く調整された臨界速度を使うよりも、運動の第一法則を直接、適用しています。 そこで、本当に休んでいるときに物体をスリープさせる力をかわりに見ていきます。 非常に多くの微妙なケースは、今ではエンジンによって正しく対処されていて、開発者が独立してそれらを見つけて修正する必要がなくなりました。 物理の問題に取り組むとき、物理の原理に立ち返ることで最終的に解決することが時々あります。


Robloxコーポレーションとこのブログは、いかなる企業もサービスも推奨も支持もしません。 また、このブログに含まれる情報の正確さや完全性について、いかなる保証または約束もしません。

このブログ記事は、元はRobloxテックブログ に掲載されたものです。