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"); } } } }
问题根源
- 单例逻辑执行顺序错误:Awake中先执行了滑块查找、PlayerPrefs初始化和滑块赋值,再处理单例判断。当存在重复实例时,会销毁当前对象,但此时
sounds数组的AudioSource还未初始化,后续旧实例的ChangeVolume方法遍历sounds时,可能会访问到未初始化的元素。 - 缺少空引用检查:无论是滑块查找、
sounds数组遍历,还是s.source的访问,都没有做空值判断,一旦某个环节返回null,就会触发异常。 - 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
相关产品推荐
相关产品推荐

