Wie löst man das Bodenkontrollproblem?

12

Ich habe ein Problem bei der Bodenkontrolle des Unity Third-Person-Controllers festgestellt.

Die Bodenkontrolle sollte feststellen, ob der Spieler auf dem Boden steht oder nicht. Dazu wird unter dem Player ein Strahl ausgesendet.

Wenn der Spieler jedoch oben und in der Mitte von zwei Kisten steht und sich zwischen diesen Kisten ein Leerzeichen befindet, schießt der Strahl in die Lücke und der Spieler denkt, dass er keinen Bodenkontakt hat, was so aussieht:

Bildbeschreibung hier eingeben

Bildbeschreibung hier eingeben

Ich kann mich nicht bewegen. Sie können deutlich sehen, dass sich der Strahl in der Lücke befindet und somit der vom Player-Animator übertragene Mischbaum aktiv ist.

Was ist der beste Weg, um dieses Problem zu lösen?

Ich dachte daran, mehrere Strahlen von demselben Ursprung, aber mit unterschiedlichen Winkeln zu schießen. Und OnGroundsollte nur wahr sein, wenn X% dieser Strahlen auf den "Boden" treffen. Oder gibt es einen besseren Weg?

Schwarz
quelle

Antworten:

18

Mehrere Strahlen funktionieren in den meisten Fällen einwandfrei, wie in der anderen Antwort beschrieben.

Sie können auch einen breiteren Haken verwenden, z. B. einen Spherecast oder einen Boxcast. Diese verwenden dasselbe Konzept wie ein Raycast, aber mit einem geometrischen Grundelement, das etwas Volumen hat, sodass es nicht in engere Risse rutschen kann, als Ihr Charakter durchfallen könnte. Es fängt auch den Fall ein, in dem Shadows In Rain erwähnt, dass Ihr Charakter auf einem schmalen Rohr steht, das von einem Raycast auf jeder Seite verfehlt werden könnte.

Ein Trigger-Collider, der nur ein kleines Stück unter dem unteren Rand des Colliders Ihres Charakters hervorsteht, kann eine ähnliche Aufgabe erfüllen. Wie die Kugel aus Kastenguss hat sie eine gewisse Breite, um den Boden auf beiden Seiten einer Lücke zu erfassen. Hier würden Sie OnTriggerEnter verwenden, um zu erkennen, wenn dieser Bodensensor mit dem Boden in Kontakt gekommen ist.

DMGregory
quelle
2
Ausgezeichnete Antwort wie immer, aber ist diese Methode nicht "leistungsstärker"? Ich nehme an, dass Unity auf diese Weise Schnittpunkte mit der gegossenen Kugel / der gegossenen Kiste und dem Boden berechnen muss. Sind Raycasts dazu nicht performanter?
9
Eigentlich nicht. Ein Spherecast ist mathematisch einem Raycast ziemlich ähnlich - wir können es uns als einen einzelnen Bewegungspunkt vorstellen, aber mit einem "Dicken" -Versatz. In meinem Profiling kostet es durchschnittlich nur etwa 30-50% mehr, eine volle Kugel anstelle eines einzelnen Strahls zu überprüfen. Das bedeutet, dass das Abfeuern einer Kugel anstelle von zwei Strahlen zu einer Nettoeinsparung von bis zu 25% bei der Leistung führen kann. Es ist unwahrscheinlich, dass Sie bei kurzen Überprüfungen, die Sie nur ein paar Mal pro Frame durchführen, einen großen Unterschied machen. Sie können dies jedoch jederzeit überprüfen, indem Sie ein Profil mit mehreren Optionen erstellen.
DMGregory
Sphere Check ist definitiv der richtige Weg, um einen Avatar mit einem Kapselcollider zu versehen.
Stephan
Gibt es dafür eine Debug-Funktion? zB wie Debug.DrawLine? Es ist schwer zu visualisieren, ich kann das Skript nicht schreiben.
Schwarz
1
@Black wir könnten jederzeit unsere eigene Visualisierungsroutine mit Debug.DrawLine als Baustein schreiben . :)
DMGregory
14

Ich denke ehrlich, dass der Ansatz "mehrere Strahlen" eine gute Idee ist. Ich würde sie aber nicht im Winkel schießen, sondern die Strahlen irgendwie versetzen, so etwas wie:

Bildbeschreibung hier eingeben

Der Spieler ist der blaue Strichmännchen; Die grünen Pfeile stellen die zusätzlichen Strahlen dar, und die orangefarbenen Punkte (RaycastHits) sind die Punkte, an denen die beiden Strahlen auf die Kästchen treffen.

Im Idealfall sollten die beiden grünen Strahlen direkt unter den Füßen des Players positioniert werden, um möglichst genau zu prüfen, ob der Player geerdet ist oder nicht.


quelle
7
Funktioniert nicht, wenn Sie auf Kanten oder dünnen Gegenständen (wie Rohren) stehen. Es ist im Grunde eine Brute-Force-Version des gleichen fehlerhaften Ansatzes. Wenn Sie es trotzdem verwenden möchten, vergewissern Sie sich, dass der Bauer von den Kanten abrutscht, indem Sie ihn in Richtung des Ursprungs des fehlenden Strahls schieben (für jeden von ihnen und nur, wenn mindestens wenige vorhanden sind).
Schatten im Regen
2
Bei diesem Ansatz sind mindestens 3 erforderlich, um zu verhindern, dass beide Strahlen in den Riss springen, wenn sie in die "glückliche" Richtung weisen.
Stephan
3
In einem PS2-Spiel, an dem ich gearbeitet habe, habe ich in jedem Frame 25 Kugelwürfe nach unten ausgeführt (in einem 5x5-Raster unter dem Player), um festzustellen, wo sich der Boden unter dem Player befindet. Vielleicht war das ein bisschen absurd, aber wenn wir es uns leisten könnten, es auf einer PS2 zu machen, könnten Sie es sich leisten, ein paar zusätzliche Kollisionstests auf modernen Maschinen durchzuführen. :)
Trevor Powell
@TrevorPowell Ja, als ich "schwerer" bei der Leistung sagte, meinte ich "" "schwerer" ", weil ich wusste, dass es keinen großen Einfluss auf das Spiel haben würde, aber ich wollte immer noch wissen, was am effizientesten ist Weg dorthin :)
2
(Ehrlich gesagt konnte ich seitdem noch nie so viele Kollisionstests durchführen. Diese PS2-Game-Engine hatte verrückte schnelle Raycasts / Spherecasts, und ich wünschte, ich wüsste, wie sie das geschafft hat.) Aber viele, viele Spherecasts zu haben, war großartig. es bedeutete, dass ich Klippen und andere Bodenmerkmale erkennen konnte, um ein bisschen schlauer zu sein, auf welcher Höhe der Spieler stehen sollte.
Trevor Powell
1

Ich glaube , ich löste es durch eine Änderung Physics.Raycastan Physics.SphereCastim Skript ThirdPersonCharacter.cs. Aber es muss noch getestet werden.

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

Ich musste auch diese Zeile auskommentieren, die den m_GroundCheckDistanceWert änderte , ansonsten gab es bei einigen Modellen ein komisches Gleiten:

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

Und ich wechselte m_GroundCheckDistance = 0.1f;zu 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;
        }
    }

Gesamtes Skript:

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;
            }
        }
    }
}
Schwarz
quelle
0

Warum nicht Unity's benutzen? OnCollisionStay- Funktion von ?

Vorteile:

  • Sie müssen keinen Raycast erstellen.

  • Es ist genauer als Raycast: Raycast ist eine Methode zum Überprüfen von Aufnahmen. Wenn Ihre Raycast-Aufnahmen nicht ausreichend erfasst sind, führt dies zu Fehlern, weshalb Sie diese Frage gestellt haben. OnCollisionStayDie Methode prüft buchstäblich, ob sich etwas berührt - sie eignet sich perfekt, um zu überprüfen, ob der Spieler den Boden berührt (oder auf etwas, auf dem der Spieler landen kann).

Code und Demo finden Sie in der folgenden Antwort: http://answers.unity.com/answers/1547919/view.html

123iamking
quelle