Search Unity

Fixed Heartbeat Delay Sound

Discussion in 'Scripting' started by cadw96, Apr 13, 2014.

  1. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    Hi,

    I'm working on a horror game and I have a problem with the heartbeat sound. I want it to play whenever the distance between the enemy and the player is smaller than 20 and I want to have the delay of the two heartbeatsounds get smaller when you come closer to the enemy. The audio plays perfect, but the delay does not update, so the delay next to the enemy is the same as far away from the enemy. This is my script:

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class Noises : MonoBehaviour {
    5.  
    6.     public Transform enemy;
    7.  
    8.     public Transform player;
    9.  
    10.     public float waitTime;
    11.  
    12.     public float distance;
    13.  
    14.     public AudioClip Heart1;
    15.  
    16.     public AudioClip Heart2;
    17.  
    18.     public bool hasplayed = false;
    19.  
    20.     bool waitActive;
    21.  
    22.     // Use this for initialization
    23.     void Start () {
    24.  
    25.     }
    26.    
    27.     // Update is called once per frame
    28.     void Update () {
    29.         float distance = Vector3.Distance (enemy.position, player.position);
    30.         waitTime = 0.010f * distance;
    31.         Debug.Log (waitTime);
    32.         heartbeat ();
    33.         }
    34.    
    35.     IEnumerator Wait(){
    36.         waitActive = true;
    37.         yield return new WaitForSeconds (40 / distance);
    38.         waitActive = false;
    39.     }
    40.  
    41.     void heartbeat ()
    42.     {
    43.         if (!hasplayed  distance <= 20) {
    44.                         StartCoroutine (Wait ());
    45.                         AudioSource.PlayClipAtPoint (Heart1, transform.position);
    46.                         StartCoroutine (Wait ());
    47.                         AudioSource.PlayClipAtPoint (Heart2, transform.position);
    48.                         hasplayed = true;
    49.         }
    50.     }
    51.  
    52. }
    53.  
    54.  
    55.  
    56.  
     
    Last edited: Apr 14, 2014
  2. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    I would do this with a stock heartbeat that plays the whole game. During the game as your threat level increases, so does the heartbeat, and your heartbeat volume.

    So say your threat is between 0 and 10. At zero, your heart beats normally, and you can't hear it. At 10 your heart beats rapidly, and is very loud.

    to do this, you simply keep track of the threat level.

    Code (csharp):
    1.  
    2. float threat = 0;
    3.  
    4. // as your threat increases
    5. audio.clip.speed = 1 + threat * 0.01f; // increase the speed as the threat level goes up.
    6. audio.volume = Mathf.Lerp(0,1, threat * 0.1f); // increase the volume based on the threat.
    7.  
    You then just play with the levels until they sound right. So if you wanted the volume to only go up when you reach level 3 then the volume would look like this:

    Code (csharp):
    1.  
    2. audio.volume = Mathf.Lerp(0,1, (threat - 3) * 0.14f); // increase the volume based on the threat.
    3.  
    4. // 0.14f  == 1 / 7;
    5.  
     
  3. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    Thanks for your reply, but audio.clip.speed does not work (It gives and error) and if it would work, it would change the audio pitch. I would like to make the pauses just shorter when come closer.
     
  4. Hikiko66

    Hikiko66

    Joined:
    May 5, 2013
    Posts:
    1,304
    Use playscheduled, and pass it a delay based on your threat level
     
  5. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    What do you mean exactly? I have read the documentation manual for Audio.PlayScheduled, but I don't really know how I should use it. :/
     
  6. jonSG

    jonSG

    Joined:
    Mar 26, 2014
    Posts:
    18
    How about something like this (just typed off the top of my head so sorry if it does not work):

    Code (csharp):
    1.  
    2. private bool heartBeatPlaying = false;
    3. private float heartBeatRange = 20f;
    4. private float heartBeatDelayPhases = 0.25f;
    5. private float heartBeatDelayBeats = 0.5f;
    6.  
    7. void Update () {
    8.     float distance = Vector3.Distance (enemy.position, player.position);
    9.     playHeartBeat (distance);
    10. }
    11.  
    12. private IEnumerator playHeartBeat( float distance ){
    13.     if (heartBeatPlaying || distance > heartBeatRange) { return; }
    14.  
    15.     heartBeatPlaying = true;
    16.  
    17.     float dangerDelayFactor = distance / heartBeatRange;
    18.  
    19.     AudioSource.PlayClipAtPoint (Heart1, transform.position);
    20.     yield return new WaitForSeconds ( Heart1.length + heartBeatDelayPhases * dangerDelayFactor );
    21.  
    22.     AudioSource.PlayClipAtPoint (Heart2, transform.position);
    23.     yield return new WaitForSeconds ( Heart2.length + heartBeatDelayBeats * dangerDelayFactor );
    24.  
    25.     heartBeatPlaying = false;
    26. }
    27.  
     
    Last edited: Apr 14, 2014
  7. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    I changed the return statement into {return false;}, otherwise it wouldn't run, but I don't get any sound.
     
    Last edited: Apr 14, 2014
  8. Quetzhal

    Quetzhal

    Joined:
    Aug 19, 2013
    Posts:
    1
    If you want it to play faster as it gets closer, I think it should actually be

    Code (csharp):
    1. yield return new (distance/40);
    instead. I'm not sure what the waitActive/waitTime in your code is for - you don't seem to use it in the script?

    That said, I tried your code to see if I could identify the problem and the audio doesn't actually play properly for me (it plays once, then hasplayed is set to true and it can't play again). Adding hasplayed = false; to the end of your IEnumerator code seems to fix it. I ended up with this:

    Code (csharp):
    1.     IEnumerator Wait(){
    2.         waitActive = true;
    3.         yield return new WaitForSeconds (distance/40);
    4.         waitActive = false;
    5.         hasplayed = false;
    6.     }
    7.    
    8.     void heartbeat ()
    9.     {
    10.         if (!hasplayed  distance <= 20) {
    11.             StartCoroutine (Wait ());
    12.             AudioSource.PlayClipAtPoint (Heart1, transform.position);
    13.             StartCoroutine (Wait ());
    14.             AudioSource.PlayClipAtPoint (Heart2, transform.position);
    15.             hasplayed = true;
    16.         }
    17.     }
    Which seems to work fine; the audio plays faster as the player moves towards the target. Heart1 and Heart2 seem to play at the same time with minimal delay, though. It's kind of weird. Like only the first StartCoroutine(Wait()); is working.

    Anyway, I got it to work by stuffing everything into the IEnumerator. It's probably not the most elegant solution, but it does work. Sorry, I'm still new to Unity. xD

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class Noises : MonoBehaviour {
    5.    
    6.     public Transform enemy;
    7.    
    8.     public Transform player;
    9.    
    10.     public float waitTime;
    11.    
    12.     public float distance;
    13.    
    14.     public AudioClip Heart1;
    15.    
    16.     public AudioClip Heart2;
    17.    
    18.     public bool hasplayed = false;
    19.    
    20.     bool waitActive;
    21.    
    22.     void Update ()
    23.     {
    24.         distance = Vector3.Distance (enemy.position, player.position);
    25.         waitTime = 0.010f * distance;
    26.         Debug.Log (waitTime);
    27.         StartCoroutine(heartbeat ());
    28.     }
    29.    
    30.     IEnumerator heartbeat()
    31.     {
    32.         if(!hasplayed  distance <=20)
    33.         {
    34.             hasplayed = true;
    35.             waitActive = true;
    36.             yield return new WaitForSeconds (distance/40);
    37.             AudioSource.PlayClipAtPoint (Heart1, transform.position);
    38.             yield return new WaitForSeconds (distance/40);
    39.             AudioSource.PlayClipAtPoint (Heart2, transform.position);
    40.             hasplayed = false;
    41.         }
    42.     }
    43. }
    Edit: Forgot to add that I removed the 'float' from

    Code (csharp):
    1.  float distance = Vector3.Distance (enemy.position, player.position);
    since distance was already defined as a variable. Setting it again for some reason was automatically setting 'distance' to 0, which made the sound play continuously.
     
    Last edited: Apr 15, 2014
  9. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    Mmmmm, if you make distance larger after coming closer, it doesn't play slower.
     
  10. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    Quetzhal:

    In your Update of the Noises you start a coroutine. This means you start this coroutine every frame. not good.

    cadw96:

    After looking at what you are requesting, I believe the answer is a bit simpler, and corresponds to what I was suggesting before. Though not in the same manor.

    Assuming that you have a separate up and down heartbeat, you will need a timer that will play them in rhythm. So in working our that rhythm, I started tapping my fingers on the desk, bump, bump... bump bump.... And I there is actually a rhythm there: bump bump off bump bump off. So timing can then be figured out mathematically that there are 3 phases to each beat. up down off, up down off.

    we can use a generic timer to handle this. BMP, or Beats Per Minute. This says that the up beat should always happen every bmp / 60 seconds. Now, all we then have to do is multiply that out to divide it by 3. So, bmp / 60 * 0.333 is the time between the up beat and down beat, and bmp / 60 * 0.667 is the time between the down beat and the next up beat, so total it is bmp / 60 beats.

    Rather than focusing on enemy vs player, I went the route of just a heart beat controller. Every bmp, it beats up, down, off.

    Now, your script simply controls the controller with simple variables: BMP, Volume and Pitch. (yes, I left pitch in there because you may need it.)

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. [RequireComponent (typeof (AudioSource))]
    5. public class HeartBeat : MonoBehaviour {
    6.     public float bpm = 60;
    7.  
    8.     public float volume = 1;
    9.     private float pitch = 1;
    10.  
    11.     public AudioClip upBeat;
    12.     public AudioClip downBeat;
    13.  
    14.     private AudioSource upBeatSource;
    15.     private AudioSource downBeatSource;
    16.  
    17.     private float nextBeat = 0;
    18.     private bool beatSwap = false;
    19.  
    20.     void Start(){
    21.         GameObject go = new GameObject ();
    22.         go.transform.position = transform.position;
    23.         go.transform.parent = transform;
    24.         upBeatSource = go.AddComponent<AudioSource> ();
    25.         upBeatSource.clip = upBeat;
    26.         upBeatSource.loop = false;
    27.  
    28.         go = new GameObject ();
    29.         go.transform.position = transform.position;
    30.         go.transform.parent = transform;
    31.         downBeatSource = go.AddComponent<AudioSource> ();
    32.         downBeatSource.clip = downBeat;
    33.         downBeatSource.loop = false;
    34.     }
    35.  
    36.     void Update(){
    37.         volume = Mathf.Clamp01 ((bpm - 80) / 100) * 6;
    38.         //pitch = Mathf.Clamp((bpm - 60) / 160, 0.9f, 1.1f);
    39.         if (Time.time > nextBeat) {
    40.             if(beatSwap){
    41.                 upBeatSource.volume = volume;
    42.                 upBeatSource.pitch = pitch;
    43.                 upBeatSource.Stop();
    44.                 upBeatSource.Play();
    45.                 nextBeat+= 60 / bpm * 0.333f;
    46.             }else{
    47.                 downBeatSource.volume = volume;
    48.                 downBeatSource.pitch = pitch;
    49.                 downBeatSource.Stop();
    50.                 downBeatSource.Play();
    51.                 nextBeat+= 60 / bpm * 0.667f;
    52.             }
    53.             beatSwap = !beatSwap;
    54.         }
    55.     }
    56. }
    57.  
    Now all you have to do is change the variables in the controller to get whatever you need.

    So now, all you have to do is concentrate on how you want that danger level thing to work. At rest, a human's heartbeat is between 60 and 100 bpm. When we run we get up to about 150 or so normally, when we run fast, we get up to about 192. So that is a good range to start with.


    Now, lets add this to an existing charcter controller. (I made this for another thread.) all I should have to do is add a line of code.
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. [RequireComponent (typeof (CharacterController))]
    6. [RequireComponent (typeof (HeartBeat))]
    7. public class PlayerCharacterHeartBeat : MonoBehaviour {
    8.     // HeartBeat controller
    9.     private float defaultBMP = 60;
    10.     private float defaultChangeRate = 0.5f;
    11.  
    12.     private float bpm = 60;
    13.     private float changeRate = 0.5f;
    14.  
    15.     public Transform enemy;
    16.     private HeartBeat heartbeat;
    17.    
    18.     // mouselook player character.
    19.     private float speed = 6;
    20.     private float lookSpeed = 2f;
    21.     public bool invertY = false;
    22.    
    23.     private Vector3 movement;
    24.     private CharacterController controller;
    25.    
    26.     // Use this for initialization
    27.     void Start () {
    28.         heartbeat = gameObject.GetComponent<HeartBeat> ();
    29.         Screen.lockCursor = true;
    30.         Screen.showCursor = false;
    31.         controller = gameObject.GetComponent<CharacterController> ();
    32.     }
    33.    
    34.     // Update is called once per frame
    35.     void Update () {
    36.         movement = controller.velocity;
    37.         UpdateSmoothLook ();
    38.         UpdateSmoothControls ();
    39.         UpdateGravity ();
    40.         UpdateHeartBeat ();
    41.  
    42.        
    43.         controller.Move (movement * Time.deltaTime);
    44.        
    45.     }
    46.  
    47.     void UpdateHeartBeat(){
    48.         float targetBPM = defaultBMP;
    49.         float targetChangeRate = defaultChangeRate;
    50.  
    51.         if (enemy) {
    52.             float threat = 1 - Mathf.Clamp01 ((enemy.position - transform.position).magnitude * 0.01f);
    53.            
    54.             // add up to 75 bmp depending on how close enemy is to us.
    55.             targetChangeRate += threat;
    56.             targetBPM += threat * 75;
    57.         }
    58.  
    59.         // run here is the Left Shift key.
    60.         // add 75 bmp for running.
    61.         if (Input.GetKey (KeyCode.LeftShift)){
    62.             targetBPM += 75;
    63.         }
    64.  
    65.         // lerp these two values so we don't have instant changes.
    66.         changeRate = Mathf.Lerp (changeRate, targetChangeRate, 4 * Time.deltaTime);
    67.         bpm = Mathf.Lerp (bpm, targetBPM, changeRate * Time.deltaTime);
    68.  
    69.         // let the controller do the work.
    70.         heartbeat.bpm = bpm;
    71.     }
    72.    
    73.     void UpdateSmoothLook(){
    74.         Transform cam = Camera.main.transform;
    75.         Vector3 look = Vector3.zero;
    76.         look.x = -Input.GetAxis ("Mouse Y") * lookSpeed;
    77.         if (invertY) look.x = -look.x;
    78.         cam.Rotate (look) ;
    79.     }
    80.    
    81.     void UpdateSmoothControls(){
    82.         Vector3 look = Vector3.zero;
    83.         look.y = Input.GetAxis ("Mouse X") * lookSpeed;
    84.         transform.Rotate (look);
    85.        
    86.         Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
    87.         input = transform.TransformDirection(input);
    88.        
    89.         float spd = speed;
    90.         if (Input.GetKey (KeyCode.LeftShift)) spd *= 2;
    91.         input *= spd;
    92.        
    93.         if (!controller.isGrounded) {
    94.             input *= Time.deltaTime * 2;
    95.             movement += input;
    96.         } else {
    97.             input.y = movement.y;
    98.             movement = input;
    99.         }
    100.        
    101.         float y = movement.y;
    102.         movement.y = 0;
    103.         if (movement.magnitude > spd) movement = movement.normalized * spd;
    104.         movement.y = y;
    105.        
    106.     }
    107.    
    108.     void UpdateGravity(){
    109.         movement += Physics.gravity * 2 * Time.deltaTime;
    110.         if (controller.isGrounded  Input.GetButtonDown ("Jump"))
    111.             movement -= Physics.gravity;
    112.     }
    113. }
    114.  
    As you can see, the whole player controller simply calls methods to do the movement stuff, this makes it easier to read what the Update is doing. All I did then is to add a method to update the bmp.
     
    Last edited: Apr 20, 2014
  11. jonSG

    jonSG

    Joined:
    Mar 26, 2014
    Posts:
    18
    did you try my suggestion?
     
  12. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    Yeah, that didn't work either :/
     
  13. rrh

    rrh

    Joined:
    Jul 12, 2012
    Posts:
    331
    What did it do instead of work?
     
  14. cadw96

    cadw96

    Joined:
    Oct 27, 2013
    Posts:
    20
    Hi,

    Can I send you my project, since I have some problems with your code?
     
  15. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    You can. Send it via PM though.
     
  16. jonSG

    jonSG

    Joined:
    Mar 26, 2014
    Posts:
    18
    This produces the effect I think you are after:
    Set the tmpDistance in the editor to test or switch it for your distance calculation.
    You will also need the audio for the two phases of the heartbeat:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class spooky : MonoBehaviour {
    6.     public AudioClip beatPhase0;
    7.     public AudioClip beatPhase1;
    8.     public float tempDistance;
    9.  
    10.     private bool heartBeatPlaying = false;
    11.     private float heartBeatRange = 20f;
    12.     private float heartBeatDelayPhases = 0.25f;
    13.     private float heartBeatDelayBeats = 0.5f;
    14.  
    15.     void Update () {
    16.         //float distance = Vector3.Distance (enemy.position, player.position);
    17.         float distance = tempDistance;
    18.         StartCoroutine ("playHeartBeat", distance);
    19.     }
    20.  
    21.     private IEnumerator playHeartBeat( float distance ){
    22.         if (heartBeatPlaying || distance > heartBeatRange) { return false; }
    23.         heartBeatPlaying = true;
    24.  
    25.         float dangerDelayFactor = distance / heartBeatRange;
    26.  
    27.         AudioSource.PlayClipAtPoint (beatPhase0, Camera.main.transform.position);
    28.         yield return new WaitForSeconds ( beatPhase0.length + heartBeatDelayPhases * dangerDelayFactor );
    29.  
    30.         AudioSource.PlayClipAtPoint (beatPhase1, Camera.main.transform.position);
    31.         yield return new WaitForSeconds ( beatPhase1.length + heartBeatDelayBeats * dangerDelayFactor );
    32.  
    33.         heartBeatPlaying = false;
    34.     }
    35. }
    36.  
     
  17. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    OK, I fixed my code above to correctly set the time between heartbeats to 60 / bpm rather than bpm / 60.
    I also added the requirement that the game object have an audio source, since it may not have had one.
    I made the volume calculation a little different as my tests showed that my volume was way too low.
    I also made the enemy game object public on the player script. Since I needed to add the enemy in.

    Other than that the whole thing worked just fine.

    To keep you up to date. I went through your first person controller and removed every script you put on him. I did the same for your camera as it looked like you had old code just hanging off of it.

    I added the Player controller from above, which added the heartbeat, character controller and audio source.

    I found the up and down beat for the heart. As I had seen in your original file, you had a heart beat sequence. In order to do what you are asking, you can not have a sequence it must be individual heart beats, up and down stroke.

    I attached the up and down beats to the correct spot and the cube of your enemy to the correct spot.

    I then made the volume public so that I could see what the volume was. (see test above)

    I then played it. the closer I got to the cube, the faster the heartbeat was. Also, if I ran, the heartbeat also came up. If I ran close to the cube, it was very fast.

    I also sent you a PM with the return of your project.
     
    Last edited: Apr 20, 2014