11. 声音管理器
- 背景音乐
- 播放、暂停、恢复、停止、音量
- 音效
- 播放、音量
Scripts/Framework/Manager/创建SoundManager.cs,给BuildResources/Audio下面添加音频
public class SoundManager : MonoBehaviour
{
AudioSource m_MusicAudio;//背景音乐
AudioSource m_SoundAudio;//音效
private float SoundVolume
{
get { return PlayerPrefs.GetFloat("SoundVolume", 1.0f); }
set
{
m_SoundAudio.volume = value;
PlayerPrefs.SetFloat("SoundVolume", value);
}
}
private float MusicVolume
{
get { return PlayerPrefs.GetFloat("MusicVolume", 1.0f); }
set
{
m_MusicAudio.volume = value;
PlayerPrefs.SetFloat("MusicVolume", value);
}
}
private void Awake()
{
//背景音乐需要循环,并且不能直接播放,因为背景音乐肯定有
m_MusicAudio = this.gameObject.AddComponent<AudioSource>();
m_MusicAudio.playOnAwake = false;
m_MusicAudio.loop = true;
//音效不需要循环,音效不一定有,不用设置playOnAwake
m_SoundAudio = this.gameObject.AddComponent<AudioSource>();
m_SoundAudio.loop = false;
}
public void PlayMusic(string name)
{
//音量小于0.1f因为听不见了,同时节省开销,直接不播放了
if (this.MusicVolume < 0.1f)
{
return;
}
//如果背景音乐是正在播放的,也跳过
string oldName = "";
if (m_MusicAudio.clip != null)
{
oldName = m_MusicAudio.clip.name;
}
if (oldName == name.Substring(0,name.IndexOf(".")))
{
//m_MusicAudio.Play();
return;
}
//否则就去播放背景音乐
Manager.Resource.LoadMusic(name, (UnityEngine.Object obj) =>
{
m_MusicAudio.clip = obj as AudioClip;
m_MusicAudio.Play();
});
}
//暂停播放背景音乐
public void PauseMusic()
{
m_MusicAudio.Pause();
}
//继续播放背景音乐
public void OnUnPauseMusic()
{
m_MusicAudio.UnPause();
}
//停止播放背景音乐
public void StopMusic()
{
m_MusicAudio.clip = null;
m_MusicAudio.Stop();
}
//设置背景音乐音量
public void SetMusicVolume(float value)
{
this.MusicVolume = value;
}
//设置音效音量
public void SetSoundVolume(float value)
{
this.SoundVolume = value;
}
}
设计UI
private static SoundManager _sound;
public static SoundManager Sound
{
get { return _sound; }
}
public void Awake()
{
_resource = this.gameObject.AddComponent<ResourceManager>();
_lua = this.gameObject.AddComponent<LuaManager>();
_ui = this.gameObject.AddComponent<UIManager>();
_entity = this.gameObject.AddComponent<EntityManager>();
_scene = this.gameObject.AddComponent<MySceneManager>();
_sound = this.gameObject.AddComponent<SoundManager>();
}
XLua.CSharpCallLua
如果希望把一个lua函数适配到一个C# delegate(一类是C#侧各种回调:UI事件,delegate参数,比如List:ForEach;另外一类场景是通过LuaTable的Get函数指明一个lua函数绑定到一个delegate)。或者把一个lua table适配到一个C# interface,该delegate或者interface需要加上该配置。
:::info
如果lua需要添加Action,并且带参数,需要添加这个标签
:::
[CSharpCallLua]
public static List<Type> mymodule_cs_call_lua_list = new List<Type>()
{
typeof(UnityEngine.Events.UnityAction<float>),
};
为什么需要这么作,因为Slider的Action要传float并且,lua中的Action也要传float,无参数的不会报错,有参数需要在静态列表中添加说明。添加完之后,在XLua-GenerateCode一下,就可以用了。
再次构建Bundle会报错,因为这些扩展方法是编辑器下使用的,lua不允许使用,避免编辑器方法生成code代码,需要生成黑名单列表
如果出现不播放声音的问题,是因为delegate之前出现的错误,导致直接把音量0写进playerprefs了,所以一直没有办法播放,音量为0.
//黑名单
[BlackList]
public static List<List<string>> BlackList = new List<List<string>>() {
new List<string>(){"UnityEngine.Light", "ShadowRadius"},
new List<string>(){"UnityEngine.Light", "shadowRadius"},
new List<string>(){"UnityEngine.Light", "SetLightDirty"},
new List<string>(){"UnityEngine.Light", "shadowAngle"},
};
重新构建Bundle就可以工作了。
function OnInit()
print("lua OnInit")
end
function OnOpen()
print("lua OnOpen")
--Manager.Scene:LoadScene("Test01","scene.Scene01")
local btn_play_music = self.transform:Find("Options Panel/ButtonsAndSlider/Play Music/Play Music On Button"):GetComponent("Button");
local btn_stop_music = self.transform:Find("Options Panel/ButtonsAndSlider/Play Music/Play Music Off Button"):GetComponent("Button");
local btn_pause_music = self.transform:Find("Options Panel/ButtonsAndSlider/Pause Music/Pause Music On Button"):GetComponent("Button");
local btn_unpause_music = self.transform:Find("Options Panel/ButtonsAndSlider/Pause Music/Pause Music Off Button"):GetComponent("Button");
local btn_play_sound = self.transform:Find("Options Panel/ButtonsAndSlider/Play Sound/Play Sound Button"):GetComponent("Button");
local slider_music_volume = self.transform:Find("Options Panel/ButtonsAndSlider/Music Volume/Music Volume Slider"):GetComponent("Slider");
local slider_sound_volume = self.transform:Find("Options Panel/ButtonsAndSlider/Sound Volume/Sound Volume Slider"):GetComponent("Slider");
btn_play_music.onClick:AddListener(
function()
Manager.Sound:PlayMusic("main.mp3");
end
)
btn_stop_music.onClick:AddListener(
function()
Manager.Sound:StopMusic();
end
)
btn_pause_music.onClick:AddListener(
function()
Manager.Sound:PauseMusic();
end
)
btn_unpause_music.onClick:AddListener(
function()
Manager.Sound:OnUnPauseMusic();
end
)
btn_play_sound.onClick:AddListener(
function()
Manager.Sound:PlaySound("ui_select.mp3");
end
)
slider_music_volume.onValueChanged:AddListener(
function(volume)
Manager.Sound:SetMusicVolume(volume);
print(volume);
end
)
slider_sound_volume.onValueChanged:AddListener(
function(volume)
Manager.Sound:SetSoundVolume(volume);
print(volume);
end
)
slider_music_volume.value = 1;
slider_sound_volume.value = 1;
end
function Update()
--print("lua Update")
end
function OnClose()
print("lua OnClose")
end
由于Action没有在退出前清理掉,会报错,虽然不影响,但还是休整一下。
function OnInit()
print("lua OnInit")
end
function OnOpen()
print("lua OnOpen")
--Manager.Scene:LoadScene("Test01","scene.Scene01")
btn_play_music = self.transform:Find("Options Panel/ButtonsAndSlider/Play Music/Play Music On Button"):GetComponent("Button");
btn_stop_music = self.transform:Find("Options Panel/ButtonsAndSlider/Play Music/Play Music Off Button"):GetComponent("Button");
btn_pause_music = self.transform:Find("Options Panel/ButtonsAndSlider/Pause Music/Pause Music On Button"):GetComponent("Button");
btn_unpause_music = self.transform:Find("Options Panel/ButtonsAndSlider/Pause Music/Pause Music Off Button"):GetComponent("Button");
btn_play_sound = self.transform:Find("Options Panel/ButtonsAndSlider/Play Sound/Play Sound Button"):GetComponent("Button");
slider_music_volume = self.transform:Find("Options Panel/ButtonsAndSlider/Music Volume/Music Volume Slider"):GetComponent("Slider");
slider_sound_volume = self.transform:Find("Options Panel/ButtonsAndSlider/Sound Volume/Sound Volume Slider"):GetComponent("Slider");
slider_music_volume.value = Manager.Sound.MusicVolume;
slider_music_volume.value = Manager.Sound.SoundVolume;
btn_play_music.onClick:AddListener(
function()
Manager.Sound:PlayMusic("main.mp3");
end
)
btn_stop_music.onClick:AddListener(
function()
Manager.Sound:StopMusic();
end
)
btn_pause_music.onClick:AddListener(
function()
Manager.Sound:PauseMusic();
end
)
btn_unpause_music.onClick:AddListener(
function()
Manager.Sound:OnUnPauseMusic();
end
)
btn_play_sound.onClick:AddListener(
function()
Manager.Sound:PlaySound("ui_select.mp3");
end
)
slider_music_volume.onValueChanged:AddListener(
function(volume)
Manager.Sound:SetMusicVolume(volume);
end
)
slider_sound_volume.onValueChanged:AddListener(
function(volume)
Manager.Sound:SetSoundVolume(volume);
end
)
end
function Update()
--print("lua Update")
end
function OnClose()
print("lua OnClose")
btn_play_music.onClick:RemoveAllListeners();
btn_stop_music.onClick:RemoveAllListeners();
btn_pause_music.onClick:RemoveAllListeners();
btn_unpause_music.onClick:RemoveAllListeners();
btn_play_sound.onClick:RemoveAllListeners();
slider_music_volume.onValueChanged:RemoveAllListeners();
slider_sound_volume.onValueChanged:RemoveAllListeners();
end
由于删除了Listener,在OnClose函数中,但是OnClose函数的调用和UILogic的逻辑相关,因此队UILgoic也进行调整
//继承LuaBehaviour
public class UILogic : LuaBehaviour
{
Action m_LuaOnOpen;
Action m_LuaOnClose;
public override void Init(string luaName)
{
base.Init(luaName);
m_ScriptEnv.Get("OnOpen", out m_LuaOnOpen);
m_ScriptEnv.Get("OnClose", out m_LuaOnClose);
}
public void OnOpen()
{
m_LuaOnOpen?.Invoke();
}
public void OnClose()
{
m_LuaOnClose?.Invoke();
}
protected override void Clear()
{
OnClose();
base.Clear();
m_LuaOnOpen = null;
m_LuaOnClose = null;
}
}
对于Package模式下,一个音乐多次播放会加载多次bundle,但是bundle不能多次加载。需要处理。
//存放加载过的Bundle
private Dictionary<string, AssetBundle> m_AssetBundles = new Dictionary<string, AssetBundle>();
AssetBundle GetBundle(string name)
{
AssetBundle bundle = null;
if (m_AssetBundles.TryGetValue(name, out bundle))
{
return bundle;
}
return null;
}
IEnumerator LoadBundleAsync(string assetName,Action<UObject> action = null)
{
if (assetName.EndsWith(".unity"))
{
action?.Invoke(null);
yield break;
}
string bundleName = m_BundleInfos[assetName].BundleName;
//这个是小写的bundle.ab的路径名
string bundlePath = Path.Combine(PathUtil.BundleResourcePath, bundleName);
List<string> dependences = m_BundleInfos[assetName].Dependeces;
//判断是否已经加载过bundle了
AssetBundle bundle = GetBundle(bundleName);
if (bundle == null)
{
if (dependences != null && dependences.Count > 0)
{
//递归加载依赖bundle,因为依赖的资源目录名就是bundle资源名
for (int i = 0; i < dependences.Count; i++)
{
yield return LoadBundleAsync(dependences[i]);
}
}
//创建异步加载bundle申请
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(bundlePath);
yield return request;
bundle = request.assetBundle;
m_AssetBundles.Add(bundleName, request.assetBundle);
}
// if (assetName.EndsWith(".unity"))
// {
// action?.Invoke(null);
// yield break;
// }
//从bundle申请加载指定路径名的文件,例如prefab
AssetBundleRequest bundleRequest = bundle.LoadAssetAsync(assetName);
yield return bundleRequest;
//如果回调和request都不为空,语法糖
action?.Invoke(bundleRequest?.asset);
}
12. 事件管理器
Framework-Manager添加EventManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 逻辑:EventManager的m_Events相当于事件中心,其他脚本的事件都先给定一个id并注册到这个字典中,当需要运行的时候,从事件中心里查找对应id的事件执行
/// </summary>
public class EventManager : MonoBehaviour
{
//一个参数给lua使用,lua的多个参数可以封装成table传进来
public delegate void EventHandler(object args);
private Dictionary<int, EventHandler> m_Events = new Dictionary<int, EventHandler>();
//订阅事件
public void Subscribe(int id, EventHandler e)
{
if (m_Events.ContainsKey(id))
//相当于实现多播委托
m_Events[id] += e;
else
m_Events.Add(id, e);
}
//取消订阅事件
public void UnSubscribe(int id, EventHandler e)
{
if (m_Events.ContainsKey(id))
{
if (m_Events[id] != null)
m_Events[id] -= e;
}
else
{
if (m_Events[id] == null)
m_Events.Remove(id);
}
}
//执行事件
public void Fire(int id, object args = null)
{
EventHandler handler;
if (m_Events.TryGetValue(id, out handler))
{
handler(args);
}
}
}
Manager添加EventManager
public static EventManager Event
{
get { return _event; }
}
public void Awake()
{
_resource = this.gameObject.AddComponent<ResourceManager>();
_lua = this.gameObject.AddComponent<LuaManager>();
_ui = this.gameObject.AddComponent<UIManager>();
_entity = this.gameObject.AddComponent<EntityManager>();
_scene = this.gameObject.AddComponent<MySceneManager>();
_sound = this.gameObject.AddComponent<SoundManager>();
_event = this.gameObject.AddComponent<EventManager>();
}
接下来GameStart等脚本中的匿名委托都可以用EventManager进行处理
public class GameStart : MonoBehaviour
{
public GameMode GameMode;
// Start is called before the first frame update
void Start()
{
//开始时订阅事件
Manager.Event.Subscribe(10000, OnLuaInit);
AppConst.GameMode = this.GameMode;
DontDestroyOnLoad(this);
Manager.Resource.ParseVersionFile();
Manager.Lua.Init();
}
void OnLuaInit(object args)
{
//初始化完成之后(lua都加载完),在执行回调
Manager.Lua.StartLua("main"); //输入的文件名
//输入的是函数名
XLua.LuaFunction func = Manager.Lua.LuaEnv.Global.Get<XLua.LuaFunction>("Main");
func.Call();
}
public void OnApplicationQuit()
{
Manager.Event.UnSubscribe(10000, OnLuaInit);
}
}
修改LuaManager
public class LuaManager : MonoBehaviour
{
//所有的lua文件名,获取所有lua,然后进行预加载,ResourceManager查找lua文件放进来
public List<string> LuaNames = new List<string>();
//缓存lua脚本内容
private Dictionary<string, byte[]> m_LuaScripts;
//定义一个lua虚拟机,消耗比较大,,全局只需要一个,,需要using XLua;
public LuaEnv LuaEnv;
//如果是Editor模式下,直接从luapath就把所有lua读取到字典中了,然后在调用,属于同步加载后同步使用的情况
//但是在其他模式下,需要从bundle异步加载lua需要等待,如果等待时start就调用了,属于异步加载同步使用的情况,需要预加载
//需要创建一个回调通知
public void Init()
{
//初始化虚拟机
LuaEnv = new LuaEnv();
//外部调用require时,会自动调用loader来获取文件
LuaEnv.AddLoader(Loader);
m_LuaScripts = new Dictionary<string, byte[]>();
#if UNITY_EDITOR
if (AppConst.GameMode == GameMode.EditorMode)
EditorLoadLuaScript();
else
#endif
LoadLuaScript();
}
void LoadLuaScript()
{
foreach (var name in LuaNames)
{
//异步的需要一个回调(=>后面那一坨,当LoadLua执行时完,执行回调并invoke把结果返回),obj就是返回的lua的对象
Manager.Resource.LoadLua(name, (UnityEngine.Object obj) =>
{
//LoadLua调用完会把bundle加载好的bundleRequest.asset传进来用obj接受
//把这个lua根据名称添加到m_LuaScripts中
AddLuaScript(name, (obj as TextAsset).bytes);
//在ResourceManager中解析版本文件时加载所有lua文件到LuaNames
//如果LuaNames全部都加载到m_LuaScripts集合中,就清空LuaNames,退出循环
if (m_LuaScripts.Count >= LuaNames.Count)
{
//所有lua文件加载完成了。就可以执行使用lua函数的方法了
Manager.Event.Fire(10000);
LuaNames.Clear();
LuaNames = null;
}
});
}
}
#if UNITY_EDITOR
//编辑器模式下直接加载lua文件,并把lua名字和内容放到集合内
void EditorLoadLuaScript()
{
//搜索所有lua文件
string[] luaFiles = Directory.GetFiles(PathUtil.LuaPath, "*.bytes", SearchOption.AllDirectories);
for (int i = 0; i < luaFiles.Length; i++)
{
string fileName = PathUtil.GetStandardPath(luaFiles[i]);
//读取lua文件
byte[] file = File.ReadAllBytes(fileName);
//把读取的lua文件添加进去
AddLuaScript(PathUtil.GetUnityPath(fileName), file);
}
Manager.Event.Fire(10000);
}
#endif
}
关于声音管理中出现的C# callback报错,是因为C#先释放了LuaEnv,但是Lua中有回调函数还没有取消订阅,在这里需要处理。
编写脚本Framework-Extension-UnityEx
using UnityEngine.UI;
[XLua.LuaCallCSharp]
public static class UnityEx
{
//对按钮的事件的监听做一个扩展
//C#监听的事件变成了C#的匿名委托了,而不是lua的方法,所以lua的方法变成了临时变量
public static void OnClickSet(this Button button, object callback)
{
XLua.LuaFunction func = callback as XLua.LuaFunction;
//监听事件前,先移除,因为不销毁这个物体,所以如果每次打开ui都监听,会监听无数个事件
button.onClick.RemoveAllListeners();
button.onClick.AddListener(
() =>
{
func?.Call();
}
);
}
public static void OnValueChangedSet(this Slider slider, object callback)
{
XLua.LuaFunction func = callback as XLua.LuaFunction;
//监听事件前,先移除,因为不销毁这个物体,所以如果每次打开ui都监听,会监听无数个事件
slider.onValueChanged.RemoveAllListeners();
slider.onValueChanged.AddListener(
(float value) =>
{
func?.Call(value);
}
);
}
}
P11扩展方法
function OnInit()
print("lua OnInit")
end
function OnOpen()
print("lua OnOpen")
--Manager.Scene:LoadScene("Test01","scene.Scene01")
btn_play_music = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Play Music/Play Music On Button"):GetComponent("Button");
btn_stop_music = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Play Music/Play Music Off Button"):GetComponent("Button");
btn_pause_music = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Pause Music/Pause Music On Button"):GetComponent("Button");
btn_unpause_music = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Pause Music/Pause Music Off Button"):GetComponent("Button");
btn_play_sound = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Play Sound/Play Sound Button"):GetComponent("Button");
slider_music_volume = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Music Volume/Music Volume Slider"):GetComponent("Slider");
slider_sound_volume = self.transform:Find("Options Page/Options Panel/ButtonsAndSlider/Sound Volume/Sound Volume Slider"):GetComponent("Slider");
slider_music_volume.value = Manager.Sound.MusicVolume;
slider_sound_volume.value = Manager.Sound.SoundVolume;
--原版btn_play_music.onClick:AddListener(
btn_play_music:OnClickSet(
function()
Manager.Sound:PlayMusic("main.mp3");
end
)
btn_stop_music:OnClickSet(
function()
Manager.Sound:StopMusic();
end
)
btn_pause_music:OnClickSet(
function()
Manager.Sound:PauseMusic();
end
)
btn_unpause_music:OnClickSet(
function()
Manager.Sound:OnUnPauseMusic();
end
)
btn_play_sound:OnClickSet(
function()
Manager.Sound:PlaySound("ui_select.mp3");
end
)
slider_music_volume:OnValueChangedSet(
function(volume)
Manager.Sound:SetMusicVolume(volume);
end
)
slider_sound_volume:OnValueChangedSet(
function(volume)
Manager.Sound:SetSoundVolume(volume);
end
)
end
function Update()
--print("lua Update")
end
function OnClose()
print("lua OnClose")
end
public class UILogic : LuaBehaviour
{
Action m_LuaOnOpen;
Action m_LuaOnClose;
public override void Init(string luaName)
{
base.Init(luaName);
m_ScriptEnv.Get("OnOpen", out m_LuaOnOpen);
m_ScriptEnv.Get("OnClose", out m_LuaOnClose);
}
public void OnOpen()
{
m_LuaOnOpen?.Invoke();
}
public void OnClose()
{
m_LuaOnClose?.Invoke();
}
protected override void Clear()
{
//OnClose();
base.Clear();
m_LuaOnOpen = null;
m_LuaOnClose = null;
}
}
把前面那些OnClose的又改回去了。
XLua.ReflectionUse
一个C#类型类型加了这个配置,xLua会生成link.xml阻止il2cpp的代码剪裁。
对于扩展方法,必须加上LuaCallCSharp或者ReflectionUse才可以被访问到。
建议所有要在Lua访问的类型,要么加LuaCallCSharp,要么加上ReflectionUse,这才能够保证在各平台都能正常运行。