在游戏开发中,我们经常需要频繁创建一些物体,诸如射击游戏的子弹、频繁的脚步声,如果频繁创建和销毁,势必会造成大量性能的开销。于是就有了缓存池,以及在缓存池基础上发展出的音效系统。

缓存池脚本

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using System.Collections.Generic;
using UnityEngine;

public class GamePoolManager : SingleMono<GamePoolManager>
{
[System.Serializable]
private class PoolItem
{
public string itemName;
public GameObject item;
public int itemCount;
}

[Header("对象池配置")]
[SerializeField] private List<PoolItem> poolList = new List<PoolItem>();

private Dictionary<string, Queue<GameObject>> poolDic = new Dictionary<string, Queue<GameObject>>();
private GameObject _poolParent;

protected override void Awake()
{
_poolParent = new GameObject("对象池父对象");
_poolParent.transform.SetParent(transform);
InitPool();
}

private void InitPool()
{
foreach (var poolItem in poolList)
{
if (string.IsNullOrEmpty(poolItem.itemName) || poolItem.item == null || poolItem.itemCount <= 0)
{
Debug.LogWarning("无效的对象池配置项");
continue;
}

if (!poolDic.ContainsKey(poolItem.itemName))
{
poolDic.Add(poolItem.itemName, new Queue<GameObject>());
}

for (int j = 0; j < poolItem.itemCount; j++)
{
var itemObj = Instantiate(poolItem.item);
itemObj.transform.SetParent(_poolParent.transform);
itemObj.SetActive(false);
poolDic[poolItem.itemName].Enqueue(itemObj);
}
}
}

public void TryGetPoolItem(string name, Vector3 position, Quaternion rotation)
{
if (poolDic.TryGetValue(name, out var queue) && queue.Count > 0)
{
GameObject item = queue.Dequeue();
item.transform.position = position;
item.transform.rotation = rotation;
item.SetActive(true);
queue.Enqueue(item);
}
else
{
Debug.LogWarning($"对象池中找不到: {name}");
}
}

public GameObject TryGetPoolItem(string name)
{
if (poolDic.TryGetValue(name, out var queue) && queue.Count > 0)
{
GameObject item = queue.Dequeue();
item.SetActive(true);
queue.Enqueue(item);
return item;
}

Debug.LogWarning($"对象池中找不到: {name}");
return null;
}
}

音效系统脚本

首先是SoundType脚本

plaintext
1
2
3
4
5
6
7
8
public enum SoundType
{
ATK,
HIT,
BLOCK,
Run,
Walk
}

其次是SoundData脚本

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "SoundData", menuName = "Scriptable Objects/SoundData")]
public class SoundData : ScriptableObject
{
[SerializeField] private List<Sound_Data> _data = new List<Sound_Data>();

[System.Serializable]
private class Sound_Data
{
public SoundType soundType;
public AudioClip[] audioClip;
}

public AudioClip GetAudioClip(SoundType type)
{
foreach (var data in _data)
{
if (data.soundType == type && data.audioClip != null && data.audioClip.Length > 0)
{
return data.audioClip[Random.Range(0, data.audioClip.Length)];
}
}

Debug.LogWarning($"找不到音效类型: {type}");
return null;
}
}

最后是Sound脚本

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using UnityEngine;

public class Sound : MonoBehaviour
{
private AudioSource audioSource;

[Header("音效设置")]
[SerializeField] private SoundType soundType;
[SerializeField] private SoundData soundData;

private void Awake()
{
audioSource = GetComponent<AudioSource>();
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
}
}

private void OnEnable()
{
PlaySound(soundType);
}

private void PlaySound(SoundType type)
{
if (soundData == null)
{
Debug.LogError("SoundData未配置!");
return;
}

AudioClip clip = soundData.GetAudioClip(type);
if (clip == null)
{
gameObject.SetActive(false);
Debug.Log("获取不到音频类型,请检查SoundType脚本和预制体是否配置了");
return;
}

audioSource.clip = clip;
audioSource.Play();
StartRecycle(clip.length);
}

private void StartRecycle(float clipLength)
{
float delay = Mathf.Max(0.1f, clipLength);
Invoke(nameof(DisableSelf), delay);
}

private void DisableSelf()
{
if (audioSource != null && audioSource.isPlaying)
audioSource.Stop();

gameObject.SetActive(false);
}
}

原理分析

GamePoolManager提供了InitPool方法和TryGetPoolItem方法,InitPool方法会创建出多个对象并默认失活,TryGetPoolItem方法既可以处理音频创建、也可以处理物体创建,会激活一个对象。

而音效系统的代码逻辑,SoundData用来存储所有的音频类型和数据,Sound预制体在被激活时会自动识别我们配置的音频类型、并随机播放音频。最后加入对象池中,形成这样的顺序:对象池创建并激活预制体——识别预制体的音频类型、随机播放对应音频。


## 使用方法

音效系统使用

一,更改SoundType脚本,里面的枚举是你的音效类型,按需修改。

二,右键Scriptable Object,创建一个SoundData,配置好你所需要的音频类型、以及对应的音频。

三,游戏里创建Sound空物体,拖入Sound.cs脚本,为每一个空物体配置好音频类型、并拖入创建好的SoundData物体,保存作为预制体。有多少个类型的音频创建多少个音频预制体。

四,游戏再创建空物体,命名GamePoolManager,拖入GamePoolManager.cs脚本。Item Name输入缓存池里物体的名字、Item拖入你上一步创建好的各个音频预制体,Item Count则是缓存池存的物体的数量。

五,配置好动画事件,在Player_Model脚本写入动画事件函数:

plaintext
1
2
3
4
5
public void FootStep()
{
//括号里传入Item Name的名字
GamePoolManager.MainInstance.TryGetPoolItem("Walk");
}