Home > front end >  Trying to change music between scenes
Trying to change music between scenes

Time:12-27

I'm trying to store the current music that is playing in the current scene that I'm on and then in the next scene change it for another one.

Here are my scripts.

AudioManager

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

public class AudioManager : MonoBehaviour {
    public static AudioManager Instance;
    private MusicList musicList;
    [SerializeField] private AudioSource _effectsSource, currentMusic;

    private void Awake() {
        if (Instance == null) {
            Instance = this;
            DontDestroyOnLoad(this);
        }
        else Destroy(this);
        Instance = this;
        musicList = GetComponentInChildren<MusicList>();
    }
    public void PlayMusic(MusicId id) {
        AudioSource musicToPlay = musicList.GetMusicSource(id);
        musicToPlay.Play();
        currentMusic = musicToPlay;
    }
    public void ChangeMusic(MusicId newMusicId) {
        currentMusic.Stop();
        AudioSource musicToPlay = musicList.GetMusicSource(newMusicId);
        musicToPlay.Play();
        currentMusic = musicToPlay;
    }
    public void PlaySound(AudioClip clip) {
        _effectsSource.PlayOneShot(clip, 0.1f);
    }
}

MusicList is a children of the AudioManager

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

public class MusicList : MonoBehaviour
{
    public Music[] musicList;
    private void Awake()
    {
        foreach (Music music in musicList) {
            music.source = gameObject.AddComponent<AudioSource>();
            music.source.clip = music.clip;
            music.source.volume = music.volume;
            music.source.loop = true;
        }
    }
    public AudioSource GetMusicSource(MusicId id) {
        foreach (Music music in musicList) {
            if (music.id == id) return music.source;
        }
        return null;
    }
}

Music

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

[System.Serializable]
public class Music
{
    public MusicId id;
    public AudioClip clip;
    [HideInInspector]
    public AudioSource source;
    public float volume;
}
public enum MusicId {
    Menu,
    Gameplay
}

When I debug in the first scene the current track playing is stored, but when I change the scene and try to access the currentMusic value it's null.

Thank you for the help!

CodePudding user response:

You have at least two approaches here. One of them is using proprietary scene data (not a technical term), meaning you use whatever info the current scene has, while the other method is using data persistence (this is a technical term) to "save" data in (normally) transient objects after scene changes. There is a third way to persist data, but it's not recommended for your use-case.

There are use-cases for all of them, each having different aims. Let's attack them:

  1. scene info

This is straight forward: you have a menu, a tutorial, 2 levels, a boss fight level then the "end game credits". This would mean 6 different songs (assuming each scene, including the end-credits has a different theme). Once you switch from menu to level and from level to level and then to "the end", a new song pops-up on the audio manager.

The simplest approach here is to have a GameObject in each scene (preferably with the same name and same components, only the song being different). Let's call it "SceneInfo". On this GO you can attach any info necessary to your level, like some description of the level, objectives, difficulty or song. You can use the Find() function to locate this GO then just access the component you need (like a component that saved the Music, then just pop-it on the AudioSource and Play() it).

Minor note: the scene GO must NOT be set as DontDestroyOnLoad(). Because you must not "carry" it in a different scene since each scene has its own stuff. And, of course, this should be different from your AudioManager GO.

This is prolly what you want for your approach.

  1. data persistence

This is normally used to "carry" player-related stuff like the player icon, player name, health, inventory etc. from an earlier scene to this one. You can "carry" the music too, I guess, assuming it's a custom playlist OR if you want to continue the song. Otherwise, I don't recommend this for music if the songs are always different between levels.

This is basically what you did with your AudioManager.

  1. storage (not recommended for what you need)

There would be a third option related to storage, by using PlayerPrefs, but I think this is overkill just for a song. But feel free to use this if you find it easier. As I said, it all depends on your use-case.

This option refers mostly to game preferences like volume, graphics settings etc. It's basically the "memory" of game settings.

Minor caveats:

I would set currentMusic; as public.

Your Awake() function has a weird flow. else Destroy(this); is missing a return. It's not wise to set the Instance to this since you're destroying the object.

Try this instead:

private void Awake()
{
    if (Instance == null)
    {
        DontDestroyOnLoad(this);
        Instance = this;
    }
    else if (Instance != this)
    {
        Destroy(this);
        return;
    }

    if (musicList == null)
        musicList = GetComponentInChildren<MusicList>();
}

You said: MusicList is a children of the AudioManager. I think you mean component instead of children, if the gameObject it's attached to is called "AudioManager", right? Else it doesn't quite make sense. A child class is something else.

Then the code in PlayMusic() is identical to ChangeMusic() (apart from stopping the previous song to update it). That means if you want to change something in the code for PlayMusic() or ChangeMusic(), you'll always have to do it twice (in each function). This is better:

public void PlayMusic(MusicId id) {
    AudioSource musicToPlay = musicList.GetMusicSource(id);
    musicToPlay.Play();
    currentMusic = musicToPlay;
}
public void ChangeMusic(MusicId newMusicId) {
    currentMusic.Stop();
    PlayMusic(newMusicId);
}

... which can be simplified even more to this:

public void PlayMusic(MusicId id) {
    currentMusic = musicList.GetMusicSource(id);
    currentMusic.Play();
}

public void ChangeMusic(MusicId newMusicId) {
    currentMusic.Stop();
    PlayMusic(newMusicId);
}

And now there is no temp variable. It complicated the code for no reason.

(of course, assuming the id is always guaranteed to be in the list, which, of course, it should be). This way you only have one place to change your code or fix potential bugs.

While you can "force" serialization of C# objects, it's better to use Unity objects (anything from UnityEngine.object, but mostly rely on ScriptableObjects, Components and Prefabs).

The music init workflow is also weird. Instead of initializing the AudioSource on each music wrapper, you do it in a loop in the list. It's a bit "backwards", but not necessarily a bad thing. I just wouldn't do it.

  • Related