一.引言
为了后续实现入门阶段的小demo,有必要先学习一个简单UI系统,比如下面会介绍的GUI
二.GUI的基本介绍
- GUI的主要作用
- 作为程序员的调试工具,创建游戏内调试工具
- 为脚本组件创建自定义检视面板
- 创建新的编辑器窗口和工具以拓展Unity本身(一般用作内置游戏工具) 注:不要用它为玩家制作UI功能,因为它是代码驱动,效率比较低,比较适合做一些设置面板或者调试面板
- GUI的工作原理
在继承MonoBehaviour的脚本中的特殊函数里调用GUI提供的方法,类似生命周期函数
private void OnGUI()
{
//在其中书写 GUI相关代码 即可显示GUI内容
}
- 它每帧执行,相当于是用于专门绘制GUI界面的函数
- 一般只在其中执行GUI相关界面绘制和操作逻辑
- 该函数 在 OnDisable之前 LateUpdate之后执行
- 只要是继承Mono的脚本都可以在OnGUI中绘制GUI
三. GUI基础控件
GUI基础控件其实大同小异,除了样式不同外,参数基本类似,尤其是构造函数,所以下面先介绍它们之间的共同参数
- 共同参数
- Rect:表示位置,和Android中自定义View的Rect类似,(x,y,width,height),和位置有关的UI位置矩阵都是通过一个左上顶点,宽高确定一个矩形范围以及位置
- 显示文本内容:参数类型是string
- Texture:表示一张图片,在Inpector视图中可以直接选择一张图片
- GUIContent:综合信息
- GUIStyle:自定义样式 所有的基础控件的构造函数参数就是上述共同参数的排列组合,下面会结合各个具体的控件进行演示,GUI中的所有控件都是静态函数,声明式UI
- Label标签
标签通常是用来展示一个文本信息,不过也可以图片加文本,通过GUIStyle可以控制它的样式
上图是通用GUIStyle的配面板配置,下面通过一张图来感受一下,很多配置信息我们都用不到,常用的就是位置,图片,字体等操作GUIContent可用于设置Label的文本内容,图片,图片默认在左边,然后就是鼠标悬停在上面的提示信息
// 声明公共的在Inspector面板中可操作的变量 public Texture tex; public Rect rect; public Rect rect1; public GUIContent content; public GUIStyle style; //在OnGUI中声明UI private void OnGUI() { //基本使用 GUI.Label(new Rect(200, 0, 200, 100), "LabelTest", style); GUI.Label(rect, tex); //综合使用 GUI.Label(rect1, content); //可以获取当前鼠标或者键盘选中的GUI控件 对应的 tooltip信息 Debug.Log(GUI.tooltip); } - Button
对于Button,介绍一下常规操作,比如和鼠标发生相应的变化以及点击事件监听
对于Button来说,我们设置Style即可,和Content关系不大,GUI中的控件事件监听都很简单,声明UI后都有返回值,这个返回值要么是点击了,要么是返回了它所容纳的内容
// 声明公共的在Inspector面板中可操作的变量 public Rect btnRect; public GUIContent btnContent; public GUIStyle btnStyle; // 点击事件监听 private void OnGUI() { if (GUI.Button(btnRect, btnContent, btnStyle)) { //处理我们按钮点击的逻辑 Debug.Log("按钮被点击"); } } - 多选框和单选框
多选框和单选框比较有意思的是参数和响应事件的结果是绑定在一起的,否则不会有效果凡是自定义样式都是传入一个可以通过Inspector外部调节的GUIStyle参数,多选框这里出现了一个padding,调整内边距,有过Android开发经验的朋友应该不会陌生,可以看到代码中,无论是单选还是多选,都会有一个变量直接或者间接地和它的相应事件结果挂钩,这样才会产生联动,否则没有变化private bool isSel; private bool isSel2; public GUIStyle style; private int nowSelIndex = 1; private void OnGUI() { #region 多选框 #region 普通样式 isSel = GUI.Toggle(new Rect(0, 0, 100, 30), isSel, "效果开关"); #endregion #region 自定义样式 显示问题 //修改固定宽高 fixedWidth和fixedHeight //修改从GUIStyle边缘到内容起始处的空间 padding isSel2 = GUI.Toggle(new Rect(0, 40, 100, 30), isSel2, "音效开关", style); #endregion #endregion #region 单选框 //单选框是基于 多选框的实现 //关键:通过一个int标识来决定是否选中 if(GUI.Toggle(new Rect(0, 100, 100, 30), nowSelIndex == 1, "选项一")) { nowSelIndex = 1; } if(GUI.Toggle(new Rect(0, 140, 100, 30), nowSelIndex == 2, "选项二")) { nowSelIndex = 2; } if(GUI.Toggle(new Rect(0, 180, 100, 30), nowSelIndex == 3, "选项三")) { nowSelIndex = 3; } #endregion }
注:- 复选框有图片的话,必须设置Fixed宽高,否则图片会默认填充整个控件的区域
- Normal和On Normal是互为对立的两个状态,分别设置图标表示失效和生效的两个状态
- 输入框
输入框分为两种,一是普通文本输入,二是密码输入,所谓密码输入就是多传入一个参数,这个参数就是用于替换明文的字符可以看到,这里输入框的内容也会相应结果挂钩,不然不会发生变化private string inputStr = ""; private string inputPW = ""; private void OnGUI() { #region 输入框 #region 普通输入 //输入框 重要参数 一个是显示内容 string //一个是 最大输入字符串的长度 inputStr = GUI.TextField(new Rect(0, 0, 100, 30), inputStr, 5); #endregion #region 密码输入 inputPW = GUI.PasswordField(new Rect(0, 50, 100, 30), inputPW, '★'); #endregion #endregion } - 拖动条
常用于设置音量大小,这里知道API如何使用即可private float nowValue = 0.5f; private void OnGUI() { #region 知识点二 拖动条 #region 水平拖动条 // 当前的值 // 最小值 left // 最大值 right nowValue = GUI.HorizontalSlider(new Rect(0, 100, 100, 50), nowValue, 0, 1); Debug.Log(nowValue); #endregion #region 竖直拖动条 nowValue = GUI.VerticalSlider(new Rect(0, 150, 50, 100), nowValue, 0, 1); #endregion #endregion } - 图片展示UI(专业术语ImageView)
作用就是展示一个图片,所以设置一个Texture就可以了,下面主要想整理一下它的三种缩放模式,很多UI系统都会有这三种模式- ScaleAndCrop: 会通过宽高比来计算图片 但是 会进行裁剪
- ScaleToFit:会自动根据宽高比进行计算 不会拉变形 会一直保持图片完全显示的状态
- StretchToFill: 始终填充满你传入的 Rect范围,可能会变形
框的绘制比较少用,呈现的形式就是一个透明背景框public Rect texPos; public Texture tex; public ScaleMode mode = ScaleMode.StretchToFill; public bool alpha = true; public float wh = 0; private void OnGUI() { #region 知识点一 图片绘制 // ScaleMode // ScaleAndCrop:也会通过宽高比来计算图片 但是 会进行裁剪 // ScaleToFit:会自动根据宽高比进行计算 不会拉变形 会一直保持图片完全显示的状态 // StretchToFill:始终填充满你传入的 Rect范围 // alpha 是用来 控制 图片是否开启透明通道的 //imageAspect : 自定义宽高比 如果不填 默认为0 就会使用 图片原始宽高 GUI.DrawTexture(texPos, tex, mode, alpha, wh); #endregion #region 知识点二 框绘制 //GUI.Box(texPos, ""); #endregion
四.GUI复合控件
复合控件就是多个单元控件组成,比如列表、网格等等,功能更加强大一点
- 工具栏
用于展示多个菜单项// 和之前的单选框一样,得动态关联一个选中索引值 private int toolbarIndex = 0; private string[] toolbarInfos = new string[] { "强化", "进阶", "幻化" }; private void OnGUI() { #region 工具栏 toolbarIndex = GUI.Toolbar(new Rect(0, 0, 200, 30), toolbarIndex, toolbarInfos); //工具栏可以帮助我们根据不同的返回索引 来处理不同的逻辑 switch (toolbarIndex) { case 0: break; case 1: break; case 2: break; } #endregion } - 选择网格
功能和工具栏一样,也是集中展示多个菜单项,但是选择网格比工具栏的排版更加灵活,可以控制列数,并且自行调整布局,工具栏默认是横向的,而选择网格如果控制列数只有1列,那么排版就是纵向的private int selGridIndex = 0; private void OnGUI() { #region 选择网格 //相对toolbar多了一个参数 xCount 代表 水平方向最多显示的按钮数量 selGridIndex = GUI.SelectionGrid(new Rect(0, 50, 60, 90), selGridIndex, toolbarInfos, 1); #endregion } - 分组管理
分组管理是复杂复合关系的一个起始点,用于包裹多个子控件,子控件的位置是相对于父控件的位置使用上就是有始有终,有begin,就有end,配对使用,如果父控件的大小承载不了所有子控件,只能进行裁剪public Rect groupPos; // 父控件相对于游戏窗口的位置 private void OnGUI() { #region 分组 // 用于批量控制控件位置 // 可以理解为 包裹着的控件加了一个父对象 // 可以通过控制分组来控制包裹控件的位置 GUI.BeginGroup(groupPos); // (0,0)是父控件内部的左上角 GUI.Button(new Rect(0, 0, 100, 50), "测试按钮"); GUI.Label(new Rect(0, 60, 100, 20), "Label信息"); GUI.EndGroup(); #endregion } - 滚动列表
在分组管理的基础上来理解滚动列表就很轻松了,滚动列表就是在分组管理的基础上增加了横向和纵向的可滚动功能,下面主要是理解它的几个参数- 参数一是滚动列表的位置以及宽高
- 参数二是记录当前滚动的位置
- 参数三是可视区域的大小以及位置 如果说可视区域大于了滚动列表的宽高就属于可滚动的状态
public Rect scPos; public Rect showPos; private Vector2 nowPos; private string[] strs = new string[] { "123", "234", "222", "111" }; private void OnGUI() { #region 滚动列表 nowPos = GUI.BeginScrollView(scPos, nowPos, showPos); GUI.Toolbar(new Rect(0, 0, 300, 50), 0, strs); GUI.Toolbar(new Rect(0, 60, 300, 50), 0, strs); GUI.Toolbar(new Rect(0, 120, 300, 50), 0, strs); GUI.Toolbar(new Rect(0, 180, 300, 50), 0, strs); GUI.EndScrollView(); #endregion } - 窗口
共有三种窗口:展示一般信息,警示框,可拖动框private Rect dragWinPos = new Rect(400, 400, 200, 150); private void OnGUI() { #region 窗口 //第一个参数 id 是窗口的唯一ID 不要和别的窗口重复 //委托参数 是用于 绘制窗口用的函数 传入即可 GUI.Window(1, new Rect(100, 100, 200, 150), DrawWindow, "测试窗口"); //id对于我们来说 有一个重要作用 除了区分不同窗口 还可以在一个函数中去处理多个窗口的逻辑 //通过id去区分他们 GUI.Window(2, new Rect(100, 350, 200, 150), DrawWindow, "测试窗口2"); #endregion #region 模态窗口 //模态窗口 可以让该其它控件不在有用 //你可以理解该窗口在最上层 其它按钮都点击不到了 //只能点击该窗口上控件 //GUI.ModalWindow(3, new Rect(300, 100, 200, 150), DrawWindow, "模态窗口"); #endregion #region 拖动窗口 //位置赋值只是前提 dragWinPos = GUI.Window(4, dragWinPos, DrawWindow, "拖动窗口"); #endregion } private void DrawWindow(int id) { switch (id) { case 1: GUI.Button(new Rect(0, 30, 30, 20), "1"); break; case 2: GUI.Button(new Rect(0, 30, 30, 20), "2"); break; case 3: GUI.Button(new Rect(0, 30, 30, 20), "3"); break; case 4: //该API 写在窗口函数中调用 可以让窗口被拖动 //传入Rect参数的重载 作用 //是决定窗口中哪一部分位置 可以被拖动 //默认不填 就是无参重载 默认窗口的所有位置都能被拖动 GUI.DragWindow(new Rect(0,0,1000,20)); break; } }- 参数中会传入一个委托,可以搭配id做分流逻辑
- 可拖动窗口可以传入一个约束可拖动范围的Rect
- 自定义皮肤样式:GUISkin
它是类似于一个Material材质一样的配置文本,另外就是集成了所有基础控件的GUIStyle,可视化配置,如下图所示需要注意的就是GUIStyle的优先级大于GUISkin,前者会覆盖后者,上面提到的全局颜色仅做了解,基本不用public GUIStyle style; public GUISkin skin; private void OnGUI() { #region 全局颜色 //全局的着色颜色 影响背景和文本颜色 //GUI.color = Color.red; //文本着色颜色 会和 全局颜色相乘 //GUI.contentColor = Color.yellow; //GUI.Button(new Rect(0, 0, 100, 30), "测试按钮"); ////背景元素着色颜色 会和 全局颜色相乘 //GUI.backgroundColor = Color.red; //GUI.Label(new Rect(0, 50, 100, 30), "测试按钮"); //GUI.color = Color.white; //GUI.Button(new Rect(0, 100, 100, 30), "测试按钮", style); #endregion #region 整体皮肤样式 GUI.skin = skin; //虽然设置了皮肤 但是绘制时 如果使用GUIStyle参数 皮肤就没有 GUI.Button(new Rect(0, 0, 100, 30), "测试按钮"); GUI.skin = null; GUI.Button(new Rect(0, 50, 100, 30), "测试按钮2"); //它可以帮助我们整套的设置 自定义样式 //相对单个控件设置Style要方便一些 #endregion } - 自动布局:GUILayout
类似于流水布局,自动调整宽高,这个不用于做UI,太灵活了,常做编辑器界面从代码上来看就是将GUI换成GUILayout,然后可以传入进行附加约束的GUILayout的约束条件,基本上就在上面了,做一个了解吧private void OnGUI() { #region 知识点一 GUILayout 自动布局 //主要用于进行编辑器开发 如果用它来做游戏UI不太合适 GUI.BeginGroup(new Rect(100, 100, 200, 300)); GUILayout.BeginVertical(); GUILayout.Button("123", GUILayout.Width(200)); GUILayout.Button("245666656565"); GUILayout.Button("235", GUILayout.ExpandWidth(false)); GUILayout.EndVertical(); GUI.EndGroup(); #endregion #region 知识点二 GUILayoutOption 布局选项 //控件的固定宽高 GUILayout.Width(300); GUILayout.Height(200); //允许控件的最小宽高 GUILayout.MinWidth(50); GUILayout.MinHeight(50); //允许控件的最大宽高 GUILayout.MaxWidth(100); GUILayout.MaxHeight(100); //允许或禁止水平拓展 GUILayout.ExpandWidth(true);//允许 GUILayout.ExpandHeight(false);//禁止 GUILayout.ExpandHeight(true);//允许 GUILayout.ExpandHeight(false);//禁止 #endregion }
五.总结及实践
这里对GUI做一个小结,个人感觉这种声明式UI使用着实繁琐,只有运行起来才能进行调整
- 优点
简单快捷
代码控制 - 缺点
重复工作量繁多
控件绘制相关代码很多
必须运行时才能查看结果
不支持分辨率自适应 针对以上缺点,下面进行对各个基础控件的封装,让它们直接可用,并且还支持分辨率自适应使用效果如上图,就类似于UGUI直接在Canvas上进行渲染,不需播放模式就能实时调整,用到的思想就是抽象加封装,也有一些小技巧吧,下面简单整理一下
- 使用
[ExecuteAlways]注解可以让其脚本直接处于播放模式 - 通过一个Root根物体管理所有的游戏物体,控制它们的渲染顺序,只需遍历执行对应的Draw方法即可
- 对Draw方法的抽象,根据上文的描述,GUI是在OnGUI函数中进行的渲染,那只需要在抽象父类的同时抽象该绘制方法即可,在根物体中进行调用
- 针对不同的GUI控件会有属于自己的特色,比如按钮,单选控件需要回调出它的结果或者响应点击事件,只需要提供一个外部可以设置的event事件即可,这里再补充一下,Inspector如果需要一个Mono对象,直接拖入它附着的游戏物体即可
- 另外一个就是9宫格设置物体的相对游戏窗体的位置,这个属于共有的属性,需要结合下图+代码进行理解
代码链接,另外附带导出的unity资源包,拖入Unity工程可直接使用上述封装的预制体