Audio模块封装

思考并回答以下问题:

  • 为什么要封装Audio模块?
  • 为什么同类型的东西多了,就需要一个管理器?
  • 能不继承MonoBehavior就不继承MonoBehavior?
  • 为什么用类来存AudioClip?

Audio

播放音效需要两个条件:

  • 1、AudioListener:有且只有一个。
  • 2、AudioSource:需要AudioClip,AudioClip可替换,即一个AudioSource可以对应多个AudioClip。

AudioSource组件非常复杂,有很多变量,占用很大空间,还继承MonoBehavior。所以AudioSource能减少一个就减少一个。

**音效分类**
  • 1、前景音乐。可以同时播放多个前景音乐。
  • 2、背景音乐。只能播放一个背景音乐。
**优化思路**

1、维持一个AudioSource的队列,需要播放音效就从队列里拿一个空闲的AudioSource出来去播放AudioClip。重复利用AudioSource,使用有限的AudioSource去播放无限的Audio Clip。

2、不是一个对象需要播放音乐了就在那个对象上添加一个AudioSource组件,那这个对象就永远有这个组件了,导致内存浪费。

3、利用外观模式去管理这两个类。从AudioSource中拿一个,从AudioClip中拿一个,两个一结合,就可以播放了。

AudioSource需要的功能:

  • 1、能够拿出一个空闲的AudioSource,重复利用AudioSource(对象池);
  • 2、无限扩展,如果队列里都忙着,可以添加AudioSource;
  • 3、释放多余的。
  • 4、播放一个音效。
  • 5、停止播放。

AudioClip:

  • 1、应该有一个管理类;
  • 2、通过配置文件读取名字加载到内存;
  • 3、通过名字能查找到对应的clip片段。

实现

SourceManager

维护一个List来管理AudioSource。

SourceManager.cs

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using System.Collections.Generic;

public class SourceManager
{
// AudioSource组件要挂载到此物体身上
GameObject ower;

List<AudioSource> allSources;

// 因为这个脚本没有挂载到游戏物体身上,所以不继承MonoBehavior,需要外界传入一个物体。
public SourceManager(GameObject tmpOwner)
{
this.ower = tmpOwner;
Initial();
}

// 初始化
public void Initial()
{
allSources = new List<AudioSource>();

// 默认有3个AudioSource
for (int i = 0; i < 3; i++)
{
// 把3个组件全部添加到游戏物体上
// 把脚本添加到物体上的过程就是实例化
// 所以实例化了3个AudioSource
AudioSource tmpSource = ower.AddComponent<AudioSource>();

allSources.Add(tmpSource);
}
}

// 对外提供的接口,得到空闲的AudioSource
public AudioSource GetFreeAudio()
{
// 怎么找到空闲的?遍历
for (int i =0 ; i < allSources.Count; i++)
{
// 没有在播放就是空闲的
if (!allSources[i].isPlaying)
return allSources[i];

// 都在播放,就创建一个新的
// 久而久之,数组会越来越大
AudioSource tmpSource = ower.AddComponent<AudioSource>();
allSources.Add(tmpSource);

return tmpSource;
}
}

// 给外界调用
// 删除多余的AudioSource,始终维持3个
public void Release()
{
// 计数
int tmpCount = 0;

// 使用中间变量存起来实现删除
List<AudioSource> tmpAudios = new List<AudioSource>();

for (int i = 0; i < allSources.Count; i++)
{
// 没在播放就可以删除
if(!allSources[i].isPlaying)
{
tmpCount++;

if(tmpCount > 3)
{
// 不能在遍历数组的时候,删除数组的成员
// allSources.RemoveAt(i);

// 把多余的对象缓存起来
tmpAudios.Add(allSources[i]);
}
}
}

if (tmpAudios.Count > 0)
{
for (int i = 0; i < tmpAudios.Count; i++)
{
// 从数组中移除
allSources.Remove(tmpAudios[i]);

// 从场景中移除组件
GameObject.Destroy(tmpAudios[i]);
}
}
tmpAudios.Clear();
}

// 根据名字得到正在播放此音效的AudioSource
public AudioSource GetAudioSource(string audioName)
{
for(int i = 0; i < allSources.Count; i++)
{
// 正在播放且名字相等
if(allSources[i].isPlaying && allSources[i].clips.name.Equals(audioName))
{
return allSources[i];
}
}

return null;
}

// 停止播放音效
public void Stop(string audioName)
{
sourceManager.StopAudio(audioName);
}

public void StopAudio(string audioName)
{
AudioSource tmpSource = GetAudioSource(audioName);

if(tmpSource != null)
{
tmpSource.Stop();
}
}
}

ClipManager

管理Clip。

AudioConfig.txt 配置文件,一般也是Excel变成txt

1
2
3
4
3
trainCollision trainCollision.mp3
trainGoing trainGoing.mp3
trainWhistle trainWhistle.mp3

ClipManager.cs

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
using System.Collections;
using System.Collections.Generic;
using System.Text;

// 能不继承MonoBehavior就不继承MonoBehavior
class class ClipManager
{
// 一般都是通过配置文件,读写txt得到
// public string[] clipNames = {"a", "b", "c"};

string[] clipNames;

public SingleClip[] allClips;

// 上面两行是一个字典存也可以。现在这两个数组是一一对应的。这样写速度快,用字典浪费内存。

// 构造的时候把所有东西加载出来
public void ClipManager
{
ReadConfig();
LoadClips();
}

// 读取配置文件
public void ReadConfig()
{
var fileAddress = System.IO.Path.Combine(Application.streamingAssetsPath, "AudioConfig.txt");

FileInfo fInfo0 = new FileInfo(fileAddress);

if (fInfo0.Exists)
{
StreamReader r = new StreamReader(fileAddress);
string tmpLine = r.ReadLine(); // 输出3

int lineCount;

// 试着解析字符串,解析成功,返回到lineCount
if (int.TryParse(tmpLine, out lineCount))
{
clipNames = new string[lineCount];

for (int i =0; i < lineCount; i ++)
{
tmpLine = r.ReadLine();

// 分割字符串变成数组
string[] splits = tmpLine.Split(" ".ToCharArray());

clipNames[i] = splits[0];
}
}

r.Close();
}
}

// 加载Audio clip到内存
public void LoadClips()
{
allClips = new SingleClip[clipNames.Length];

// 加载Audio资源
for (int i = 0; i < clipNames.Length; i++)
{
// 从硬盘里动态加载进来
// 拖曳的方式每次拖一个就实例化了,相当于把所有的clips都放到内存里了,等待播放。
AudioClip tmpClip = Resources.Load(clipNames[i]) as AudioClip;
// 加载进来之后clip应该存起来,用一个类来存
// 加载完成了new这个类
// 为什么用类来存,因为这个AudioClip是需要进行逻辑运算的,需要一个AudioSource
allClips[i] = new SingleClip(tmpClip);
}
}

// 根据名字得到在数组中的索引
public int GetClipIndex(string tmpClipName)
{
for (int i = 0; i < clipNames.Length; i++)
{
// 找到了
if(tmpClipName.Equals(clipNames[i]))
{
return i;
}
}

return -1;
}

// 根据名字找到SingleClip
public SingleClip FindClipByName(string tmpClipName)
{
int tmpIndex = GetClipIndex( tmpClipName);

if(tmpIndex != -1)
{
return allClips[tmpIndex];
}

return null;
}
}

SingleClip

用对象把AudioClip存起来。

SingleClip.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SingleClip
{
AudioClip myClip;

AudioSource source;

public SingleClip(AudioClip tmpClip)
{
myClip = tmpClip;
}

// 给一个空闲的AudioSource就能播放
public void Play(AudioSource tmpSource)
{
tmpSource.clip = myClip;
tmpSource.Play();
}
}

AudioManager

外观类,对外的接口。

AudioManager.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AudioManager : MonoBehavior
{
public static AudioManager Instance;

SourceManager sourceManager;
ClipManager clipManager;

void Awake()
{
Instance = this;

sourceManager = new SourceManager(gameObject);
clipManager = new ClipManager();
}

// 对外提供的接口,传音效的名称就可以播放
public void Play(string audioName)
{
// 拿一个空闲的AudioSource
AudioSource tmpSource = sourceManager.GetFreeAudio();

// 找到clip
SingleClip tmpClip = clipManager.FindClipByName(audioName);


if (tmpClip != null)
{
// 两个结合
tmpClip.Play(tmpSource);
}
}

// 停止播放
public void Stop(string audioName)
{
sourceManager.Stop(audioName);
}
}

使用

UseAudio.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UseAudio : MonoBehavior
{
void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
AudioManager.Instance.Play("trainGoing");
}

if (Input.GetKeyDown(KeyCode.B))
{
AudioManager.Instance.Stop("trainGoing");
}
}
}
0%