¿Cómo resolver el problema de verificación de terreno?

12

Noté un problema en la verificación de terreno del controlador en tercera persona de Unity.

La verificación en el suelo debe detectar si el jugador está parado o no en el suelo. Lo hace enviando un rayo debajo del jugador.

Sin embargo, si el jugador se para arriba y en el medio de dos cajas y hay un espacio entre estas cajas, entonces el rayo dispara hacia el hueco y el jugador piensa que no está en contacto con el suelo, que se ve así:

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

No puedo moverme. Puede ver claramente que el rayo está en la brecha y, por lo tanto, el árbol de mezcla en el aire del animador del jugador está activo.

¿Cuál es la mejor manera de resolver este problema?

Estaba pensando en disparar múltiples rayos, del mismo origen pero con diferentes ángulos. Y OnGroundsolo debería ser cierto si el X% de estos rayos tocan el "suelo". ¿O hay un mejor camino?

Negro
fuente

Respuestas:

18

Los rayos múltiples funcionan bien en la mayoría de los casos, como se describe en la otra respuesta.

También puede usar una verificación más amplia, como un fieldcast o boxcast. Estos usan el mismo concepto que un Raycast, pero con una primitiva geométrica que tiene algo de volumen, por lo que no puede deslizarse en grietas más estrechas de las que su personaje podría caer. También capta el caso que menciona Shadows In Rain, donde su personaje está parado en una tubería estrecha que podría perderse por un rayo a cada lado.

Un colisionador de gatillo que sobresale un poco debajo de la parte inferior del colisionador de tu personaje puede realizar una tarea similar. Al igual que la esfera de la caja, tiene cierto ancho para detectar el suelo a cada lado de un espacio. Aquí usaría OnTriggerEnter para detectar cuándo este sensor de tierra ha entrado en contacto con la tierra.

DMGregory
fuente
2
Excelente respuesta como siempre, pero ¿no es este método "más pesado" en rendimiento? Supongo que de esta manera Unity tiene que calcular las intersecciones con el molde de la esfera / caja y el suelo, así que ... ¿no son los rayos una forma más efectiva de hacer esto?
99
No estrictamente hablando. Una proyección de esfera es matemáticamente bastante similar a una emisión de rayos: podemos considerarla como un solo punto de viaje, pero con un desplazamiento de "grosor". En mi perfil, cuesta solo un 30-50% extra para verificar una esfera completa en lugar de un solo rayo en promedio. Lo que significa que disparar una esfera en lugar de dos rayos puede ser un ahorro neto en el rendimiento de hasta ~ 25%. Es poco probable que marque una gran diferencia en ambos sentidos para las comprobaciones breves que realiza solo unas pocas veces por fotograma, pero siempre puede validar esto perfilando un par de opciones.
DMGregory
La verificación de la esfera es definitivamente el camino a seguir con un colisionador de cápsulas en un avatar.
Stephan
¿Hay una función de depuración para esto? por ejemplo, como Debug.DrawLine? Es difícil de visualizar, no puedo escribir el guión.
Negro
1
@Black siempre podríamos escribir nuestra propia rutina de visualización usando Debug.DrawLine como bloque de construcción. :)
DMGregory
14

Sinceramente, creo que el enfoque de "rayos múltiples" es una buena idea. Sin embargo, no los dispararía en ángulo, sino que compensaría los rayos, algo como esto:

ingrese la descripción de la imagen aquí

El jugador es el stickman azul; Las flechas verdes representan los rayos adicionales, y los puntos naranjas (RaycastHits) son los puntos donde los dos rayos golpean las cajas.

Idealmente, los dos rayos verdes deben colocarse justo debajo de los pies del jugador, para obtener la mayor precisión para verificar si el jugador está conectado a tierra o no;)


fuente
77
No funcionará estando parado en bordes u objetos delgados (como tuberías). Es básicamente una versión de fuerza bruta del mismo enfoque defectuoso. Si va a usarlo de todos modos, asegúrese de que el peón se deslice desde los bordes deslizándolo hacia el origen del rayo perdido (para cada uno de ellos, y solo si hay al menos unos pocos).
Shadows In Rain
2
Necesitará al menos 3 con este enfoque para evitar que ambos rayos salten a la grieta si se enfrenta a la dirección "afortunada".
Stephan
3
En un juego de PS2 en el que trabajé, hice 25 lanzamientos de esferas hacia abajo cada cuadro (en un patrón de cuadrícula de 5x5 debajo del jugador), solo para determinar dónde estaba el suelo debajo del jugador. Quizás eso fue un poco absurdo, pero si pudiéramos permitirnos hacerlo en una PS2, puede permitirse el lujo de usar algunas pruebas de colisión adicionales en máquinas modernas. :)
Trevor Powell
@TrevorPowell sí, cuando dije "más pesado" en el rendimiento quise decir "" "" más pesado "" "" porque sabía que no iba a tener un gran impacto en el juego, pero aún quería saber cuál era el más eficiente camino a esto :)
2
(Honestamente, nunca he podido usar tantas pruebas de colisión desde entonces; ese motor de juego de PS2 tenía rayos / esferas vertiginosas, y desearía saber cómo logró eso). Pero tener muchísimas transmisiones de esfera fue genial; significaba que podía detectar acantilados y otras características del suelo, para ser un poco más inteligente sobre la altura a la que el jugador debería estar parado.
Trevor Powell
1

Creo que lo resolví cambiando Physics.Raycasta Physics.SphereCasten el guión ThirdPersonCharacter.cs. Pero aún necesita pruebas.

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

También tuve que comentar esta línea que estaba cambiando el m_GroundCheckDistancevalor, de lo contrario, hubo algunos deslizamientos extraños en algunos modelos:

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

Y cambié m_GroundCheckDistance = 0.1f;a 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;
        }
    }

Guión completo:

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;
            }
        }
    }
}
Negro
fuente
0

¿Por qué no usar la función OnCollisionStay de Unity ?

Pros:

  • No tienes que crear raycast.

  • Es más preciso que Raycast: Raycast es un método de disparar para verificar, si su disparo de Raycast no es suficiente cobertura, entonces genera un error, razón por la cual hizo esta pregunta. OnCollisionStayEl método literalmente verifica si algo se está tocando: encaja perfectamente con el propósito de verificar si el jugador está tocando el suelo (o cualquier cosa en la que el jugador pueda aterrizar).

Para ver el código y la demostración, consulte esta respuesta: http://answers.unity.com/answers/1547919/view.html

123iamking
fuente