Comment résoudre le problème de vérification au sol?

12

J'ai remarqué un problème dans la vérification au sol du contrôleur à la troisième personne d'Unity.

La vérification au sol doit détecter si le joueur se tient ou non au sol. Pour ce faire, il envoie un rayon sous le lecteur.

Cependant, si le joueur se tient au-dessus et au milieu de deux cases et qu'il y a un espace entre ces cases, alors le rayon tire dans l'espace et le joueur pense qu'il n'est pas en contact avec le sol, ce qui ressemble à ceci:

entrez la description de l'image ici

entrez la description de l'image ici

Je suis incapable de bouger. Vous pouvez clairement voir que le rayon est dans l'espace et donc l'arbre de mélange aéroporté du joueur animateur est actif.

Quelle est la meilleure façon de résoudre ce problème?

Je pensais à filmer plusieurs rayons, de la même origine mais avec des angles différents. Et cela OnGroundne devrait être vrai que si X% de ces rayons touchent le «sol». Ou existe-t-il une meilleure façon?

Noir
la source

Réponses:

18

Plusieurs rayons fonctionnent très bien dans la plupart des cas, comme décrit dans l'autre réponse.

Vous pouvez également utiliser un chèque plus large, comme un spherecast ou un boxcast. Ceux-ci utilisent le même concept qu'un raycast, mais avec une primitive géométrique qui a un certain volume, de sorte qu'il ne peut pas glisser dans des fissures plus étroites que votre personnage pourrait traverser. Il attrape également le cas mentionné par Shadows In Rain, où votre personnage se tient sur un tuyau étroit qui pourrait être raté par un lancer de rayons de chaque côté.

Un collisionneur déclencheur qui dépasse juste un tout petit peu en dessous du bas du collisionneur de votre personnage peut accomplir une tâche similaire. Comme la sphère de boîte moulée, elle a une certaine largeur pour détecter le sol de chaque côté d'un espace. Ici, vous utiliseriez OnTriggerEnter pour détecter quand ce capteur de sol est entré en contact avec le sol.

DMGregory
la source
2
Excellente réponse comme toujours, mais cette méthode n'est-elle pas "plus lourde" sur les performances? Je suppose que de cette façon, Unity doit calculer les intersections avec la sphère / boîte moulée et le sol, alors .. les raycasts ne sont-ils pas un moyen plus performant de le faire?
9
Pas à proprement parler. Un spherecast est mathématiquement assez similaire à un raycast - nous pouvons le considérer comme un seul point de déplacement, mais avec un décalage "d'épaisseur". Dans mon profilage, il ne coûte qu'environ 30 à 50% supplémentaires pour vérifier une sphère complète au lieu d'un seul rayon en moyenne. Ce qui signifie que tirer une sphère au lieu de deux rayons peut être une économie nette de performances allant jusqu'à ~ 25%. Il est peu probable que cela fasse une grande différence dans les deux cas pour les vérifications courtes que vous effectuez seulement quelques fois par image, mais vous pouvez toujours valider cela en profilant quelques options.
DMGregory
La vérification de la sphère est certainement la voie à suivre avec un collisionneur de capsules sur un avatar.
Stephan
Existe-t-il une fonction de débogage pour cela? par exemple comme Debug.DrawLine? C'est difficile à visualiser, je n'arrive pas à écrire le script.
Black
1
@Black nous pourrions toujours écrire notre propre routine de visualisation en utilisant Debug.DrawLine comme bloc de construction. :)
DMGregory
14

Je pense honnêtement que l'approche des "rayons multiples" est une assez bonne idée. Je ne les tirerais pas en angle cependant, je compenserais plutôt les rayons, quelque chose comme ceci:

entrez la description de l'image ici

Le joueur est le stickman bleu; Les flèches vertes représentent les rayons supplémentaires et les points oranges (RaycastHits) sont les points où les deux rayons frappent les cases.

Idéalement, les deux rayons verts devraient être placés juste sous les pieds du joueur, afin d'obtenir le plus de précision possible pour vérifier si le joueur est mis à la terre ou non;)


la source
7
Ne fonctionnera pas lorsque vous vous tenez sur des bords ou des objets fins (comme des tuyaux). C'est essentiellement une version brutale de la même approche imparfaite. Si vous allez l'utiliser de toute façon, assurez-vous que le pion glisse des bords en le faisant glisser vers l'origine du rayon manquant (pour chacun d'eux, et seulement s'il y en a au moins peu).
Shadows In Rain
2
Vous aurez besoin d'au moins 3 avec cette approche pour empêcher les deux rayons de sauter dans la fissure s'ils font face à la direction «chanceuse».
Stephan
3
Dans un jeu PS2 sur lequel j'ai travaillé, j'ai fait 25 lancers de sphères vers le bas chaque image (dans un motif de grille 5x5 sous le joueur), juste pour déterminer où le sol était sous le joueur. C'était peut-être un peu absurde, mais si nous pouvions nous permettre de le faire sur une PS2, vous pouvez vous permettre d'utiliser quelques tests de collision supplémentaires sur des machines modernes. :)
Trevor Powell
@TrevorPowell ouais, quand j'ai dit "plus lourd" sur les performances, je voulais dire "" "" plus lourd "" "" parce que je savais que ça n'allait pas avoir un grand impact sur le jeu, mais je voulais quand même savoir ce qui était le plus efficace moyen d'y parvenir :)
2
(En toute honnêteté, je n'ai jamais pu utiliser autant de tests de collision depuis; ce moteur de jeu PS2 avait des raycasts / spherecasts incroyablement rapides, et j'aimerais savoir comment il a géré cela). Mais avoir beaucoup, beaucoup de spherecasts était super; cela signifiait que je pouvais détecter des falaises et d'autres caractéristiques du sol, pour être un peu plus intelligent sur la hauteur à laquelle le joueur devrait se tenir.
Trevor Powell
1

Je pense que je l'ai résolu en passant Physics.Raycastà Physics.SphereCastdans le script ThirdPersonCharacter.cs. Mais il doit encore être testé.

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

J'ai également dû commenter cette ligne qui modifiait la m_GroundCheckDistancevaleur, sinon il y avait un glissement bizarre sur certains modèles:

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

Et j'ai changé m_GroundCheckDistance = 0.1f;pour 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 entier:

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;
            }
        }
    }
}
Noir
la source
0

Pourquoi ne pas utiliser la fonction OnCollisionStay d'Unity ?

Avantages:

  • Vous n'avez pas à créer de raycast.

  • C'est plus précis que raycast: Raycast est une méthode de prise de vue à vérifier, si votre prise de vue raycast n'est pas suffisamment couverte, cela conduit à un bug qui est la raison pour laquelle vous avez posé cette question. OnCollisionStayLa méthode vérifie littéralement si quelque chose est en contact - elle convient parfaitement au but, vérifiant si le joueur touche le sol (ou tout ce sur quoi le joueur peut atterrir).

Pour le code et la démo, vérifiez cette réponse: http://answers.unity.com/answers/1547919/view.html

123iamking
la source