Como resolver o problema de verificação do solo?

12

Percebi um problema na verificação de solo do controlador de terceira pessoa do Unity.

A checagem no solo deve detectar se o jogador está ou não no chão. Para isso, envia um raio sob o player.

No entanto, se o jogador estiver em cima e no meio de duas caixas e houver um espaço entre essas caixas, o raio disparará para o espaço e o jogador pensará que não está em contato com o chão, que se parece com o seguinte:

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Eu sou incapaz de me mover. Você pode ver claramente que o raio está no espaço e, assim, a árvore de mistura aerotransportadora do animador do jogador está ativa.

Qual é a melhor maneira de resolver esse problema?

Eu estava pensando em disparar vários raios, da mesma origem, mas com ângulos diferentes. E OnGroundsó deve ser verdade se X% desses raios atingirem o "solo". Ou há um jeito melhor?

Preto
fonte

Respostas:

18

Vários raios funcionam bem na maioria dos casos, como descrito na outra resposta.

Você também pode usar uma verificação mais ampla - como um spherecast ou boxcast. Eles usam o mesmo conceito de um raycast, mas com um primitivo geométrico que tem algum volume, para que não caia em fendas mais estreitas do que o seu personagem poderia cair. Ele também pega o caso que Shadows In Rain menciona, onde seu personagem está parado em um cano estreito que pode passar despercebido por um raio de raio em cada lado dele.

Um colisor de gatilhos que se projeta um pouquinho abaixo do colisor do seu personagem pode realizar uma tarefa semelhante. Como a esfera da caixa moldada, ela possui alguma largura para detectar o solo em ambos os lados de uma lacuna. Aqui você usaria o OnTriggerEnter para detectar quando esse sensor de aterramento entrou em contato com o solo.

DMGregory
fonte
2
Excelente resposta como sempre, mas esse método não é "mais pesado" no desempenho? Suponho que desta maneira o Unity tenha que calcular interseções com a esfera / caixa lançada e o solo, então ... os raycasts não são uma maneira mais eficiente de fazer isso?
9
Não estritamente falando. Um spherecast é matematicamente bastante semelhante a um raycast - podemos pensar nele apenas como um único ponto de viagem, mas com um deslocamento de "espessura". No meu perfil, custa apenas cerca de 30 a 50% extras para verificar uma esfera completa em vez de um único raio, em média. O que significa que disparar uma esfera em vez de dois raios pode ser uma economia líquida de desempenho de até ~ 25%. É improvável que faça uma grande diferença de qualquer maneira para verificações curtas que você está realizando apenas algumas vezes por quadro, mas você sempre pode validar isso criando um perfil de algumas opções.
DMGregory
A verificação de esfera é definitivamente o caminho a seguir com um colisor de cápsulas em um avatar.
Stephan
Existe uma função de depuração para isso? por exemplo, como Debug.DrawLine? É difícil visualizar, não consigo escrever o script.
Preto
1
@Black, sempre poderíamos escrever nossa própria rotina de visualização usando Debug.DrawLine como um bloco de construção. :)
DMGregory
14

Sinceramente, acho que a abordagem dos "raios múltiplos" é uma boa idéia. Eu não as atiraria em ângulo, mas sim compensaria os raios, algo como isto:

insira a descrição da imagem aqui

O jogador é o stickman azul; As setas verdes representam os raios adicionais e os pontos laranja (RaycastHits) são os pontos em que os dois raios atingem as caixas.

Idealmente, os dois raios verdes devem ser posicionados logo abaixo dos pés do jogador, a fim de obter a maior precisão possível para verificar se o jogador está aterrado ou não;)


fonte
7
Não funciona em pé em bordas ou objetos finos (como canos). É basicamente a versão de força bruta da mesma abordagem defeituosa. Se você for usá-lo de qualquer maneira, certifique-se de que o peão escorregue das bordas, deslizando-o em direção à origem do raio perdido (para cada um deles, e somente se houver pelo menos alguns deles).
Shadows In Rain
2
Você precisará de pelo menos três com essa abordagem para evitar que os dois raios entrem na fenda se estiverem na direção da "sorte".
Stephan
3
Em um jogo de PS2 em que trabalhei, fiz 25 lançamentos de esferas para baixo em cada quadro (em um padrão de grade de 5x5 sob o jogador), apenas para determinar onde o terreno estava embaixo do jogador. Talvez isso tenha sido um pouco absurdo, mas se pudermos fazê-lo em um PS2, você poderá usar alguns testes de colisão extras em máquinas modernas. :)
Trevor Powell
@TrevorPowell Sim, quando eu disse "mais pesado" no desempenho, quis dizer "" "" mais pesado "" "" porque sabia que isso não causaria grande impacto no jogo, mas eu ainda queria saber qual era o mais eficaz caminho para isso :)
2
(Com toda a honestidade, nunca pude usar tantos testes de colisão desde então; esse mecanismo de jogo do PS2 tinha raycasts / previsões de velocidade loucos, e eu gostaria de saber como ele conseguiu isso). Mas ter muitas e muitas previsões de desempenho foi ótimo; isso significava que eu podia detectar falésias e outras características do solo, para ser um pouco mais inteligente sobre a altura em que o jogador deveria estar.
Trevor Powell
1

Acho que resolvi alterando Physics.Raycastpara Physics.SphereCastno script ThirdPersonCharacter.cs. Mas ainda precisa de testes.

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

Eu também tive que comentar essa linha que estava alterando o m_GroundCheckDistancevalor, caso contrário, houve alguns deslizamentos estranhos em alguns 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;
    }

E eu mudei m_GroundCheckDistance = 0.1f;para 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;
        }
    }

Script inteiro:

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;
            }
        }
    }
}
Preto
fonte
0

Por que não usar a função OnCollisionStay do Unity ?

Prós:

  • Você não precisa criar raycast.

  • É mais preciso que o raycast: o Raycast é um método de disparar para verificar, se o seu disparo com raycast não é cobertura suficiente, isso leva a um bug, razão pela qual você fez esta pergunta. OnCollisionStayO método literalmente verifica se algo está tocando - ele se encaixa perfeitamente para o objetivo de verificar se o jogador está tocando o chão (ou qualquer coisa em que o jogador possa pousar).

Para código e demonstração, verifique esta resposta: http://answers.unity.com/answers/1547919/view.html

123iamking
fonte