AudioSource is null when loading with SceneManager

I have a player GO with an AudioSource component to play different sounds when hit or killed.
When testing my play scene in editor it works great, but when I reload the scene (main scene > gameover > Title Scene > Main Scene) I get ArgumentNullException when calling PlayOneShot.

I added a setter to see when the Audio source gets set to null but it only triggers in my Awake function and is really set to the correct value.

Breakpoints troubleshooting reveals this behaviour :
Awake() => AudioSource has a value, references the component from GetComponent();
AudioSource setter triggered with the debug log correctly displaying in console
When OnPlayerHit is triggered, AudioSource is now null, but the setter was never called.
Both AudioClips are set in inspector and have correct value .

public class PlayerHealth : MonoBehaviour
{
    [SerializeField]
    IntVariable playerStartHp;
    [SerializeField]
    IntVariable playerCurrentHp;
    [SerializeField]
    AudioClip audioHit;
    [SerializeField]
    AudioClip audioDead;

    AudioSource source;

    public AudioSource Source {
        get { return source; }
        set
        {
            source = value;
            Debug.Log($"audio source set to {value}");
        }
    }

    private void Awake()
    {
        Source = GetComponent<AudioSource>();
        playerCurrentHp.valueDown += OnPlayerHit;
        playerCurrentHp.value = playerStartHp.value;
    }

    //private void OnDestroy()
    //{
    //       audioDead.Play();
    //}

    void OnPlayerHit()
    {
        if (playerCurrentHp.value == 0)
        {
            StartCoroutine(DieCoroutine());
        }
        else Source.PlayOneShot(audioHit);
    }

    IEnumerator DieCoroutine()
    {
        Source.PlayOneShot(audioDead);
        while (Source.isPlaying)
        {
            yield return null;
        }
        Destroy(gameObject);
    }
}

I don’t really understand your logic here, but what I immediately noticed that you do not call
playerCurrentHp.valueDown -= OnPlayerHit; anywhere.
If playerCurrentHp is some sort of global object, maybe even static, then you remain subscribed there and you can call dead objects.
I would follow the Unity standard here and put

private void OnEnable() => playerCurrentHp.valueDown += OnPlayerHit;
private void OnDisable() => playerCurrentHp.valueDown -= OnPlayerHit;

in there. Maybe that would help your case. Obviously I can’t test it because I don’t have the same setup.

1 Like

Oh nice, that was my problem.

I thought this was not needed since the scene is reloaded, but this playerCurrentHp is a ScriptableObject so the value is kept even between the scenes, and the events too I guess…

ScriptableObjects are tricky that way… they live in this twilight-ish long-lived lifecycle.

Only within the past year or two Unity began to reliably call OnEnable/OnDisable on SOs… prior to that, En/Dis was NOT called every time you pressed play, leading to really weird bugs if you counted on it.

But on the other hand, every field (including private!!!) not marked as [NonSerialized] used to be serialized for the duration of your Unity editor session, which ALSO could be confusing, as private bool Initialized; would stay “true” until you left the editor. (!!!)