13.总结
13.1 知识点
导出GUI自定义控件资源包,在其他项目尝试导入使用,提前感受UGUI

13.2 核心要点速览
ExecuteAlways 与 OnGUI
[ExecuteAlways] 写在 class 上,不是方法上。挂上以后,编辑模式也会跑 MonoBehaviour 那一套生命周期;教程里从 Awake 一路到 OnDestroy 都能在编辑态看到,其中 Update、OnGUI 是否「每帧都打满」还看物体激不激活、脚本启不启用、编辑器有没有在重画界面(例如拖 Game 视口也会带动刷新)。
不进 Play 就想在 Scene / Game 里画 IMGUI、验布局,这是本系列的前置条件。反过来,编辑态里 OnGUI 一样可能很勤,往里塞 Debug.Log 或重计算,Console 和主线程照样会炸;教程用日志是为了看清调用顺序,自己项目里该节流就节流,不用编辑态驱动就把组件关掉或去掉特性。
[ExecuteAlways]
public class Demo : MonoBehaviour
{
void Update() { /* 编辑态也可能每帧进 */ }
private void OnGUI() { /* IMGUI 入口 */ }
}
想做成什么样
目标是一组常用控件:按钮、文本、Toggle、单选组、输入框、滑动条、贴图等,全部能做成 Prefab 拖进场景,在 编辑模式 下就能摆、能看,而不是「必须 Play,再在运行时 OnGUI 里临时堆一坨」才看得见。
拆数据时按三块想比较顺:控件贴在屏幕哪、长什么样、显示什么内容。位置和分辨率、九宫格对齐、Rect 换算绑在一起,字段多、分支多,单独做成 CustomGUIPos 这类序列化类型;样式和内容更像 Inspector 里能调的参数,收在抽象基类 CustomGUIControl 里,下面再派生具体控件。
两套九宫格在算什么
屏幕按 3×3 取锚点:原点在左上角,宽 w 高 h,比如中心点是 (w/2, h/2),右下是 (w, h)。分辨率一变,这些点重新算一遍,控件跟着锚点走,就是自适应在位置层干的事。
控件自己也有 3×3:在矩形里选一个点当枢轴,去对齐上面的屏幕锚点。GUI.Button 一类 API 要的 Rect 是 左上角 + 宽高,所以要把「枢轴在控件上的位置」换成左上角;课件图里用控件宽 cw、高 ch 给了九种偏移,例如整体居中常见 (-cw/2, -ch/2)。
屏幕锚点算出来的点,加上控件枢轴偏移,再加上 Inspector 里的 pos 微调,最后得到绘制原点。E_Alignment_Type 同一套枚举名用在两个字段上:screen_Alignment_Type 管贴屏哪一格,control_Center_Alignment_Type 管矩形里哪一点去对齐那个锚点,语义拆开才组合得开。
CustomGUIPos:从字段到 Rect
类上加 [System.Serializable],不要继承 MonoBehaviour,挂在控件组件里才能在 Inspector 展开。
CalcCenterPos() 只读控件宽高和 control_Center_Alignment_Type,把枢轴换算成相对控件左上角的 centerPos。CalcPos() 再按 screen_Alignment_Type 取屏幕锚点,和 centerPos、pos 合成 rPos 的 x、y;竖直方向有的分支会配合 Screen.height 做减号,和 Y 向下、从下往上贴边有关,具体以项目里的 switch 为准。Pos 的 getter 顺序固定:先中心偏移,再屏幕位置,最后写入宽高,因为屏幕公式里要用到已经算好的 centerPos。
| 成员 | 含义 |
|---|---|
screen_Alignment_Type |
锚在屏幕哪一格 |
control_Center_Alignment_Type |
控件上哪一点对齐该锚点 |
pos |
额外平移 |
width / height |
控件尺寸 |
Pos |
给 GUI.* 用的 Rect |
public Rect Pos
{
get
{
CalcCenterPos();
CalcPos();
rPos.width = width;
rPos.height = height;
return rPos;
}
}
基类、根节点、绘制顺序
CustomGUIControl 持有 guiPos、content、style、styleOnOrOff。对外就一个 DrawGUI(),里面 switch 到 StyleOnDraw / StyleOffDraw 两个 abstract 方法;子类必须两条都实现,相当于用编译器卡死「有皮肤 / 没皮肤」两套绘制,而不是每个子类自己抄一大段 if。
CustomGUIRoot 同样 [ExecuteAlways],挂在场景里当根。OnGUI 里 GetComponentsInChildren<CustomGUIControl>(),按返回数组顺序调 DrawGUI(),Hierarchy 里兄弟节点的上下关系大致就是叠放顺序。只在 Start 缓存数组的话,子物体隐藏、增删之后列表对不上,编辑态会画出已经关掉的东西;正文选择在 每一帧 OnGUI 里重新取,用遍历成本换层级一致。子控件多了再考虑脏标记或别的事件驱动刷新。
各子类对接哪些 GUI API
绘制统一写 guiPos.Pos。即时模式约定:GUI.Toggle、GUI.TextField、HorizontalSlider 等返回值表示 本帧之后 应有的状态,必须写回字段,下一帧才能接着画对;封装里再拿旧值比一比,只在变化时抛事件,避免每帧 Invoke 把外面刷爆。
| 子类 | 调用 | 事件 / 注意 |
|---|---|---|
CustomGUILabel |
GUI.Label |
只画,GUIContent 带文案 |
CustomGUIButton |
GUI.Button |
返回 true 当帧 clickEvent |
CustomGUIToggle |
GUI.Toggle |
结果写 isSel;和 isOldSel 比,变了再 changeValue |
CustomGUIInput |
GUI.TextField |
写回 content.text;变了把当前全文交给 textChange,再更新 oldStr |
CustomGUISlider |
横/纵 Slider |
写回 nowValue;开样式时多传 styleThumb |
CustomGUITexture |
GUI.DrawTexture |
content.image + ScaleMode;正文里 On/Off 两路一样,不区分 GUIStyle |
CustomGUIToggleGroup 不是 CustomGUIControl 派生,里面 CustomGUIToggle[]。Start 里默认第一项为真,给每个 Toggle 订 changeValue:谁变成 true 就把其它 isSel 清掉并记下 frontTrueToggle;若当前被关掉的是「上一个为真的那个」,就强行设回 true,避免单选组被点成全 false。lambda 里用循环内的局部变量接住当前 toggle,别直接闭包循环变量本身。
场景里怎么摆
根物体挂 CustomGUIRoot,下面再挂面板空物体和各个控件子物体,子物体上挂对应脚本,Gui Pos、内容、样式在 Inspector 里配。测试用 CustomGUITestPanel 把引用拖成 public 字段,在 Start 里 += 各种事件。给 toggles[i] 写 lambda 时写 int index = i 再捕获,否则所有回调里的下标会糊成最后一个。跑通后 Game 里能看到整套皮肤测试界面,Console 里能验证点击和数值回调。
13.3 面试题精选
基础题
1. [ExecuteAlways] 加在脚本上之后,编辑模式会多发生什么;写 OnGUI 时要注意什么
题目
特性加在类上还是方法上?不进 Play 时哪些回调可能照样跑?工程上容易踩哪类坑?
深入解析
- 加在 class 上。对象在编辑模式下也会被驱动,常见生命周期和
Update、OnGUI等都可能进,不必依赖播放模式。 - 是否「每帧都打满」仍受激活、启用和编辑器重绘影响;拖 Game 视口等操作也会带动 GUI 相关回调。
OnGUI里每帧打日志、做重计算或加载资源,在编辑态同样会拖主线程;不需要编辑态预览就关组件或去掉特性。
答题示例
打在类上,编辑模式也会跑 Mono 生命周期和
OnGUI这类回调,方便不播放就画界面。注意编辑态也会频繁进
OnGUI,别在里面刷日志或干重活。
参考文章
- 1.必备知识点-编辑模式下让指定代码运行
2. 为什么 GUI.Toggle、GUI.TextField 要把返回值写回字段;封装里为什么还要和「旧值」比一下再抛事件
题目
即时模式 GUI 每一帧在干什么?不写回会怎样?isOldSel、oldStr 这类变量在解决什么问题?
深入解析
- IMGUI 每帧按当前状态重画;控件 API 的返回值是 本帧交互之后 应有的新状态,不存进成员,下一帧仍用旧数据,勾选会弹回、输入串不起来。
GUI.Toggle每帧都会调用,多数帧选中状态不变;若每帧都Invoke,订阅方和日志会被无意义冲刷。和旧值比较只在变化时通知,是边沿触发,Slider、Input 同理。
答题示例
返回值是本帧之后的状态,必须写回字段,下一帧才能接着画对。
和旧值比是为了只在状态变的时候回调,避免每帧打扰外面。
参考文章
- 8.自定义常用控件-自定义多选框
- 10.自定义常用控件-自定义输入框和拖动条
进阶题
1. CustomGUIPos.Pos 里为什么要先 CalcCenterPos 再 CalcPos;屏幕九宫格和控件枢轴两套枚举各管什么
题目
getter 里顺序能不能调换?screen_Alignment_Type 和 control_Center_Alignment_Type 为何各要一份?
深入解析
CalcCenterPos只依赖宽高和控件中心对齐,产出centerPos;CalcPos里算屏幕锚点时要加上这个偏移。顺序反了屏幕分支用的是错的或未更新的centerPos。宽高最后写入rPos再返回完整Rect。- 屏幕九宫格决定控件贴在整屏的哪一格基准点;控件九宫格决定矩形里哪一个点去对齐那个基准点。
GUI.*要的是左上角矩形,枢轴不同则换算不同,两套语义拆开才能组合出「贴右下但把手在角上」这类布局。
答题示例
先算控件枢轴相对左上角的偏移,再带进屏幕锚点公式;后面一步依赖
centerPos。屏幕枚举管贴屏哪,控件枚举管自身哪一点去对齐屏幕锚点,两个维度不能混成一个。
参考文章
- 3.九宫格概念
- 4.控件位置信息类
2. CustomGUIRoot 为什么在 OnGUI 里每帧 GetComponentsInChildren;单选组里 frontTrueToggle 和闭包里的 toggle / index 分别在防什么
题目
和只在 Start 缓存数组相比利弊?CustomGUIToggleGroup 为何不能把当前选中项改成 false 就完事?循环里挂 lambda 为什么要局部变量接住下标?
深入解析
Start只取一次时,子物体隐藏或层级变化后数组过期,编辑态会继续画不该画的东西;每帧取一遍成本高,但和 Hierarchy 一致。子控件多了再考虑缓存失效策略。- 再点当前选中 Toggle 时 IMGUI 可能把它打成
false,若不拦会出现全不选;用frontTrueToggle记住上一个为真的项,必要时强制拉回true。给每个 Toggle 订事件时 lambda 要捕获当次循环的toggle或int index = i,否则闭包会粘到循环终值,逻辑全乱。
答题示例
每帧取子控件是为了编辑态层级和显隐和画面一致;缓存一次会画错。
frontTrueToggle防止单选组被点成全 false;闭包里用局部副本防止所有委托共用一个循环变量。
参考文章
- 6.控件根对象
- 9.自定义常用控件-自定义单选框
- 12.使用自定义控件
深度题
1. 把 IMGUI 拆成 Prefab + 组件 + CustomGUIRoot 统一画,和单脚本写死 OnGUI 相比,换来什么、付出什么;ExecuteAlways 什么时候不该滥用
题目
这套结构适合什么场景?主要开销和调试成本在哪?玩法逻辑能不能默认全挂编辑态每帧跑?
深入解析
- 收益:编辑态能摆能看、Inspector 配位置和样式、Hierarchy 顺序大致对应绘制顺序、控件可复用成 Prefab,分工接近 Canvas 那套习惯。
- 代价:多个
MonoBehaviour、序列化字段、根上每帧遍历子控件;调试要同时看场景结构和绘制代码。极简调试 UI 或控件数量极大时,未必值得这层抽象。 ExecuteAlways适合工具、预览、编辑器内可视化;核心玩法、物理、强状态逻辑应以 Play 为主,长期编辑态 Tick 容易和序列化、Undo、真实帧率纠缠,副作用大。
答题示例
换来预制体工作流和编辑态验收,付出组件数量和每帧查找成本。
玩法别默认绑在编辑态每帧上;
ExecuteAlways留给工具类和预览。
参考文章
- 2.需求分析
- 1.必备知识点-编辑模式下让指定代码运行
- 6.控件根对象
- 12.使用自定义控件
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com