You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中使用PlayerPrefs时持续触发NullReferenceException问题排查

问题:AudioManager中ChangeVolume方法触发NullReferenceException

我实现了玩家可调节的音量滑块,调整时会将音量值保存到PlayerPrefs中,确保切换场景或重启游戏后设置仍能保留。滑块未被设置为SetActive(false),但在ChangeVolume方法的s.source.volume = PlayerPrefs.GetFloat("SFX_Volume");代码行持续出现NullReferenceException。执行PlayerPrefs.DeleteAll()时代码可正常运行,但重启游戏且不删除PlayerPrefs时就会报错。目前编辑器中还未添加“isMusic”类型的音效,我已检查编辑器设置无误,查阅NullReferenceException相关资料后仍未解决问题,相关代码如下:

public class AudioManager : MonoBehaviour
{
    public Sound[] sounds;

    public static AudioManager instance;
    GameObject sliderSFX;
    GameObject sliderMusic;

    void Awake()
    {
        sliderSFX = GameObject.Find("SFX_Slider");
        sliderMusic = GameObject.Find("Music_Slider");
        
        if (PlayerPrefs.HasKey("SFX_Volume") != true){
            PlayerPrefs.SetFloat("SFX_Volume", 1);
        }
        if (PlayerPrefs.HasKey("Music_Volume") != true){
            PlayerPrefs.SetFloat("Music_Volume", 1);
        }
        
        sliderSFX.GetComponent<Slider>().value = PlayerPrefs.GetFloat("SFX_Volume");
        sliderMusic.GetComponent<Slider>().value = PlayerPrefs.GetFloat("Music_Volume");


        if (instance == null){
            instance = this;
        }
        else{
            Destroy(gameObject);
            return;
        }

        DontDestroyOnLoad(gameObject);

        foreach(Sound s in sounds){
            s.source = gameObject.AddComponent<AudioSource>();
            s.source.clip = s.clip;
            if (s.isMusic == true && s.isSFX == false){
                s.source.volume = PlayerPrefs.GetFloat("Music_Volume");
            }
            if (s.isSFX == true && s.isMusic == false){
                s.source.volume = PlayerPrefs.GetFloat("SFX_Volume");
            }
            s.source.pitch = s.pitch;
            s.source.playOnAwake = s.playOnAwake;
            s.source.loop = s.loop;
        }
    }

    public void ChangeVolume()
    {
        var s_SFX_Slider = sliderSFX.GetComponent<Slider>().value;
        var s_MusicSlider = sliderMusic.GetComponent<Slider>().value;

        PlayerPrefs.SetFloat("SFX_Volume", s_SFX_Slider);
        PlayerPrefs.SetFloat("Music_Volume", s_MusicSlider);

        foreach(Sound s in sounds){
            if (s.isMusic == true && s.isSFX == false){
                s.source.volume = PlayerPrefs.GetFloat("Music_Volume");
            }
            else if (s.isSFX == true && s.isMusic == false){
                s.source.volume = PlayerPrefs.GetFloat("SFX_Volume");
            }
        }
    }
}

问题根源

  1. 单例逻辑执行顺序错误:Awake中先执行了滑块查找、PlayerPrefs初始化和滑块赋值,再处理单例判断。当存在重复实例时,会销毁当前对象,但此时sounds数组的AudioSource还未初始化,后续旧实例的ChangeVolume方法遍历sounds时,可能会访问到未初始化的元素。
  2. 缺少空引用检查:无论是滑块查找、sounds数组遍历,还是s.source的访问,都没有做空值判断,一旦某个环节返回null,就会触发异常。
  3. PlayerPrefs未强制保存:修改PlayerPrefs后没有调用PlayerPrefs.Save(),可能导致值未及时写入磁盘,重启后读取异常。

修复方案

1. 调整单例逻辑顺序

把单例判断和DontDestroyOnLoad移到Awake最开头,确保只有合法实例才会执行后续初始化:

void Awake()
{
    // 优先处理单例,避免无效初始化
    if (instance == null)
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
    }
    else
    {
        Destroy(gameObject);
        return;
    }

    sliderSFX = GameObject.Find("SFX_Slider");
    sliderMusic = GameObject.Find("Music_Slider");
    
    // 简化PlayerPrefs判断逻辑
    if (!PlayerPrefs.HasKey("SFX_Volume")) PlayerPrefs.SetFloat("SFX_Volume", 1);
    if (!PlayerPrefs.HasKey("Music_Volume")) PlayerPrefs.SetFloat("Music_Volume", 1);
    
    // 增加滑块空判断,避免查找失败报错
    if (sliderSFX != null)
        sliderSFX.GetComponent<Slider>().value = PlayerPrefs.GetFloat("SFX_Volume");
    if (sliderMusic != null)
        sliderMusic.GetComponent<Slider>().value = PlayerPrefs.GetFloat("Music_Volume");

    foreach (Sound s in sounds)
    {
        // 跳过数组中空元素
        if (s == null) continue;
        
        s.source = gameObject.AddComponent<AudioSource>();
        s.source.clip = s.clip;
        
        // 简化条件判断
        if (s.isMusic && !s.isSFX)
            s.source.volume = PlayerPrefs.GetFloat("Music_Volume");
        else if (s.isSFX && !s.isMusic)
            s.source.volume = PlayerPrefs.GetFloat("SFX_Volume");
            
        s.source.pitch = s.pitch;
        s.source.playOnAwake = s.playOnAwake;
        s.source.loop = s.loop;
    }
}

2. 完善ChangeVolume方法的空检查

public void ChangeVolume()
{
    // 滑块不存在直接返回
    if (sliderSFX == null || sliderMusic == null) return;
    
    var sfxVolume = sliderSFX.GetComponent<Slider>().value;
    var musicVolume = sliderMusic.GetComponent<Slider>().value;

    PlayerPrefs.SetFloat("SFX_Volume", sfxVolume);
    PlayerPrefs.SetFloat("Music_Volume", musicVolume);
    PlayerPrefs.Save(); // 强制保存到磁盘

    foreach (Sound s in sounds)
    {
        // 空元素或未初始化source直接跳过
        if (s == null || s.source == null) continue;
        
        if (s.isMusic && !s.isSFX)
            s.source.volume = musicVolume;
        else if (s.isSFX && !s.isMusic)
            s.source.volume = sfxVolume;
    }
}

3. 优化滑块查找方式(可选)

放弃GameObject.Find,直接在编辑器中把Slider拖到AudioManager的公共字段,更稳定:

public Slider sliderSFX;
public Slider sliderMusic;

然后移除Awake中的GameObject.Find代码,避免动态查找的不确定性。

4. 检查Sound数组配置

确保sounds数组中没有空元素,所有Sound对象的isSFX/isMusic属性都正确设置(目前没有音乐音效,所以所有Sound都应标记为isSFX=true、isMusic=false)。


内容的提问来源于stack exchange,提问作者Tomas Eli Olafsson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 08:01:01