思考并回答以下问题:
- Resources.Load后都需要使用Dictionary缓存吗?都需要写在管理类中吗?管理类需要设置成单例吗?
- 为什么Asset文件夹和Hierarchy视图中的都需要管理?如何管理?
- 为什么单例模式要分成可以继承MonoBehavior和不继承MonoBehavior的?
- 为什么要使用单例继承?
- UIFramework原来是3个类,后来多加了4个类。共7个类。为什么这样做?
- 将多个UI管理起来成一个系统。怎么理解?
- 当你不知道要传什么类时,就使用反射。是什么意思?
- 为什么要自动挂载脚本到预制体上?
- AddComponet(typeof(A))和AddComponet\()的区别是什么?
- 枚举只有一种类型的成员:命名的整数值常量。定义枚举,ENUMName,声明EnumName ConstName = EnumName.ConcreteConstant。
- 按钮点不了两种情况:1.被其他面板挡住了。2.EventSystem被删除掉了。怎么理解?
- 窗体之间进行解耦合怎么理解?怎么做到?
普通切换方式的弊端
三个UI界面进行切换。
MainPanel.cs
1 | using UnityEngine; |
PackPanel.cs
1 | using UnityEngine; |
LevelPanel.cs
1 | using UnityEngine; |
这种组织方式存在的问题:
- 1.商业项目中UI窗体会非常多,现在这种处理方式非常杂乱,需要进行一个总体上的管理;
- 2.窗体之间互相进行引用(在每个窗体内都引用了其他窗体,public GameObject packPanel;),耦合度太高,一旦修改起来很麻烦;
- 3.脚本代码重复很多;
- 4.所有的面板直接在场景中会导致包体变大。
解决方案:
- 1.需要把窗体管理起来,需要一个窗体的管理类;
- 2.窗体有很多属性是重复的,比如显示隐藏,需要一个基类把相同的行为特征封装起来,不同的窗体继承这个基类进行扩展,当然每个窗体的继承基类的子类脚本还是都要写;
- 3.把UI变成预制体(Prefab),进行动态加载后缓存。
搭建框架
框架很多,代码各不相同,但是思想是一致的。
关键点:
- 在Hierarchy视图中的UI游戏对象是可以变成Prefab的,Hierarchy中的一切都可以变成预制体(Hierarchy视图中的任何显示都是对象,都可以变成Prefab,无论是UI、空游戏对象还是模型。)。
- 组件都是类型,可以直接理解为是类,组件就是一个个类。GetComponent\
,T指的是类型,类是类型的一种。类名是确定的,对象名是不确定的,每次new类的时候都可以取新的对象名。 - 缓存就是使用Dictionary\
这个数据结构。缓存什么呢?缓存从Resources文件夹中加载的对象,Resources.Load是加载到Hierarchy视图中的,自然就变成游戏对象(GameObject)了。但是TValue不能写一个GameObject吧?A、B游戏对象的组件的脚本的类型是各异的,而且脚本可以满足里氏替换原则。 - 脚本自动加载。即脚本写好放在那,然后动态附加到游戏对象上。
工具类
通用函数工具类 GameTool.cs
1 | using UnityEngine; |
GameDefine定义一些枚举。枚举只有一种类型的成员:命名的整数值常量。
GameDefine.cs
1 | using UnityEngine; |
把日志也封装起来,这样可以总体控制日志的开关。然后转换成dll的形式,放进Plugins文件夹下。
GameDebugger.cs
1 | using UnityEngine; |
Singleton.cs
1 | // 单例模式(两种) |
上面不继承于Mono的单例模式继承存在问题,因为子类还是可以new,可以使用反射来实现真正的单例继承,查看单例的模板与最佳实践。
问:
- 为什么要区分继承和不继承MonoBehavior的单例?
- 单例的类不是直接调用吗?A类写成单例,就A.Instance使用,为什么还有单例继承这个东西?
- UIManager为什么使用单例模式?所有的Manager类都要吗?
答:
- 我所了解的使用单例模式有两个目的,一个是类似计算用,保持一份不会导致计算错误;二是节省内存。UIManager使用单例模式的原因是这个类负责管理游戏内的功能,它判断说这个没在加载,那就需要加载才能进行下一步,如果是多个对象同时运行,则可能会出现顺序错误。就像公司的总经理只有一个人一样,做具体工作的办公人员有很多,但最终调配决定的人只有总经理,不能两个总经理在发号施令,很容易造成工作指令的错发,漏发,重复发。把Manager类比成总经理。一般来说,做调配工作的,汇总工作的Manager需要设置成单例。
脚本管理
把Asset中的几个脚本统一管理,明显需要一个基类。这个基类最重要的是把窗体进行编号,因为编号后有每个窗体的id,可以知道是从哪一个窗体过来的。
问:
- 让你提取UI的共同元素,你能提取出来吗?
- 窗体之间如何识别彼此?
界面的显示类型有两种:
- 1.一直保持在最前面显示的界面;
- 2.会隐藏其他窗体的窗体,但是不会隐藏保持在最前面显示的界面。
隐藏方式有三种:
- 1.HideOther。显示的时候隐藏其他窗体。
- 2.HideAll。界面显示出来的时候,隐藏所有窗体。
- 3.DoNothing。不去隐藏其他任何窗体。
UI面板脚本基类 BaseUI.cs
1 | using UnityEngine; |
窗体管理
把Hierarchy层次视图中的几个UI Panel面板对象(GameObject)也进行管理,需要一个管理类。
需要做的事情有:
- 动态加载后存储。Resource.Load
()加载之后会得到一个对象,加载的代码处用到了,别的类,别的地方也需要用到,难道用一次加载一次?所以需要存储起来。对象要一处加载,多处使用的话就需要放进Dictionary数据结构中,在游戏的运行过程中这个字典会在内存中一直存在。 - 这个Manager是在游戏开始的就要执行的。所以需要直接展示主窗体(或欢迎窗体)。
- 之前做单例继承的时候提到了,这个脚本因为继承了单例,所以要手动绑定在UnitySingletonObject对象上。UnitySingletonObject放在Canvas下。组织关系如下图所示,注意KeepAboveUIRoot在NormalUIRoot的下方。
UIManager.cs
1 | using UnityEngine; |
消息中心
使用观察者模式的消息中心。这边没有新建观察者的接口并实现子类,而是通过C#语言的委托来代替观察者。
观察者模式最重要的一点是建立一个消息中心类,维护一个消息枚举和观察者的字典。
简单版本的观察者。缺点在于:1.对参数的装箱拆箱影响性能;2.参数只能传1个。
MessageCenter.cs
1 | using UnityEngine; |
EventController是观察者模式必备的维护事件与观察者列表的类。
Dispatch是发送的意思。
EventController.cs
1 | using System; |
EventDispatcher.cs
1 | using System; |
实现了解耦合,一个UI不再需要引用另一个UI。
配置表
一般有三种配置格式,Excel、JSON和XML。
ExcelData.cs
1 | using UnityEngine; |
使用1
2
初始化
InitGame.cs
1 | void Init() |
场景加载
Loading场景的作用:
- 1.从场景A直接切换到场景B后,场景A的资源依然在内存里。所以一般新增一个中间场景,叫做Loading场景。此场景展示加载进度。此时内存中有Loading场景和A场景的资源。当加载到B场景时,内存中只有B场景和Loading场景的资源。Loading场景占用资源非常少,可以忽略。
- 2.减少卡顿。
LoadSceneHelper.cs 需要先把此脚本拖曳到UnitySingletonObj对象下
1 | using UnityEngine; |
新增窗体
框架完成后,发挥威力的地方在于新增窗体时。
新增窗体需要以下几个步骤:
写一个UI脚本的步骤:
- 1.UI要继承于BaseUI;
- 2.重写两个重要的方法:InitUiOnAwake()和InitDataOnAwake()(InitUIOnAwake主要是初始化UI元素,比如获取按钮,监听按钮单击等等;InitDataOnAwake主要是给该窗体的ID赋值及设置父节点与显示方式。);
- 3.重写了以上两个方法后,在InitDataOnAwake()方法中一定要给该窗体的ID赋值。
MainUI.cs
1 | using UnityEngine; |
PackUI.cs
1 | using UnityEngine; |
LevelUI.cs
1 | using UnityEngine; |
全局协程控制类
协程和Update函数一样,是会中断的,什么时候中断呢?当协程所在的脚本附加的对象SetActive设置为false时,协程就会中断(此时Update也会中断)。而有些时候,协程不能中断,必须执行完,所以定义一个全局协程类脚本,保证其所附加的对象不能被销毁,不会被设置为false。
1 | // 这个脚本需要手动附加到UnitySingletonObj对象上 |
调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Demo : MonoBehaviour
{
void Start()
{
// StartCoroutine(ShowLog());
CoroutineCtrl.Instance.StartCoroutine(ShowLog());
// StartCoroutine是MonoBehavior类的方法,只要是继承MonoBehavior的类就可以直接使用
}
IEnumerator ShowLog()
{
yield return new WaitForSeconds(3f);
for(i = 0; i < 500; i++)
{
Debug.Log(i);
}
}
}
场景管理
GameSceneManager.cs
1 | using UnityEngine; |
AudioManager
AudioManager.cs 对Audio一个简单的封装
1 | using UnityEngine; |