Làm thế nào để giải quyết vấn đề kiểm tra mặt đất?


12

Tôi nhận thấy một vấn đề trong kiểm tra mặt đất của bộ điều khiển người thứ ba của Unity.

Việc kiểm tra mặt đất sẽ phát hiện xem người chơi có đứng trên mặt đất hay không. Nó làm như vậy bằng cách gửi ra một tia bên dưới máy nghe nhạc.

Tuy nhiên, nếu người chơi đứng trên và giữa hai hộp và có một khoảng trống giữa các hộp này, thì tia sẽ bắn vào khoảng trống và người chơi nghĩ rằng anh ta không tiếp xúc với mặt đất, trông như thế này:

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Tôi không thể di chuyển. Bạn có thể thấy rõ rằng tia nằm trong khoảng trống và do đó cây pha trộn không khí hoạt hình của người chơi đang hoạt động.

Cách tốt nhất để giải quyết vấn đề này là gì?

Tôi đã nghĩ đến việc chụp nhiều tia, từ cùng một nguồn gốc nhưng với các góc khác nhau. Và OnGroundchỉ nên đúng, nếu X% các tia này chạm "mặt đất". đây có phải là cách tốt hơn không?

Câu trả lời:


18

Nhiều tia hoạt động tốt trong hầu hết các trường hợp, như được mô tả trong câu trả lời khác.

Bạn cũng có thể sử dụng một kiểm tra rộng hơn - như spherecast hoặc boxcast. Chúng sử dụng khái niệm tương tự như một raycast, nhưng với nguyên thủy hình học có một số âm lượng, vì vậy nó không thể rơi vào các vết nứt hẹp hơn nhân vật của bạn có thể rơi vào. Nó cũng bắt được trường hợp Shadows In Rain đề cập đến, trong đó nhân vật của bạn đang đứng trên một đường ống hẹp có thể bị bỏ lỡ bởi một tia phát sóng ở mỗi bên của nó.

Một máy va chạm kích hoạt nhô ra chỉ một chút nhỏ bên dưới đáy máy va chạm của nhân vật của bạn có thể hoàn thành một nhiệm vụ tương tự. Giống như hình cầu của hộp đúc, nó có một số chiều rộng để phát hiện mặt đất ở hai bên của một khoảng trống. Tại đây, bạn sẽ sử dụng OnTrigger Entry để phát hiện khi cảm biến mặt đất này tiếp xúc với mặt đất.


2
Câu trả lời tuyệt vời như mọi khi, nhưng không phải phương pháp này "nặng hơn" về hiệu suất? Tôi cho rằng theo cách này, Unity phải tính toán các giao điểm với khối cầu / hộp và mặt đất, vì vậy .. không phải là phát sóng một cách hiệu quả hơn để làm điều này sao?

9
Không nói đúng. Một spherecast về mặt toán học khá giống với một raycast - chúng ta có thể nghĩ nó chỉ là một điểm di chuyển duy nhất, nhưng với phần bù "độ dày". Trong hồ sơ của tôi, chi phí chỉ khoảng 30-50% để kiểm tra một quả cầu đầy đủ thay vì trung bình một tia. Điều đó có nghĩa là bắn một quả cầu thay vì hai tia có thể là một khoản tiết kiệm ròng trong hiệu suất lên tới ~ 25%. Dù sao thì cũng không thể tạo ra sự khác biệt lớn đối với các kiểm tra ngắn mà bạn chỉ thực hiện một vài lần trong một khung, nhưng bạn luôn có thể xác thực điều này bằng cách lược tả một vài tùy chọn.
DMGregory

Kiểm tra hình cầu chắc chắn là cách để đi với một máy va chạm viên nang trên hình đại diện.
Stephan

Có một chức năng gỡ lỗi cho điều này? vd như thế Debug.DrawLinenào? Thật khó để hình dung, tôi không thể viết kịch bản.
Đen

1
@Black chúng tôi luôn có thể viết thói quen trực quan hóa của riêng mình bằng cách sử dụng Debug.DrawLine làm một khối xây dựng. :)
DMGregory

14

Tôi thành thật nghĩ rằng phương pháp "nhiều tia" là một ý tưởng hay. Mặc dù vậy, tôi sẽ không bắn chúng ở góc, thay vào đó tôi sẽ bù lại các tia, đại loại như thế này:

nhập mô tả hình ảnh ở đây

Người chơi là cây gậy xanh; Mũi tên màu xanh lá cây biểu thị các tia bổ sung và các điểm màu cam (RaycastHits) là các điểm mà hai tia chạm vào các hộp.

Lý tưởng nhất là hai tia màu xanh lá cây phải được đặt ngay dưới chân người chơi, để có được độ chính xác nhất để kiểm tra xem người chơi có nối đất hay không;)


7
Không hoạt động trong khi đứng trên các cạnh hoặc vật mỏng (như ống). Về cơ bản, đây là phiên bản vũ phu của cách tiếp cận thiếu sót. Nếu bạn định sử dụng nó bằng mọi cách, hãy đảm bảo trượt cầm đồ từ các cạnh bằng cách trượt nó về phía gốc của tia bị mất (đối với mỗi tia và chỉ khi có ít nhất một vài trong số chúng).
Bóng tối trong mưa

2
Bạn sẽ cần ít nhất 3 với phương pháp này để ngăn cả hai tia lọt vào vết nứt nếu đối mặt với hướng "may mắn".
Stephan

3
Trong một trò chơi PS2 tôi đã làm việc, tôi đã thực hiện 25 quả cầu rơi xuống mỗi khung hình (theo mô hình lưới 5x5 bên dưới trình phát), chỉ để xác định vị trí của mặt đất bên dưới trình phát. Có lẽ đó là một chút vô lý, nhưng nếu chúng tôi có thể đủ khả năng để làm điều đó trên một chiếc máy PS3, bạn có thể đủ khả năng sử dụng một vài thử nghiệm va chạm bổ sung trên các máy hiện đại. :)
Trevor Powell

@TrevorPowell yeah, khi tôi nói "nặng hơn" về hiệu suất, tôi có nghĩa là "" "" nặng hơn "" "vì tôi biết rằng nó sẽ không ảnh hưởng lớn đến trò chơi, nhưng tôi vẫn muốn biết điều gì là hiệu quả nhất cách này :)

2
(Thành thật mà nói, tôi chưa bao giờ có thể sử dụng nhiều thử nghiệm va chạm kể từ đó; công cụ trò chơi PS2 đó có các luồng / phát sóng cực nhanh, và tôi ước tôi biết cách nó quản lý nó). Nhưng có rất nhiều và rất nhiều dự báo là tuyệt vời; điều đó có nghĩa là tôi có thể phát hiện các vách đá và các đặc điểm mặt đất khác, để thông minh hơn một chút về độ cao mà người chơi nên đứng ở độ cao nào.
Trevor Powell

1

Tôi nghĩ rằng tôi đã giải quyết nó bằng cách thay đổi Physics.Raycastthành Physics.SphereCasttrong kịch bản ThirdPersonCharacter.cs. Nhưng nó vẫn cần thử nghiệm.

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

Tôi cũng đã phải bình luận về dòng này đang thay đổi m_GroundCheckDistancegiá trị, nếu không thì có một số trượt kỳ lạ ở một số mô hình:

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

Và tôi đổi m_GroundCheckDistance = 0.1f;thành 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;
        }
    }

Toàn bộ kịch bản:

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

Tại sao không sử dụng chức năng OnCollisionStay của Unity ?

Ưu điểm:

  • Bạn không phải tạo raycast.

  • Nó chính xác hơn raycast: Raycast là một phương pháp kiểm tra bắn, nếu việc chụp raycast của bạn không đủ vùng phủ sóng, thì nó sẽ dẫn đến lỗi đó là lý do tại sao bạn hỏi câu hỏi này. OnCollisionStayPhương pháp kiểm tra theo nghĩa đen nếu có thứ gì đó chạm vào - nó hoàn toàn phù hợp với mục đích kiểm tra xem người chơi có chạm đất không (hoặc bất cứ thứ gì mà người chơi có thể đáp xuống).

Để biết mã và bản demo, hãy kiểm tra câu trả lời này: http://answers.unity.com/answers/1547919/view.html

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.