0
\$\begingroup\$

I'm using the ThirdPersonController prefab on Unity and trying to make my character footsteps be heard. To add sound to the character's steps I'm using the animation event method, where I assign a function to a keyframed event. The code for step sound works well. However, every time I move my character the steps are heard twice, even when I stop walking I can heard a second step. I've noticed that the animations have "Loop Time" on them. When I unchecked this option, I can hear the steps correctly but the character won't walk and instead the animation would freeze. I can only walk if I jump and move forward.

The ThirdPersonController code:

    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;

    namespace UnityStandardAssets.Characters.ThirdPerson
    {
    [RequireComponent(typeof(Rigidbody))]
    [RequireComponent(typeof(CapsuleCollider))]
    [RequireComponent(typeof(Animator))]
    public class ThirdPersonCharacter : MonoBehaviour
    {
        private int count;
        public Text countText;

        [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;
        public bool isStop = false;
        public GameObject destroyedVersion;




        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;
            count = 0;
            SetCountText();

        }


        public void Move(Vector3 move, bool crouch, bool jump)
        {
            if (isStop) {

                m_Rigidbody.velocity = Vector3.zero;
                m_ForwardAmount = 0f;
                m_TurnAmount = 0f;
                UpdateAnimator(move);

                return;

            }

            // 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, ~0, 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, ~0, 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(transform.position + (Vector3.up * 0.1f), transform.position + (Vector3.up * 0.1f) + (Vector3.down * m_GroundCheckDistance));
#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
            if (Physics.Raycast(transform.position + (Vector3.up * 0.1f), Vector3.down, out hitInfo, m_GroundCheckDistance))
            {
                m_GroundNormal = hitInfo.normal;
                m_IsGrounded = true;
                m_Animator.applyRootMotion = true;
            }
            else
            {
                m_IsGrounded = false;
                m_GroundNormal = Vector3.up;
                m_Animator.applyRootMotion = false;
            }
        }
        void OnTriggerEnter(Collider other)
        {
            if (other.gameObject.CompareTag("Puntos"))
            {
                other.gameObject.SetActive(false);
                count = count + 1;
                SetCountText();
                AudioSource source = GetComponent<AudioSource>();
                source.Play();
            }


        }
        void SetCountText() {
            countText.text = "Count: " + count.ToString();
        }

Steps code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerSound : MonoBehaviour {

    public AudioClip pisadita;
    public AudioSource audioS;


    /*// Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }*/

    void Pisar()
    {
        //audioS.Play();

        audioS.PlayOneShot(pisadita);
    }
}

How could I fix either the ThirdPersonController code to have animations without the need of "Time Loop" or having my animation events not being played twice?

Thanks!

\$\endgroup\$
1
\$\begingroup\$

When you have something like an animation event such as a sound and it returns twice each time it should happens once, it means that there's is potentially something wrong with one of the following things:

• 2 Animations with an Event calling the same method happens at the same time. This is a typical thing when you got a run and a walk animation blended (with a Blend Tree) in the animator layout. When you have a blend tree with 2 or more animations, if any of those animation have even just a tiny bit of weight, their events will play.

Considering that your use float(s) to manage a Blend Tree, the chance that 1 of 2 blended animation in a Blend Tree nod reach a perfect 0 is close to impossible. You might have noticed sometimes 0 (zeroes) becomes something like 0.0234e0392 or the likes of that? That's called Float Precision where the 2 last digits of a float can be wrong by a few units each frame it's used. In terms of Unity Animator's Blend Tree, the same Float Precision issue happens and you might have 1.0000001 instead of 1 in speed which means the animation is a blend of 0.00000009% Running and 99.9999999% walking or vise-versa. Even at 0.0000009% of Running, the even in the Running will still be called. This means that the Event calls (which calls for the step sound) will play in the walking and running animation that are blended together.

How can you fix this? These are 2 possible ways:

  1. Don't use Animation Event with Blended animations to produce the sounds themselves. What I usually do is using a kind of Boolean double-checking pattern. I use a script that has something like 2 Boolean called CanStepL and CanStepR turned False by default. In the animation, I set an Event Call that turn that CanStedR to true at precise times when the right foot is lifted from the ground and CanStepL when it's the left foot that is lifted. All in the meanwhile, the script take notice of the distance between each foot and the ground as well as the type of ground while I'm at it so that I can change the sound if the ground is a liquid or metallic or someone else. (Why wasting such a convenient drawcall that can return what's below each foot?) When the CanStepR or CanStepL are turned True (by the Event Call) and the distance between the targeted foot (be it right or left based on which Boolean is true or false), I turn the Boolean false and play the sound so that it doesn't play twice. The main advantage of this method comes from the fact that you can exploit the raycast for more than just the sounds. As I previously stated, with this you can change the sound based on what's below the player's foot. (You do need to script some kind of Ground types system that allows the script to check the ground's script to get the infos. It's not a magic thing that is instantly done.)

  2. Add some kind of conditions to the sound script command (Pisar). In the ThirdPersonController script, you got multiple data that are related to how the player move. For example, the Crouch Boolean, if true, means your character is crouched so, normally, the animation where the character is crouched should be considered as the "source" for playing the step sounds. Make it so that ThirdPersonController can communicate with the PlayerSound script and also change its internal value. In other words, add some kind of Int (Integer) or Boolean in the PlayerSound script which can be modified in some way by the ThirdPersonController to "set" the state of the player (if he's walking, running. Is he crouched? Etc.) From this, in the Pisar function that is called to play the step sound, add some conditions for the sound to be played. This way, if you got the run animation AND the walk animation both to send the EventCall "Pisar", if the script "knows" that you're either running or walking, it will only play the sound once and will ignore 1 of the 2 or even 3 (if you consider crouching) EventCalls. This method has the advantage of being easy and simple to implement as well as faster on the CPU. It doesn't give any shortcut to get anything more so it's also limited in possibilities unless you add more layers of stuff in it.

So, the general rule when using Animation EventCalls is to always consider that EventCalls in Blended animations will get called all the time even if the animation is not visually playing. Never relies on EventCalls that can be called by more than 1 animation. This is also true with animation transition times. The white (or orange) line with an arrow in the animator represent the transitions. If you select one that is white, you'll notice the "Has Exit Time" checker and a "Setting" dropdown. In that dropdown you can see the "Transition Duration (s)" and "Transition Offset". Even the Transition duration is the amount of frame/time in the animation where the current and next animation will be fused and mixed. During that transition time, both animation's EventCalls will be called. So, if you jump which cut the walking or running animation and the jump's animation transition overlapse the part of the walk/run animation that has an EventCalls for the step, you'll hear the step while the character is jumping too.

This is why it's really important to always work in controls and understanding what you're working with. In terms of game development, it's always better to have too much controls instead of not enough. Lacking in controls is what bring glitches and bugs. When animations and blended animation are involved, always keep things in a way that makes the animations only "work with" the other system and not make it take full controls.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.