【unity框架开发11】从零手搓一个声音/音频/音效管理器,包括2d、3d音效播放,AudioMixer的使用和如何控制音量,实现bgm背景音和BGS环境音的淡入淡出效果(2024/12/3补充)
在游戏开发中,音乐和音效的管理是一个重要的环节。好的音乐和合适的音效可以为游戏增添氛围并提升玩家的体验。为了更好地管理音乐和音效,我们可以封装一个专门的音乐和音效管理器。
最终效果
前言
在游戏开发中,音乐和音效的管理是一个重要的环节。好的音乐和合适的音效可以为游戏增添氛围并提升玩家的体验。为了更好地管理音乐和音效,我们可以封装一个专门的音乐和音效管理器。
一、音频管理器
1、初始化
创建播放BGM背景音乐、Sound音效、BGS环境音效、MS提示音效、Voice角色语音的游戏对象
/// <summary>
/// 音频管理器
/// </summary>
public class AudioManager : SingletonMono<AudioManager>
{
//各个声道的AudioSource组件
AudioSource bgmAudioSource;
AudioSource bgsAudioSource;
AudioSource voiceAudioSource;
//各个声道的游戏对象
GameObject bgmController;
GameObject soundController;
GameObject bgsController;
GameObject msController;
GameObject voiceController;
//控制器的名字
string bgmControllerName = "BgmController";
string soundControllerName = "SoundController";
string bgsControllerName = "BgsController";
string msControllerName = "MsController";
string voiceControllerName = "VoiceController";
void Awake()
{
//创建并设置背景音乐的控制器
bgmController = CreateController(bgmControllerName, transform);
bgmAudioSource = bgmController.AddComponent<AudioSource>();
bgmAudioSource.playOnAwake = false;
bgmAudioSource.loop = true;
//创建音效控制器
soundController = CreateController(soundControllerName, transform);
//创建并设置环境音效的控制器
bgsController = CreateController(bgsControllerName, transform);
bgsAudioSource = bgsController.AddComponent<AudioSource>();
bgsAudioSource.playOnAwake = false;
bgsAudioSource.loop = true;
//创建提示音效的控制器
msController = CreateController(msControllerName, transform);
//创建并设置角色语音的控制器
voiceController = CreateController(voiceControllerName, transform);
voiceAudioSource = voiceController.AddComponent<AudioSource>();
voiceAudioSource.playOnAwake = false;
voiceAudioSource.loop = false;
}
GameObject CreateController(string name, Transform parent)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent);
return go;
}
}
效果
2、播放、暂停、取消暂停、停止BGM
/// <summary>
/// 播放BGM
/// </summary>
/// <param name="bgm">背景音乐</param>
/// <param name="loop">是否循环</param>
public void PlayBGM(AudioClip bgm, bool loop = true)
{
if (bgm == null)
{
Debug.LogWarning("播放BGM失败!要播放的BGM为null");
return;
}
bgmAudioSource.loop = loop;
bgmAudioSource.clip = bgm;
bgmAudioSource.Play();
}
/// <summary>
/// 暂停BGM
/// </summary>
public void PauseBGM()
{
bgmAudioSource.Pause();
}
/// <summary>
/// 取消暂停BGM
/// </summary>
public void UnPauseBGM()
{
bgmAudioSource.UnPause();
}
/// <summary>
/// 停止BGM
/// </summary>
public void StopBGM()
{
bgmAudioSource.Stop();
bgmAudioSource.clip = null;
}
测试调用
public class AudioTest : MonoBehaviour
{
AudioClip BGM1;
AudioClip BGM2;
void Start()
{
BGM1 = Resources.Load<AudioClip>("Audio/Bgm/BGM1");
BGM2 = Resources.Load<AudioClip>("Audio/Bgm/BGM2");
}
private void OnGUI()
{
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
buttonStyle.fontSize = 50; // 设置字体大小
if (GUI.Button(new Rect(0, 0, 400, 150), "播放BGM1", buttonStyle))
{
AudioManager.Instance.PlayBGM(BGM1);
}
if (GUI.Button(new Rect(0, 150, 400, 150), "播放BGM2", buttonStyle))
{
AudioManager.Instance.PlayBGM(BGM2);
}
if (GUI.Button(new Rect(0, 300, 400, 150), "暂停BGM", buttonStyle))
{
AudioManager.Instance.PauseBGM();
}
if (GUI.Button(new Rect(0, 450, 400, 150), "取消暂停BGM", buttonStyle))
{
AudioManager.Instance.UnPauseBGM();
}
if (GUI.Button(new Rect(0, 600, 400, 150), "停止BGM", buttonStyle))
{
AudioManager.Instance.StopBGM();
}
}
}
3、播放2D音效
/// <summary>
/// 播放2D音效
/// </summary>
public void PlaySound(AudioClip sound)
{
//临时的空物体,用来播放音效。
GameObject go = new GameObject(sound.name);
//设置父物体
go.transform.SetParent(soundController.transform);
//如果该游戏对象身上没有AudioSource组件,则添加AudioSource组件并设置参数。
if (!go.TryGetComponent<AudioSource>(out AudioSource audioSource))
{
audioSource = go.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.loop = false;
}
//设置要播放的音效
audioSource.clip = sound;
//播放音效
audioSource.Play();
//每隔1秒检测一次,如果该音效播放完毕,则销毁音效的游戏对象。
StartCoroutine(DestroyWhenFinished());
//每隔1秒检测一次,如果该音效播放完毕,则销毁音效的游戏对象。
IEnumerator DestroyWhenFinished()
{
do
{
yield return new WaitForSeconds(1);
if (go == null || audioSource == null) yield break;//如果播放音频的游戏对象,或者AudioSource组件被销毁了,则直接跳出协程。
} while (audioSource != null && audioSource.time > 0);
if (go != null) Destroy(go);
}
}
调用
AudioManager.Instance.PlaySound(Sound1);
4、在指定目标对象身上播放3D音效
/// </summary>
/// <summary>
/// 在指定目标对象身上播放3D音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="target">目标</param>
public void PlaySound(AudioClip sound, GameObject target)
{
if (sound == null)
{
Debug.LogWarning("播放Sound失败!要播放的Sound为null");
return;
}
if (target == null)
{
Debug.LogWarning("播放Sound失败!无法在目标对象身上播放Sound,因为目标对象为null");
return;
}
//临时的空物体,用来播放音效。
GameObject go = new GameObject(sound.name);
//把用于播放音效的游戏对象放到目标物体之下,作为它的子物体
go.transform.SetParent(target.transform);
go.transform.localPosition = Vector3.zero;
//如果该游戏对象身上没有AudioSource组件,则添加AudioSource组件并设置参数。
if (!go.TryGetComponent<AudioSource>(out AudioSource audioSource))
{
audioSource = go.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.loop = false;
audioSource.spatialBlend = 1f;//3D效果,近大远小。
}
//设置要播放的音频
audioSource.clip = sound;
//播放音频
audioSource.Play();
//每隔1秒检测一次,如果该音频播放完毕,则销毁游戏对象。
StartCoroutine(DestoryWhenFinisied());
//每隔1秒检测一次,如果该音频播放完毕,则销毁游戏对象。
IEnumerator DestoryWhenFinisied()
{
do
{
yield return new WaitForSeconds(1);
if (go == null || audioSource == null) yield break;//如果播放音频的游戏对象,或者AudioSource组件被销毁了,则直接跳出协程。
} while (audioSource != null && audioSource.time > 0);
if (go != null) Destroy(go);
}
}
调用
AudioManager.Instance.PlaySound(Sound2, gameObject);
5、音效对象池
其实之前我们已经写过了对象池管理器:【unity进阶知识7】对象池的使用,如何封装一个对象池管理器
但是为了降低代码耦合性,就不用之前的对象池管理器了,我们从新建一个音频对象池,专门用来对音频服务
新增AudioObjectPool 音频对象池代码
/// <summary>
/// 音频对象池
/// </summary>
public class AudioObjectPool : Singleton<AudioObjectPool>
{
/// <summary>
/// 获取对象
/// </summary>
/// <param name="SoundPool">对象池</param>
/// <param name="parent">父级</param>
/// <returns></returns>
public GameObject Spawn(GameObject SoundPool, Transform parent = null)
{
GameObject go = null;
for (int i = 0; i < SoundPool.transform.childCount; i++)
{
//如果对象池中有,则从对象池中取出来用。
if (!SoundPool.transform.GetChild(i).gameObject.activeSelf)
{
go = SoundPool.transform.GetChild(i).gameObject;
go.SetActive(true);
break;
}
}
//如果对象池中没有,则创建一个游戏对象。
go = go ? go : new GameObject(SoundPool.name + "_Pool");
if (parent) go.transform.SetParent(parent);
go.transform.localPosition = Vector3.zero;
return go;
}
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go">对象</param>
/// <param name="parent">父级</param>
public void Despawn(GameObject go, Transform parent = null)
{
if (parent) go.transform.SetParent(parent);
go.transform.localPosition = Vector3.zero;
go.SetActive(false);
}
}
6、封装用对象池播放音效方法
/// <summary>
/// 播放音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="controller">对象池</param>
/// <param name="worldPosition">世界空间的位置</param>
/// <param name="parent">父级</param>
/// <param name="is3D">是否是3D音效</param>
void PlaySound(AudioClip sound, GameObject controller, Vector3 worldPosition, Transform parent = null, bool is3D = false)
{
if (sound == null)
{
Debug.LogWarning("播放Sound失败!要播放的Sound为null");
return;
}
GameObject go = AudioObjectPool.Instance.Spawn(controller, parent);
//如果该游戏对象身上没有AudioSource组件,则添加AudioSource组件
if (!go.TryGetComponent<AudioSource>(out AudioSource audioSource))
{
audioSource = go.AddComponent<AudioSource>();
}
if(is3D) audioSource.spatialBlend = 1f;//3D效果,近大远小。
go.transform.position = worldPosition;
//播放音效
StartCoroutine(PlaySoundCoroutine(go, sound, controller.transform));
}
/// <summary>
/// 携程控制播放音效
/// </summary>
/// <param name="go">音频对象</param>
/// <param name="sound">音效</param>
/// <param name="parent">回收父级</param>
/// <returns></returns>
IEnumerator PlaySoundCoroutine(GameObject go, AudioClip sound, Transform parent = null)
{
AudioSource audioSource = go.GetComponent<AudioSource>();
//播放音效
audioSource.PlayOneShot(sound);
// 等待音效播放完成
yield return CoroutineTool.WaitForSeconds(sound.length);
// 音效播放完成后的逻辑
audioSource.clip = null;
AudioObjectPool.Instance.Despawn(go, parent);//放入对象池
}
7、使用对象池播放2D音效
/// <summary>
/// 播放2D音效
/// </summary>
public void PlaySound(AudioClip sound)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound2DController], Vector3.zero);
}
8 使用对象池在指定目标对象身上播放3D音效
/// </summary>
/// <summary>
/// 在指定目标对象身上播放3D音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="target">目标</param>
public void PlaySound(AudioClip sound, Transform target)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound3DController], Vector3.zero, target, true);
}
9、使用对象池在世界空间中指定的位置播放3D音效
/// <summary>
/// 在世界空间中指定的位置播放3D音效
/// </summary>
public void PlaySound(AudioClip sound, Vector3 worldPosition)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound3DController], worldPosition, null, true);
}
10、播放、暂停、取消暂停、停止BGS环境音效,和bgm差不多
/// <summary>
/// 播放环境音效
/// </summary>
public void PlayBGS(AudioClip bgs, bool loop = true)
{
if (bgs == null)
{
Debug.LogWarning("播放BGS失败!要播放的BGS为null");
return;
}
bgsAudioSource.loop = loop;
bgsAudioSource.clip = bgs;
bgsAudioSource.Play();
}
/// <summary>
/// 暂停环境音效
/// </summary>
public void PauseBGS()
{
bgsAudioSource.Pause();
}
/// <summary>
/// 取消暂停环境音效
/// </summary>
public void UnPauseBGS()
{
bgsAudioSource.UnPause();
}
/// <summary>
/// 停止BGS
/// </summary>
public void StopBGS()
{
bgsAudioSource.Stop();
bgsAudioSource.clip = null;
}
测试调用
//BGS环境音
if (GUI.Button(new Rect(width*2, 0, width, height), "播放环境音1", buttonStyle))
{
AudioManager.Instance.PlayBGS(BGS1);
}
if (GUI.Button(new Rect(width*2, height, width, height), "播放环境音2", buttonStyle))
{
AudioManager.Instance.PlayBGS(BGS2);
}
if (GUI.Button(new Rect(width*2, height*2, width, height), "暂停环境音", buttonStyle))
{
AudioManager.Instance.PauseBGS();
}
if (GUI.Button(new Rect(width*2, height*3, width, height), "取消暂停环境音", buttonStyle))
{
AudioManager.Instance.UnPauseBGS();
}
if (GUI.Button(new Rect(width*2, height*4, width, height), "停止环境音", buttonStyle))
{
AudioManager.Instance.StopBGS();
}
11、使用对象池播放MS提示音效
/// <summary>
/// 播放提示音效
/// </summary>
/// <param name="ms">提示音效</param>
public void PlayMS(AudioClip ms)
{
PlaySound(ms, audioGameObjects[E_AudioController.MsController], Vector3.zero);
}
12、播放和停止Voice角色语音
当然角色语言也可以通过3D的方式播放,代入感更强,具体取决于项目
/// <summary>
/// 播放角色语音
/// </summary>
public void PlayVoice(AudioClip voice)
{
voiceAudioSource.clip = voice;
voiceAudioSource.Play();
}
/// <summary>
/// 停止角色语音
/// </summary>
public void StopVoice()
{
voiceAudioSource.Stop();
}
13、阶段性代码
E_AudioController.cs
/// <summary>
/// Audio控制器类型枚举
/// </summary>
public enum E_AudioController
{
BgmController,
SoundController,
Sound2DController,
Sound3DController,
BgsController,
MsController,
VoiceController,
}
AudioObjectPool.cs
/// <summary>
/// 获取对象
/// </summary>
/// <param name="SoundPool">对象池</param>
/// <param name="parent">父级</param>
/// <returns></returns>
public GameObject Spawn(GameObject SoundPool, Transform parent = null)
{
GameObject go = null;
for (int i = 0; i < SoundPool.transform.childCount; i++)
{
//如果对象池中有,则从对象池中取出来用。
if (!SoundPool.transform.GetChild(i).gameObject.activeSelf)
{
go = SoundPool.transform.GetChild(i).gameObject;
go.SetActive(true);
break;
}
}
//如果对象池中没有,则创建一个游戏对象。
go = go ? go : new GameObject(SoundPool.name + "_Pool");
if(!parent) parent = SoundPool.transform;
go.transform.SetParent(parent);
go.transform.localPosition = Vector3.zero;
return go;
}
AudioManager.cs
/// <summary>
/// 音频管理器
/// </summary>
public class AudioManager : SingletonMono<AudioManager>
{
#region 初始化
//各个声道的AudioSource组件
AudioSource bgmAudioSource;
AudioSource bgsAudioSource;
AudioSource voiceAudioSource;
//记录每个Audio控制器类型的父物体
private Dictionary<E_AudioController, GameObject> audioGameObjects;
void Awake()
{
//创建并设置背景音乐的控制器
GameObject bgmController = CreateController(E_AudioController.BgmController.ToString(), transform);
bgmAudioSource = bgmController.AddComponent<AudioSource>();
bgmAudioSource.playOnAwake = false;
bgmAudioSource.loop = true;
//创建音效控制器
GameObject soundController = CreateController(E_AudioController.SoundController.ToString(), transform);
GameObject sound2DController = CreateController(E_AudioController.Sound2DController.ToString(), soundController.transform);
GameObject sound3DController = CreateController(E_AudioController.Sound3DController.ToString(), soundController.transform);
//创建并设置环境音效的控制器
GameObject bgsController = CreateController(E_AudioController.BgsController.ToString(), transform);
bgsAudioSource = bgsController.AddComponent<AudioSource>();
bgsAudioSource.playOnAwake = false;
bgsAudioSource.loop = true;
//创建提示音效的控制器
GameObject msController = CreateController(E_AudioController.MsController.ToString(), transform);
//创建并设置角色语音的控制器
GameObject voiceController = CreateController(E_AudioController.VoiceController.ToString(), transform);
voiceAudioSource = voiceController.AddComponent<AudioSource>();
voiceAudioSource.playOnAwake = false;
voiceAudioSource.loop = false;
//记录每个Audio控制器类型的父物体
audioGameObjects = new Dictionary<E_AudioController, GameObject>
{
{ E_AudioController.BgmController, bgmController },
{ E_AudioController.SoundController, soundController },
{ E_AudioController.Sound2DController, sound2DController },
{ E_AudioController.Sound3DController, sound3DController },
{ E_AudioController.BgsController, bgsController },
{ E_AudioController.MsController, msController },
{ E_AudioController.VoiceController, voiceController }
};
}
GameObject CreateController(string name, Transform parent)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent);
return go;
}
#endregion
#region BGM背景音乐
/// <summary>
/// 播放BGM
/// </summary>
/// <param name="bgm">背景音乐</param>
/// <param name="loop">是否循环</param>
public void PlayBGM(AudioClip bgm, bool loop = true)
{
if (bgm == null)
{
Debug.LogWarning("播放BGM失败!要播放的BGM为null");
return;
}
bgmAudioSource.loop = loop;
bgmAudioSource.clip = bgm;
bgmAudioSource.Play();
}
/// <summary>
/// 暂停BGM
/// </summary>
public void PauseBGM()
{
bgmAudioSource.Pause();
}
/// <summary>
/// 取消暂停BGM
/// </summary>
public void UnPauseBGM()
{
bgmAudioSource.UnPause();
}
/// <summary>
/// 停止BGM
/// </summary>
public void StopBGM()
{
bgmAudioSource.Stop();
bgmAudioSource.clip = null;
}
#endregion
#region 2D3D音效
/// <summary>
/// 播放2D音效
/// </summary>
public void PlaySound(AudioClip sound)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound2DController], Vector3.zero);
}
/// </summary>
/// <summary>
/// 在指定目标对象身上播放3D音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="target">目标</param>
public void PlaySound(AudioClip sound, Transform target)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound3DController], Vector3.zero, target, true);
}
/// <summary>
/// 在世界空间中指定的位置播放3D音效
/// </summary>
public void PlaySound(AudioClip sound, Vector3 worldPosition)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound3DController], worldPosition, null, true);
}
#endregion
#region BGS环境音效
/// <summary>
/// 播放环境音效
/// </summary>
public void PlayBGS(AudioClip bgs, bool loop = true)
{
if (bgs == null)
{
Debug.LogWarning("播放BGS失败!要播放的BGS为null");
return;
}
bgsAudioSource.loop = loop;
bgsAudioSource.clip = bgs;
bgsAudioSource.Play();
}
/// <summary>
/// 暂停环境音效
/// </summary>
public void PauseBGS()
{
bgsAudioSource.Pause();
}
/// <summary>
/// 取消暂停环境音效
/// </summary>
public void UnPauseBGS()
{
bgsAudioSource.UnPause();
}
/// <summary>
/// 停止BGS
/// </summary>
public void StopBGS()
{
bgsAudioSource.Stop();
bgsAudioSource.clip = null;
}
#endregion
#region 提示音效
/// <summary>
/// 播放提示音效
/// </summary>
/// <param name="ms">提示音效</param>
public void PlayMS(AudioClip ms)
{
PlaySound(ms, audioGameObjects[E_AudioController.MsController], Vector3.zero);
}
#endregion
#region 角色语言
/// <summary>
/// 播放角色语音
/// </summary>
public void PlayVoice(AudioClip voice)
{
voiceAudioSource.clip = voice;
voiceAudioSource.Play();
}
/// <summary>
/// 停止角色语音
/// </summary>
public void StopVoice()
{
voiceAudioSource.Stop();
}
#endregion
#region 封装播放音效方法
/// <summary>
/// 播放音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="controller">对象池</param>
/// <param name="worldPosition">世界空间的位置</param>
/// <param name="parent">父级</param>
/// <param name="is3D">是否是3D音效</param>
void PlaySound(AudioClip sound, GameObject controller, Vector3 worldPosition, Transform parent = null, bool is3D = false)
{
if (sound == null)
{
Debug.LogWarning("播放Sound失败!要播放的Sound为null");
return;
}
GameObject go = AudioObjectPool.Instance.Spawn(controller, parent);
//如果该游戏对象身上没有AudioSource组件,则添加AudioSource组件
if (!go.TryGetComponent<AudioSource>(out AudioSource audioSource))
{
audioSource = go.AddComponent<AudioSource>();
}
if(is3D) audioSource.spatialBlend = 1f;//3D效果,近大远小。
go.transform.position = worldPosition;
//播放音效
StartCoroutine(PlaySoundCoroutine(go, sound, controller.transform));
}
/// <summary>
/// 携程控制播放音效
/// </summary>
/// <param name="go">音频对象</param>
/// <param name="sound">音效</param>
/// <param name="parent">回收父级</param>
/// <returns></returns>
IEnumerator PlaySoundCoroutine(GameObject go, AudioClip sound, Transform parent = null)
{
AudioSource audioSource = go.GetComponent<AudioSource>();
//播放音效
audioSource.PlayOneShot(sound);
// 等待音效播放完成
yield return CoroutineTool.WaitForSeconds(sound.length);
// 音效播放完成后的逻辑
audioSource.clip = null;
AudioObjectPool.Instance.Despawn(go, parent);//放入对象池
}
#endregion
}
三、AudioMixer混音器的作用
1、Audio Mixer简单的使用
1.1、创建Audio Mixer
创建方式:创建一个Mixer文件夹——>鼠标右键——>Create——>Audio Mixer即可
1.2、新建不同的音频控制器组
master就是主音量,里面每个又相互独立,可以单独控制各模块的音量
1.3、配置音频控制器组
1.4、调整音量
1.5、暴露音量控制参数
依次按照上图操作,得到下图,修改参数
代码控制音量变化
public AudioMixer audioMixer; // 进行控制的Mixer变量
public void SetMasterVolume(float volume) // 控制主音量的函数
{
// MasterVolume为我们暴露出来的Master的参数
audioMixer.SetFloat("MasterVolume", volume);
}
2、AudioMixer和我们的音效管理器结合,控制音量
绑定混音器控制器组
bgmAudioSource.outputAudioMixerGroup = audioMixer.FindMatchingGroups("BGM")[0];
音量控制,注意重置音量,不要用audioMixer.ClearFloat,获取音量时会有所延迟
/// <summary>
/// 设置总音量
/// </summary>
/// <param name="volume">音量</param>
public void SetMasterVolume(float volume)
{
if (volume > 20f || volume < -80f)
{
Debug.LogWarning("音量必须在20到60之间");
return;
}
audioMixer.SetFloat(MatserAudioMixerVolumeName, volume);
}
/// <summary>
/// 设置BGM音量
/// </summary>
/// <param name="volume">音量</param>
public void SetBGMVolume(float volume)
{
// 确保 volume 在 0 到 1 之间
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(BGMAudioMixerVolumeName, volume);
}
/// <summary>
/// 设置音效音量
/// </summary>
/// <param name="volume">音量</param>
public void SetSoundVolume(float volume)
{
// 确保 volume 在 0 到 1 之间
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(SoundAudioMixerVolumeName, volume);
}
/// <summary>
/// 设置环境音音量
/// </summary>
/// <param name="volume">音量</param>
public void SetBGSVolume(float volume)
{
// 确保 volume 在 0 到 1 之间
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(BGSAudioMixerVolumeName, volume);
}
/// <summary>
/// 设置角色语音音量
/// </summary>
/// <param name="volume">音量</param>
public void SetVoiceVolume(float volume)
{
// 确保 volume 在 0 到 1 之间
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(VoiceAudioMixerVolumeName, volume);
}
/// <summary>
/// 重置音量
/// </summary>
public void ResetVolume()
{
SetMasterVolume(0.8f);
SetBGMVolume(0.8f);
SetSoundVolume(0.8f);
SetBGSVolume(0.8f);
SetVoiceVolume(0.8f);
}
调用
if (GUI.Button(new Rect(width*5, 0, width, height), "设置总音量为1", buttonStyle))
{
AudioManager.Instance.SetMasterVolume(1);
}
if (GUI.Button(new Rect(width*5, height, width, height), "设置BGM音量为0.8", buttonStyle))
{
AudioManager.Instance.SetBGMVolume(0.8f);
}
if (GUI.Button(new Rect(width*5, height*2, width, height), "设置音效音量为0.5", buttonStyle))
{
AudioManager.Instance.SetSoundVolume(0.5f);
}
if (GUI.Button(new Rect(width*5, height*3, width, height), "设置环境音音量为0.2", buttonStyle))
{
AudioManager.Instance.SetBGSVolume(0.2f);
}
if (GUI.Button(new Rect(width*5, height*4, width, height), "设置角色语音音量为0", buttonStyle))
{
AudioManager.Instance.SetVoiceVolume(0);
}
if (GUI.Button(new Rect(width*5, height*5, width, height), "重置音量", buttonStyle))
{
AudioManager.Instance.ResetVolume();
}
3、实现bgm背景音和BGS环境音的淡入淡出效果,使切换不会那么突兀
新增携程,控制声音淡入淡出
//声音淡入淡出
private IEnumerator FadeOutAndPlay(AudioSource audioSource, AudioClip newBGM, float duration, bool loop, string VolumeName)
{
// 获取当前音量值
audioMixer.GetFloat(VolumeName, out float startVolume);
// 淡出当前 BGM
if (audioSource.clip){
yield return StartCoroutine(FadeAudio(-30f, duration, VolumeName)); // 将音量降到 -30dB,持续 duration 秒
}else{
audioMixer.SetFloat(VolumeName, -30f);
}
// 播放新 BGM
audioSource.loop = loop;
audioSource.clip = newBGM;
audioSource.Play();
// 淡入新 BGM
yield return StartCoroutine(FadeAudio(startVolume, duration, VolumeName)); // 将音量升回startVolume,持续 duration 秒
}
private IEnumerator FadeAudio(float targetVolume, float duration, string VolumeName)
{
// 获取当前音量值
audioMixer.GetFloat(VolumeName, out float startVolume);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float newVolume = Mathf.Lerp(startVolume, targetVolume, elapsed / duration);
audioMixer.SetFloat(VolumeName, newVolume);
yield return CoroutineTool.WaitForFrame();
}
// 确保最终音量准确
audioMixer.SetFloat(VolumeName, targetVolume);
}
修改BGM和BGS播放方法
/// <summary>
/// 播放BGM
/// </summary>
/// <param name="bgm">背景音乐</param>
/// <param name="duration">过渡时间</param>
/// <param name="loop">是否循环</param>
/// <param name="isFade">是否淡入淡出</param>
public void PlayBGM(AudioClip bgm, float duration = 10f, bool loop = true, bool isFade = true)
{
if (bgm == null)
{
Debug.LogWarning("播放BGM失败!要播放的BGM为null");
return;
}
if (isFade)
{
StartCoroutine(FadeOutAndPlay(bgmAudioSource, bgm, duration, loop, BGMVolumeName));
}
else
{
bgmAudioSource.loop = loop;
bgmAudioSource.clip = bgm;
bgmAudioSource.Play();
}
}
/// <summary>
/// 播放环境音
/// </summary>
/// <param name="bgs">环境音</param>
/// <param name="duration">过渡时间</param>
/// <param name="loop">是否循环</param>
/// <param name="isFade">是否淡入淡出</param>
public void PlayBGS(AudioClip bgs, float duration = 10f, bool loop = true, bool isFade = true)
{
if (bgs == null)
{
Debug.LogWarning("播放BGS失败!要播放的BGS为null");
return;
}
if (isFade)
{
StartCoroutine(FadeOutAndPlay(bgsAudioSource, bgs, duration, loop, BGSVolumeName));
}
else
{
bgsAudioSource.loop = loop;
bgsAudioSource.clip = bgs;
bgsAudioSource.Play();
}
}
4、获取音量
public float MatserVolume => GetVolume(MatserVolumeName);
public float BGMVolume => GetVolume(BGMVolumeName);
public float BGSVolume => GetVolume(BGSVolumeName);
public float SoundVolum => GetVolume(SoundVolumeName);
public float VoiceVolume => GetVolume(VoiceVolumeName);
public float GetVolume(string VolumeName){
audioMixer.GetFloat(VolumeName, out float volume);
Debug.Log(volume);
// 使用 Mathf.Lerp 将 volume 从[-80, 20]映射到[0, 1]
volume = Mathf.Lerp(0f, 1f, (volume + 80f) / 100f);;
Debug.Log(volume);
return volume;
}
测试调用
//获取音量
float matserVolume = AudioManager.Instance.MatserVolume;
float bGMVolume = AudioManager.Instance.BGMVolume;
float bGSVolume = AudioManager.Instance.BGSVolume;
float soundVolum = AudioManager.Instance.SoundVolum;
float voiceVolume = AudioManager.Instance.VoiceVolume;
最终AudioManager代码
/// <summary>
/// 音频管理器
/// </summary>
public class AudioManager : SingletonMono<AudioManager>
{
#region 参数
//各个声道的AudioSource组件
AudioSource bgmAudioSource;
AudioSource bgsAudioSource;
AudioSource voiceAudioSource;
//记录每个Audio控制器类型的父物体
private Dictionary<E_AudioController, GameObject> audioGameObjects;
//混音器
AudioMixer audioMixer;
//混音器控制器组
string BGMAudioMixerGroupName = "BGM";
string SoundAudioMixerGroupName = "Sound";
string BGSAudioMixerGroupName = "BGS";
string VoiceAudioMixerGroupName = "Voice";
//混音器控制器组暴露的音量变量
string MatserVolumeName = "MasterVolume";
string BGMVolumeName = "BGMVolume";
string SoundVolumeName = "SoundVolume";
string BGSVolumeName = "BGSVolume";
string VoiceVolumeName = "VoiceVolume";
//是否正在淡入淡出
private bool isBGMFading = false;
private bool isBGSFading = false;
#endregion
#region 初始化
void Awake()
{
//加载混音器
audioMixer = Resources.Load<AudioMixer>("AudioMixer/AudioMixer");
//创建并设置背景音乐的控制器
GameObject bgmController = CreateController(E_AudioController.BgmController.ToString(), transform);
bgmAudioSource = bgmController.AddComponent<AudioSource>();
bgmAudioSource.playOnAwake = false;
bgmAudioSource.loop = true;
bgmAudioSource.outputAudioMixerGroup = audioMixer.FindMatchingGroups(BGMAudioMixerGroupName)[0];
//创建音效控制器
GameObject soundController = CreateController(E_AudioController.SoundController.ToString(), transform);
GameObject sound2DController = CreateController(E_AudioController.Sound2DController.ToString(), soundController.transform);
GameObject sound3DController = CreateController(E_AudioController.Sound3DController.ToString(), soundController.transform);
//创建并设置环境音效的控制器
GameObject bgsController = CreateController(E_AudioController.BgsController.ToString(), transform);
bgsAudioSource = bgsController.AddComponent<AudioSource>();
bgsAudioSource.playOnAwake = false;
bgsAudioSource.loop = true;
bgsAudioSource.outputAudioMixerGroup = audioMixer.FindMatchingGroups(BGSAudioMixerGroupName)[0];
//创建提示音效的控制器
GameObject msController = CreateController(E_AudioController.MsController.ToString(), transform);
//创建并设置角色语音的控制器
GameObject voiceController = CreateController(E_AudioController.VoiceController.ToString(), transform);
voiceAudioSource = voiceController.AddComponent<AudioSource>();
voiceAudioSource.playOnAwake = false;
voiceAudioSource.loop = false;
voiceAudioSource.outputAudioMixerGroup = audioMixer.FindMatchingGroups(VoiceAudioMixerGroupName)[0];
//记录每个Audio控制器类型的父物体
audioGameObjects = new Dictionary<E_AudioController, GameObject>
{
{ E_AudioController.BgmController, bgmController },
{ E_AudioController.SoundController, soundController },
{ E_AudioController.Sound2DController, sound2DController },
{ E_AudioController.Sound3DController, sound3DController },
{ E_AudioController.BgsController, bgsController },
{ E_AudioController.MsController, msController },
{ E_AudioController.VoiceController, voiceController }
};
}
GameObject CreateController(string name, Transform parent)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent);
return go;
}
#endregion
#region BGM背景音乐
/// <summary>
/// 播放BGM
/// </summary>
/// <param name="bgm">背景音乐</param>
/// <param name="duration">过渡时间</param>
/// <param name="loop">是否循环</param>
/// <param name="isFade">是否淡入淡出</param>
public void PlayBGM(AudioClip bgm, float duration = 10f, bool loop = true, bool isFade = true)
{
if (bgm == null)
{
Debug.LogWarning("播放BGM失败!要播放的BGM为null");
return;
}
if (isBGMFading)
{
Debug.LogWarning("当前BGM正在淡入淡出,请稍后再试!");
return;
}
if (isFade)
{
isBGMFading = true;
StartCoroutine(FadeOutAndPlay(bgmAudioSource, bgm, duration, loop, BGMVolumeName));
}
else
{
bgmAudioSource.loop = loop;
bgmAudioSource.clip = bgm;
bgmAudioSource.Play();
}
}
/// <summary>
/// 暂停BGM
/// </summary>
public void PauseBGM()
{
bgmAudioSource.Pause();
}
/// <summary>
/// 取消暂停BGM
/// </summary>
public void UnPauseBGM()
{
bgmAudioSource.UnPause();
}
/// <summary>
/// 停止BGM
/// </summary>
public void StopBGM()
{
bgmAudioSource.Stop();
bgmAudioSource.clip = null;
}
#endregion
#region 角色语音
/// <summary>
/// 播放角色语音
/// </summary>
public void PlayVoice(AudioClip voice)
{
voiceAudioSource.clip = voice;
voiceAudioSource.Play();
}
/// <summary>
/// 停止角色语音
/// </summary>
public void StopVoice()
{
voiceAudioSource.Stop();
}
#endregion
#region BGS环境音
/// <summary>
/// 播放环境音
/// </summary>
/// <param name="bgs">环境音</param>
/// <param name="duration">过渡时间</param>
/// <param name="loop">是否循环</param>
/// <param name="isFade">是否淡入淡出</param>
public void PlayBGS(AudioClip bgs, float duration = 10f, bool loop = true, bool isFade = true)
{
if (bgs == null)
{
Debug.LogWarning("播放BGS失败!要播放的BGS为null");
return;
}
if (isBGSFading)
{
Debug.LogWarning("当前BGS正在淡入淡出,请稍后再试!");
return;
}
if (isFade)
{
// 停止当前对象上所有正在运行的协程
isBGSFading = true;
StartCoroutine(FadeOutAndPlay(bgsAudioSource, bgs, duration, loop, BGSVolumeName));
}
else
{
bgsAudioSource.loop = loop;
bgsAudioSource.clip = bgs;
bgsAudioSource.Play();
}
}
/// <summary>
/// 暂停环境音效
/// </summary>
public void PauseBGS()
{
bgsAudioSource.Pause();
}
/// <summary>
/// 取消暂停环境音效
/// </summary>
public void UnPauseBGS()
{
bgsAudioSource.UnPause();
}
/// <summary>
/// 停止BGS
/// </summary>
public void StopBGS()
{
bgsAudioSource.Stop();
bgsAudioSource.clip = null;
}
#endregion
#region 2D3D音效
/// <summary>
/// 播放2D音效
/// </summary>
public void PlaySound(AudioClip sound)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound2DController], Vector3.zero);
}
/// </summary>
/// <summary>
/// 在指定目标对象身上播放3D音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="target">目标</param>
public void PlaySound(AudioClip sound, Transform target)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound3DController], Vector3.zero, target, true);
}
/// <summary>
/// 在世界空间中指定的位置播放3D音效
/// </summary>
public void PlaySound(AudioClip sound, Vector3 worldPosition)
{
PlaySound(sound, audioGameObjects[E_AudioController.Sound3DController], worldPosition, null, true);
}
#endregion
#region 提示音效
/// <summary>
/// 播放提示音效
/// </summary>
/// <param name="ms">提示音效</param>
public void PlayMS(AudioClip ms)
{
PlaySound(ms, audioGameObjects[E_AudioController.MsController], Vector3.zero);
}
#endregion
#region 封装播放音效方法
/// <summary>
/// 播放音效
/// </summary>
/// <param name="sound">音效</param>
/// <param name="controller">对象池</param>
/// <param name="worldPosition">世界空间的位置</param>
/// <param name="parent">父级</param>
/// <param name="is3D">是否是3D音效</param>
void PlaySound(AudioClip sound, GameObject controller, Vector3 worldPosition, Transform parent = null, bool is3D = false)
{
if (sound == null)
{
Debug.LogWarning("播放Sound失败!要播放的Sound为null");
return;
}
GameObject go = AudioObjectPool.Instance.Spawn(controller, parent);
//如果该游戏对象身上没有AudioSource组件,则添加AudioSource组件
if (!go.TryGetComponent<AudioSource>(out AudioSource audioSource))
{
audioSource = go.AddComponent<AudioSource>();
}
if (is3D) audioSource.spatialBlend = 1f;//3D效果,近大远小。
audioSource.outputAudioMixerGroup = audioMixer.FindMatchingGroups(SoundAudioMixerGroupName)[0];
go.transform.position = worldPosition;
//播放音效
StartCoroutine(PlaySoundCoroutine(go, sound, controller.transform));
}
/// <summary>
/// 携程控制播放音效
/// </summary>
/// <param name="go">音频对象</param>
/// <param name="sound">音效</param>
/// <param name="parent">回收父级</param>
/// <returns></returns>
IEnumerator PlaySoundCoroutine(GameObject go, AudioClip sound, Transform parent = null)
{
AudioSource audioSource = go.GetComponent<AudioSource>();
//播放音效
audioSource.PlayOneShot(sound);
// 等待音效播放完成
yield return CoroutineTool.WaitForSeconds(sound.length);
// 音效播放完成后的逻辑
audioSource.clip = null;
AudioObjectPool.Instance.Despawn(go, parent);//放入对象池
}
#endregion
#region 设置音量
/// <summary>
/// 设置总音量
/// </summary>
/// <param name="volume">音量</param>
public void SetMasterVolume(float volume)
{
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(MatserVolumeName, volume);
}
/// <summary>
/// 设置BGM音量
/// </summary>
/// <param name="volume">音量</param>
public void SetBGMVolume(float volume)
{
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(BGMVolumeName, volume);
}
/// <summary>
/// 设置音效音量
/// </summary>
/// <param name="volume">音量</param>
public void SetSoundVolume(float volume)
{
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(SoundVolumeName, volume);
}
/// <summary>
/// 设置环境音音量
/// </summary>
/// <param name="volume">音量</param>
public void SetBGSVolume(float volume)
{
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(BGSVolumeName, volume);
}
/// <summary>
/// 设置角色语音音量
/// </summary>
/// <param name="volume">音量</param>
public void SetVoiceVolume(float volume)
{
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-80, 20]
volume = Mathf.Lerp(-80f, 20f, volume);
audioMixer.SetFloat(VoiceVolumeName, volume);
}
/// <summary>
/// 重置音量
/// </summary>
public void ResetVolume()
{
SetMasterVolume(0.8f);
SetBGMVolume(0.8f);
SetSoundVolume(0.8f);
SetBGSVolume(0.8f);
SetVoiceVolume(0.8f);
}
#endregion
#region 获取音量
public float MatserVolume => GetVolume(MatserVolumeName);
public float BGMVolume => GetVolume(BGMVolumeName);
public float BGSVolume => GetVolume(BGSVolumeName);
public float SoundVolum => GetVolume(SoundVolumeName);
public float VoiceVolume => GetVolume(VoiceVolumeName);
public float GetVolume(string VolumeName){
audioMixer.GetFloat(VolumeName, out float volume);
// 使用 Mathf.Lerp 将 volume 从[-80, 20]映射到[0, 1]
volume = Mathf.Lerp(0f, 1f, (volume + 80f) / 100f);
return volume;
}
#endregion
#region 声音淡入淡出
//声音淡入淡出
private IEnumerator FadeOutAndPlay(AudioSource audioSource, AudioClip newBGM, float duration, bool loop, string VolumeName)
{
// 获取当前音量值
audioMixer.GetFloat(VolumeName, out float startVolume);
// 淡出当前 BGM
if (audioSource.clip)
{
yield return StartCoroutine(FadeAudio(-30f, duration, VolumeName)); // 将音量降到 -30dB,持续 duration 秒
}
else
{
audioMixer.SetFloat(VolumeName, -30f);
}
// 播放新 BGM
audioSource.loop = loop;
audioSource.clip = newBGM;
audioSource.Play();
// 淡入新 BGM
yield return StartCoroutine(FadeAudio(startVolume, duration, VolumeName)); // 将音量升回startVolume,持续 duration 秒
isBGMFading = false;
isBGSFading = false;
}
private IEnumerator FadeAudio(float targetVolume, float duration, string VolumeName)
{
// 获取当前音量值
audioMixer.GetFloat(VolumeName, out float startVolume);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float newVolume = Mathf.Lerp(startVolume, targetVolume, elapsed / duration);
audioMixer.SetFloat(VolumeName, newVolume);
yield return CoroutineTool.WaitForFrame();
}
// 确保最终音量准确
audioMixer.SetFloat(VolumeName, targetVolume);
}
#endregion
}
四、制作设置音量面板
可以和我们之前做的UI框架配合,制作一个设置音量面板
1、UI绘制
2、SettingsUI参考代码
public class SettingsUI : UIBase {
void Awake(){
OnBtnClick("bg/退出按钮", OnCloseBtn);
OnBtnClick("bg/重置按钮", OnResetBtn);
OnSliderChanged("bg/Volume/Master/Slide", OnSliderMasterVolume);
OnSliderChanged("bg/Volume/BGM/Slide", OnSliderBGMVolume);
OnSliderChanged("bg/Volume/BGS/Slide", OnSliderBGSVolume);
OnSliderChanged("bg/Volume/Sound/Slide", OnSliderSoundVolume);
OnSliderChanged("bg/Volume/Voice/Slide", OnSliderVoiceVolume);
SetSliderValue();
}
void OnCloseBtn(){
//关闭界面
Close();
//提示
UIManager.Instance.ShowTips("关闭界面!", Color.red);
}
void OnResetBtn(){
UIManager.Instance.ShowTips("重置成功!", Color.blue);
AudioManager.Instance.ResetVolume();
SetSliderValue();
}
//修改总音量
void OnSliderMasterVolume(float value){
AudioManager.Instance.SetMasterVolume(value);
}
void OnSliderBGMVolume(float value){
AudioManager.Instance.SetBGMVolume(value);
}
void OnSliderBGSVolume(float value){
AudioManager.Instance.SetBGSVolume(value);
}
void OnSliderSoundVolume(float value){
AudioManager.Instance.SetSoundVolume(value);
}
void OnSliderVoiceVolume(float value){
AudioManager.Instance.SetVoiceVolume(value);
}
void SetSliderValue(){
transform.Find("bg/Volume/Master/Slide").GetComponent<Slider>().value = AudioManager.Instance.MatserVolume;
transform.Find("bg/Volume/BGM/Slide").GetComponent<Slider>().value = AudioManager.Instance.BGMVolume;
transform.Find("bg/Volume/BGS/Slide").GetComponent<Slider>().value = AudioManager.Instance.BGSVolume;
transform.Find("bg/Volume/Sound/Slide").GetComponent<Slider>().value = AudioManager.Instance.SoundVolum;
transform.Find("bg/Volume/Voice/Slide").GetComponent<Slider>().value = AudioManager.Instance.VoiceVolume;
}
}
3、打开设置面板
UIManager.Instance.ShowUI<SettingsUI>();
4、效果
音量映射范围取为[-20,0](2024/12/3补充)
这个建议来源于评论大佬的补充,SetXXXVolume函数将[0,1]映射到[-80,20],这里映射范围取为[-20,0]即原音量的0.1-1倍会更符合需求,也会使slider的调整更平滑一些。
确实,将映射范围改为 [-20, 0] 更符合用户调节音量时的需求,特别是当你希望音量调整更加精细和平滑时。这个范围使得音量变化不会太过于急剧,特别是在较低音量时调整会更加平滑,提升了用户体验。
改进后的代码类似:
/// <summary>
/// 设置总音量
/// </summary>
/// <param name="volume">音量</param>
public void SetMasterVolume(float volume)
{
if (volume < 0f || volume > 1f)
{
Debug.LogWarning("音量必须在 0 到 1 之间");
return;
}
// 使用 Mathf.Lerp 将 volume 从 [0, 1] 映射到 [-20, 0]
volume = Mathf.Lerp(-20f, 0f, volume);
audioMixer.SetFloat(MatserVolumeName, volume);
}
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
更多推荐
所有评论(0)