Bagaimana cara mengatasi masalah ground check?

12

Saya melihat ada masalah dalam pemeriksaan darat pengontrol orang ketiga Unity.

Pemeriksaan darat harus mendeteksi apakah pemain berdiri di tanah atau tidak. Ia melakukannya dengan mengirimkan sinar di bawah pemain.

Namun, jika pemain berdiri di atas dan di tengah dua kotak dan ada ruang di antara kotak-kotak ini, maka sinar menembak ke celah dan pemain berpikir dia tidak bersentuhan dengan tanah, yang terlihat seperti ini:

masukkan deskripsi gambar di sini

masukkan deskripsi gambar di sini

Saya tidak bisa bergerak. Anda dapat dengan jelas melihat bahwa sinar ada di celah dan dengan demikian pohon animator pemain campuran udara aktif.

Apa cara terbaik untuk mengatasi masalah ini?

Saya berpikir untuk memotret banyak sinar, dari asal yang sama tetapi dengan sudut yang berbeda. Dan OnGroundseharusnya hanya benar, jika X% dari sinar ini mengenai "tanah". Atau ada cara yang lebih baik?

Hitam
sumber

Jawaban:

18

Sinar banyak berfungsi dengan baik dalam banyak kasus, seperti yang dijelaskan dalam jawaban lainnya.

Anda juga dapat menggunakan pemeriksaan yang lebih luas - seperti spherecast atau boxcast. Ini menggunakan konsep yang sama dengan raycast, tetapi dengan primitif geometris yang memiliki volume, sehingga tidak bisa masuk ke celah yang lebih sempit daripada karakter Anda bisa jatuh. Ini juga menangkap kasus Shadows In Rain menyebutkan, di mana karakter Anda berdiri di atas pipa sempit yang mungkin terlewatkan oleh raycast di setiap sisi itu.

Pemicu collider yang sedikit menonjol di bawah bagian bawah collider karakter Anda dapat menyelesaikan tugas yang serupa. Seperti bola kotak, ia memiliki beberapa lebar untuk mendeteksi tanah di kedua sisi celah. Di sini Anda akan menggunakan OnTriggerEnter untuk mendeteksi kapan sensor ground ini bersentuhan dengan ground.

DMGregory
sumber
2
Jawaban luar biasa seperti biasa, tetapi bukankah metode ini "lebih berat" pada kinerja? Saya mengira bahwa dengan cara ini Unity harus menghitung persimpangan dengan bola / kotak gips dan tanah, jadi .. bukankah raycast cara yang lebih berkinerja untuk melakukan ini?
9
Tidak sepenuhnya berbicara. Spherecast secara matematis sangat mirip dengan raycast - kita dapat menganggapnya hanya sebagai satu titik perjalanan, tetapi dengan offset "ketebalan". Dalam profiling saya harganya hanya sekitar 30-50% ekstra untuk memeriksa bola penuh daripada rata-rata satu sinar. Yang berarti bahwa menembakkan satu bola alih-alih dua sinar bisa menjadi penghematan bersih dalam kinerja hingga ~ 25%. Tidak mungkin membuat perbedaan besar dengan cara apa pun untuk pemeriksaan singkat yang Anda lakukan hanya beberapa kali dalam satu frame, tetapi Anda selalu dapat memvalidasi ini dengan membuat profil beberapa opsi.
DMGregory
Sphere check jelas merupakan cara untuk menggunakan collider kapsul pada avatar.
Stephan
Apakah ada fungsi debug untuk ini? misalnya suka Debug.DrawLine? Sulit untuk memvisualisasikan, saya tidak dapat menulis skrip.
Black
1
@Black kita selalu bisa menulis rutin visualisasi kita sendiri menggunakan Debug.DrawLine sebagai blok bangunan. :)
DMGregory
14

Sejujurnya saya berpikir bahwa pendekatan "banyak sinar" adalah ide yang cukup bagus. Saya tidak akan menembak mereka pada sudut, tapi saya akan mengimbangi sinar, seperti ini:

masukkan deskripsi gambar di sini

Pemain adalah stickman biru; Panah hijau mewakili sinar tambahan, dan titik oranye (RaycastHits) adalah titik di mana kedua sinar itu mengenai kotak.

Idealnya dua sinar hijau harus diposisikan tepat di bawah kaki pemain, untuk mendapatkan ketelitian paling untuk memeriksa apakah pemain di-ground atau tidak;)


sumber
7
Tidak akan berfungsi saat berdiri di tepi atau benda tipis (seperti pipa). Ini pada dasarnya versi brute-force dari pendekatan cacat yang sama. Jika Anda akan menggunakannya, pastikan pion tergelincir dari tepi dengan menggesernya ke arah asal sinar yang terlewat (untuk masing-masingnya, dan hanya jika setidaknya ada beberapa).
Shadows In Rain
2
Anda akan memerlukan setidaknya 3 dengan pendekatan ini untuk mencegah kedua sinar dari melompat ke celah jika menghadapi arah "beruntung".
Stephan
3
Dalam permainan PS2 yang saya kerjakan, saya melakukan 25 bola yang mengarah ke bawah setiap frame (dalam pola grid 5x5 di bawah pemain), hanya untuk menentukan di mana tanah berada di bawah pemain. Mungkin itu sedikit tidak masuk akal, tetapi jika kami mampu melakukannya pada PS2, Anda dapat menggunakan beberapa tes tabrakan tambahan pada mesin modern. :)
Trevor Powell
@TrevorPowell ya, ketika saya mengatakan "lebih berat" pada kinerja yang saya maksud "" "" lebih berat "" "" karena saya tahu itu tidak akan membuat dampak besar pada permainan, tetapi saya masih ingin tahu apa yang paling efisien cara untuk ini :)
2
(Dalam semua kejujuran, saya tidak pernah bisa menggunakan banyak tes tabrakan sejak itu; bahwa mesin game PS2 memiliki raycast / spherecast yang sangat cepat, dan saya berharap saya tahu bagaimana cara mengaturnya). Tetapi memiliki banyak spherecast sangat bagus; itu berarti saya bisa mendeteksi tebing dan fitur-fitur dasar lainnya, untuk menjadi sedikit lebih pintar tentang ketinggian berdiri pemain.
Trevor Powell
1

Saya pikir saya menyelesaikannya dengan mengubah Physics.Raycastke Physics.SphereCastdalam skrip ThirdPersonCharacter.cs. Tetapi masih perlu pengujian.

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
);

Saya juga harus mengomentari baris ini yang mengubah m_GroundCheckDistancenilai, jika tidak ada beberapa sliding aneh pada beberapa model:

    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;
    }

Dan saya berubah m_GroundCheckDistance = 0.1f;menjadi 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;
        }
    }

Seluruh Skrip:

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;
            }
        }
    }
}
Hitam
sumber
0

Mengapa tidak menggunakan fungsi OnCollisionStay Unity ?

Pro:

  • Anda tidak harus membuat raycast.

  • Ini lebih akurat daripada raycast: Raycast adalah metode shoot-to-check, jika pemotretan raycast Anda tidak cukup cakupan, maka itu mengarah ke bug yang menjadi alasan mengapa Anda mengajukan pertanyaan ini. OnCollisionStayMetode benar-benar memeriksa apakah ada sesuatu yang menyentuh - itu sangat cocok untuk tujuan memeriksa apakah pemain menyentuh tanah (atau apa pun yang pemain dapat mendarat).

Untuk kode dan demo, periksa jawaban ini: http://answers.unity.com/answers/1547919/view.html

Aku bisa tidur
sumber