1. 项目概述:为什么UI是Unity开发者的必修课?
如果你刚开始接触Unity,或者已经做了一段时间的游戏逻辑开发,但一遇到UI就头疼,那这篇文章就是为你准备的。我见过太多开发者,能把角色移动、物理碰撞、敌人AI写得行云流水,但一到做界面,比如一个简单的背包或者血条,就感觉无从下手,代码写得又乱又难维护。这太正常了,因为Unity的UI系统,尤其是Canvas和它那一套组件,初看之下确实有点“反直觉”。它不像处理3D物体那样直接操作Transform,而是有一套自己的渲染、布局和交互规则。
简单来说,Unity UI系统是构建游戏界面、菜单、HUD(抬头显示)的核心工具集。而Canvas(画布)是整个UI世界的“舞台”和“渲染管理器”,所有UI元素都必须放在Canvas下才能被正确显示。组件则是构建这个舞台上每一个“演员”(按钮、文本、图片等)的积木。不理解Canvas,你的UI可能位置错乱、渲染异常;不熟悉核心组件,你就无法高效地搭建出功能丰富、响应灵敏的界面。无论是做一个简单的开始按钮,还是一个复杂的可拖拽背包系统,都绕不开对这两者的深入理解。接下来,我会带你从最根本的原理出发,拆解Canvas的奥秘,并详解那些最常用也最关键的UI组件,让你不仅能“做出来”,更能“做得好”。
2. Canvas深度解析:UI世界的基石与渲染管家的秘密
Canvas远不止是一个容器。它定义了UI元素的坐标空间、渲染顺序以及如何与屏幕适配。理解它的三种渲染模式,是避免UI显示问题的第一步。
2.1 三种渲染模式的选择与实战场景
创建Canvas时,第一个要做的关键决策就是选择Render Mode(渲染模式)。这个选择直接影响UI的显示方式、性能以及与其他游戏对象的交互。
Screen Space - Overlay(屏幕空间 - 覆盖):这是最常用、性能开销通常最小的模式。在此模式下,Canvas会被直接渲染在屏幕的最上层,无视任何3D场景中的相机。它的坐标系就是屏幕的像素坐标(左下角是(0,0),右上角是(Screen.width, Screen.height))。你通过RectTransform设置的锚点和位置,都是相对于屏幕边缘来计算的。
注意:Overlay模式下的UI不会出现在任何相机的渲染纹理(Render Texture)中,如果你需要将UI渲染到RT上做后期处理,此模式不适用。
适用场景:绝大多数2D游戏的UI、手机游戏的界面、简单的菜单和HUD。例如,一个始终显示在屏幕顶部的分数文本、一个覆盖全屏的暂停菜单。
Screen Space - Camera(屏幕空间 - 相机):这种模式下,Canvas被放置在一个指定相机前方的固定距离上。它本质上是一个被渲染到该相机视图的平面。UI的大小和位置会受到相机视口(Viewport)的影响。你需要将一个Camera组件拖拽到Canvas的“Render Camera”槽中。
核心优势与考量:这种模式允许3D场景中的物体(通过调整它们的渲染顺序)显示在UI的前面或后面,实现诸如“角色走到菜单后面”的效果。同时,UI可以受到相机的后期效果(如Bloom, Color Grading)影响。但它的性能开销略高于Overlay模式,且如果相机移动或旋转,UI也会随之移动(除非你做特殊处理)。
适用场景:需要UI与3D场景有深度交互的游戏。例如,在一个第一人称游戏中,将血条和弹药信息渲染在手持武器模型附近;或者在一个有景深效果的场景中,让UI也产生自然的模糊。
World Space(世界空间):在此模式下,Canvas完全变成一个3D空间中的物体,拥有真实的大小(以Unity单位计)和3D位置。你可以像移动一个Cube一样移动、旋转、缩放它。
实战应用:这是制作“游戏内UI”或“Diegetic UI”(叙事性UI)的利器。比如,一个漂浮在角色头顶的姓名板、一个可交互的终端屏幕、或者一个需要玩家在3D世界中走近才能查看的信息面板。
实操心得:使用World Space模式时,你需要手动为Canvas添加一个“Graphic Raycaster”组件,并可能需要调整Event Camera,才能让UI响应鼠标或触摸事件。同时,要特别注意Canvas的Scale(缩放),一个1x1单位的Canvas在屏幕上可能非常小,通常需要将其缩放至一个合适的尺寸(如0.001或0.002),以便于在3D场景中布局。
2.2 RectTransform:比Transform更强大的布局引擎
所有UI元素都使用RectTransform,它是Transform的“超级进化版”,专为矩形区域的布局而设计。理解它的四个核心概念,是精确控制UI位置和大小的关键。
1. 锚点(Anchors):这是RectTransform的灵魂。锚点定义了UI矩形与其父矩形(可能是Canvas,也可能是另一个UI元素)四个边的相对关系。它不是固定位置,而是一种“约束规则”。
- 预设锚点:Unity提供了快捷的预设(如左上、居中、拉伸),但理解其本质更重要。
- 自定义锚点:你可以分别拖动四个小三角形锚点图标。锚点的值(0到1)表示相对于父矩形宽度或高度的百分比。例如,左锚点为0.2,右锚点为0.8,意味着该UI元素的左右边将始终固定在父矩形宽度20%和80%的位置。
- “拉伸”模式:当左右锚点不重合(即左锚点X值不等于右锚点X值),且上下锚点也不重合时,UI元素会随着父矩形的大小变化而拉伸。此时,PosX/Y变为Left/Bottom等偏移值。
2. 轴心点(Pivot):轴心点是UI元素自身旋转、缩放和位置计算的参考点。它的值也是(0,1)之间的比例。默认(0.5,0.5)是中心。如果你想让一个按钮从它的左下角缩放,就把Pivot设为(0,0);如果你想让一个血条从左边向右减少,通常会将Pivot设为(0,0.5),这样修改宽度时,血条的右端会收缩,左端保持不动。
3. 位置与尺寸:根据锚点的不同,这里显示的属性会变化。
- 当锚点重合为一个点时(如中心锚点),显示的是
Pos X, Pos Y和Width, Height。Pos是相对于锚点的偏移,尺寸是绝对像素值。 - 当锚点处于“拉伸”模式,显示的是
Left, Right, Top, Bottom,表示各边距离父矩形对应锚定边的距离。
4. 实战布局技巧:
- 响应式布局:利用锚点的拉伸特性,可以轻松实现适应不同屏幕分辨率的UI。例如,一个底部的工具栏,可以将其下锚点固定在底部,左右锚点分别固定在左右两侧,这样无论屏幕多宽,工具栏都会自动拉伸至全屏宽度。
- 组合使用:一个复杂的UI,如一个对话框,可能整体使用中心锚点定位在屏幕中央,但其内部的标题栏使用左上锚点,内容区域使用拉伸锚点,关闭按钮使用右上锚点。通过父子层级和不同的锚点设置,可以构建出结构清晰、易于维护的UI层级。
3. 核心UI组件详解:从静态展示到动态交互
掌握了Canvas和RectTransform,我们就有了舞台和定位系统。现在,该请出舞台上的“演员”们了——也就是各种UI组件。我们按功能分类,逐一拆解。
3.1 基础显示组件:Image与Text
Image组件:用于显示精灵(Sprite)、纹理(Texture)或纯色。它是构建UI视觉的基础。
- Source Image:指定要显示的图片资源。这里通常使用Sprite类型的资源。
- Color:对图片进行整体着色。常用技巧:设置一个白色半透明的Color,可以快速实现“禁用”或“高亮”效果。
- Material:可以指定自定义材质,实现特殊的UI效果(如溶解、流光)。
- Image Type:
- Simple:直接拉伸显示整张图片。
- Sliced(九宫格):这是最实用的类型之一。你需要为Sprite设置九宫格边界(Border)。渲染时,四个角保持不变,四条边单向拉伸,中间部分双向拉伸。这保证了UI按钮、面板在任意缩放时,边框不会变形。这是制作可伸缩UI边框的标配。
- Tiled:平铺显示,适用于制作无缝背景。
- Filled:填充显示,可以制作圆形或扇形的进度条、血条、技能冷却图标。通过设置
Fill Method和Fill Amount来控制。
Text - TextMeshPro (TMP):强烈建议,永远使用TextMeshPro替代旧的Unity UI Text组件。TMP在渲染质量、功能丰富度和性能上都是碾压级优势。
- 字体资产(Font Asset):TMP使用自己生成的字体图集,支持更清晰的字体渲染和动态字体回退。
- 富文本(Rich Text):原生支持
<b>粗体</b>、<i>斜体</i>、<color=#FF0000>红色</color>等标签,无需额外代码即可实现复杂文本样式混合。 - 额外设置:字符间距、行间距、字间距、溢出省略号(…)、文字环绕等高级排版功能一应俱全。
- 性能提示:对于大量且频繁更新的文本(如排行榜),考虑使用“静态”文本,或使用对象池复用TMP组件,避免频繁的网格重建(Rebuild)。
3.2 交互组件:Button、Toggle、Slider、Scroll View
Button组件:本质上是带有Image和Text (TMP)子物体,并挂载了Button脚本的GameObject。
- 交互状态(Transition):
- Color Tint:最常用,通过改变颜色反馈交互(正常、高亮、按下、禁用)。
- Sprite Swap:为不同状态切换不同的精灵图片,视觉变化更丰富。
- Animation:可以指定一个Animator Controller,实现复杂的交互动画。
- OnClick() 事件:这是按钮的核心。你可以在Inspector中拖拽目标对象,并选择其上的方法。但更推荐在代码中使用
AddListener动态绑定,这样更利于解耦和管理。yourButton.onClick.AddListener(() => { Debug.Log("按钮被点击!"); // 执行你的逻辑 });
Toggle组件(复选框/开关):包含一个背景Image、一个表示勾选状态的Image(通常是打勾图标)和一个可选的Text标签。
- Is On:控制当前开关状态。
- Toggle Group:将多个Toggle放入同一个Toggle Group,它们就变成了单选按钮组(Radio Buttons)。
- OnValueChanged事件:监听开关状态变化,传递一个
bool参数。
Slider组件(滑动条):用于在一个范围内选择数值。由背景、填充区域和手柄(Thumb)组成。
- Value, Min Value, Max Value:定义当前值及范围。
- Whole Numbers:勾选后,Value将只能是整数。
- Direction:设置滑动方向(从左到右、从下到上等)。
- OnValueChanged事件:当值改变时触发,常用于实时更新(如音量调节、进度显示)。
Scroll View组件:这是一个复杂的复合组件,用于创建可滚动区域。理解其结构至关重要:
- Scroll Rect:核心脚本,管理滚动行为。
- Viewport:一个遮罩(Mask)组件,限定内容的可见区域。
- Content:实际承载所有可滚动内容的容器。它的尺寸通常会大于Viewport。
- 滚动条(可选):Horizontal/Vertical Scrollbar,关联到Scroll Rect。
- 关键设置:
- Movement Type:
Unrestricted(自由拖动)、Elastic(拖动超出边界后有回弹效果)、Clamped(严格限制在边界内)。 - Inertia:是否开启惯性滑动。
- Scroll Sensitivity:滚动灵敏度。
- Movement Type:
- 性能优化:对于超长列表(如聊天记录、背包),直接放几百个元素进去会严重卡顿。此时必须使用对象池(Object Pooling)或Unity的UI Toolkit或第三方资产(如EnhancedScroller, SuperScrollView)来实现动态创建和回收,只渲染可视区域内的少量元素。
3.3 容器与布局组件:自动排列的魔法
当UI元素数量动态变化时(如道具列表、技能栏),手动调整位置是噩梦。布局组件(Layout Group)可以自动帮你排列子物体。
Vertical/Horizontal Layout Group:垂直/水平布局组。它会按照顺序排列子物体,并可以控制间距(Spacing)、内边距(Padding)和对齐方式(Child Alignment)。
- 控制子物体大小:通过
Child Control Size和Child Force Expand选项,可以控制是否让布局组自动调整子物体的宽度和高度。
Grid Layout Group:网格布局组。按固定的单元格大小(Cell Size)和间隔(Spacing)排列子物体,自动换行。
- Constraint:可以固定行数或列数,或者根据容器大小自适应。
Content Size Fitter:内容尺寸适配器。通常用在被布局组管理的子物体上,或者Content上。它可以自动调整RectTransform的大小以匹配其子物体的总尺寸或自身文本/图片的尺寸。
- Horizontal/Vertical Fit:
Unconstrained(不约束)、Min Size(调整为最小尺寸)、Preferred Size(调整为偏好尺寸)。
实战组合:一个典型的动态列表,其结构可能是:Scroll View -> Viewport -> Content。在Content上挂载Vertical Layout Group和Content Size Fitter(Vertical Fit设为Preferred Size)。这样,当你动态在Content下添加或删除子项时,布局组会自动排列它们,Content Size Fitter会自动调整Content的高度,Scroll View就能正确滚动了。
4. 事件系统与输入处理:让UI“活”起来
UI光有样子不行,还得能点、能拖、能交互。这一切都由Unity的EventSystem和Raycasters驱动。
4.1 EventSystem与Graphic Raycaster的工作原理
EventSystem:这是一个单例管理器,负责协调所有输入事件(鼠标、触摸、游戏手柄、键盘导航)。场景中只需要一个。它查询所有Raycaster组件来检测输入落在了哪个UI对象上。
Graphic Raycaster:通常挂在Canvas上。它的工作是:当有输入事件发生时,从摄像机(对于Screen Space - Camera和World Space)或屏幕(对于Overlay)发射一条射线,检测这条射线是否与Canvas下任何带有CanvasRenderer(所有Graphic组件如Image、Text都有)且启用了Raycast Target的UI元素相交。它按照渲染顺序(由Canvas的Sort Order和同层级下的顺序决定)从后往前检测,并返回第一个被击中的对象。
重要设置:
- Blocking Objects:指定哪些类型的3D/2D物体会阻挡射线,使UI无法被点击。
- Blocking Mask:通过Layer层来更精细地控制阻挡。
4.2 事件接口与代码监听
除了在Inspector面板上拖拽配置事件,在代码中实现特定接口是更灵活强大的方式。常用的接口有:
IPointerClickHandler:处理点击事件(OnPointerClick)。IPointerEnterHandler/IPointerExitHandler:处理鼠标进入/离开事件(OnPointerEnter,OnPointerExit),常用于实现悬停高亮效果。IPointerDownHandler/IPointerUpHandler:处理按下和抬起事件,可用于开始拖拽。IBeginDragHandler,IDragHandler,IEndDragHandler:专门用于实现拖拽逻辑的完整链条。IScrollHandler:处理滚轮滚动事件。
示例:实现一个可拖拽的UI面板
using UnityEngine; using UnityEngine.EventSystems; public class DraggablePanel : MonoBehaviour, IBeginDragHandler, IDragHandler { private RectTransform panelRectTransform; private Canvas canvas; private Vector2 originalLocalPointerPosition; private Vector3 originalPanelLocalPosition; void Awake() { panelRectTransform = GetComponent<RectTransform>(); // 通常建议在最高层的Canvas上获取,以正确转换坐标 canvas = GetComponentInParent<Canvas>(); } public void OnBeginDrag(PointerEventData eventData) { // 记录开始拖拽时的位置 RectTransformUtility.ScreenPointToLocalPointInRectangle( panelRectTransform, eventData.position, eventData.pressEventCamera, out originalLocalPointerPosition ); originalPanelLocalPosition = panelRectTransform.localPosition; } public void OnDrag(PointerEventData eventData) { if (RectTransformUtility.ScreenPointToLocalPointInRectangle( canvas.transform as RectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPointerPosition )) { // 计算偏移量并更新面板位置 Vector3 offsetToOriginal = localPointerPosition - originalLocalPointerPosition; panelRectTransform.localPosition = originalPanelLocalPosition + offsetToOriginal; } } }这段代码的关键在于使用RectTransformUtility.ScreenPointToLocalPointInRectangle进行正确的坐标转换,这是处理UI拖拽的核心。
4.3 输入模块与多平台适配
EventSystem依赖Input Module来获取输入。默认是Standalone Input Module,处理鼠标和键盘输入。如果是移动平台,你需要切换到Touch Input Module,或者使用Input System Package提供的更现代、统一的输入系统。
使用新版Input System:Unity的新输入系统功能更强大,支持动作映射(Action Maps)和复合输入。要让UI与新输入系统协同工作,你需要:
- 安装
Input System包。 - 在
Player Settings中启用Active Input Handling为Input System Package或Both。 - 删除旧的
Standalone Input Module,为EventSystem添加Input System UI Input Module组件。 - 在Canvas上,确保
Graphic Raycaster的Event Camera已正确设置。
5. 性能优化与最佳实践:让UI流畅如丝
糟糕的UI是性能杀手。尤其是在低端移动设备上,UI造成的卡顿和发热非常明显。以下是一些经过验证的优化策略。
5.1 绘制调用(Draw Call)与合批(Batching)
这是UI性能最核心的概念。每一次绘制调用都是CPU向GPU发起的一次绘制指令。调用次数越多,CPU开销越大。
合批规则:Unity UI(基于UGUI)会自动尝试将同一Canvas下、使用相同材质(Material)和纹理(Texture)的UI元素合并到一个Draw Call中。理解并利用这个规则至关重要。
优化策略:
- 纹理图集(Sprite Atlas):这是减少Draw Call的终极武器。将多个UI小图标、背景碎片打包到一张大图里,它们就可以共享同一个材质和纹理,从而被合批。Unity有内置的
Sprite Atlas功能,务必学会使用。将频繁同时出现的UI元素打包在一起。 - Canvas分层:不要将所有UI元素都塞进一个Canvas。因为Canvas的任何顶点变化(位置、颜色、UV等)都会导致整个Canvas的网格重建(Rebuild)和重新合批。应该根据更新频率进行分层:
- 静态Canvas:放置几乎不变的UI,如背景图、静态边框。一个Draw Call可能就搞定。
- 动态Canvas:放置频繁变化的UI,如血条数字、滚动列表。将变化元素隔离,避免它们导致静态元素也重新合批。
- 技巧:对于像血条这种需要频繁更新填充(Fill Amount)的Image,如果它单独在一个Canvas下,那么每次更新只会引起这个小型Canvas的重绘,影响范围最小。
- 避免重叠:完全重叠的UI(即使在不同层级)有时会打断合批。尽量规划好UI布局。
5.2 网格重建(Rebuild)的触发与规避
当UI元素的属性发生变化(如Text的文本内容、Image的填充量、布局变化)时,Unity需要重新计算其网格(Mesh),这个过程叫Rebuild。它是性能的另一个主要瓶颈。
主要触发原因:
- 改变
Text.text或TMP_Text.text。 - 改变
Image.fillAmount或sprite。 - 改变任何影响布局的属性(如RectTransform的尺寸、激活/禁用带有Layout Group的子物体)。
- 改变颜色、材质等。
优化策略:
- 减少频繁更新:对于每秒更新多次的文本(如帧率显示),可以考虑每0.1秒或0.2秒更新一次,而不是每帧。
- 对象池(Object Pooling):对于动态列表(如聊天消息、道具图标),绝对不要频繁Instantiate和Destroy。使用对象池复用UI元素,只是更新其内容,这能极大减少Instantiate带来的开销和GC(垃圾回收)压力。
- 谨慎使用Layout Group:Layout Group在子物体变化时会触发昂贵的布局计算。对于数量庞大的动态列表,考虑使用自定义的简单布局计算,或者仅在必要时(如列表初始化、数量剧变)才启用Layout Group,平时禁用它。
- 使用Canvas.WillRenderCanvases事件:这是一个在Canvas即将渲染前调用的委托。你可以将一帧内多次的UI更新操作累积起来,在这个事件中一次性执行,从而将多次Rebuild合并为一次。
5.3 内存与资源管理
- 字体内存:TMP字体图集会占用内存。对于包含大量字符的字体(如中文),要合理设置字体图集大小和包含的字符集,避免生成不必要的巨大图集。可以考虑动态加载字体或使用字体回退链。
- 纹理内存:确保UI纹理的尺寸是2的幂次方(NPOT),并正确设置压缩格式(如ASTC for Android, PVRTC for iOS)。使用Sprite Atlas也能帮助管理纹理内存。
- 隐藏而非销毁:对于暂时不用的复杂UI界面(如设置菜单),如果频繁开关,可以考虑将其移出屏幕或缩放至0,而不是SetActive(false)和Destroy()。因为重新实例化和初始化可能比隐藏更耗时。但这需要权衡内存占用。
6. 常见问题排查与调试技巧实录
即使理解了原理,在实际开发中还是会遇到各种诡异的问题。这里记录了一些我踩过的坑和解决方法。
6.1 UI点击无响应或穿透
问题现象:点击UI按钮没反应,或者点击时触发了后面3D物体上的事件。
排查步骤:
- 检查Raycast Target:首先确认你需要点击的UI元素(Image, Text等)的
Raycast Target复选框是勾选的。这是最常被忽略的一点。 - 检查层级与遮挡:确认该UI元素在Hierarchy中的顺序(越靠下渲染越前,且射线检测优先级越高)。确认没有被其他
Raycast Target为true且完全覆盖它的UI元素挡住。 - 检查Canvas设置:确认Canvas的
Render Mode正确,且Graphic Raycaster组件存在并启用。对于World Space Canvas,检查Event Camera是否已正确赋值。 - 检查EventSystem:场景中必须有一个且仅一个激活的
EventSystem对象。检查其上的Input Module是否适合当前平台。 - 检查阻挡设置:如果UI后面有3D物体,检查
Graphic Raycaster的Blocking Objects和Blocking Mask设置,确保射线不会被意外阻挡。 - Debug射线:写一个简单的脚本,在
IPointerClickHandler的OnPointerClick方法里打印日志,看事件是否被触发。这能帮你定位问题是出在事件接收前还是接收后。
6.2 UI显示异常、错位或拉伸
问题现象:UI在部分设备或分辨率下位置不对,或者被意外拉伸变形。
排查步骤:
- 锚点检查:这是99%问题的根源。仔细检查RectTransform的锚点预设是否是你想要的。对于需要自适应的UI,是否应该使用“拉伸”锚点而非“中心”锚点。
- Canvas Scaler设置:Canvas Scaler是控制UI整体缩放适配的组件。检查其
UI Scale Mode:Constant Pixel Size:UI始终保持相同像素大小,在不同分辨率下物理尺寸会变。Scale With Screen Size:最常用。基于一个参考分辨率(如1920x1080)进行缩放。Match选项(Width/Height)决定了以宽度还是高度作为主要缩放基准,这会影响UI在不同宽高比屏幕上的表现。Constant Physical Size:试图保持物理尺寸(英寸),很少用。
- 父物体RectTransform影响:子物体的位置和大小受父物体RectTransform的锚点和位置影响。检查整个UI层级链上是否有父物体设置了非标准的缩放或锚点。
- Canvas渲染顺序:如果有多个Canvas,检查它们的
Sort Order。更高的值渲染在更上层。同Canvas内,子物体的顺序(Hierarchy中越靠下越后渲染)也会影响显示。
6.3 性能问题快速定位
问题现象:游戏在打开某个UI界面时明显卡顿,或UI滚动不流畅。
排查工具与方法:
- Unity Profiler:打开Profiler窗口,重点关注:
- CPU Usage:查看
Canvas.SendWillRenderCanvases的耗时,这直接反映了UI网格重建的开销。如果这一项很高,说明存在频繁的UI属性更新。 - Rendering:查看
Draw Calls的数量。在UI界面打开前后对比Draw Call的激增情况。使用Frame Debugger可以精确查看每一个Draw Call绘制了什么。
- CPU Usage:查看
- Frame Debugger:这是分析Draw Call和合批情况的利器。打开Frame Debugger,逐帧查看渲染指令。你可以清晰地看到哪些UI元素被合批到一个Draw Call,哪些没有,以及为什么没有(通常是材质或纹理不同)。
- 简单检查清单:
- 是否使用了纹理图集?
- 动态和静态UI是否分在了不同的Canvas?
- 是否有频繁更新的Text或Image?
- 是否在每帧都实例化/销毁UI对象?
6.4 输入系统相关疑难杂症
问题现象:在编辑器里运行正常,打包后点击无效;或者手柄/触摸输入不工作。
排查步骤:
- 平台输入模块:确保打包平台的Input Module正确。移动平台需要
Touch Input Module。 - 新输入系统兼容性:如果使用了新的Input System Package,确保所有相关组件(
Input System UI Input Module,PlayerInput等)配置正确,并且输入动作(Actions)已正确映射到UI导航事件。 - EventSystem锁死:有时在场景切换或异步加载时,EventSystem可能会出现异常,导致所有输入无响应。可以尝试在代码中查找或创建一个EventSystem实例。
void EnsureEventSystem() { if (FindObjectOfType<EventSystem>() == null) { var eventSystem = new GameObject("EventSystem"); eventSystem.AddComponent<EventSystem>(); eventSystem.AddComponent<StandaloneInputModule>(); // 或 eventSystem.AddComponent<InputSystemUIInputModule>(); } } - 多相机渲染:如果场景中有多个相机渲染UI(比如一个主相机,一个渲染小地图的相机),需要确保
Graphic Raycaster的Event Camera指向正确的相机,或者使用Physics Raycaster等组件来处理多相机情况下的输入分发。