グラウンドチェックの問題を解決するには?


12

Unityのサードパーソンコントローラーのグラウンドチェックに問題があることに気付きました。

グラウンドチェックは、プレーヤーが地面に立っているかどうかを検出する必要があります。これは、プレイヤーの下に光線を送信することにより行われます。

ただし、プレイヤーが2つのボックスの真上に立ち、これらのボックスの間にスペースがある場合、レイは隙間に飛び込み、プレイヤーは自分が地面に接触していないと考えます。これは次のようになります。

ここに画像の説明を入力してください

ここに画像の説明を入力してください

動けません。光線が隙間にあるため、プレイヤーアニメーターの空中ブレンドツリーがアクティブになっていることがはっきりとわかります。

この問題を解決する最良の方法は何ですか?

同じ原点から異なる角度で複数の光線を照射することを考えていました。そしてOnGround、これらの光線のX%が「地面」に当たった場合にのみ、真になるはずです。または、より良い方法はありますか?

回答:


18

他の回答で説明されているように、ほとんどの場合、複数の光線は問題なく機能します。

また、スフィアキャストやボックスキャストなど、より広範なチェックを使用することもできます。これらはレイキャストと同じ概念を使用しますが、ある程度のボリュームを持つジオメトリプリミティブを使用しているため、キャラクターが通り抜けるよりも狭い亀裂に滑り込むことはできません。また、Shadows In Rainが言及しているケースもキャッチします。ここでは、キャラクターが両側のレイキャストで見逃される可能性のある細いパイプの上に立っています。

キャラクターのコライダーの下部からほんの少し下に突き出ているトリガーコライダーでも、同様のタスクを実行できます。ボックスキャストの球体のように、ギャップの両側の地面を検出するための幅があります。ここでは、OnTriggerEnterを使用して、この地上センサーが地面に接触したことを検出します。


2
いつものように優れた答えですが、この方法はパフォーマンス上「重く」なりませんか?この方法では、Unityが球体/ボックスキャストと地面との交点を計算する必要があるので、レイキャストはこれを行うためのより高性能な方法ではないと思います。

9
厳密には言えません。スフィアキャストは数学的にレイキャストに非常に似ています-単一の移動ポイントと考えることができますが、「厚さ」オフセットがあります。私のプロファイリングでは、平均して単一の光線ではなく、完全な球体をチェックするのに約30〜50%しかかかりません。つまり、2つの光線ではなく1つの球体を発射すると、最大で最大25%のパフォーマンスの節約になります。フレームの数回しか実行していない短いチェックでは、どちらの方法でも大きな違いはありませんが、いくつかのオプションをプロファイリングすることでこれをいつでも検証できます。
DMGregory

球体チェックは、間違いなく、アバターにカプセルコライダーを使用する方法です。
ステファン

これにデバッグ機能はありますか?例えばDebug.DrawLine?視覚化するのは難しく、スクリプトを書くことができません。
ブラック

1
@Black Debug.DrawLineを構成要素として使用して、独自の視覚化ルーチンをいつでも作成できました。:)
DMGregory

14

正直に言って、「複数の光線」アプローチは非常に良い考えだと思います。私はそれらを角度で撮影しませんが、代わりに次のような光線をオフセットします:

ここに画像の説明を入力してください

プレイヤーは青い棒人間です。緑の矢印は追加の光線を表し、オレンジの点(RaycastHits)は2つの光線がボックスに当たる点です。

プレイヤーが接地しているかどうかを最も正確に確認するために、2つの緑の光線をプレイヤーの足の真下に配置することが理想的です;)


7
エッジや細いオブジェクト(パイプなど)の上に立っていると機能しません。それは基本的に同じ欠陥アプローチのブルートフォースバージョンです。とにかくそれを使用する場合は、ポーンを逃した光線の原点に向かってスライドさせてエッジから滑り込ませてください(それらのそれぞれに対して、少なくともそれらが少数の場合のみ)。
雨の影

2
「幸運な」方向を向いた場合に両方の光線が亀裂に飛び込むのを防ぐには、このアプローチで少なくとも3つ必要です。
ステファン

3
私が取り組んだPS2ゲームでは、プレーヤーの下の地面がどこにあるかを判断するために、各フレームに25回の球体キャストを行いました(プレーヤーの下に5x5のグリッドパターンで)。おそらくそれは少しばかげたことですが、PS2でそれを行う余裕があれば、最新のマシンでいくつかの追加の衝突テストを使用する余裕があります。:)
トレバーパウエル

@TrevorPowellええ、パフォーマンスで「より重い」と言ったとき、私は「 "" "より重い" ""」を意味しました。これへの道:)

2
(正直なところ、それ以来、これほど多くの衝突テストを使用することはできませんでした。そのPS2ゲームエンジンには、非常に高速なレイキャスト/スフィアキャストがありました。しかし、たくさんのスフィアキャストを持つことは素晴らしいことでした。つまり、崖やその他の地面の特徴を検出でき、プレイヤーがどの高さに立つべきかについて少し賢くなりました。
トレバーパウエル

1

スクリプトでに変更Physics.Raycastすることで解決したと思いPhysics.SphereCastますThirdPersonCharacter.cs。しかし、まだテストが必要です。

bool condition = Physics.SphereCast(
    m_Capsule.transform.position + m_Capsule.center + (Vector3.up * 0.1f),
    m_Capsule.height / 2,
    Vector3.down, 
    out hitInfo,
    m_GroundCheckDistance
);

また、m_GroundCheckDistance値を変更していたこの行をコメントアウトする必要がありました。そうしないと、一部のモデルで奇妙なスライドが発生しました。

    void HandleAirborneMovement()
    {
        // apply extra gravity from multiplier:
        Vector3 extraGravityForce = (Physics.gravity * m_GravityMultiplier) - Physics.gravity;
        m_Rigidbody.AddForce(extraGravityForce);

        //m_GroundCheckDistance = m_Rigidbody.velocity.y < 0 ? m_OrigGroundCheckDistance : 0.01f;
    }

そして、私はに変更m_GroundCheckDistance = 0.1f;しましたm_GroundCheckDistance = m_OrigGroundCheckDistance;

    void HandleGroundedMovement(bool crouch, bool jump)
    {
        // check whether conditions are right to allow a jump:
        if (jump && !crouch && m_Animator.GetCurrentAnimatorStateInfo(0).IsName("Grounded"))
        {
            // jump!
            m_Rigidbody.velocity = new Vector3(m_Rigidbody.velocity.x, m_JumpPower, m_Rigidbody.velocity.z);
            m_IsGrounded = false;
            m_Animator.applyRootMotion = false;
            m_GroundCheckDistance = m_OrigGroundCheckDistance;
        }
    }

スクリプト全体:

using UnityEngine;

namespace UnityStandardAssets.Characters.ThirdPerson
{
    [RequireComponent(typeof(Rigidbody))]
    [RequireComponent(typeof(CapsuleCollider))]
    [RequireComponent(typeof(Animator))]
    public class ThirdPersonCharacter : MonoBehaviour
    {
        [SerializeField] float m_MovingTurnSpeed = 360;
        [SerializeField] float m_StationaryTurnSpeed = 180;
        [SerializeField] float m_JumpPower = 12f;
        [Range(1f, 4f)][SerializeField] float m_GravityMultiplier = 2f;
        [SerializeField] float m_RunCycleLegOffset = 0.2f; //specific to the character in sample assets, will need to be modified to work with others
        [SerializeField] float m_MoveSpeedMultiplier = 1f;
        [SerializeField] float m_AnimSpeedMultiplier = 1f;
        [SerializeField] float m_GroundCheckDistance = 0.1f;

        Rigidbody m_Rigidbody;
        Animator m_Animator;
        bool m_IsGrounded;
        float m_OrigGroundCheckDistance;
        const float k_Half = 0.5f;
        float m_TurnAmount;
        float m_ForwardAmount;
        Vector3 m_GroundNormal;
        float m_CapsuleHeight;
        Vector3 m_CapsuleCenter;
        CapsuleCollider m_Capsule;
        bool m_Crouching;


        void Start()
        {
            m_Animator = GetComponent<Animator>();
            m_Rigidbody = GetComponent<Rigidbody>();
            m_Capsule = GetComponent<CapsuleCollider>();
            m_CapsuleHeight = m_Capsule.height;
            m_CapsuleCenter = m_Capsule.center;

            m_Rigidbody.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY | RigidbodyConstraints.FreezeRotationZ;
            m_OrigGroundCheckDistance = m_GroundCheckDistance;
        }

        public void Move(Vector3 move, bool crouch, bool jump)
        {

            // convert the world relative moveInput vector into a local-relative
            // turn amount and forward amount required to head in the desired
            // direction.
            if (move.magnitude > 1f) move.Normalize();

            move = transform.InverseTransformDirection(move);
            CheckGroundStatus();
            move = Vector3.ProjectOnPlane(move, m_GroundNormal);
            m_TurnAmount = Mathf.Atan2(move.x, move.z);
            m_ForwardAmount = move.z;

            ApplyExtraTurnRotation();

            // control and velocity handling is different when grounded and airborne:
            if (m_IsGrounded) {
                HandleGroundedMovement(crouch, jump);
            } else {
                HandleAirborneMovement();
            }

            ScaleCapsuleForCrouching(crouch);
            PreventStandingInLowHeadroom();

            // send input and other state parameters to the animator
            UpdateAnimator(move);


        }

        void ScaleCapsuleForCrouching(bool crouch)
        {
            if (m_IsGrounded && crouch)
            {
                if (m_Crouching) return;
                m_Capsule.height = m_Capsule.height / 2f;
                m_Capsule.center = m_Capsule.center / 2f;
                m_Crouching = true;
            }
            else
            {
                Ray crouchRay = new Ray(m_Rigidbody.position + Vector3.up * m_Capsule.radius * k_Half, Vector3.up);
                float crouchRayLength = m_CapsuleHeight - m_Capsule.radius * k_Half;
                if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, Physics.AllLayers, QueryTriggerInteraction.Ignore))
                {
                    m_Crouching = true;
                    return;
                }
                m_Capsule.height = m_CapsuleHeight;
                m_Capsule.center = m_CapsuleCenter;
                m_Crouching = false;
            }
        }

        void PreventStandingInLowHeadroom()
        {
            // prevent standing up in crouch-only zones
            if (!m_Crouching)
            {
                Ray crouchRay = new Ray(m_Rigidbody.position + Vector3.up * m_Capsule.radius * k_Half, Vector3.up);
                float crouchRayLength = m_CapsuleHeight - m_Capsule.radius * k_Half;
                if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, Physics.AllLayers, QueryTriggerInteraction.Ignore))
                {
                    m_Crouching = true;
                }
            }
        }

        void UpdateAnimator(Vector3 move)
        {
            // update the animator parameters
            m_Animator.SetFloat("Forward", m_ForwardAmount, 0.1f, Time.deltaTime);
            m_Animator.SetFloat("Turn", m_TurnAmount, 0.1f, Time.deltaTime);
            m_Animator.SetBool("Crouch", m_Crouching);
            m_Animator.SetBool("OnGround", m_IsGrounded);
            if (!m_IsGrounded) {
                m_Animator.SetFloat("Jump", m_Rigidbody.velocity.y);
            }

            // calculate which leg is behind, so as to leave that leg trailing in the jump animation
            // (This code is reliant on the specific run cycle offset in our animations,
            // and assumes one leg passes the other at the normalized clip times of 0.0 and 0.5)
            float runCycle =
                Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime + m_RunCycleLegOffset, 1);

            float jumpLeg = (runCycle < k_Half ? 1 : -1) * m_ForwardAmount;
            if (m_IsGrounded) {
                m_Animator.SetFloat("JumpLeg", jumpLeg);
            }

            // the anim speed multiplier allows the overall speed of walking/running to be tweaked in the inspector,
            // which affects the movement speed because of the root motion.
            if (m_IsGrounded && move.magnitude > 0) {
                m_Animator.speed = m_AnimSpeedMultiplier;
            } else {
                // don't use that while airborne
                m_Animator.speed = 1;
            }
        }

        void HandleAirborneMovement()
        {
            // apply extra gravity from multiplier:
            Vector3 extraGravityForce = (Physics.gravity * m_GravityMultiplier) - Physics.gravity;
            m_Rigidbody.AddForce(extraGravityForce);

            //m_GroundCheckDistance = m_Rigidbody.velocity.y < 0 ? m_OrigGroundCheckDistance : 0.01f;
        }

        void HandleGroundedMovement(bool crouch, bool jump)
        {
            // check whether conditions are right to allow a jump:
            if (jump && !crouch && m_Animator.GetCurrentAnimatorStateInfo(0).IsName("Grounded"))
            {
                // jump!
                m_Rigidbody.velocity = new Vector3(m_Rigidbody.velocity.x, m_JumpPower, m_Rigidbody.velocity.z);
                m_IsGrounded = false;
                m_Animator.applyRootMotion = false;
                //m_GroundCheckDistance = 0.1f;
            }
        }

        void ApplyExtraTurnRotation()
        {
            // help the character turn faster (this is in addition to root rotation in the animation)
            float turnSpeed = Mathf.Lerp(m_StationaryTurnSpeed, m_MovingTurnSpeed, m_ForwardAmount);
            transform.Rotate(0, m_TurnAmount * turnSpeed * Time.deltaTime, 0);
        }

        public void OnAnimatorMove()
        {
            // we implement this function to override the default root motion.
            // this allows us to modify the positional speed before it's applied.
            if (m_IsGrounded && Time.deltaTime > 0)
            {
                Vector3 v = (m_Animator.deltaPosition * m_MoveSpeedMultiplier) / Time.deltaTime;

                // we preserve the existing y part of the current velocity.
                v.y = m_Rigidbody.velocity.y;
                m_Rigidbody.velocity = v;
            }
        }

        void CheckGroundStatus()
        {
            RaycastHit hitInfo;

#if UNITY_EDITOR
            // helper to visualise the ground check ray in the scene view

            Debug.DrawLine(
                m_Capsule.transform.position + m_Capsule.center + (Vector3.up * 0.1f),
                m_Capsule.transform.position + (Vector3.down * m_GroundCheckDistance), 
                Color.red
            );

#endif
            // 0.1f is a small offset to start the ray from inside the character
            // it is also good to note that the transform position in the sample assets is at the base of the character
            bool condition = Physics.SphereCast(
                m_Capsule.transform.position + m_Capsule.center + (Vector3.up * 0.1f),
                m_Capsule.height / 2,
                Vector3.down, 
                out hitInfo,
                m_GroundCheckDistance
            );

            if (condition) {
                m_IsGrounded = true;
                m_GroundNormal = hitInfo.normal;
                m_Animator.applyRootMotion = true;

            } else {
                m_IsGrounded = false;
                m_GroundNormal = Vector3.up;
                m_Animator.applyRootMotion = false;
            }
        }
    }
}

0

Unityを使用しない理由 OnCollisionStay機能をですか?

長所:

  • レイキャストを作成する必要はありません。

  • それはレイキャストよりも正確です。レイキャストは、チェックする方法であり、レイキャストの撮影が十分なカバレッジでない場合、この質問をした理由であるバグにつながります。OnCollisionStayメソッドは文字通り何かが触れているかどうかをチェックします-プレイヤーが地面(またはプレイヤーが着地できるもの)に触れているかどうかをチェックする目的に完全に適合します。

コードとデモについては、この回答を確認してくださいhttp : //answers.unity.com/answers/1547919/view.html

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.