第一个界面
用插件自带的 HelloDreamGUI.dui 走一遍从文本到屏幕的全过程,逐行读懂这个文件,再把同一个类立进关卡里。
插件自带一个样例:Content/Samples/HelloDreamGUI.dui。它是"还算一个真界面"的最小 .dui ——
一个锚定的根、一个 Overlay、一张卡片、一列竖排的文字。从文本到屏幕上的 UI,就是下面这四步,
别的什么都不用配:屏幕根、射线检测器(raycaster)和事件系统都会在需要时自动创建。
先把文件拷进项目
把它拷到项目的 DUI/ 目录下,例如 <Project>/DUI/HelloDreamGUI.dui,然后只改这一份。两个原因:
- 插件目录里那份是参考,插件更新会覆盖它;
- 选文件时,DreamGUI 会把路径存成相对某个
DUI/根的形式 —— 项目里的存成HelloDreamGUI.dui,插件里的存成Plugin.X:…。不在任何DUI/根下的文件只能存成绝对路径,换一台机器就找不到了。
.dui 是源码,不是资产,所以不放在 Content/ 下。详见项目里的布局。
四步上屏
建一个控件蓝图
内容浏览器里右键 → DreamUI → DreamUI Widget Blueprint。会先后弹出两个选择框:
- 父类:选 DreamUI Text User Widget(
UDreamTextUserWidget)。只有以它为父类的控件蓝图才从.dui构建自己的树; 父类是别的时,设计器工具栏上根本不会出现选源文件的按钮。 - 根布局:随便选,选 None 也行 —— 编译时文件里的树会整个替换掉它。
放在 /Game/UI,命名为 WBP_HelloDreamGUI。文件第一行 class /Game/UI/WBP_HelloDreamGUI 说的就是这个位置;
两边对不上时编译会给出警告 DUI6003,要么挪资产,要么改那一行。
把它指向这个文件
打开蓝图。设计器工具栏上有一个源文件按钮,还没指定文件时它显示 No Source File。打开它的菜单 → Set Source File... → 选中刚才拷进来的文件。之后按钮上显示文件名,悬停提示是解析后的完整路径。
同一个菜单里还有 Create Source File...(类还没有源文件时才出现,写一个带起始层级的新 .dui 并指向它)、
Open in Default Editor、Show in Explorer,装了 VS Code 扩展时还有 Reveal in VS Code。
编译
层级面板里出现的就是文件描述的树:Root、Card、Backdrop、Column、Heading、Subheading。
设计器编辑的是同一个类,你在设计器里改的东西会写回这个文件(见写回);
反过来,在外部编辑器里保存这个文件,已加载的、以它为源的类会自动重新编译。
显示出来
蓝图里用 Add Widget Of Class To Viewport 节点(UDreamUIBPLibrary::AddWidgetOfClassToViewport),
比如放在关卡蓝图的 BeginPlay 里,类选 WBP_HelloDreamGUI。C++ 里是同一个调用,模块依赖 DreamGUI:
#include "DreamUIBPLibrary.h"
// HelloClass 是 TSubclassOf<UDreamUserWidget>,例如一个指向 WBP_HelloDreamGUI 的 UPROPERTY。
UDreamUIBPLibrary::AddWidgetOfClassToViewport(this, HelloClass);它返回加到屏幕上的根控件,可选的第三个参数 SortOrder 决定它和别的屏幕页的前后。拿下来用
UDreamUIBPLibrary::RemoveFromViewport。
逐行读这个文件
下面是样例去掉文件头注释和一条分隔线后的样子,仍然是一个完整、能编译的文件:
class /Game/UI/WBP_HelloDreamGUI
style Title {
Font = /DreamGUI/DefaultFont_DistanceField
FontSize = 28
Color = #E6E9F0
HAlign = Center
}
style Body {
Font = /DreamGUI/DefaultFont_DistanceField
FontSize = 15
Color = #8C93A6
HAlign = Center
}
Widget Root {
AnchorData.AnchorMin = (0, 0)
AnchorData.AnchorMax = (1, 1)
AnchorData.SizeDelta = (0, 0)
+ Overlay {}
Widget Card {
AnchorData.SizeDelta = (420, 220)
@slot HorizontalAlignment = Center
@slot VerticalAlignment = Center
+ Overlay {}
Image Backdrop {
Brush.TintColor = #1B1E26FF
@slot HorizontalAlignment = Fill
@slot VerticalAlignment = Fill
}
Widget Column {
@slot HorizontalAlignment = Fill
@slot VerticalAlignment = Fill
+ VerticalBox {
Spacing = 10
Padding = (24, 28, 24, 28)
}
Text Heading : Title {
AnchorData.SizeDelta = (0, 34)
Text = "Hello, DreamGUI"
WrapTextAt = 372
@slot HorizontalAlignment = Fill
@slot SizeRule = Auto
}
Text Subheading : Body {
AnchorData.SizeDelta = (0, 46)
Text = "This screen is a text file. Edit it, compile, and the class changes with it."
WrapTextAt = 360
@slot HorizontalAlignment = Fill
@slot SizeRule = Auto
}
}
}
}class
这个文件是哪个控件蓝图的源码。一个文件最多一行。它的第二个作用是做本地化命名空间:文件里每个本地化字符串的 key 都挂在这个路径下,所以文件改名时 key 不变。没有这一行时,用文件自己的名字做命名空间。
style
一组有名字的属性行,用 节点 : 样式名 套到节点上(Text Heading : Title)。节点自己写的行优先于样式里的。
顺序随意:样式可以声明在用它的节点下面。
Widget
一个没有自己可视部分(visual)的节点 —— 一个只为了装别的东西而存在的矩形。节点由类型、id 和一个可选的块组成。
id(Root、Card……)就是这个控件的身份:控件的名字、蓝图图表里读到的成员变量、绑定解析时用的 key、
本地化 key 都是它。它必须是合法的 C++ 标识符,并且在文件内不区分大小写地唯一(DUI3001)。
+ Overlay {}
挂在节点上的一个组件。布局住在组件里(Overlay、VerticalBox),而不是住在控件类型里 ——
这就是为什么一个节点可以按一种方式排布、按另一种方式绘制。一个节点只有一个布局容器。
Overlay 按声明顺序叠放子节点,所以 Backdrop 写在前面,就画在后面。
Image / Text
带一个会画东西的 visual 的节点。属性名可以是点分路径,伸进结构体属性里(Brush.TintColor);
颜色是 sRGB 十六进制,3、4、6、8 位都行;(24, 28, 24, 28) 这样的元组是什么结构体由属性决定。
@slot 和 AnchorData.SizeDelta
整个文件里最值得读两遍的就是这一处区别:
AnchorData.*是节点自己的矩形。Root锚在 (0, 0)–(1, 1)、SizeDelta为 (0, 0),意思是"四边贴住父级、 不留边",在任何分辨率下都铺满它被加进去的东西;Card的SizeDelta(420, 220) 就是它的尺寸。@slot …是这个节点对它上层那个面板提的要求,也就是父级怎么安排它:HorizontalAlignment、VerticalAlignment、SizeRule。父级没有布局面板时写 slot 行是 DUI5003。
两段文字上的 WrapTextAt 也和这个区别有关。一段文本有 WrapTextAt 时在那里折行,没有时按自己的宽度折行;
而竖排的列在给子节点宽度之前先问它想要多高。所以放在 Fill 插槽里的文本要自己报一个折行宽度:
这里取卡片预期的最大宽度 420,减去列两侧各 24 的内边距,得 372。不写的话,标题会按它自己的宽度 0 被测量,
变成一行一个字。
同一棵树还有更短的写法:布局容器可以直接当节点类型(Overlay Card、VerticalBox Column),slot 行可以合进
@slot { … } 块,@fill 代替 @slot SizeRule = Fill。样例用长写法,是为了把"组件"和"插槽"两件事分开讲清楚。
语言的完整说明从 .dui 概览开始。
下面是卡片和标题用短写法的样子(只留了标题):
class /Game/UI/WBP_HelloDreamGUI
Widget Root {
AnchorData.AnchorMin = (0, 0)
AnchorData.AnchorMax = (1, 1)
AnchorData.SizeDelta = (0, 0)
+ Overlay {}
Overlay Card {
AnchorData.SizeDelta = (420, 220)
@slot { HorizontalAlignment = Center VerticalAlignment = Center }
Image Backdrop {
Brush.TintColor = #1B1E26FF
@slot { HorizontalAlignment = Fill VerticalAlignment = Fill }
}
VerticalBox Column {
Spacing = 10
Padding = (24, 28, 24, 28)
@slot { HorizontalAlignment = Fill VerticalAlignment = Fill }
Text Heading {
AnchorData.SizeDelta = (0, 34)
Text = "Hello, DreamGUI"
FontSize = 28
HAlign = Center
WrapTextAt = 372
@slot { HorizontalAlignment = Fill SizeRule = Auto }
}
}
}
}同一个类,放进世界里
同一个类可以不当屏幕上的一层,而当关卡里的一块面。把 WBP_HelloDreamGUI 从内容浏览器拖进关卡,或者在
Place Actors 面板里放一个 DreamUI World Widget Actor:两种方式得到的都是一个 ADreamWorldWidgetActor,
它的全部内容就是一个 UDreamWorldWidgetComponent。像任何场景组件一样移动、旋转、挂接它。
组件托管这棵树,但不重新定义它。蓝图自己的根画布仍然说了算;组件只把渲染模式、排序和追踪通道写到那块画布上, 其余不动。
| 属性 | 作用 |
|---|---|
WidgetClass | 要加载的控件蓝图类。只有改它会重建整棵树 |
Backend | DreamUIRenderer:DreamGUI 自己的渲染器,平面、不受光、不受后处理影响,绘制顺序精确;UERenderer:走引擎的管线,像关卡里别的网格一样受光、投影、雾和遮挡,按半透明规则排序 |
bUseDesignSize / DrawSize | 树落地的尺寸。默认开,用蓝图设计时的尺寸;关掉后用 DrawSize(世界单位,默认 1920 × 1080) |
Pivot | Actor 原点落在这个矩形的哪里,(0.5, 0.5) 居中 |
SortOrder | 和其它世界空间画布之间的先后 |
TraceChannel | 这块画布应答的通道,默认 Visibility。射线检测器只看通道与自己一致的画布 |
这些属性是"活的":改尺寸、轴心、后端或排序会直接作用到已经加载的树上。
交互不用配置。 BeginPlay 时组件为每个本地玩家要一个事件系统和一个 UDreamWorldSpaceRaycaster,缺哪个补哪个,
所以按下 Play 就能点到挂在世界里的按钮。射线从光标出发,或者从屏幕中心出发(PointerSource:Mouse /
ScreenCenter)。bOccludeByWorld 默认开,让实心几何体像挡住一次 line trace 一样挡住点击;要穿墙点击,在你自己的
射线检测器上关掉它,或者对玩家的那个调用 SetOccludeByWorld(false)。在任何 Actor 上放一个用户索引相同的射线检测器,
插件就不再额外添加 —— 它检查的是"有没有",不是"是不是插件建的"。
代码里的路径是 UDreamUIBPLibrary::ConstructWidget 之后 UDreamUIBPLibrary::AttachWidgetToSceneComponent,
后者把根挂到某个场景组件上,根必须带一块 DreamCanvas。ADreamWorldWidgetActor 可以直接被 Level Sequence 持有,
组件上的 WidgetOpacity、WidgetOffset 和 bWidgetVisible 都能在 Sequencer 里打关键帧。