Unity3D程序基础框架-转载
前言:
最近在B站看到了唐老师出的课程——学习Unity3D基础框架,我觉得对我会很有用,特写此文章做以记录。
这篇需要你了解一些C#的知识。
单例模式基类模块
我们创建一个新项目,创建标准的目录结构
在Base下面新建脚本——BaseManager.cs
单例模式基类模块的作用主要是——减少单例模式重复代码的书写
我们都知道单例模式是怎么一种设计模式,那么什么是单例模式基类模块呢?其实就是单例模式的基础模板。
代码奉上
public class BaseManager<T> where T:new() { private static T _instance; public static T GetInstance() { if (_instance == null) { _instance = new T(); } return _instance; } }
之后只要项目中需要用到单例模式,只要继承此类即可。
public class BaseManager where T:new()
这一句后面的部分可能很多朋友不了解,这是一个C#中泛型的知识点——泛型约束。
关于泛型约束的概念可以参考: 这篇文章
缓存池模块 (用完收起来)
①基本原理与实现
缓存池模块主要是为了节约性能,减少CPU和内存消耗。
Unity每一次创建对象,实际都是C#在内存中申请了一块空间,之后在Unity场景中Destroy这个物体对象,实际上只是断开了实例化对象对于内存的那块空间的一个引用,内存中依然存在那个对象对应的空间。
直到内存占用满了,CPU才会回过头来找内存中没有被引用的空间(一次GC),然后释放该空间,然后建立新的对象,如此往复。
这个GC步骤是比较消耗CPU的,所以在比较不好的机器上可能造成游戏的卡顿,所以,我们需要缓存池。
缓存池可以将你创建的对象在你用完后收录起来,等到你再要调用的时候,就可以直接调用缓存池中收录的对象,如此往复,形成闭环。不必再去申请新的内存。
我们在ProjectBase目录下创建一个新目录——Pool,用来存放缓存池脚本。
public class PoolMgr : BaseManager<PoolMgr> { //这里是缓存池模块 //创建字段存储容器 public Dictionary<string,List<GameObject>> pool1Dic =new Dictionary<string, List<GameObject>>(); //取得游戏物体 public GameObject GetObj(string name) { GameObject obj = null; //ContainsKey判断是否包含指定的“键名” //.count 获得符合条件的个数 if (pool1Dic.ContainsKey(name) && pool1Dic[name].Count > 0) { //取得List中的第一个 obj = pool1Dic[name][0]; //移除第零个(这样才能允许同时创建多个物体) //这样才是真正的“拿出来” pool1Dic[name].RemoveAt(0); } else { //缓存池中没有该物体,我们去目录中加载 //外面传一个预设体的路径和名字,我内部就去加载它 //Resources类允许你从指定的路径查找或访问资源(api) obj = GameObject.Instantiate(Resources.Load<GameObject>(name)); //创建对象后,将对象的名字与池中名字相符 obj.name = name; } //让物体显示出来 obj.SetActive(true); return obj; } //外界返还游戏物体 public void PushObj(string name,GameObject obj) { //让物体失活 obj.SetActive(false); //里面有记录这个键(有这个抽屉) if (pool1Dic.ContainsKey(name)) { pool1Dic[name].Add(obj); } //未曾记录这个键(没有这个抽屉) else { pool1Dic.Add(name, new List<GameObject>() { obj}); } } }
我们来做个试验,给一个场景创建这样的脚本,实现点击鼠标创建物体
public class test : MonoBehaviour { void Update() { //实现了左键方块右键圆,不过只是单纯的拿 没放 if (Input.GetMouseButtonDown(0)) { //向缓存池中拿东西 PoolMgr.GetInstance().GetObj("Cube"); } if (Input.GetMouseButtonDown(1)) { PoolMgr.GetInstance().GetObj("Sphere"); } } }
物体身上挂着另一个脚本
public class DelayPush : MonoBehaviour { //不能用start 因为只执行一次(只放一次) //OnEnable() 是当对象被激活时 调用这个函数 void OnEnable() { //unity自带的延迟方法 在time秒后,延迟调用方法methodName Invoke("Push", 1); } //放回去 void Push() { PoolMgr.GetInstance().PushObj(transform.name, this.gameObject); } }
最终效果
这样创造新物体实际就是唤醒旧物体,虽然好像占用了内存,但是却为CPU有很大好处,减少了GC,增加了游戏体验感。
(短期来看 物体始终还是占用内存 并且释放不了 但是对于体验感来说 上升却不止一个层次)
②优化 – 规范化
为了使资源管理更加工整,我们考虑到应该让生产的物体都存放在一个空物体下,这样会非常便利我们的管理。
(思路:父亲先设置为空,然后返还的时候挂在空物体上)
public class PoolMgr : BaseManager<PoolMgr> { //这里是缓存池模块 //创建字段存储容器 public Dictionary<string,List<GameObject>> pool1Dic =new Dictionary<string, List<GameObject>>(); private GameObject poolObj; //取得游戏物体 public GameObject GetObj(string name) { GameObject obj = null; if (pool1Dic.ContainsKey(name) && pool1Dic[name].Count > 0) { //取得List中的第一个 obj = pool1Dic[name][0]; //移除第零个(这样才能允许同时创建多个物体) pool1Dic[name].RemoveAt(0); } else { //缓存池中没有该物体,我们去目录中加载 //外面传一个预设体的路径和名字,我内部就去加载它 obj = GameObject.Instantiate(Resources.Load<GameObject>(name)); //创建对象后,将对象的名字与池中名字相符 obj.name = name; } //让物体显示出来 obj.SetActive(true); //断开了缓存池物体与poolObj的父子关系 obj.transform.parent = null; return obj; } //外界返还游戏物体 public void PushObj(string name,GameObject obj) { if (poolObj == null) { poolObj = new GameObject("Pool"); } //将这个物体设置父亲为空物体 obj.transform.parent = poolObj.transform; //让物体失活 obj.SetActive(false); //里面有记录这个键 if (pool1Dic.ContainsKey(name)) { pool1Dic[name].Add(obj); } //未曾记录这个键 else { pool1Dic.Add(name, new List<GameObject>() { obj}); } } }
③场景跳转问题
还有一个问题,当场景切换时,缓存池的对象物体都会被销毁,但是引用还存在,这会占用内存又没有用处,
我们可以给PoolMgr添加一个清空方法来应对场景转换的情况。
//清空缓存池的方法 //主要用在场景切换时 public void Clear() { pool1Dic.Clear();//字典 clear 方法 poolObj = null; }
这样,跳转场景之前调用这个Clear方法就好了。
④更细节的规范化
我们现在虽然可以实现生成的缓存对象全部在Pool这个空物体下,但是却没有明确的划分,如果各式各样的物体很多,就会很杂乱。(强迫症福音)
所以我们可以修改代码对他们进行分类。
思路是这样的:我们创建了一个字典来保存记录缓存对象,键是字符串、值是Gameobject的集合,我们可以将值替换成一个新的类型——PoolData。
下面是最终的PoolMgr.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; //抽屉数据,池子中的一列容器 public class PoolData { //抽屉中,对象挂载的父节点 public GameObject fatherObj; //对象的容器 public List<GameObject> poolList; //进行分类的方法 public PoolData(GameObject obj, GameObject poolObj) { //根据obj创建一个同名父类空物体,它的父物体为总Pool空物体 fatherObj = new GameObject(obj.name); fatherObj.transform.parent = poolObj.transform; poolList = new List<GameObject>() { }; PushObj(obj); } //像抽屉里面压东西并且设置好父对象 public void PushObj(GameObject obj) { //存起来 poolList.Add(obj); //设置父对象 obj.transform.parent = fatherObj.transform; //失活,让其隐藏 obj.SetActive(false); } //像抽屉中取东西 public GameObject GetObj() { GameObject obj = null; //取出第一个 obj = poolList[0]; poolList.RemoveAt(0); //激活,让其展示 obj.SetActive(true); //断开父子关系 obj.transform.parent = null; return obj; } } public class PoolMgr : BaseManager<PoolMgr> { //这里是缓存池模块 //创建字段存储容器 public Dictionary<string, PoolData> pool1Dic =new Dictionary<string, PoolData>(); private GameObject poolObj; //取得游戏物体 public GameObject GetObj(string name) { GameObject obj = null; if (pool1Dic.ContainsKey(name) && pool1Dic[name].poolList.Count > 0) { //取得List中的第一个 obj = pool1Dic[name].GetObj(); } else { //缓存池中没有该物体,我们去目录中加载 //外面传一个预设体的路径和名字,我内部就去加载它 obj = GameObject.Instantiate(Resources.Load<GameObject>(name)); //创建对象后,将对象的名字与池中名字相符 obj.name = name; } return obj; } //外界返还游戏物体 public void PushObj(string name,GameObject obj) { if (poolObj == null) { poolObj = new GameObject("Pool"); } //里面有记录这个键 if (pool1Dic.ContainsKey(name)) { pool1Dic[name].PushObj(obj); } //未曾记录这个键 else { pool1Dic.Add(name, new PoolData(obj,poolObj) { }); } } //清空缓存池的方法 //主要用在场景切换时 public void Clear() { pool1Dic.Clear(); poolObj = null; } }
事件中心模块-收集全场事件(基于观察者设计模式)
这个模块时游戏开发过程中非常重要的模块,他可以减小程序的耦合性(文件关联度小,独立性大),减小复杂性。
事件中心模块主要就是说收集全场的事件,在得到某个事件后(比如怪物X死亡),令关心这个事件的对象(比如玩家)进行相应的处理(比如加金币)。
如果对观察者模式有疑问可以参考这篇文章
我们先创建一个脚本——EventCenter.cs,这个就是事件中心
这个观察者模式的事件中心模块中也是通过字典来存储相应对象的。
基本框架奉上
前言:UnityAction本质就是委托(delegate) 具体可以参考这篇文章
public class EventCenter : BaseManager<EventCenter> { //字典中,key对应着事件的名字, //value对应的是监听这个事件对应的委托方法们(重点圈住:们) private Dictionary<string, UnityAction> eventDic = new Dictionary<string, UnityAction>(); //添加事件监听 //第一个参数:事件的名字 //第二个参数:处理事件的方法 public void AddEventListener(string name, UnityAction action) { //有没有对应的事件监听(有没有相应的键) //有的情况 if (eventDic.ContainsKey(name)) { eventDic[name] += action; } //没有的情况 else { eventDic.Add(name, action); } } //通过事件名字进行事件触发 public void EventTrigger(string name) { //有没有对应的事件监听 //有的情况(有人关心这个事件) if (eventDic.ContainsKey(name)) { //调用委托(依次执行委托中的方法) eventDic[name](); } } } //后面还有两个重要方法,一会你就知道了
其实代码并不多,但是效果非常牛逼。
我们来一段测试
test.cs (创建被观察者 并提供状态改变的方法)
public class test : MonoBehaviour { void Update() { if (Input.GetMouseButtonDown(0)) { //触发事件 EventCenter.GetInstance().EventTrigger("LeftMouse"); } else if (Input.GetMouseButtonDown(1)) { //触发事件 EventCenter.GetInstance().EventTrigger("RightMouse"); } } }
Player.cs(创建观察者 并提供状态改变时相应的方法)
public class Player : MonoBehaviour { private void Start() { //建立两个委托,分别为两个委托添加方法 UnityAction LeftAction=null; LeftAction += leftDown; LeftAction += leftDown2; UnityAction RightAction = null; RightAction += RightDown; RightAction += RightDown2; //对该字典 进行添加 EventCenter.GetInstance().AddEventListener("LeftMouse", LeftAction); EventCenter.GetInstance().AddEventListener("RightMouse", RightAction); } private void leftDown() { Debug.Log("左键按下"); } private void leftDown2() { Debug.Log("白天了"); } private void RightDown() { Debug.Log("右键按下"); } private void RightDown2() { Debug.Log("晚上了"); } }
相信我不用多解释,你也能看懂,我们按下左键就调用了两个方法,按下右键又是另外两个方法,这只是个小demo,如果是大型项目,这种中心事件处理极大的降低了耦合度,想要触发事件直接触发就好,想要给事件添加方法直接添加就好,一切都是向 中心事件 发送消息,很好的管理了原本错综复杂的事件们。
但是我们还有几个问题,就是如果你销毁了某个物体的话,这个物体身上脚本的AddEventListener添加的监听的绑定并没有消除,他们之间总是建立着引用关系,这样下去慢慢会造成内存泄露。
所以我们在上面的基础上添加一个移除监听的方法
//移除对应的事件监听 public void RemoveEventListener(string name, UnityAction action) { if (eventDic.ContainsKey(name)) { //移除这个委托 eventDic[name] -= action; } }
在建立中心事件监听的对象的OnDestroy方法中,可以调用该方法实现及时的移除。
考虑到在跳转场景中Unity会将所有旧场景物体对象删除,所以我们不如写一个清空方法在每次跳转场景时确保不要有不合适的内存问题。
//清空所有事件监听(主要用在切换场景时) public void Clear() { eventDic.Clear(); }
另外,考虑到可能会传来不同的参数,我们可以给委托添加一个泛型,从而允许给方法传入参数
public class Player : MonoBehaviour { private void Start() { UnityAction<object> LeftAction=null; LeftAction += leftDown; LeftAction += leftDown2; UnityAction<object> RightAction = null; RightAction += RightDown; RightAction += RightDown2; EventCenter.GetInstance().AddEventListener("LeftMouse", LeftAction); EventCenter.GetInstance().AddEventListener("RightMouse", RightAction); } private void leftDown(object info) { Debug.Log("左键按下"); //将Object的类型转换成test类,从而调用test属性name Debug.Log("test的对象的name是:" + (info as test).name); } private void leftDown2(object info) { Debug.Log("白天了"); } private void RightDown(object info) { Debug.Log("右键按下"); Debug.Log("test的对象的name是:" + (info as test).name); } private void RightDown2(object info) { Debug.Log("晚上了"); } }
public class test : MonoBehaviour { public string name="123"; void Update() { if (Input.GetMouseButtonDown(0)) { //触发事件 EventCenter.GetInstance().EventTrigger("LeftMouse",this); } else if (Input.GetMouseButtonDown(1)) { //触发事件 EventCenter.GetInstance().EventTrigger("RightMouse",this); } } }
public class EventCenter : BaseManager<EventCenter> { //字典中,key对应着事件的名字, //value对应的是监听这个事件对应的委托方法们(重点圈住:们) private Dictionary<string, UnityAction<object>> eventDic = new Dictionary<string, UnityAction<object>>(); //添加事件监听 //第一个参数:事件的名字 //第二个参数:处理事件的方法 public void AddEventListener(string name, UnityAction<object> action) { //有没有对应的事件监听 //有的情况 if (eventDic.ContainsKey(name)) { eventDic[name] += action; } //没有的情况 else { eventDic.Add(name, action); } } //通过事件名字进行事件触发 public void EventTrigger(string name,objectinfo) { //有没有对应的事件监听 //有的情况(有人关心这个事件) if (eventDic.ContainsKey(name)) { //调用委托(依次执行委托中的方法) eventDic[name](info); } } //移除对应的事件监听 public void RemoveEventListener(string name, UnityAction<object> action) { if (eventDic.ContainsKey(name)) { //移除这个委托 eventDic[name] -= action; } } //清空所有事件监听(主要用在切换场景时) public void Clear() { eventDic.Clear(); }}
公共Mono模块(没有继承Monobehavior,仍可使用)
我们都知道,继承了Monobehavior之后,我们就可以使用Unity给我们提供的一些生命周期函数。
但是有的时候,我们会有的类没有继承Monobehavior,但是我们又希望使用到Mono的东西,比如帧更新方法Update、协程等,这该怎么做呢?
公共Mono模块就可以解决这个问题。
①帧更新
我们在ProjectBase下面新建一个目录——Mono,里面新建一个脚本MonoController.cs
代码奉上
//Mono的管理者 public class MonoController : MonoBehaviour { private event UnityAction updateEvent; private void Start() { //此对象不可移除 //从而方便别的对象找到该物体,从而获取脚本,从而添加方法 DontDestroyOnLoad(this.gameObject); } private void Update() { if (updateEvent != null) { updateEvent(); } } //为外部提供的添加帧更新事件的方法 public void AddUpdateListener(UnityAction func) { updateEvent += func; } //为外部提供的移除帧更新事件的方法 public void RemoveUpdateListener(UnityAction func) { updateEvent -= func; } }
意思就是说,一个外部的没有继承mono的脚本,它调用了MonoController的AddUpdateLisener方法,把自己的一个方法添加到了updateEvent中,而在MonoController中每一帧都执行了updateEvent,也就每一帧都执行了没有继承mono的脚本的某个方法,也就实现了没有继承mono的脚本具有了Update方法!
(说白点 他有updata 不断运行委托里指向的那个方法 把某个方法放进这个委托里 不断运行这个委托 就相当于我自己有了updata)
但是如果我们每次去调用,都要找到这个物体,找到这个方法,不免有些麻烦,所以,我们可以这样做:
在 MonoController.cs 同目录下新建一个文件 MonoMgr.cs
public class MonoMgr : BaseManager<MonoMgr> { private MonoController controller; public MonoMgr() { //新建一个物体(等同于Instantiate) GameObject obj = new GameObject("MonoController"); //给物体添加组件 controller = obj.AddComponent<MonoController>(); } public void AddUpdateListener(UnityAction func) { controller.AddUpdateListener(func); } public void RemoveUpdateListener(UnityAction func) { controller.RemoveUpdateListener(func); } }
还记得单例模式模板的代码吗?
public static T GetInstance() { > if (_instance == null) { > _instance = new T(); > } > return _instance; } >
我们这个MonoMgr也是一个单例模式,所以外界第一次调用它的时候,就会实例化对象,从而触发MonoMgr的构造方法。
并且也保证了MonoController对象的唯一性!
下面是测试代码test.cs文件中
public class testtest { public void choot() { Debug.Log("123"); } } public class test : MonoBehaviour { private void Start() { testtest t = new testtest(); MonoMgr.GetInstance().AddUpdateListener(t.choot); } }
然后,就会看到控制台不断打印123.
②协程
实现协程非常简单,只要在MonoMgr中重写协程方法就好了
public class MonoMgr : BaseManager<MonoMgr> { private MonoController controller; public MonoMgr() { //新建一个物体 GameObject obj = new GameObject("MonoController"); //给物体添加组件 controller = obj.AddComponent<MonoController>(); } public void AddUpdateListener(UnityAction func) { controller.AddUpdateListener(func); } public void RemoveUpdateListener(UnityAction func) { controller.RemoveUpdateListener(func); } public Coroutine StartCoroutine(IEnumerator routine) { return controller.StartCoroutine(routine); } //[DefaultValue("null")]设置默认值为空 //接受一个对象的参数(object) public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value) { return controller.StartCoroutine(methodName,value); } public Coroutine StartCoroutine(string methodName) { return controller.StartCoroutine(methodName); } }
测试调用
public class testtest { public testtest(){ MonoMgr.GetInstance().StartCoroutine(Test123()); } IEnumerator Test123() { yield return new WaitForSeconds(1f); Debug.Log("等了1秒钟"); } } public class test : MonoBehaviour { private void Start() { testtest t = new testtest(); } }
隔了一秒之后,会输出“等了1秒钟”
场景切换模块(实时加载人物-lol)
很多时候,我们加载场景是需要动态创建一些角色的,比如王者荣耀,你不能把出场的英雄各种搭配组合都先摆在场景中吧?
(场景里的角色是不确定的,是动态加载的)
这个时候,我们就需要“场景切换模块”
ProjectBase下面新建一个Scenes目录,下面再创建一个ScenesMgr.cs
代码参考
① 同步加载
在场景加载的过程中,不可以继续进行其余操作,要等到场景切换完成才可以。
ps: using UnityEngine.SceneManagement;
/// <summary> /// 场景切换管理类 /// </summary> public class SceneLoadManager : Singleton<SceneLoadManager> { /// <summary> /// 场景切换 同步加载(场景加载完之后,再执行其余相关的方法) /// </summary> /// <param name="sceneName">加载的场景</param> /// <param name="func">切换场景要执行的方法</param> public void LoadScene(string sceneName, UnityAction func) { // 切换场景 SceneManager.LoadScene(sceneName); // 场景切换之后再执行相应的其余的加载 func(); }
② 异步加载
提要:关于同步切换场景(LoadSceneAsync),或者异步切换场景(AsyncOperation),要想了解,可以参考这篇文章
在切换场景的时候,通过为切换场景的方法开启协程,可以实现场景切换的过程中继续实现其余方法的加载。
/// <summary> /// 场景切换异步加载的封装 /// </summary> /// <param name="sceneName">加载的场景</param> /// <param name="func">切换场景要执行的方法</param> public void LoadSceneAsync(string sceneName, UnityAction func) { // 利用Mono管理器实现没有继承MonoBehavior开启协程 MonoManeger.GetInstance().StartCoroutine(LoadingSceneAsync(sceneName, func)); } /// <summary> /// 场景切换 异步加载(场景加载的过程中,可以同时执行其余相关的方法) /// </summary> /// <param name="sceneName">加载的场景</param> /// <param name="func">切换场景要执行的方法</param> public IEnumerator LoadingSceneAsync(string sceneName, UnityAction func) { AsyncOperation ao = SceneManager.LoadSceneAsync(sceneName); // 如果没有场景加载完成,通过协程实现进度的更新,可以用于事件分发,供外部监听 while (!ao.isDone) { //EventCenter.GetInstance().EventAddListener("LoadScenePorgress", ao.progress); yield return ao.progress; } yield return ao; func(); }
在这个过程中,我们还可以利用加载进度来让玩家知道场景加载时画面并不是卡死的,而且可以在加载过程中开启协程,向外派发事件供外部监听。
异步加载中,我们将加载进度信息分发给了事件中心,如果你想对加载信息做一些处理,都可以在事件中心进行处理。
func方法就是进行动态加载的方法,想写什么都可以写。
资源加载模块
本篇我们实现unity里的加载模块,他的主要功能是,业务传入资源名字和资源类型,加载模块加载到对应的资源后返回给业务,业务不需要关心该资源是从本地加载还是从AssetBundle里加载。
ProjectBase中新建一个目录Res,再里面新建一个脚本——ResMgr.cs //资源加载模块 public class ResMgr : BaseManager<ResMgr> { //同步加载资源 public T Load<T>(string name) where T:Object{ T res = Resources.Load<T>(name); //resources.Load-加载资源 //如果对象是一个GameObject类型的,我把它实例化后,再返回出去直接使用。 //is检查一个对象是否兼容于指定的类型 if (res is GameObject) return GameObject.Instantiate(res);//实例化 else //else情况示例:TextAsset、AudioClip return res; } //异步加载资源 public void LoadAsync<T>(string name,UnityAction<T> callback) where T:Object { //开启异步加载的协程 MonoMgr.GetInstance().StartCoroutine(ReallyLoadAsync<T>(name,callback)); } private IEnumerator ReallyLoadAsync<T>(string name,UnityAction<T> callback) where T:Object{ { //Resources.LoadAsync 异步加载Resources文件夹中的资源。 //ResourceRequest从资源包异步加载请求。 ResourceRequest r=Resources.LoadAsync<T>(name); yield return r; if (r.asset is GameObject) { //实例化一下再传给方法 callback(GameObject.Instantiate(r.asset) as T); } else { //直接传给方法 callback(r.asset as T); } } }
需要注意的是,异步加载中,调用异步加载后,物体并没有被加载,需要等n帧才会被加载,所以我们不能直接返回物体,而是在方法中将物体传出去。
我这里来简单示例一下如何使用异步加载
private void Update() { if (Input.GetMouseButtonDown(1)) { ResMgr.GetInstance().LoadAsync<GameObject>("cube", DoSome); } } private void DoSome(GameObject obj) { Debug.Log("调用了回调函数"); }
缓存池结合异步加载
既然我的做出了异步加载的模块,那么在我们的缓存池中,完全就可以通过异步加载来得到(创建)物体。
作用:使得加载很大的东西时避免卡顿感。
新的PoolMgr.cs,主要修改了PoolMgr中的GetObj方法。
//抽屉数据,池子中的一列容器 public class PoolData { //抽屉中,对象挂载的父节点 public GameObject fatherObj; //对象的容器 public List<GameObject> poolList; public PoolData(GameObject obj, GameObject poolObj) { //根据obj创建一个同名父类空物体,它的父物体为总Pool空物体 fatherObj = new GameObject(obj.name); fatherObj.transform.parent = poolObj.transform; poolList = new List<GameObject>() { }; PushObj(obj); } //像抽屉里面压东西并且设置好父对象 public void PushObj(GameObject obj) { //存起来 poolList.Add(obj); //设置父对象 obj.transform.parent = fatherObj.transform; //失活,让其隐藏 obj.SetActive(false); } //像抽屉中取东西 public GameObject GetObj() { GameObject obj = null; //取出第一个 obj = poolList[0]; poolList.RemoveAt(0); //激活,让其展示 obj.SetActive(true); //断开父子关系 obj.transform.parent = null; return obj; } } public class PoolMgr : BaseManager<PoolMgr> { //这里是缓存池模块 //创建字段存储容器 public Dictionary<string, PoolData> pool1Dic =new Dictionary<string, PoolData>(); private GameObject poolObj; //取得游戏物体 public void GetObj(string name,UnityAction<GameObject> callback) { if (pool1Dic.ContainsKey(name) && pool1Dic[name].poolList.Count > 0) { //拖过委托返回给外部,让外部进行使用 callback(pool1Dic[name].GetObj()); } else { //缓存池中没有该物体,我们去目录中加载 //外面传一个预设体的路径和名字,我内部就去加载它 ResMgr.GetInstance().LoadAsync<GameObject>(name,(o)=> { o.name = name; callback(o); }); } } //外界返还游戏物体 public void PushObj(string name,GameObject obj) { if (poolObj == null) { poolObj = new GameObject("Pool"); } //里面有记录这个键 if (pool1Dic.ContainsKey(name)) { pool1Dic[name].PushObj(obj); } //未曾记录这个键 else { pool1Dic.Add(name, new PoolData(obj,poolObj) { }); } } //清空缓存池的方法 //主要用在场景切换时 public void Clear() { pool1Dic.Clear(); poolObj = null; } }
外界调用时,第二个参数直接给GetObj传递一个Lambda表达式即可
输入控制模块(收集全场输入)
我们可以将游戏中所有的输入检测汇集到一个地方,搭配事件中心模块,从而降低耦合性。
老样子,在ProjectBase目录下新建一个目录——Input,然后在下面新建一个文件——InputMgr.cs
最基础版本如下:
public class InputMgr : BaseManager<InputMgr> { private bool isStart = false; //构造方法中,添加Update监听 public InputMgr() { MonoMgr.GetInstance().AddUpdateListener(MyUpdate); } //检测是否需要开启输入检测 public void StartOrEndCheck(bool isOpen) { isStart = isOpen; } private void MyUpdate() { //没有开启输入检测,就不去检测 if (!isStart) return; CheckKeyCode(KeyCode.A); CheckKeyCode(KeyCode.D); CheckKeyCode(KeyCode.W); CheckKeyCode(KeyCode.S); } private void CheckKeyCode(KeyCode key) { if (Input.GetKeyDown(key)) { //事件中心模块,分发按下抬起事件(把哪个按键也发送出去) EventCenter.GetInstance().EventTrigger("KeyisDown", key); } if (Input.GetKeyUp(key)) { EventCenter.GetInstance().EventTrigger("KeyisUp", key); } } }
对应测试代码
public class Inputtest : MonoBehaviour { void Start() { //开启输入检测 InputMgr.GetInstance().StartOrEndCheck(true); //添加事件监听 EventCenter.GetInstance().AddEventListener("KeyisDown", CheckInputDown); EventCenter.GetInstance().AddEventListener("KeyisUp", CheckInputUp); } private void CheckInputDown(object obj) { KeyCode code = (KeyCode)obj; switch (code) { case KeyCode.W: Debug.Log("前进"); break; case KeyCode.A: break; case KeyCode.S: break; case KeyCode.D: break; } } private void CheckInputUp(object obj) { KeyCode code = (KeyCode)obj; switch (code) { case KeyCode.W: Debug.Log("不再前进"); break; case KeyCode.A: break; case KeyCode.S: break; case KeyCode.D: break; } } private void OnDestroy() { EventCenter.GetInstance().RemoveEventListener("KeyisDown", CheckInputDown); EventCenter.GetInstance().RemoveEventListener("KeyisUp", CheckInputUp); } }
事件中心优化
在进行事件中心的优化之前,你需要来了解一下什么是C#中的装箱和拆箱——参考这里
我们现在的代码是允许传进来的委托是object类型,这样会有个装箱拆箱的过程,我们现在做优化就是想要避免这个装箱拆箱的过程。
处理方法很骚气,使用泛型来处理不同的类型,但是EventCenter是单例模式,所以不能再字典中把值类型设置成泛型,所以有个骚气操作——使用一个空接口
关于里式转换,更多可以参考这里
EventCenter.cs文件
public interface IEventInfo { //这是一个空接口 } public class EventInfo<T> : IEventInfo { public UnityAction<T> actions; public EventInfo(UnityAction<T> action) { actions += action; } } public class EventCenter : BaseManager<EventCenter> { //字典中,key对应着事件的名字, //value对应的是监听这个事件对应的委托方法们(重点圈住:们) private Dictionary<string, IEventInfo> eventDic = new Dictionary<string, IEventInfo>(); //添加事件监听 //第一个参数:事件的名字 //第二个参数:处理事件的方法(有参数(类型为T)的委托) public void AddEventListener<T>(string name, UnityAction<T> action) { //有没有对应的事件监听 //有的情况 if (eventDic.ContainsKey(name)) { (eventDic[name] as EventInfo<T>).actions += action; } //没有的情况 else { eventDic.Add(name, new EventInfo<T>(action)); } } //通过事件名字进行事件触发 public void EventTrigger<T>(string name,T info) { //有没有对应的事件监听 //有的情况(有人关心这个事件) if (eventDic.ContainsKey(name)) { //调用委托(依次执行委托中的方法) //?是一个C#的简化操作 (eventDic[name] as EventInfo<T>).actions?.Invoke(info); } } //移除对应的事件监听 public void RemoveEventListener<T>(string name, UnityAction<T> action) { if (eventDic.ContainsKey(name)) { //移除这个委托 (eventDic[name] as EventInfo<T>).actions -= action; } } //清空所有事件监听(主要用在切换场景时) public void Clear() { eventDic.Clear(); }}
外部使用的话,直接给泛型指定类型就好了。
修改我们之前写的那个输入控制模块的测试代码,
public class Inputtest : MonoBehaviour { void Start() { //开启输入检测 InputMgr.GetInstance().StartOrEndCheck(true); //添加事件监听 EventCenter.GetInstance().AddEventListener<KeyCode>("KeyisDown", CheckInputDown); EventCenter.GetInstance().AddEventListener<KeyCode> ("KeyisUp", CheckInputUp); } private void CheckInputDown(KeyCode obj) { switch (obj) { case KeyCode.W: Debug.Log("前进"); break; case KeyCode.A: break; case KeyCode.S: break; case KeyCode.D: break; } } private void CheckInputUp(KeyCode obj) { switch (obj) { case KeyCode.W: Debug.Log("不再前进"); break; case KeyCode.A: break; case KeyCode.S: break; case KeyCode.D: break; } } private void OnDestroy() { EventCenter.GetInstance().RemoveEventListener<KeyCode>("KeyisDown", CheckInputDown); EventCenter.GetInstance().RemoveEventListener<KeyCode>("KeyisUp", CheckInputUp); } }
到这里,基本的OK了。
还可以完善一下,就是说,如果我不需要传参数呢?重载一个不需要泛型的EventInfo类,然后将EventCenter中的一些方法重载即可。
public interface IEventInfo { //这是一个空接口 } public class EventInfo<T> : IEventInfo { public UnityAction<T> actions; public EventInfo(UnityAction<T> action) { actions += action; } } public class EventInfo : IEventInfo { public UnityAction actions; public EventInfo(UnityAction action) { actions += action; } } public class EventCenter : BaseManager<EventCenter> { //字典中,key对应着事件的名字, //value对应的是监听这个事件对应的委托方法们(重点圈住:们) private Dictionary<string, IEventInfo> eventDic = new Dictionary<string, IEventInfo>(); //添加事件监听 //第一个参数:事件的名字 //第二个参数:处理事件的方法 public void AddEventListener<T>(string name, UnityAction<T> action) { //有没有对应的事件监听 //有的情况 if (eventDic.ContainsKey(name)) { (eventDic[name] as EventInfo<T>).actions += action; } //没有的情况 else { eventDic.Add(name, new EventInfo<T>(action)); } } //对于不需要参数的情况的重载方法 public void AddEventListener(string name, UnityAction action) { if (eventDic.ContainsKey(name)) { (eventDic[name] as EventInfo).actions += action; } else { eventDic.Add(name, new EventInfo(action)); } } //通过事件名字进行事件触发 public void EventTrigger<T>(string name,T info) { //有没有对应的事件监听 //有的情况(有人关心这个事件) if (eventDic.ContainsKey(name)) { //调用委托(依次执行委托中的方法) //?是一个C#的简化操作,存在,则直接调用委托 (eventDic[name] as EventInfo<T>).actions?.Invoke(info); } } //对于不需要参数的情况的重载方法 public void EventTrigger(string name) { if (eventDic.ContainsKey(name)) { (eventDic[name] as EventInfo).actions?.Invoke(); } } //移除对应的事件监听 public void RemoveEventListener<T>(string name, UnityAction<T> action) { if (eventDic.ContainsKey(name)) { //移除这个委托 (eventDic[name] as EventInfo<T>).actions -= action; } } //对于不需要参数的情况的重载方法 public void RemoveEventListener(string name, UnityAction action) { if (eventDic.ContainsKey(name)) { (eventDic[name] as EventInfo).actions -= action; } } //清空所有事件监听(主要用在切换场景时) public void Clear() { eventDic.Clear(); }}
音效管理模块
顾名思义,作用:统一管理音乐音效相关
ProjectBase下创建一个目录Music,再在下面创建一个MusicMgr,它也是继承自单例模式模块
以前我们处理播放声音是怎么搞?每一个需要播放声音的地方分开弄代码,这样很麻烦,改起来也不好改。
Resource目录下面新建一个Music目录存放我们的音乐,Sounds存放音效,bk存放背景音乐
代码
public class MusicMgr : BaseManager<MusicMgr> { private AudioSource bkMusic = null; private float bkVaule = 1; private float soundVaule = 1; private GameObject soundObj = null; private List<AudioSource> soundList = new List<AudioSource>(); public MusicMgr() { MonoMgr.GetInstance().AddUpdateListener(update); } //每帧执行list里的音乐是否正在播放 没播放则删除 private void update() { for (int i = soundList.Count-1; i >= 0; i--) { if (!soundList[i].isPlaying) { GameObject.Destroy(soundList[i]); soundList.RemoveAt(i); } } } //播放背景音乐 public void PlayBKMusic(string name) { if (bkMusic == null) { GameObject obj = new GameObject("BKMusic"); bkMusic = obj.AddComponent<AudioSource>(); } //异步加载背景音乐并且加载完成后播放 ResMgr.GetInstance().LoadAsync<AudioClip>("Music/bk/"+name,(clip) => { bkMusic.clip = clip; bkMusic.loop = true; //调整大小 bkMusic.volume = bkVaule; bkMusic.Play(); }); } //改变音量大小 public void ChangeBKValue(float v) { bkVaule = v; if (bkMusic == null) { return; } bkMusic.volume = bkVaule; } //暂停背景音乐 public void PauseBKMusic() { if (bkMusic == null) { return; } bkMusic.Pause(); } //停止背景音乐 public void StopBKMusic() { if (bkMusic == null) { return; } bkMusic.Stop(); } //播放音效 public void PlaySound(string name,bool isLoop,UnityAction<AudioSource> callback=null ) { if (soundObj == null) { soundObj = new GameObject(); soundObj.name = "Sounds"; } AudioSource source=soundObj.AddComponent<AudioSource>(); ResMgr.GetInstance().LoadAsync<AudioClip>("Music/Sounds/" + name, (clip) => { source.clip = clip; source.loop = isLoop; //调整大小 source.volume = soundVaule; source.Play(); //音效资源异步加载结束后,将这个音效组件加入集合中 soundList.Add(source); if (callback != null) { callback(source); } }); } //改变所有音效大小 public void ChangeSoundValue(float value) { soundVaule = value; for (int i = 0; i < soundList.Count; ++i) { soundList[i].volume = value; } } //停止音效 public void StopSound(AudioSource source) { if (soundList.Contains(source)) { soundList.Remove(source); source.Stop(); GameObject.Destroy(source); } } }
没什么好说的,很好理解。
使用的时候,注意好路径
UI管理模块
①UI基类
统一管理UI以及相关(UGUI),提高代码复用率,降低文件耦合性。
在ProjectBase下面新建一个目录——UI,再在下面新建两个文件——UIManager.cs、BasePanel.cs
//面板基类 //找到所有自己面板下的控件对象 //提供显式/隐藏的行为 public class BasePanel : MonoBehaviour { //UGUI的基础组件都最终继承自UIBehaviour //通过里式转换原则,来存储所有的UI控件 private Dictionary<string, List<UIBehaviour>> controlDic = new Dictionary<string, List<UIBehaviour>>(); private void Awake() { FindChildControl<Button>(); FindChildControl<Image>(); FindChildControl<Text>(); FindChildControl<Toggle>(); FindChildControl<ScrollRect>(); FindChildControl<Slider>(); } //得到对应名字的对应控件脚本 protected T GetControl<T>(string controlName) where T:UIBehaviour { //用字典ContainsKey找里面有没有这个键 if (controlDic.ContainsKey(controlName)) { for (int i = 0; i < controlDic[controlName].Count; i++) { //对应字典的值(是个集合)中,符合要求的类型的 //则返回出去,这样外部就可以获取到了 if (controlDic[controlName][i] is T) { return controlDic[controlName][i] as T; } } } return null; } //找到相对应的UI控件并记录到字典中 private void FindChildControl<T>() where T:UIBehaviour { T[] controls = this.GetComponentsInChildren<T>(); string objname; for (int i = 0; i < controls.Length; i++) { objname = controls[i].gameObject.name; if (controlDic.ContainsKey(objname)) { controlDic[objname].Add(controls[i]); } else { controlDic.Add(objname, new List<UIBehaviour>() { controls[i] }); } } } //让子类重写(覆盖)此方法,来实现UI的隐藏与出现 public virtual void ShowMe() { } public virtual void HideMe() { } }
使用方法很简单,在UI的Canvas上挂载一个脚本继承自这个BasePanel,这样这个Canvas下面所有的UI都可以被GetControl直接找到,很方便
示例
public class UItest : BasePanel { private void Start() { GetControl<Button>("Button").onClick.AddListener(() => { print("开始游戏"); }); GetControl<Button>("Button2").onClick.AddListener(() => { print("设置"); }); } }
②UI管理器
我们现在来编写UI管理器,首先修改场景Canvas的属性来保证分辨率问题
主要是修改Canvas Scaler (画布定标器)
看代码之前请先来理解一下思路:比如某UI结构是这样的
纠错:这里顺序错了,top层应该在最下面,bot在最上面
Canvas就不用说了,top、mid、bot分别是我们的三个层,为了应对一些层级UI情况,再往下的各个Panel才是放置UI的重点面板。
我们将我们准备的几个Panel做成预制体,然后删掉场景中的它们。
我们再将Canvas(子物体有top、mid、bot三个层)以及EventSystem再做成两个预制体,这两个物体生成到场景中,我们会设置成DontDestroyOnLoad。
后面如果面板中有按钮等组件,一定要放在顶层
我们的某个面板,比如LoginPanel,需要挂载一个脚本,挂载的脚本需要继承 BasePanel
像是这样
public class LoginPanel : BasePanel { private void Start() { GetControl<Button>("Button").onClick.AddListener(() => { print("我是顶层"); }); } public void InitInfo() { print("初始化了数据"); } }
然后,我们的UIManager.cs文件内容如下
//UI层级枚举 public enum E_UI_Layer { Bot, Mit, Top } //UI管理器(管理面板) //管理所有显示的面板 //提供给外部 显示和隐藏 public class UIManager : BaseManager<UIManager> { public Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>(); //这是几个UI面板 private Transform bot; private Transform mid; private Transform top; public UIManager() { //去找Canvas(做成了预设体在Resources/UI下面) GameObject obj=ResMgr.GetInstance().Load<GameObject>("UI/Canvas"); Transform canvas = obj.transform; //创建Canvas,让其过场景的时候不被移除 GameObject.DontDestroyOnLoad(obj); //找到各层 bot = canvas.Find("bot"); mid = canvas.Find("mid"); top = canvas.Find("top"); //加载EventSystem,有了它,按钮等组件才能响应 obj =ResMgr.GetInstance().Load<GameObject>("UI/EventSystem"); //创建Canvas,让其过场景的时候不被移除 GameObject.DontDestroyOnLoad(obj); } public void ShowPanel<T>(string panelName, E_UI_Layer layer=E_UI_Layer.Top, UnityAction<T> callback=null) where T:BasePanel { //已经显示了此面板 if (panelDic.ContainsKey(panelName)) { //调用重写方法,具体内容自己添加 panelDic[panelName].ShowMe(); if (callback!=null) callback(panelDic[panelName] as T); return; } ResMgr.GetInstance().LoadAsync<GameObject>("UI/"+panelName,(obj)=> { //把它作为Canvas的子对象 //并且设置它的相对位置 //找到父对象 Transform father = bot; switch (layer) { case E_UI_Layer.Mit: father = mid; break; case E_UI_Layer.Top: father = top; break; } //设置父对象 obj.transform.SetParent(father); //设置相对位置和大小 obj.transform.localPosition = Vector3.zero; obj.transform.localScale = Vector3.one; (obj.transform as RectTransform).offsetMax = Vector2.zero; (obj.transform as RectTransform).offsetMin = Vector2.zero; //得到预设体身上的脚本(继承自BasePanel) T panel = obj.GetComponent<T>(); //执行外面想要做的事情 if (callback != null) { callback(panel); } //在字典中添加此面板 panelDic.Add(panelName, panel); }); } //隐藏面板 public void HidePanel(string panelName) { if (panelDic.ContainsKey(panelName)) { //调用重写方法,具体内容自己添加 panelDic[panelName].HideMe(); GameObject.Destroy(panelDic[panelName].gameObject); panelDic.Remove(panelName); } } }
在外部调用的示例
public class UItest :MonoBehaviour { private void Start() { //新建一个挂载着LoginPanel的叫做LoginPanel的面板, //新建到top层下面,异步加载完成后调用ShowPanelOver UIManager.GetInstance().ShowPanel<LoginPanel>("LoginPanel", E_UI_Layer.Top, ShowPanelOver); //如果是同步加载,则可以直接如下调用面板中的方法 //不过咱们是异步加载,所以必须在回调中调用方法(因为此刻面板尚未创建) //LoginPanel p=this.gameobject.GetComponent<LoginPanel>(); //p.IninInfo(); } private void ShowPanelOver(LoginPanel panel) { panel.InitInfo(); Invoke("DelayHide", 6); } private void DelayHide() { UIManager.GetInstance().HidePanel("LoginPanel"); } }
- 视频教程-热更新框架设计之Xlua基础视频课程-Unity3D
- OSG-基础知识-程序框架
- 从单片机基础到程序框架(吴坚鸿)
- 让.Net程序脱离.net framework框架运行的方法(转载)
- 【SSH三大框架】Hibernate基础第一篇:编写第一个Hibernate程序
- 框架程序基础
- iPhone SDK开发基础之iPhone程序框架
- PyQT 入门(1):程序基础框架
- 让.Net 程序脱离.net framework框架运行的方法(转载)
- [unity3D基础知识]NGUI中深度depth和z轴关系 (转载)
- 自动打卡程序 模拟鼠标按键代码【unity3D基础教程】
- Android开发之道(4)程序框架基础
- iPhone SDK开发基础之iPhone程序框架
- 框架基础js篇:小程序js介绍
- 【SSH三大框架】Struts2基础第一篇:编写第一个Struts2程序
- 【转载】微服务架构的基础框架选择:Spring Cloud还是Dubbo?
- 【SSH三大框架】Hibernate基础第一篇:编写第一个Hibernate程序
- 【MFC】程序框架及基础知识
- SuperSocket框架学习笔记3-构建Unity3D__WebSocket4Net客户端程序
- 【计算机基础】程序数据的5种存储位置(堆与栈) (转载)