239
社区成员




这个作业属于哪个课程 | 2023年福大-软件工程实践-W班 |
---|---|
这个作业要求在哪里 | 软件工程实践总结&个人技术博客 |
这个作业的目标 | 个人技术总结和新技能的使用心得 |
其他参考文献 | 参考下文 |
对象池技术旨在优化资源管理与程序性能。它预先创建并存储一批可重复利用的对象,当程序运行过程中需要这些对象时,直接从对象池中取出并激活,使用完毕后将其回收至池中,而非频繁地创建与销毁对象,从而有效减少内存分配与垃圾回收的开销,提升系统的响应速度与稳定性。
以一款飞机射击游戏为例,子弹是大量重复使用的物体。按常规操作,子弹击中目标或出界就销毁,需要时再创建,会不断重复创建与销毁过程。而采用对象池,提前生成一定数量的子弹存于池中,用的时候取出激活,用完回收,能省去繁琐操作,减少卡顿。
图例:
为了更好地表述,我先简单介绍下音乐的生成方法,再过渡到Pool方法的使用。
SoundDetailsList_SO类用于集中管理声音资源的详细信息。其内部包含一个List类型的成员变量soundDetailsList,而SoundDetails结构体又详细定义了声音的名称、音频剪辑、音高范围以及音量。通过GetSoundDetails方法,可以依据声音名称方便地从列表中查找并获取对应的声音细节信息,为后续声音的播放配置提供了基础数据支持。
[CreateAssetMenu(fileName = "SceneSoundList_SO", menuName = "Sound/SceneSoundList")]
public class SceneSoundList_SO : ScriptableObject
{
public List<SceneSoundItem> sceneSoundList;
public SceneSoundItem GetSceneSoundItem(string name)
{
return sceneSoundList.Find(s => s.sceneName == name);
}
}
[System.Serializable]
public class SceneSoundItem
{
[SceneName] public string sceneName;
public SoundName ambient;
public SoundName music;
}
该Sound类主要负责配置音频的音量和音高,通过SetSound方法获取传入的SoundDetails信息,进而设置对应的AudioSource组件的clip、volume以及pitch属性,使得音频能够按照设定好的参数进行播放。
[RequireComponent(typeof(AudioSource))]
public class Sound : MonoBehaviour
{
[SerializeField] private AudioSource audioSource;
public void SetSound(SoundDetails soundDetails)
{
audioSource.clip = soundDetails.soundClip;
audioSource.volume = soundDetails.soundVolume;
audioSource.pitch = Random.Range(soundDetails.soundPitchMin, soundDetails.soundPitchMax);
}
}
这是整个声音管理的核心类,采用了单例模式(继承自Singleton)来确保全局只有一个实例,方便在不同场景和脚本中统一管理声音相关操作。
PlaySoundRoutine协程:如果获取到的环境音和背景音乐的SoundDetails都不为空,先调用PlayAmbientClip方法播放环境音效,再调用PlayMusicClip方法播放背景音乐,以此实现平滑的场景声音过渡。
private IEnumerator PlaySoundRoutine(SoundDetails music, SoundDetails ambient)
{
if (music != null && ambient != null)
{
PlayAmbientClip(ambient, 1f);
yield return new WaitForSeconds(MusicStartSecond);
PlayMusicClip(music, musicTransitionSecond);
}
}
特定声音播放:当OnPlaySoundEvent被触发,会先从soundDetailsData中获取对应的SoundDetails,若获取成功,则通过事件系统触发CallInitSoundEffect来播放对应的音效。
private void PlayMusicClip(SoundDetails soundDetails, float transitionTime)
{
audioMixer.SetFloat("MusicVolume", ConvertSoundVolume(soundDetails.soundVolume));
gameSource.clip = soundDetails.soundClip;
if (gameSource.isActiveAndEnabled)
gameSource.Play();
normalSnapShot.TransitionTo(transitionTime);
}
尽管上述声音播放机制已较为完善,但在实际游戏运行过程中会发现,频繁地创建和销毁AudioSource 组件的 GameObject 带来了较大的性能问题。例如在一些游戏场景中,声音的播放极为频繁,如战斗场景中的脚步音效、睡觉音效和环境音效的持续切换等等,每次创建和销毁这些声音对象都会消耗大量的 CPU 和内存资源,并且会产生大量垃圾内存,增加垃圾回收的频率和时间,这可能导致游戏出现卡顿现象,严重影响游戏体验。
为解决这一问题,我搜索了相关方法,并选择了ObjectPool 功能。ObjectPool 相关功能主要在 PoolManager 类中实现,它与 AudioManager 紧密协作管理声音对象。
创建PoolManager:
public class PoolManager : MonoBehaviour
{
public List<GameObject> poolPrefabs;
public List<ObjectPool<GameObject>> poolEffectList = new List<ObjectPool<GameObject>>();
private Queue<GameObject> soundQueue = new Queue<GameObject>();
//......
}
位置:ObjectPool相关功能主要在PoolManager类中实现,它与AudioManager紧密协作来管理声音对象。
作用:用于管理声音播放对象,避免频繁地创建和销毁GameObject,从而提高性能。
遍历poolPrefabs列表中的每个GameObject预制体来创建对象池,对每个预制体创建一个新的GameObject作为父对象,将其Transform设置为PoolManager子对象,用于组织对象池中的对象。同时使用ObjectPool创建一个新的对象池,传入四个委托函数:创建对象委托、激活对象委托、停用对象委托、销毁对象委托。最后,将创建的对象池保存。
private void CreatePool()
{
foreach (GameObject item in poolPrefabs)
{
Transform parent = new GameObject(item.name).transform;
parent.SetParent(transform);
var newPool = new ObjectPool<GameObject>(
() => Instantiate(item, parent),
e => { e.SetActive(true); },
e => { e.SetActive(false); },
e => { Destroy(e); }
);
poolEffectList.Add(newPool);
}
}
创建一个新的GameObject作为父对象,将其Transform设置为PoolManager对象的子对象。
将poolPrefabs[0]设置为父对象的子对象(因为只有声音用到了pool,所以就是poolPrefabs[0]),停用该对象并将其添加到soundQueue队列中。这个soundQueue队列用于存储声音对象,方便后续获取和管理。
private void CreateSoundPool()
{
var parent = new GameObject(poolPrefabs[0].name).transform;
parent.SetParent(transform);
GameObject newObj = Instantiate(poolPrefabs[0], parent);
newObj.SetActive(false);
soundQueue.Enqueue(newObj);
}
首先通过GetPoolObject方法获取对象,然后通过该对象的Sound组件(GetComponent)调用SetSound方法来设置声音细节。激活对象后启动协程DisableSound,在声音播放完后停用该对象并将其放回soundQueue队列中,实现了声音对象的循环利用。
private void InitSoundEffect(SoundDetails soundDetails)
{
var obj = GetPoolObject();
obj.GetComponent<Sound>().SetSound(soundDetails);
obj.SetActive(true);
StartCoroutine(DisableSound(obj, soundDetails.soundClip.length));
}
该协程方法用于在声音播放完成后停用声音对象并将其放回对象池。
private IEnumerator DisableSound(GameObject obj, float duration)
{
yield return new WaitForSeconds(duration);
obj.SetActive(false);
soundQueue.Enqueue(obj);
}
问题:即便使用对象池预加载了声音对象,在游戏首次触发声音播放时,仍会出现短暂延迟,就好像对象并未真正预加载成功,影响了游戏体验的即时性。
解决过程:检查预加载逻辑,可能是在CreatePool或CreateSoundPool方法中,对象虽已实例化并放入池中,但相关组件的初始化未完成。确保在预加载时,不仅创建对象,还对其关键组件进行完整初始化,避免首次使用时才进行这些操作导致延迟。
问题:在声音对象复用过程中,发现播放的音频有时会出现错乱,比如播放的音频与预期的不匹配,或者音频播放到一半切换成其他音频等问题。
解决过程:问题在于对象复用前未对音频相关状态进行彻底重置。在 PoolManager 的 InitSoundEffect 方法中,在设置新的 SoundDetails 之前,先对音频组件进行全面重置操作。包括将 AudioSource 的 clip 设置为 null、停止当前可能正在播放的音频、清除音频缓冲区等。这样可以确保每次对象复用播放新音频时,音频组件处于初始干净的状态,避免之前残留的音频数据或播放状态对新音频播放造成干扰。
1、减少实例化开销:在游戏中,如果频繁地创建和销毁GameObject,会带来较大的性能开销。使用对象池,对象在初始化阶段就预先创建好并存储在池中,后续需要使用时直接从池中获取已经创建好的对象,避免了频繁的实例化操作,减少了 CPU 和内存的消耗。
2、减少垃圾回收压力:频繁创建和销毁对象会产生大量的垃圾内存,这会增加垃圾回收的频率和时间。对象池通过重复利用对象,减少了垃圾内存的产生,从而减轻了垃圾回收的压力,使游戏运行更加流畅。
3、集中管理对象:通过对象池,可以将声音播放对象集中管理。所有的声音对象都存储在对象池中,方便对它们进行统一的控制和管理,例如批量设置属性、检查状态等。
4、易于控制对象数量:可以根据游戏的实际需求,在创建对象池时确定初始对象数量(如CreateSoundPool方法中创建 20 个声音对象),并且在运行过程中可以根据需要动态地调整对象池中的对象数量,确保资源的合理利用。
1、b站M_Studio老师课程
2、Unity 对象池(Object Pooling)理解与简单应用
3、Unity脚本API