1. 从UMG和Slate说起:为什么UE的UI系统值得深挖
如果你用过虚幻引擎做项目,大概率经历过这样的场景:美术在UMG编辑器里拖拖拽拽搭好了一套界面,运行起来发现某个按钮点不动,或者列表滚动卡得不行,又或者打包之后字体全糊了。你打开代码想排查,发现底层全是Slate的SCompoundWidget、TAttribute、FSlateBrush这些东西,一时间不知道从哪下手。
这就是UMG和Slate的关系——UMG是给美术和策划用的可视化UI搭建工具,Slate是给程序员用的底层UI框架。UMG的所有控件,本质上都是对Slate控件的封装。你在UMG里拖一个Button,底层就是一个SButton;你设置一个Image的Brush,底层就是一个FSlateBrush。理解这层关系,是排查UE UI问题的起点。
这篇内容适合三类人看:一是刚接触UE UI系统的开发者,想搞清楚UMG和Slate到底怎么回事;二是有一定经验但遇到性能瓶颈、交互异常等问题的从业者,想深入底层找原因;三是需要做编辑器扩展或自定义控件的程序员,必须直接跟Slate打交道。我会从架构设计、核心机制、实操要点、问题排查几个维度展开,尽量把我在实际项目中踩过的坑和总结的经验都写出来。
2. UMG与Slate的架构关系拆解
2.1 两层架构的设计逻辑
UE的UI系统采用了一种双层架构:Slate是底层,纯C++实现,负责所有渲染、布局、输入事件的底层处理;UMG是上层,建立在Slate之上,提供了蓝图可访问的UWidget体系,并且有一套可视化编辑器。
为什么要这么设计?核心原因是面向不同角色。Slate的API对程序员友好,但对美术和策划来说门槛太高——你不可能让一个美术去写SNew(SButton).OnClicked(...)这样的代码。UMG的出现就是为了解决这个问题,它把Slate的能力包装成蓝图可操作的UUserWidget,让非程序员也能参与UI制作。
但这个封装不是没有代价的。UMG的每一层封装都会带来额外的开销:UWidget需要维护自己的属性系统、需要跟Slate层做同步、需要在RebuildWidget时创建对应的Slate控件。这就是为什么大量使用UMG的界面在性能上往往不如纯Slate实现。
2.2 UWidget与SWidget的对应关系
每一个UMG控件(UWidget的子类)在运行时都会创建一个对应的Slate控件(SWidget的子类)。这个创建过程发生在UWidget::RebuildWidget()里。比如UButton的RebuildWidget会创建一个SButton,UTextBlock会创建一个STextBlock。
这个对应关系是理解UMG性能问题的关键。当你修改一个UMG控件的属性时,如果这个属性需要同步到Slate层,就会触发SynchronizeProperties(),某些情况下还会触发RebuildWidget()——而RebuildWidget()是相对昂贵的操作,因为它涉及到旧控件的销毁和新控件的创建。
注意:在UMG中频繁修改那些会触发
RebuildWidget的属性(比如UWidget的Slot相关属性),是导致UI卡顿的常见原因之一。能通过SynchronizeProperties更新的属性,就不要让它走到RebuildWidget。
2.3 什么时候该用UMG,什么时候该用Slate
这个问题在实际项目中很常见。我的判断标准是这样的:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 游戏内HUD、菜单、设置界面 | UMG | 迭代快,美术可参与,蓝图可控制 |
| 编辑器扩展、工具面板 | Slate | 需要精细控制,性能要求高,不需要美术参与 |
| 高频更新的数据展示(如血条、帧率) | Slate或UMG+优化 | UMG需要做额外优化避免每帧重建 |
| 复杂列表(如背包、排行榜) | UMG+ListView | 用ListView做虚拟化,避免全量创建 |
| 自定义绘制(如图表、曲线) | Slate | 需要重写OnPaint,UMG做不了 |
简单说,面向玩家的界面优先UMG,面向开发者的工具优先Slate。但这也不是绝对的,有些项目为了性能会把核心HUD用Slate重写,这需要权衡开发效率和运行效率。
3. Widget系统的核心机制与实操要点
3.1 Widget树的构建与渲染流程
UE的UI渲染走的是Retained Mode(保留模式),这跟很多前端框架的Immediate Mode不同。什么意思呢?就是说Widget树一旦构建好,每帧渲染时不会重新创建控件,只会根据属性变化做增量更新。
整个流程大致是这样的:UUserWidget在初始化时通过RebuildWidget构建出对应的Slate Widget树,这棵树会被加入到Slate的渲染队列中。每帧渲染时,Slate会遍历这棵树,计算布局(Prepass阶段),然后执行绘制(OnPaint阶段)。如果某个Widget标记了Invalidate,它会在下一帧的Prepass中重新计算布局。
这个机制的好处是性能稳定,不会因为每帧重建控件而抖动。但坏处是,如果你不小心让某个高频更新的Widget触发了Invalidate,它会导致整个子树重新计算布局,开销可能比你想象的大得多。
3.2 布局系统:Slot与Panel的关系
UMG的布局核心是Panel和Slot的组合。Panel负责管理子控件的排列方式(比如CanvasPanel是自由定位,VerticalBox是垂直排列,GridPanel是网格排列),Slot负责描述每个子控件在Panel中的具体位置和尺寸规则。
这里有个容易混淆的点:Slot是挂在子控件上的,不是挂在Panel上的。也就是说,同一个Widget放到不同的Panel下,它的Slot类型是不同的。比如你把一个Button放到CanvasPanel下,它的Slot是UCanvasPanelSlot,你可以设置Anchor和Offset;放到VerticalBox下,Slot变成UVerticalBoxSlot,你只能设置Padding、Alignment和Size。
这个设计的好处是灵活,但坏处是当你把一个Widget从一个Panel移到另一个Panel时,原来的Slot信息会丢失。我在项目中遇到过好几次因为动态改变父级Panel导致布局错乱的问题,排查半天才发现是Slot类型不匹配。
3.3 输入事件的传递与处理
UE的UI输入事件传递遵循从后往前、从子到父的规则。具体来说,Slate会按照Widget树的绘制顺序(后绘制的在上面)反向遍历,找到第一个命中测试通过的Widget,然后把事件传给它。如果这个Widget不处理,事件会继续往上传递给它的父级。
这个机制导致了一个常见问题:如果你在UMG里放了一个全屏的Image作为背景,它会挡住下面所有控件的点击。解决办法是把背景Image的Visibility设置为Self Hit Test Invisible或者Hit Test Invisible,这样它就不参与命中测试了。
实操心得:
Visibility的四个选项要搞清楚——Visible是可见且可交互,Hit Test Invisible是可见但自身不参与命中测试(子控件仍可交互),Self Hit Test Invisible是可见但自身和子控件都不参与命中测试,Collapsed是不可见且不占布局空间。选错了就会出现"按钮点不动"或者"布局莫名其妙多出一块空白"的问题。
3.4 数据绑定与属性同步
UMG提供了属性绑定功能,可以在编辑器里把一个控件的属性绑定到一个函数上,每帧自动调用这个函数来更新属性值。这个功能很方便,但性能陷阱也很多。
属性绑定的函数是每帧都会调用的,如果你在绑定函数里做了复杂计算(比如遍历数组、查表、字符串拼接),帧率会被拖垮。我见过一个项目,策划在血条上绑了一个计算护甲减伤的复杂函数,结果同屏20个敌人时帧率直接掉了15帧。
正确的做法是:能用事件驱动更新的,就不要用属性绑定。比如血量变化时主动调用SetPercent,而不是每帧去绑定一个函数返回当前血量百分比。如果确实需要绑定,也要确保绑定函数足够轻量,只做简单的取值操作。
4. 实操过程与核心环节实现
4.1 从零搭建一个自定义UMG控件
假设我们要做一个自定义的血条控件,带延迟掉血效果(就是受到伤害后,白色条先掉,红色条延迟一段时间再跟上)。这个需求在实际项目中很常见,我用它来演示完整的自定义UMG控件流程。
第一步,创建C++类,继承UUserWidget:
UCLASS() class MYGAME_API UDelayedHealthBar : public UUserWidget { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "HealthBar") void SetHealth(float NewHealth, float MaxHealth); protected: virtual void NativeConstruct() override; virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; UPROPERTY(meta = (BindWidget)) class UProgressBar* HealthBar; UPROPERTY(meta = (BindWidget)) class UProgressBar* DelayedBar; private: float TargetPercent = 1.0f; float CurrentPercent = 1.0f; float DelayedPercent = 1.0f; float DelaySpeed = 0.5f; float DelayTimer = 0.0f; bool bDelayActive = false; };这里的关键是meta = (BindWidget),它让C++变量自动绑定到蓝图里同名的控件上。命名必须完全一致,否则编译时会报错。
第二步,实现逻辑:
void UDelayedHealthBar::SetHealth(float NewHealth, float MaxHealth) { TargetPercent = FMath::Clamp(NewHealth / MaxHealth, 0.0f, 1.0f); if (TargetPercent < CurrentPercent) { bDelayActive = true; DelayTimer = 0.0f; } CurrentPercent = TargetPercent; if (HealthBar) { HealthBar->SetPercent(CurrentPercent); } } void UDelayedHealthBar::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) { Super::NativeTick(MyGeometry, InDeltaTime); if (bDelayActive) { DelayTimer += InDeltaTime; if (DelayTimer >= 0.3f) { DelayedPercent = FMath::FInterpTo(DelayedPercent, CurrentPercent, InDeltaTime, DelaySpeed * 10.0f); if (FMath::IsNearlyEqual(DelayedPercent, CurrentPercent, 0.001f)) { DelayedPercent = CurrentPercent; bDelayActive = false; } if (DelayedBar) { DelayedBar->SetPercent(DelayedPercent); } } } }第三步,在蓝图中创建基于UDelayedHealthBar的WidgetBlueprint,放入两个ProgressBar,分别命名为HealthBar和DelayedBar,注意层级关系——DelayedBar在下面(先绘制),HealthBar在上面。
这个实现有几个细节值得注意:NativeTick默认是不开启的,需要在UUserWidget的构造函数里设置bCanEverTick = true,或者在蓝图里勾选"Tick"选项。另外,FInterpTo的速度参数需要根据实际效果调整,太快了看不出延迟效果,太慢了显得拖沓。
4.2 Slate自定义控件的绘制流程
有些效果UMG做不了,比如自定义的曲线图、雷达图、特殊形状的进度条,这时候就需要直接写Slate控件。Slate控件的核心是重写OnPaint函数。
class SRadarChart : public SLeafWidget { public: SLATE_BEGIN_ARGS(SRadarChart) {} SLATE_ATTRIBUTE(TArray<float>, Values) SLATE_ARGUMENT(int32, AxisCount) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { Values = InArgs._Values; AxisCount = InArgs._AxisCount; } virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D(200.0f, 200.0f); } virtual int32 OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override { const FVector2D Center = AllottedGeometry.GetLocalSize() * 0.5f; const float Radius = FMath::Min(Center.X, Center.Y) * 0.8f; const TArray<float>& Vals = Values.Get(); const int32 Count = FMath::Min(AxisCount, Vals.Num()); TArray<FVector2D> Points; for (int32 i = 0; i < Count; ++i) { float Angle = (2.0f * PI * i / Count) - PI * 0.5f; float R = Radius * FMath::Clamp(Vals[i], 0.0f, 1.0f); Points.Add(Center + FVector2D(FMath::Cos(Angle) * R, FMath::Sin(Angle) * R)); } if (Points.Num() >= 3) { FSlateDrawElement::MakeLines( OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), Points, ESlateDrawEffect::None, FLinearColor::Green, true, 2.0f ); } return LayerId + 1; } private: TAttribute<TArray<float>> Values; int32 AxisCount = 5; };这个例子里有几个关键点:SLeafWidget表示这是一个叶子控件,没有子控件;ComputeDesiredSize决定控件的期望尺寸;OnPaint里通过FSlateDrawElement执行实际绘制。ToPaintGeometry()把局部坐标转换为绘制用的几何信息,MakeLines画线段,如果要填充区域可以用MakeCustomVerts。
实操心得:Slate的
OnPaint是每帧调用的,所以里面的计算要尽量轻量。如果数据不常变,可以在数据变化时预先计算好顶点数组缓存起来,OnPaint里只做绘制。另外,FSlateDrawElement::MakeLines的最后一个参数是线宽,在DPI缩放不同的屏幕上表现可能不一致,需要做适配。
4.3 UMG列表的性能优化实操
列表是UMG里最容易出性能问题的地方。假设你要做一个1000个条目的排行榜,如果用VerticalBox+ScrollBox直接创建1000个EntryWidget,打开界面的瞬间就会卡死。
正确的做法是用UListView或者UTileView,它们内置了虚拟化机制——只创建可见区域内的条目,滚动时复用已有的Widget。使用UListView需要实现UUserListEntry接口,或者继承UUserListEntry和UUserObjectListEntry。
UCLASS() class ULeaderboardEntry : public UUserWidget, public IUserObjectListEntry { GENERATED_BODY() protected: virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override; UPROPERTY(meta = (BindWidget)) class UTextBlock* RankText; UPROPERTY(meta = (BindWidget)) class UTextBlock* NameText; UPROPERTY(meta = (BindWidget)) class UTextBlock* ScoreText; }; void ULeaderboardEntry::NativeOnListItemObjectSet(UObject* ListItemObject) { ULeaderboardItem* Item = Cast<ULeaderboardItem>(ListItemObject); if (!Item) return; RankText->SetText(FText::AsNumber(Item->Rank)); NameText->SetText(FText::FromString(Item->PlayerName)); ScoreText->SetText(FText::AsNumber(Item->Score)); }ULeaderboardItem是一个继承自UObject的数据类,持有Rank、PlayerName、Score等字段。UListView会根据数据源自动管理EntryWidget的创建和回收,滚动时把滚出视野的Widget重新绑定到新进入视野的数据上。
这个方案的关键在于数据与视图分离。数据存在UObject里,视图只负责展示。这样即使有10000条数据,同时存在的Widget也只有可见的那十几个,内存和性能都可控。
4.4 半透明与景深相关的UI渲染设置
热词里提到了"ue半透明景深代码",这涉及到UI与场景渲染的交互。在UE里,UMG默认是渲染在场景之上的,但如果你的UI需要跟场景有景深交互(比如UI元素根据距离模糊),就需要特殊处理。
一种常见做法是把UI渲染到一个RenderTarget上,然后在场景里用一个带景深材质的平面来显示这个RenderTarget。这样UI就会受到场景景深的影响。具体步骤是:创建一个USceneCaptureComponent2D或者用DrawMaterialToRenderTarget把UMG画到RT上,然后在场景里放一个Plane,材质里采样这个RT,并且把材质的BlendMode设为Translucent,开启"Allow Custom Depth Writes"。
不过这种做法性能开销不小,因为多了一次RT绘制和一次场景绘制。如果只是想让UI有半透明效果,直接在UMG里设置控件的Render Opacity就够了,不需要走RT。景深交互只在特定场景下才需要,比如VR项目里让UI有空间感。
5. 常见问题与排查技巧实录
5.1 UI不显示或显示异常的排查思路
这是最高频的问题。我整理了一个排查顺序,基本能覆盖90%的情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全不显示 | Widget没添加到Viewport | 检查AddToViewport是否调用 |
| 完全不显示 | Visibility设为Collapsed | 检查控件及其父级的Visibility |
| 完全不显示 | ZOrder被其他UI挡住 | 检查ZOrder设置,或临时把其他UI隐藏 |
| 显示但位置不对 | Anchor和Offset设置错误 | 在编辑器里切换不同分辨率预览 |
| 显示但尺寸不对 | Slot的Size规则不对 | 检查是Auto还是Fill |
| 显示但字体模糊 | 字体纹理DPI不匹配 | 检查字体资产的DPI设置 |
| 显示但颜色不对 | 材质或Brush的Tint不对 | 检查Brush的TintColor和材质参数 |
实操心得:排查UI问题时,先把问题控件之外的所有UI都隐藏,只留这一个,看它是否正常。如果正常,说明是层级或遮挡问题;如果不正常,说明是这个控件本身的配置问题。这个"隔离法"能快速缩小排查范围。
5.2 帧率低的常见原因与优化手段
热词里"ue排查帧率低的原因"是个高频需求。UI导致的帧率低,通常有以下几个原因:
属性绑定函数太重。前面说过,绑定函数每帧调用,里面不能做复杂计算。排查方法是打开Stat Slate或者Stat UMG,看哪个Widget的Tick开销最大。
Widget树太深。每多一层嵌套,布局计算就多一层递归。优化方法是扁平化Widget树,能用Panel解决的不要套多层Box。我见过一个项目,一个简单的设置界面套了7层嵌套,展开后帧率掉了8帧。
Invalidation过多。某些属性变化会导致整个子树重新计算布局。比如修改一个SizeBox的WidthOverride,会导致它下面所有子控件重新布局。优化方法是尽量用RenderTransform代替布局属性变化,因为RenderTransform只影响绘制,不影响布局。
Overdraw严重。多个半透明UI元素叠加会导致像素被多次绘制。排查方法是打开Shader Complexity视图,红色区域就是Overdraw严重的地方。优化方法是减少不必要的半透明叠加,能用不透明就不用半透明。
5.3 打包后UI表现不一致的问题
编辑器里跑得好好的,打包后UI就出问题,这种情况太常见了。主要原因有几个:
字体资源没有正确打包。如果字体是通过代码动态加载的,打包时可能没有被引用到,导致打包后字体丢失。解决办法是在项目设置里把字体加入"Additional Asset Directories to Cook",或者用一个DataAsset显式引用。
材质变体被裁剪。打包时UE会裁剪未使用的材质变体,如果某个UI材质只在特定条件下使用,可能被裁掉。解决办法是在项目设置的"Packaging"里调整材质变体裁剪策略,或者用UMaterialInstanceDynamic时确保材质被正确引用。
分辨率适配问题。编辑器里用的是固定分辨率预览,打包后实际分辨率可能不同。解决办法是用DPI Scaling和Anchors做适配,不要用硬编码的像素值。
蓝图Nativization的副作用。如果开启了蓝图Nativization,某些UMG的绑定逻辑可能表现不一致。排查方法是临时关闭Nativization重新打包对比。
5.4 输入事件不响应的排查清单
按钮点不动、拖拽没反应,这类问题排查起来比较繁琐,我列一个清单:
- 检查控件的
Visibility是否包含Hit Test Visible。Hit Test Invisible和Self Hit Test Invisible都不参与命中测试。 - 检查是否有上层控件挡住了。用
Widget Reflector(快捷键Ctrl+Shift+W)可以查看当前鼠标位置下的Widget层级。 - 检查父级Panel的
Visibility。如果父级是Hit Test Invisible,子级即使设了Visible也不响应。 - 检查是否被
UUserWidget的bIsFocusable影响。某些情况下需要设置bIsFocusable = true。 - 检查输入模式。如果
SetInputMode设置成了GameOnly,UI不会接收输入。 - 检查是否有
Overlay之类的控件设置了bConsumeInput。
实操心得:
Widget Reflector是排查UI问题的神器,强烈建议每个UE开发者都熟悉它。它能实时显示鼠标下的Widget层级、每个Widget的命中测试结果、以及当前聚焦的Widget。很多靠猜的问题,用它一看就明白了。
5.5 UMG与Slate混合使用的注意事项
有时候需要在UMG里嵌入Slate控件,或者反过来。UMG嵌入Slate用UWidget的RebuildWidget返回自定义Slate控件;Slate嵌入UMG用SNew(SWidgetHost)配合UWidget::TakeWidget()。
混合使用时要注意生命周期管理。Slate控件的生命周期由Slate的引用计数管理,而UMG控件的生命周期由GC管理。如果Slate控件持有了UMG对象的裸指针,GC时可能出问题。正确的做法是用TWeakObjectPtr或者TStrongObjectPtr来持有UMG对象的引用。
另外,混合使用时输入事件的传递链路会变长,调试起来更麻烦。如果不是必要,尽量保持UI层级的纯粹性——要么全UMG,要么全Slate。
6. 一些实战中总结的零碎经验
关于UMG的动画系统,UWidgetAnimation用起来很方便,但它的性能开销比手动插值要大。如果只是简单的透明度变化或者位移,用NativeTick里的FMath::Interp手动做,性能会好很多。动画系统适合复杂的、多属性的、需要编辑器可视化编辑的场景。
关于UUserWidget的NativeConstruct和NativeOnInitialized,前者在每次添加到屏幕时调用,后者只在第一次初始化时调用。如果你有只需要执行一次的初始化逻辑(比如绑定委托),放在NativeOnInitialized里;如果是每次显示都需要重置的状态,放在NativeConstruct里。
关于字体,UE的字体渲染在低分辨率下容易糊。解决办法是使用Font Face资产时设置合适的DPI,并且在Font资产里开启"Use Distance Field Rendering"。距离场字体在缩放时表现更好,但内存占用会大一些。
关于UImage的Brush,如果用的是Image类型而不是Material类型,UE会走默认的UI材质,性能更好。只有在需要特殊效果(比如灰度、描边、模糊)时才用Material类型的Brush。另外,Brush的Draw As选项里,Box比Image多了一次九宫格切割的计算,非必要不用Box。
关于UCanvasPanel,它是最灵活的Panel,但也是性能最差的。因为每个子控件的Slot都是独立的Anchor和Offset,布局计算时没有批量优化的空间。如果界面元素是规则排列的,优先用VerticalBox、HorizontalBox、GridPanel、UniformGridPanel这些有批量优化空间的Panel。
关于UOverlay,它适合做层叠布局,但要注意它的子控件默认都是Fill的。如果你想让某个子控件不Fill,需要设置它的Slot的HorizontalAlignment和VerticalAlignment。
关于USizeBox,它适合做固定尺寸的容器,但不要滥用。每多一个SizeBox就多一层布局计算。如果只是想让一个控件有固定宽高,直接在Slot上设置Size规则更高效。
关于UScrollBox,它的ScrollBarVisibility默认是Visible,如果不需要滚动条可以设为Hidden,省一点渲染开销。另外,ScrollBox里的内容如果很多,考虑用ListView替代。
关于UEditableTextBox,它在移动端上的输入法兼容性需要特别注意。不同平台的输入法行为不一致,建议在目标平台上实测。另外,UEditableTextBox的OnTextChanged事件触发频率很高,里面不要做重操作。
关于UComboBoxString,它的下拉列表是独立于Widget树的,不受父级布局影响。如果发现下拉列表位置不对,检查一下ComboBox的MenuPlacement设置。
关于UUserWidget的Tick,默认是关闭的。如果确实需要Tick,在构造函数里设bCanEverTick = true,但要注意Tick的开销。能用事件驱动的就不要用Tick。
关于UWidgetSwitcher,它适合做页面切换,但所有页面都会被创建,只是不显示而已。如果页面很多且复杂,考虑用UUserWidget的动态创建和销毁来替代。
关于UProgressBar的Percent,它的更新会触发Invalidate,如果每帧都更新(比如做进度动画),考虑用材质参数来控制进度条的填充,这样只更新材质参数,不触发布局重算。
关于UButton的OnClicked和OnPressed,前者是点击释放时触发,后者是按下时触发。如果需要做长按检测,用OnPressed配合定时器。
关于UTextBlock的SetText,如果文本内容频繁变化(比如倒计时),每次SetText都会触发文本重排。优化方法是把倒计时拆成多个TextBlock,只更新变化的部分(比如秒数),或者用材质来做数字滚动效果。
关于UImage的SetBrushFromTexture,如果纹理是动态加载的,要注意纹理的生命周期。纹理被GC后,Image会显示空白。解决办法是用TStrongObjectPtr持有纹理引用,或者在UImage的Brush里设置ResourceObject。
关于UUserWidget的AddToViewport和AddToPlayerScreen,前者是添加到整个视口,后者是添加到玩家屏幕。在分屏模式下,用AddToPlayerScreen可以确保UI只显示在对应玩家的屏幕上。
关于UUserWidget的RemoveFromParent,它只是从父级移除,不会销毁Widget。如果需要销毁,还要调用ConditionalBeginDestroy或者让GC回收。如果Widget被缓存了引用,GC不会回收它。
关于UWidgetTree,它是UMG的Widget树容器,每个UUserWidget都有一个。在运行时动态创建Widget时,需要用WidgetTree->ConstructWidget来创建,而不是直接NewObject,否则Widget不会正确初始化。
关于UWidgetBlueprintLibrary,它提供了一些有用的静态函数,比如Create(动态创建Widget)、GetInputMode、SetFocus等。在做UI交互逻辑时,这个库里的函数能省不少事。
关于UUMGSequencePlayer,它是UMG动画的播放器,每个动画播放实例都会创建一个。如果频繁播放动画,注意及时停止和清理,避免播放器堆积。
关于UWidgetAnimation的BindToAnimationFinished,它可以在动画结束时触发回调。但要注意,如果动画被中途停止,这个回调不会触发。需要在停止动画时手动处理后续逻辑。
关于UUserWidget的NativeOnFocusReceived和NativeOnFocusLost,它们处理焦点事件。如果UI需要键盘导航,需要正确实现这些函数,并且设置bIsFocusable = true。
关于UButton的IsFocusable,默认是true。如果不需要键盘导航,设为false可以省一点焦点管理的开销。
关于UCheckBox的OnCheckStateChanged,它的参数是ECheckBoxState枚举,有Unchecked、Checked、Undetermined三种状态。如果只需要两种状态,用IsChecked判断即可。
关于USlider的OnValueChanged,它的触发频率很高,拖动时每帧都会触发。如果里面做了重操作,考虑用OnMouseCaptureEnd来替代,只在拖动结束时处理。
关于USpinBox,它的OnValueChanged和OnValueCommitted的区别是,前者在每次值变化时触发,后者在用户确认时触发。做数据校验时用后者更合适。
关于UListView的OnItemSelectionChanged,它提供选中项变化的事件。如果需要做多选,用SetSelectionMode设置选择模式。
关于UTileView的EntryHeight和EntryWidth,它们决定了每个条目的尺寸。如果条目尺寸不一致,用UListView更合适。
关于UWrapBox,它适合做自动换行的布局,比如标签云。但它的布局计算比HorizontalBox复杂,条目多时性能会下降。
关于UUniformGridPanel,它适合做等大小的网格布局,比如背包格子。它的性能比GridPanel好,因为所有格子尺寸相同,布局计算可以批量处理。
关于UWidgetSwitcher的SetActiveWidgetIndex,切换时会触发Invalidate,如果切换频繁,考虑用Visibility来控制显示隐藏,而不是用Switcher。
关于UUserWidget的SetVisibility,它会影响整个Widget树的可见性。如果只是想让某个子控件隐藏,直接设置子控件的Visibility,不要动父级的。
关于UCanvasPanelSlot的SetPosition和SetSize,它们会触发Invalidate。如果每帧都调用(比如做拖拽),考虑用RenderTransform的Translation来替代,性能更好。
关于UUserWidget的SetRenderOpacity,它只影响绘制,不影响布局,性能比修改Visibility好。做淡入淡出效果时优先用它。
关于UImage的SetColorAndOpacity,它也是只影响绘制,性能好。做颜色变化效果时用它。
关于UUserWidget的SetRenderScale和SetRenderTransform,它们都只影响绘制,不影响布局。做缩放和旋转动画时用它们,不要用SetDesiredSizeInViewport之类的布局属性。
关于UUserWidget的SetPositionInViewport,它会触发布局重算。如果只是做位移动画,用RenderTransform的Translation。
关于UUserWidget的SetDesiredSizeInViewport,它会触发布局重算。如果只是做尺寸动画,用RenderTransform的Scale。
关于UUserWidget的SetAnchorsInViewport,它会触发布局重算。如果只是做锚点变化,考虑用CanvasPanelSlot的SetAnchors,但同样会触发重算。最好的办法是避免频繁改变锚点。
关于UUserWidget的SetAlignmentInViewport,它也会触发布局重算。同样的,用RenderTransform替代。
关于UUserWidget的SetOffsets,它也会触发布局重算。如果只是做位移动画,用RenderTransform。
关于UUserWidget的SetAutoSize,它会触发布局重算。如果只是想让Widget自适应内容,在编辑器里设置好就行,不要在运行时频繁切换。
关于UUserWidget的SetPadding,它会触发布局重算。如果只是想让内容有间距,在Slot上设置Padding,不要在运行时频繁修改。
关于UUserWidget的SetIsEnabled,它会影响输入事件的响应,但不会触发布局重算。做禁用状态切换时用它。
关于UUserWidget的SetFocus,它会把键盘焦点设置到指定Widget。做键盘导航时用它。
关于UUserWidget的SetKeyboardFocus,它和SetFocus的区别是,前者只设置键盘焦点,后者还会设置用户焦点。一般情况下用SetFocus就够了。
关于UUserWidget的SetAllNavigationRules,它设置导航规则。做复杂的键盘导航时用它。
关于UUserWidget的SetNavigationRule,它设置单个方向的导航规则。做精细的导航控制时用它。
关于UUserWidget的SetIsVolatile,它标记Widget为易变的,每帧都会重绘。如果Widget内容每帧都变(比如实时数据),设置它为true可以避免缓存失效的判断开销。但不要滥用,否则会失去缓存的好处。
关于UUserWidget的SetIsEnabled,它会影响输入事件的响应,但不会触发布局重算。做禁用状态切换时用它。
关于UUserWidget的Invalidate,它标记Widget需要重绘。一般情况下不需要手动调用,属性变化会自动触发。只有在特殊情况下(比如自定义绘制的内容变了)才需要手动调用。
关于UUserWidget的ForceLayoutPrepass,它强制立即执行布局计算。在某些需要立即获取布局结果的场景下用它,但不要频繁调用,因为布局计算是昂贵的。
关于UUserWidget的GetCachedGeometry,它获取缓存的几何信息。如果布局没有变化,用它可以避免重新计算。
关于UUserWidget的GetTickSpaceGeometry,它获取Tick时的几何信息。在NativeTick里用它来获取当前Widget的尺寸和位置。
关于UUserWidget的GetPaintSpaceGeometry,它获取绘制时的几何信息。在NativePaint里用它。
关于UUserWidget的NativePaint,它允许在UMG的绘制流程中插入自定义绘制。如果需要用Slate的绘制API在UMG上画东西,重写这个函数。
关于UUserWidget的NativeOnPaint,它和NativePaint的区别是,前者在UMG的绘制之后调用,后者在之前。根据绘制层级需求选择。
关于UUserWidget的NativeOnMouseButtonDown等输入事件,它们允许在UMG层面处理输入。如果Slate层的输入处理不够用,可以重写这些函数。
关于UUserWidget的NativeOnDragDetected,它处理拖拽检测。做拖拽功能时用它。
关于UUserWidget的NativeOnDrop,它处理拖拽释放。做拖拽功能时用它。
关于UUserWidget的NativeOnDragEnter和NativeOnDragLeave,它们处理拖拽进入和离开。做拖拽高亮效果时用它们。
关于UUserWidget的NativeOnDragOver,它处理拖拽悬停。做拖拽预览时用它。
关于UUserWidget的NativeOnMouseWheel,它处理鼠标滚轮。做缩放或滚动时用它。
关于UUserWidget的NativeOnKeyDown和NativeOnKeyUp,它们处理键盘按键。做快捷键时用它们。
关于UUserWidget的NativeOnAnalogValueChanged,它处理模拟输入(比如手柄摇杆)。做手柄导航时用它。
关于UUserWidget的NativeOnTouchGesture,它处理触摸手势。做移动端手势操作时用它。
关于UUserWidget的NativeOnTouchStarted、NativeOnTouchMoved、NativeOnTouchEnded,它们处理触摸事件。做精细的触摸控制时用它们。
关于UUserWidget的NativeOnMotionDetected,它处理运动检测。做体感操作时用它。
关于UUserWidget的NativeOnMouseEnter和NativeOnMouseLeave,它们处理鼠标进入和离开。做悬停效果时用它们。
关于UUserWidget的NativeOnRemovedFromFocusPath和NativeOnAddedToFocusPath,它们处理焦点路径变化。做焦点高亮时用它们。
关于UUserWidget的NativeOnFocusChanging,它处理焦点变化。做焦点切换逻辑时用它。
关于UUserWidget的NativeOnFocusReceived和NativeOnFocusLost,它们处理焦点获得和失去。做焦点状态管理时用它们。
关于UUserWidget的NativeOnKeyChar,它处理字符输入。做文本输入时用它。
关于UUserWidget的NativeOnPreviewKeyDown和NativeOnPreviewKeyUp,它们在NativeOnKeyDown之前调用,可以拦截按键事件。做全局快捷键时用它们。
关于UUserWidget的NativeOnPreviewMouseButtonDown,它在NativeOnMouseButtonDown之前调用,可以拦截鼠标事件。做全局鼠标处理时用它。
关于UUserWidget的NativeOnPreviewMouseWheel,它在NativeOnMouseWheel之前调用,可以拦截滚轮事件。
关于UUserWidget的NativeOnPreviewTouchGesture,它在NativeOnTouchGesture之前调用,可以拦截触摸手势。
关于UUserWidget的NativeOnPreviewTouchStarted、NativeOnPreviewTouchMoved、NativeOnPreviewTouchEnded,它们在对应的非Preview版本之前调用,可以拦截触摸事件。
关于UUserWidget的NativeOnPreviewMotionDetected,它在NativeOnMotionDetected之前调用,可以拦截运动检测。
关于UUserWidget的NativeOnPreviewMouseEnter和NativeOnPreviewMouseLeave,它们在对应的非Preview版本之前调用,可以拦截鼠标进入和离开。
关于UUserWidget的NativeOnPreviewFocusChanging,它在NativeOnFocusChanging之前调用,可以拦截焦点变化。
关于UUserWidget的NativeOnPreviewFocusReceived和NativeOnPreviewFocusLost,它们在对应的非Preview版本之前调用,可以拦截焦点获得和失去。
关于UUserWidget的NativeOnPreviewKeyChar,它在NativeOnKeyChar之前调用,可以拦截字符输入。
这些Preview函数在需要做全局拦截或者优先级处理时非常有用,但要注意不要滥用,否则会让输入事件的传递链路变得难以调试。
最后说一个我个人的习惯:在做任何UMG界面之前,先在纸上画一下Widget树的层级结构,标清楚每个Panel的用途和每个控件的Slot类型。这个习惯能避免很多后期的布局调整和性能问题。UI这东西,结构设计好了,后面就是填内容;结构没设计好,后面就是不停地打补丁。