How to fade out and destroy audio on load - c#

I have got a basic script that allows the music to continue playing from scene 2 until scene 6, but I have been trying most of the day to get it where the audio starts to fade out and then is destroyed on scene 6, before loading into scene 7.
I have searched on here, google , youtube and even the Unity forums, but nothing works as all i am getting in my search is how to make the music continue
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Music : MonoBehaviour
{
// Start is called before the first frame update
public GameObject theManager;
public AudioSource music;
public string loadLevel;
public float fadeOutTime = 3f;
bool fading;
float fadePerSec;
void Awake()
{
DontDestroyOnLoad(theManager);
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
void Update()
{
if (fading)
{
music.volume = Mathf.MoveTowards(
music.volume, 0, fadePerSec * Time.deltaTime);
}
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if ((scene.buildIndex != 2) && (scene.buildIndex != 3) && (scene.buildIndex != 4) && (scene.buildIndex != 5) && (scene.buildIndex != 6))
{
fading = true;
fadePerSec = music.volume / fadeOutTime;
Destroy(theManager, fadeOutTime);
}
}
}
Here are 2 screenshots of my project
Project layout
Inspector of ScriptManager
Any help appreciated and thanks in advance

Main Issue: The GameObject with this script attached is detroyed
The ScriptManager GameObject having this script attached itself is destroyed when you switch a Scene! Therefore OnSceneLoaded is probably never even called at all.
Then I would not use the field public GameObject theManager; ... you already have the field public AudioSource music; so I would rather use music.gameObject whenever a GameObject reference is required. Just to avoid mistakes.
Finally I would not use the Update method for checking a bool flag constantly just for a one-time event. I would recommend to rather use a Coroutine:
so something like
public class Music : MonoBehaviour
{
public AudioSource music;
public string loadLevel;
public float fadeOutTime = 3f;
private static Music _instance;
void Awake()
{
DontDestroyOnLoad(music.gameObject);
// If necessary use a singleton pattern to make sure this exists only once
if(_instance)
{
Destroy(gameObject);
return;
}
_instance = this;
// Also don't destroy yourself!
DontDestroyOnLoad(gameObject);
// Just to be sure before adding a callback you should always remove it
// This is valid even if it wasn't added yet
// but it makes sure it is only added exactly once
SceneManager.sceneLoaded -= OnSceneLoaded;
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private IEnumerator FadeOutAndDestroy()
{
var fadePerSec = music.volume / fadeOutTime;
while(music.volume > 0)
{
music.volume = Mathf.MoveTowards(music.volume, 0, fadePerSec * Time.deltaTime);
// yield says: Interupt the routine here, render this frame
// and continue from here in the next frame
// In other words: Coroutines are like small temporary Update methods
yield return null;
}
// Now the volume is 0
Destroy(music.gameObject);
// and since no longer needed also destroy this object
Destroy(gameObject);
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (scene.buildIndex != 7) return;
StartCoroutine(FadeOutAndDestroy());
}
}

Related

Background music destroyed when build index is 0

I've got this background sound playing throughout the game.
The problem is that I want it to stop when the scene index is 0.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class BackgroundMusic : MonoBehaviour
{
void Start()
{
GameObject[] objs = GameObject.FindGameObjectsWithTag("music");
if (objs.Length > 1)
{
Destroy(this.gameObject);
}
if (SceneManager.GetActiveScene().buildIndex != 0)
{
DontDestroyOnLoad(this.gameObject);
}
}
I've been using this script but still doesn't work
Any suggestions?
If you use DontDestroyOnLoad it is "forever" you don't have to do it for each scene again.
And then note that after that the Start is not called for every scene load again only the first time!
Rather use SceneManager.sceneLoaded like e.g.
public class BackgroundMusic : MonoBehaviour
{
private static BackgroundMusic _instace;
private void Awake()
{
if (_instance && _instance != this)
{
Destroy(this.gameObject);
return;
}
DontDestroyOnLoaddd(this.gameObject);
_instance = this;
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDestroy ()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded (Scene scene, LoadSceneMode mode)
{
if(scene.buildIndex == 0)
{
// TODO disable música
// NOTE: I wouldn't Destroy this though but rather only stop the music
}
else
{
// TODO enable the music
}
}
}
The SIMPLEST way to do this with success :
Check the name of the music and if the name is different, you destroy the Game Object, else you keep it playing and don't destroy it ( exemple : if you die when you are playing, the music continue to play and do not start from the beggining when the level restart, but if you go to the menu or change the level, the music will change without any problem and the old music is destroyed safely. )
Here my own code to solve this problem :
private void Awake() {
GameObject[] audioSource = GameObject.FindGameObjectsWithTag("Music");
if (audioSource.Length>1)
{
if (audioSource[0].GetComponent<AudioSource>().clip.name != _audioSource.clip.name)
{
Destroy(audioSource[0].gameObject);
} else {
Destroy(this.gameObject);
}
}
DontDestroyOnLoad(this.gameObject);
}

Trying to get death and respawn working in my game

I can't get my respawning in my game to work, I have followed numerous tutorials, but have been failing to change one small aspect of them. I have a health variable in my game, and all the tutorials have the player die and respawn right upon touching the enemy. I want it to be so you take damage when touching an enemy, and when your health reaches 0 you are brought to a game over scene. I just can't seem to figure it out. I am new to game dev so I have tried my absolute hardest to solve the problem on my own, but to no avail. This is pretty much my last resort. I appreciate any help I can get.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerHealth : MonoBehaviour
{
public static PlayerHealth instance;
public int maxHealth;
public int health;
public int GameOver;
public event Action DamageTaken;
public int Health
{
get
{
return health;
}
}
// Start is called before the first frame update
void Awake()
{
if(instance == null)
{
instance = this;
}
}
private void Start()
{
health = maxHealth;
}
// Update is called once per frame
public void TakeDamage()
{
if(health <= 0)
{
return;
}
health -= 1;
if(DamageTaken != null)
{
DamageTaken();
}
}
public void heal()
{
if (health >= maxHealth)
{
return;
}
health += 1;
if (DamageTaken != null)
{
DamageTaken();
}
}
void OnCollisionEnter(Collision other)
{
if(other.gameObject.tag == "Enemy")
{
TakeDamage();
}
if(health <= 0)
{
SceneManager.LoadScene(GameOver);
}
}
}
I don't see anything wrong with your code specifically. It could be a different issue in the editor, which is usually the case. To debug try one of the following:
Check that the OnCollisionEnter is actually being triggered
Check that the Enemey has the Enemy Tag.
You can check 1 & 2 with the following code:
void OnCollisionEnter(Collision other)
{
// Check what the tag is
print(other.gameObject.tag);
...
}
There should now be messages when you run the program. Other things to check:
Make sure the player object has the PlayerHealth script attached to it.
Make sure that the player has a RigidBody
Make sure the enemy has a collider
Make sure the enemy collider is not set to Trigger

Beginner having Problems with Restarting Scenes when Dying in Unity

so I’m trying to create a respawn system as a beginner, and everything seems to be working the first time but the second time it seems to be unbehaving. If anyone knows how to help me, I would appreciate it
LevelControl:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelControl : MonoBehaviour
{
public int index;
public string levelName;
public GameObject GameOverPanel;
public static LevelControl instance;
private void Awake()
{
if (instance == null)
{
DontDestroyOnLoad(gameObject);
instance = GetComponent<LevelControl>();
}
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
//Loading level with build index
SceneManager.LoadScene(index);
//Loading level with scene name
SceneManager.LoadScene(levelName);
//Restart level
//SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
}
public void LoadLevel1()
{
SceneManager.LoadScene("Game");
}
public GameObject GetGameOverScreen()
{
return GameOverPanel;
}
}
PlayerMovement:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public CharacterController2D controller;
public float runSpeed = 40f;
float horizontalMove = 0f;
bool jump = false;
bool crouch = false;
// Update is called once per frame
void Update()
{
horizontalMove = Input.GetAxisRaw("Horizontal") * runSpeed;
if (Input.GetButtonDown("Jump"))
{
jump = true;
}
if (Input.GetButtonDown("Crouch"))
{
crouch = true;
}
else if (Input.GetButtonUp("Crouch"))
{
crouch = false;
}
}
void FixedUpdate()
{
// Move our character
controller.Move(horizontalMove * Time.fixedDeltaTime, crouch, jump);
jump = false;
//FindObjectOfType<GameManager>().EndGame();
}
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
//Destroy(FindObjectOfType<CharacterController2D>().gameObject);
GameObject.Find("Player").SetActive(false);
LevelControl.instance.GetGameOverScreen().SetActive(true);
}
}
}
Error In Unity Video: https://imgur.com/a/Sr0YCWk
If your wondering what I'm trying to do, well, when the 2 colliders of the Player and Enemy collide, I want a restart button to pop up, and the Character to get destroyed, and after that restart the level as is.
You didn't provide much but I try to work with what we have.
In LevelController you have
private void Awake()
{
if (instance == null)
{
DontDestroyOnLoad(gameObject);
instance = GetComponent<LevelControl>();
}
}
First of all just use
instance = this;
;)
Then you are doing
LevelControl.instance.GetGameOverScreenn().SetActive(true);
I don't see your setup but probably GetGameOverScreenn might not exist anymore after the Scene reload while the instance still does due to the DontDestroyOnLoad.
Actually, why even use a Singleton here? If you reload the entire scene anyway you could just setup the references once via the Inspector and wouldn't have to worry about them later after scene changes...
Also
GameObject.Find("Player").SetActive(false);
seems odd .. isn't your PlayerController attached to the Player object anyway? You could just use
gameObject.SetActive(false);
Ok, Normally it would be easier just to do this:
if (collision.gameObject.tag == "Enemy")
{
//Destroy(FindObjectOfType<CharacterController2D>().gameObject);
gameObject.SetActive(false);
LevelControl.instance.GetGameOverScreen().SetActive(true);
But this will NOT work if you want to attach the script to any other gameobjects for any reason. If you are then first make a game object variable containing the player, like this:
public GameObject Player = GameObject.Find("Player");
Then say
Player.SetActive(false);
This creates a player gameobject which you can access by calling the variable.

Why is OnLevelWasLoaded () called twice and why are my variable's values different in each call?

I currently have 2 scenes, house scene and overworld scene. Whenever the player enters the house scene, I will save the previous scene with currentArea and position with currentPosition in that scene the player was at. After exiting the house scene, the player will go back to where they last were by checking if the loaded scene's name is currentArea. However, I am currently encountering a problem as I notice that OnLevelWasLoaded() is called twice when a scene is loaded. Furthermore, currentArea will contain the previous scene's data in one call but be empty in another call.
I have tried putting the code in Awake() or Start().
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneTransition : MonoBehaviour
{
public static SceneTransition instance = null;
[SerializeField] string currentArea = "";
[SerializeField] Vector3 currentPosition;
void Awake()
{
if (instance == null)
instance = this;
else if (instance != this)
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
if (SceneManager.GetActiveScene().name == currentArea)
{
BasePlayer.instance.transform.position = currentPosition;
CameraController.instance.transform.position = new Vector3(currentPosition.x, currentPosition.y, CameraController.instance.transform.position.z);
}
else if (GameObject.FindGameObjectWithTag("StartingPoint"))
{
BasePlayer.instance.transform.position = GameObject.FindGameObjectWithTag("StartingPoint").transform.position;
CameraController.instance.transform.position = new Vector3(GameObject.FindGameObjectWithTag("StartingPoint").transform.position.x, GameObject.FindGameObjectWithTag("StartingPoint").transform.position.y, CameraController.instance.transform.position.z);
}
}
private void OnLevelWasLoaded(int level)
{
if (SceneManager.GetActiveScene().name == currentArea)
{
BasePlayer.instance.transform.position = currentPosition;
CameraController.instance.transform.position = new Vector3(currentPosition.x, currentPosition.y, CameraController.instance.transform.position.z);
}
else if (GameObject.FindGameObjectWithTag("StartingPoint"))
{
BasePlayer.instance.transform.position = GameObject.FindGameObjectWithTag("StartingPoint").transform.position;
CameraController.instance.transform.position = new Vector3(GameObject.FindGameObjectWithTag("StartingPoint").transform.position.x, GameObject.FindGameObjectWithTag("StartingPoint").transform.position.y, CameraController.instance.transform.position.z);
}
}
public void LoadArea(string nextArea)
{
SceneManager.LoadScene(nextArea);
}
public void LoadEvent(string nextScene)
{
SaveCurrentArea();
SceneManager.LoadScene(nextScene);
}
public void SaveCurrentArea()
{
currentArea = SceneManager.GetActiveScene().name;
currentPosition = BasePlayer.instance.transform.position;
}
public void LoadCurrentArea()
{
SceneManager.LoadScene(currentArea);
}
}
I expected currentArea to be the name of the previous area the player was at but it is sometimes null
You can try out the alternative approach described here
using UnityEngine.SceneManagement;
void OnEnable()
{
//Tell our 'OnLevelFinishedLoading' function to start listening for a scene change as soon as this script is enabled.
SceneManager.sceneLoaded += OnLevelFinishedLoading;
}
void OnDisable()
{
//Tell our 'OnLevelFinishedLoading' function to stop listening for a scene change as soon as this script is disabled. Remember to always have an unsubscription for every delegate you subscribe to!
SceneManager.sceneLoaded -= OnLevelFinishedLoading;
}
void OnLevelFinishedLoading(Scene scene, LoadSceneMode mode)
{
Debug.Log("Level Loaded");
Debug.Log(scene.name);
Debug.Log(mode);
}

Getting print to only print once when countdown Deltatime reaches certain number (Unity)

I have a 10-second countdown script that runs on DeltaTime.
In my Update function, I'm trying to make it print, only once, "Hello" whenever it reaches second 8.
The problem is that deltaTime is repeatedly printing "Hello" while it hangs on the 8th second, rather than only printing once, and I don't know how to stop that behavior.
I kept trying to introduce triggers in the if-block that set to 0 as soon as the block is entered but it still keeps continuously printing "Hello" so long as the timer is on second 8.
Countdown Timer
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CountdownTimer : MonoBehaviour
{
float currentTime = 0f;
float startingTime = 10f;
public int n = 0;
public int switcher = 0;
// Start is called before the first frame update
void Start()
{
currentTime = startingTime;
}
// Update is called once per frame
void Update()
{
currentTime -= 1 * Time.deltaTime; //does it each frame
n = Convert.ToInt32(currentTime);
if (n == 8)
{
switcher = 1;
}
}
}
Update Method in different class
if (CountdownTimer.switcher == 1)
{
CountdownTimer.switcher = 0;
print("hey");
}
Any ideas on how to make print("hey") only happen once? It's important because later I would replace the print code with an important method and I need to make sure the method happens only once.
This is where you want to implement a system of subscriber with event/listener.
Add an event to the countdown, if countdown is meant to be unique, you can even make it static. Also, if the update is no longer needed after the setting of switcher to 1 then you can convert that to coroutine
public class CountdownTimer : MonoBehaviour
{
float currentTime = 0f;
float startingTime = 10f;
public static event Action RaiseReady;
// Start is called before the first frame update
void Start()
{
currentTime = startingTime;
StartCoroutine(UpdateCoroutine());
}
// Update is called once per frame
IEnumerator UpdateCoroutine()
{
while(true)
{
currentTime -= 1 * Time.deltaTime; //does it each frame
int n = Convert.ToInt32(currentTime);
if (n == 8)
{
RaiseReady?.Invoke();
RaiseReady = null; // clean the event
yield break; // Kills the coroutine
}
yield return null;
}
}
}
Any component that needs to know:
public class MyClass : MonoBehaviour
{
void Start()
{
CountdownTimer.RaiseReady += CountdownTimer_RaiseReady;
}
private void CountdownTimer_RaiseReady()
{
Debug.Log("Done");
// Remove listener though the other class is already clearing it
CountdownTimer.RaiseReady -= CountdownTimer_RaiseReady;
}
}
The solution #Everts provides is pretty good, but as you are using unity I would recommend a couple of tweaks to play nicer with the editor. Instead of a generic event I recommend using a UnityEvent from the UnityEngine.Events namespace. I would also advise against statics due to how unity goes about serializing them across scenes. There are some weird edge cases you can get into if you aren't familiar with how unity handles their serialization. If you just need to send a message to another object in the same scene I would actually recommend a game manager. You can safely do a GameObject.Find() in onvalidate() and link your variables to avoid a performance hit at runtime doing the find. If that data needs to carry across to a different scene for this message then use a ScriptableObject instead. It would look something like below.
Put this component on the scene's "Game Manager" GameObject
public class CountingPassthrough : MonoBehaviour
{
public CountdownTimer countdownTimer;
}
put this component on the scene's "Timer" GameObject
public class CountdownTimer : MonoBehaviour
{
public float startingTime = 10f;
public UnityEvent timedOut = new UnityEvent();
private void OnValidate()
{
if(FindObjectOfType<CountingPassthrough>().gameObject.scene == gameObject.scene && FindObjectOfType<CountingPassthrough>() != new UnityEngine.SceneManagement.Scene())
FindObjectOfType<CountingPassthrough>().countdownTimer = this;
}
private void Start()
{
StartCoroutine(TimerCoroutine());
}
// Coroutine is called once per frame
private IEnumerator TimerCoroutine()
{
float currentTime = 0f;
while (currentTime != 0)
{
currentTime = Mathf.Max(0, currentTime - Time.deltaTime);
yield return null;//wait for next frame
}
timedOut.Invoke();
}
}
Put this component on the GameObject you want to use the timer
public class user : MonoBehaviour
{
[SerializeField, HideInInspector]
private CountingPassthrough timerObject;
private void OnValidate()
{
if(FindObjectOfType<CountingPassthrough>().gameObject.scene == gameObject.scene && FindObjectOfType<CountingPassthrough>() != new UnityEngine.SceneManagement.Scene())
timerObject = FindObjectOfType<CountingPassthrough>();
}
private void OnEnable()
{
timerObject.countdownTimer.timedOut.AddListener(DoSomething);
}
private void OnDisable()
{
timerObject.countdownTimer.timedOut.RemoveListener(DoSomething);
}
private void DoSomething()
{
//do stuff here...
}
}
This workflow is friendly to prefabs too, because you can wrap the find() in onvalidate() with if(FindObjectOfType<CountingPassthrough>().gameObject.scene == gameObject.scene) to prevent grabbing the wrong asset from other loaded scenes. And again, if you need this to carry data across scenes then have CountingPassthrough inherit from ScriptableObject instead of MonoBehaviour, create the new ScriptableObject to your project folder somewhere, and ignore that extra if check to constrain scene matching. Then just make sure you use a function to find it that includes assets if you use the cross-scene ScriptableObject approach.
EDIT:Forgot nested prefabs edgecase in unity 2018+ versions. You need to add this to account for it: && FindObjectOfType<CountingPassthrough>() != new UnityEngine.SceneManagement.Scene() I've updated the code snippet above. Sorry about that.
Since Updtade() is called once per frame, then switcher is set to 1 once per frame during the 8th second (and there is a lot of frames in 1 sec).
An answer could be something like this to prevent it from printing again :
if (CountdownTimer.switcher == 1)
{
if (!AlreadyDisplayed)
{
print("hey");
AlreadyDisplayed = true;
}
}
Where AlreadyDisplayed is a Boolean set to false when declared.
This should do what you want to achieve. :)

Categories