Как решить проблему наземной проверки?


12

Я заметил проблему в наземной проверке контроллера Unity от третьего лица.

Наземная проверка должна определить, стоит ли игрок на земле. Это делается путем отправки луча под игроком.

Однако, если игрок стоит сверху и в середине двух ящиков, и между этими ящиками есть промежуток, то луч попадает в промежуток, и игрок думает, что он не касается земли, что выглядит следующим образом:

введите описание изображения здесь

введите описание изображения здесь

Я не могу двигаться. Вы можете ясно видеть, что луч находится в промежутке, и, таким образом, дерево смешивания игрока аниматора игрока активно.

Как лучше всего решить эту проблему?

Я думал о съемке нескольких лучей одного происхождения, но с разных точек зрения. И OnGroundдолжно быть правдой только в том случае, если Х% этих лучей попадет на «землю». Или есть лучший способ?

Ответы:


18

Несколько лучей в большинстве случаев работают просто отлично, как описано в другом ответе.

Вы также можете использовать более широкую проверку - например, сферу или boxcast. Они используют ту же концепцию, что и лучевая трансляция, но с геометрическим примитивом, имеющим некоторый объем, поэтому он не может проскользнуть в более узкие трещины, чем мог бы провалиться ваш персонаж. В нем также упоминается случай, когда Shadows In Rain упоминает о том, что ваш персонаж стоит на узкой трубе, которая может быть пропущена при помощи радиопередачи с каждой стороны.

Триггерный коллайдер, чуть выступающий чуть ниже дна коллайдера вашего персонажа, может выполнить аналогичную задачу. Как и сфера литой коробки, она имеет некоторую ширину, чтобы обнаружить землю по обе стороны от зазора. Здесь вы используете OnTriggerEnter, чтобы определить, когда этот датчик заземления соприкоснулся с землей.


2
Отличный ответ, как всегда, но разве этот метод не "тяжелее" по производительности? Я полагаю, что таким образом Unity должна рассчитывать пересечения со сферой / рамкой и землей, так что ... не являются ли радиопередачи более эффективным способом сделать это?

9
Не строго говоря. Сферическая передача математически очень похожа на лучевую трансляцию - мы можем думать о ней как о единственной точке перемещения, но со смещением «толщины». В моем профилировании стоит только около 30-50%, чтобы проверить полную сферу вместо одного луча в среднем. Это означает, что выстрел одной сферы вместо двух лучей может дать чистую экономию до ~ 25%. Маловероятно, что в любом случае это будет иметь большое значение для коротких проверок, которые вы выполняете всего несколько раз за кадр, но вы всегда можете проверить это, профилировав несколько вариантов.
DMGregory

Сферическая проверка - определенно способ использовать капсульный коллайдер на аватаре.
Стефан

Есть ли для этого функция отладки? например как Debug.DrawLine? Это трудно визуализировать, я не могу написать сценарий.
черный

1
@ Чёрный, мы всегда могли написать нашу собственную процедуру визуализации, используя Debug.DrawLine в качестве строительного блока. :)
DMGregory

14

Честно говоря, я считаю, что подход с несколькими лучами - неплохая идея. Я бы не стал снимать их под углом, вместо этого я бы сместил лучи, что-то вроде этого:

введите описание изображения здесь

Игрок - синий человечек; Зеленые стрелки представляют дополнительные лучи, а оранжевые точки (RaycastHits) - это точки, где два луча попадают в боксы.

В идеале два зеленых луча должны быть расположены прямо под ногами игрока, чтобы максимально точно проверить, заземлен ли игрок или нет;)


7
Не будет работать, стоя на краях или тонких предметах (например, трубы). Это в основном брутфорс версия того же ошибочного подхода. Если вы собираетесь использовать его в любом случае, убедитесь, что пешка соскальзывает с краев, сдвинув его к началу пропущенного луча (для каждого из них, и только если их хотя бы немного).
Shadows In Rain

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

Почему бы не использовать функцию OnCollisionStay Unity ?

Плюсы:

  • Вам не нужно создавать raycast.

  • Это точнее, чем raycast: Raycast - это метод «стрельбы для проверки», если ваша съемка с использованием радиопередачи недостаточно освещена, это приводит к ошибке, из-за которой вы задали этот вопрос. OnCollisionStayМетод буквально проверяет, соприкасается ли что-то - он идеально подходит для проверки того, касается ли игрок земли (или чего-либо, на что игрок может приземлиться).

Для кода и демонстрации, проверьте этот ответ: http://answers.unity.com/answers/1547919/view.html

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.