🏓

【Unity】どれだけ速く振ってもボールをすり抜けないVRラケットの実装

2023/11/09に公開
5

はじめに

こんにちは、まつさこ です。

この記事では、UnityVRアプリにおいてプレイヤーが手に持ったオブジェクトを他の物体に当てるときに、どれだけ速く手を動かしてもコライダーがすり抜けないようにする工夫を紹介します。

卓球のVRゲームを例に、実装を紹介します。

開発環境

筆者の開発環境は以下です。

  • Unity 2021.3.5f1
  • Rider 2023.2.3

VRにおけるすり抜け問題

そもそも本記事で解決したい課題について、簡単に解説します。

VRコンテンツにおいて、プレイヤーは手に持ったラケットを自分の好きなスピード、角度、タイミングで振ることが出来ます。直感に即してボールを打ち返したい場合、ラケットの面がボールの位置と一致した瞬間に衝突判定を実行し、ボールが跳ね返って飛んでいく必要があります。

しかし、ゲームにおいては「フレーム」という概念が存在し、フレームごとの処理の連続によってゲーム体験全体が成立しています。いわゆる「すり抜け」というのは、ラケットがボールに当たる瞬間がこのフレームとフレームの間に発生することで、衝突判定処理が実施されない ことによって生じます。

ラケットがボールに当たった前後のフレームでは、ラケットとボールは接触しておらず、フレーム処理が走るタイミング(つまりコンピュータが知りえる状態認識)にはラケットとボールが接触した事実が存在しないことになります。

解決策と実装

前述の問題は、「ラケットを振る速度が速いほど、ラケットとボールのコライダー接触の時間が短い」ことに起因します。このことから、「ラケットを振る速度に応じてコライダーの形を変えて接触時間を長くする」 ことで解決できると考えられます。

ラケットを振る速度に応じて下の画像のコライダー(緑の枠)を ラケットの面方向(青の矢印方向)に厚くする ことで、ボールとの接触時間を長くします。また、コライダーの厚みだけでなく、コライダーの位置を後ろ方向に少しずらすことで、ボールを直感的な時刻で打ち返したと思えるように補正しています。

次のスクリプトをラケットオブジェクトにアタッチします。

using UnityEngine;

public class ContinuousCollider : MonoBehaviour
{
    [SerializeField] Transform _colliderObject; //ラケットのコライダーをアタッチする
    Vector3 _prePosition;
    float _minZ = 0;

    void Start()
    {
        //初期値を保存
        _minZ = _colliderObject.localScale.z/2;
        _prePosition = transform.position;
    }

    //物理演算に関係するのでFixedUpdateで
    void FixedUpdate()
    {
        //前フレームからの変位を計算
        Vector3 def = transform.position - _prePosition;

        //前フレームからの変位をラケットの正面方向(transform.forward)に射影することで、ラケットがどれだけ前後に動いたか(defZ)を計算
        float defZ = Vector3.Dot(def, transform.forward);

        //ラケットが前に閾値(_minZ)以上動いた場合、あるいは後ろに閾値(_minZ)以上動いた場合、コライダーの位置とスケールをそれぞれ調整
        if (defZ > _minZ)
        {
            _colliderObject.position = transform.position - (defZ-_minZ) / 2 * transform.forward;
            _colliderObject.localScale = new Vector3(_colliderObject.localScale.x, _colliderObject.localScale.y, defZ+_minZ);
        }
        else if (defZ < -_minZ)
        {
        
            _colliderObject.position = transform.position + (-defZ-_minZ) / 2 * transform.forward;
            _colliderObject.localScale = new Vector3(_colliderObject.localScale.x, _colliderObject.localScale.y, -defZ+_minZ);
        }
        else
        {
            _colliderObject.position = transform.position;
            _colliderObject.localScale = new Vector3(_colliderObject.localScale.x, _colliderObject.localScale.y, _minZ*2);
        }
    
        //現在のラケットの位置を保持しておき次フレームでの比較に使用
        _prePosition = transform.position;
    }

}

FixedUpdate() で物理演算フレームにて毎フレーム呼び出しています。現フレームと前フレーム間でラケットがどれだけ移動したかを計算し、その移動量(defZ)がある閾値(_minZ)を超えた場合にコライダーの位置とスケールを動的に調整しています。

ラケットが早く振られるとコライダーが大きくなり、ゆっくり振られるとコライダーが小さくなります。

▼少しわかりづらいかもしれませんが、ラケットを速く振るとコライダーのボックスが大きくなっています。

まとめ

この記事では、UnityVRアプリにおいてプレイヤーが手に持ったオブジェクトを他の物体に当てるときに、どれだけ速く手を動かしてもコライダーがすり抜けないようにする工夫を紹介しました。
テニスや野球など、様々なVRコンテンツ開発に応用できるかと思います。

読んでくださりありがとうございました🤗

追記(2023年11月10日)

Rigidbody コンポーネントの Collision Detection についていくつか質問をいただきましたので、追記します。

今回の実装では、ラケットにつけた RigidbodyCollision Detection 設定は Discrete にしています。

MetaQuest2などのAndroidデバイスで動作させる前提で作成したため、できるだけ処理を軽くしたいという意図から、 Discrete 設定にしています。

詳しくは公式ドキュメント等を参照ください。
Rigidbody.collisionDetectionMode - Scripting Reference
Unityの当たり判定がすり抜ける時の対処方法!

また、ボール側の ColliderIs Trigger にチェックを入れ、OnTriggerEnter で衝突判定を検知しています。

Discussion

炉田謙 KenRoda炉田謙 KenRoda

参考になる記事をありがとうございます。
質問があります。記事中ではCollision DetectionはDiscreteであることが前提のようですが、Collision DetectionをDiscreteとは別の値にするのと比較して記事の手法はどういった違いがあるのでしょうか?よろしくお願いいたします。

まつさこまつさこ

コメントありがとうございます。
はじめは CollisionDetectionModContinuous にして試したのですが、VR環境だとなぜかすり抜けることがあり、またパフォーマンス面でも懸念がありました。
今回「ラケットの面をボールに当てる」という要件を満たす、低コストで一番計算量の少ない手法を考案しました。

以下はChatGPTによる Continous モードの説明です。

Continous モードでは移動前後の補完としてレイをキャストする一方、本記事の方法ではコライダーの厚みを変更するという点で違いそうです。

テニス侍テニス侍

非常に参考になる記事ありがとうございます.
質問ですが,
FixedTimestep,MaximumAllowedTimestepはどのくらいの値に設定されていますでしょうか.