1.前言
在自渲染方案中实现Lua与UE4进行控件绑定,实现根据不同的界面抽象数据自动生成UE4的UI界面。UE4中开发界面主要使用的是UMG和Slate两种方式,因此对UMG和Slate进行了调研,UMG使用可视化拖拽的方式进行开发,是对Slate进行了一层封装,实现了与蓝图的交互。
UMG控件的核心实现在Slate中,因此主要是对Slate进行调研。
2.Slate框架
SlateCore
- UI layout
- Fonts, Image, icons
- Input testing
- Styling
- Base widget types[SWidget]
Slate
- Specialized Slate Widgets — Buttons, labels, value inputs — Slides, drop-downs — Scrolling, views
- GUI application framework
SlateRHIRendering
- UI rendering
UMG
- UMG/UObject widgets[UWidget]
- Blueprintable widgets[UUserWidget]
- Property bindings
- Motion graphics, animation
UMGEditor
- Editor tools for widget authoring
- Widget Blueprints
3.Slate开发:
Slate是一个用户界面框架,既可以用于创建虚幻引擎编辑器,也可以用于在项目中创建自定义用户界面。
在Slate中,控件分为三种类型:
SLeafWidget不带子槽的控件,如TextBlock;
SPanel子槽数量为动态的控件,如SHorizontalBox,SVerticalBox;
SCompoundWidget子槽数量固定的控件如SDockTab,Border,Button等。
以上都继承于SWidget。
1)Slate使用C++进行开发,通过阅读源码,其主要实现是通过重载[]操作符和宏定义,比如:
SNew(SBox).HAlign(HAlign_Fill).VAlign(VAlign_Fill)[
SNew(STextBlock).Text(FText::FromString("测试1")),
SNew(STextBlock).Text(FText::FromString("测试2")),
SNew(STextBlock).Text(FText::FromString("测试3")),
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()[
SNew(STextBlock).Text(FText::FromString("测试4"))
]
]
2)SBox的属性是HAlign是通过SLATE_ARGUMENT宏定义的:
SLATE_ARGUMENT(EHorizontalAlignment, HAlign)
定义:
#define SLATE_ARGUMENT( ArgType, ArgName ) \
ArgType _##ArgName; \
WidgetArgsType& ArgName( ArgType InArg ) \
{ \
_##ArgName = InArg; \
return this->Me(); \
}
UI事件也是用类似的实现方式:
// SButton
SLATE_EVENT( FOnClicked, OnClicked )
SLATE_EVENT( FSimpleDelegate, OnPressed )
SLATE_EVENT( FSimpleDelegate, OnHovered )
3)Slate中的基类是SWidget,可以通过实现OnPaint方法用Canvas自绘控件,比如SButton的绘制:
int32 SButton::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
bool bEnabled = ShouldBeEnabled(bParentEnabled);
bool bShowDisabledEffect = GetShowDisabledEffect();
const FSlateBrush* BrushResource = !bShowDisabledEffect && !bEnabled ? DisabledImage : GetBorder();
ESlateDrawEffect DrawEffects = bShowDisabledEffect && !bEnabled ? ESlateDrawEffect::DisabledEffect : ESlateDrawEffect::None;
if (BrushResource && BrushResource->DrawAs != ESlateBrushDrawType::NoDrawType)
{
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
BrushResource,
DrawEffects,
BrushResource->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint() * BorderBackgroundColor.Get().GetColor(InWidgetStyle)
);
}
return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bEnabled);
}
4)SListView简单示例:
// 创建数据、进行绑定
TArray<TSharedPtr<FItemData>> Items;
Items.Empty();
Items.Add(MakeShareable(new FItemData{1, "Test1", 1}));
Items.Add(MakeShareable(new FItemData{2, "Test2", 2}));
Items.Add(MakeShareable(new FItemData{3, "Test3", 3}));
TSharedPtr<SListView<TSharedPtr<FItemData>>> MyListView = SNew(SListView<TSharedPtr<FItemData>>).
ListItemsSource(&Items)
.OnGenerateRow(this, &SMyCompoundWidget::OnGenerateWidgetForList);
MainWidget->AddSlot()[
SNew(SBox).WidthOverride(100)
[
SNew(SListView<TSharedPtr<FItemData>>)
.ListItemsSource(&Items)
.OnGenerateRow(this, &SMyCompoundWidget::OnGenerateWidgetForList)
.OnSelectionChanged(this, &SMyCompoundWidget::OnComponentSelected)
]
];
// 单元格
TSharedRef<ITableRow> SMyCompoundWidget::OnGenerateWidgetForList(TSharedPtr<FItemData> Item,
const TSharedRef<STableViewBase>& OwnerTable)
{
return
SNew(STableRow< TSharedPtr<FString> >, OwnerTable)
[
SNew(STextBlock).ColorAndOpacity(FLinearColor::Blue).Text(FText::FromString(*Item.Get()->Name)).
ShadowColorAndOpacity(FLinearColor::Gray)
];
}
6)使用Widget Reflector来查看UMG的布局:
直接用Slate开发会比较费劲,先用UMG快速拖拽出来,然后用工具查看层级来写Slate,后面熟悉了就可以直接用Slate进行开发。
4.Slate绘制
第一块是控件绘制,在主线程中,给每个控件分配LayerId,并从控件抽象出FSlateDrawElement。
第二块是绘制指令生成渲染指令,也是在主线程中,把FSlateDrawElement包装成FSlateRenderBatch,并根据控件的信息生成VertexBuffer。
第三块是合批并执行渲染,在渲染线程,把之前生成的FSlateRenderBatch按照LayerId从小到大排序,并尝试合批。最后把UI渲染到BackBuffer。
5.Slate使用体验:
1.Slate只能使用声明式语法进行创建控件和参数的设置,在复杂布局下,嵌套层级较多;
2.Slate参数不是继承的,扩展类需要重新实现这些参数;
3.参数可以在构造时设置,提供修改的属性相比Android或iOS要少很多;
4.界面修改需要重新编译或启动项目;
从整体的调研来看,Slate能够实现大部分常见的控制绘制,但写起来有一定的难度,如果常见的控件不满足需求,可以自定义控件。操作符重复嵌套也会增加后期的维护成本,如果将控件进行重写,使用起来会更加方便,但也会增加前期的开发成本。
1)了解更多
2)自定义widget示例:
void SMyWidget::Construct(const FArguments& InArgs)
{
int32 BoxID = 0;
CheckBoxes.SetNum(2);
SUserWidget::Construct(
SUserWidget::FArguments().HAlign(HAlign_Fill).VAlign(VAlign_Fill)
[
SNew(SGridPanel)
.FillColumn(0, 1).FillColumn(1, 3)
.FillRow(0, 1).FillRow(1, 1).FillRow(2, 1).FillRow(3, 1)
+ SGridPanel::Slot(0, 0).HAlign(HAlign_Center).VAlign(VAlign_Center)
[
SNew(STextBlock).Text(FText::FromString(TEXT("百分比")))
]
+ SGridPanel::Slot(0, 1).HAlign(HAlign_Center).VAlign(VAlign_Center)
[
SNew(STextBlock).Text(FText::FromString(TEXT("二选一")))
]
+ SGridPanel::Slot(0, 2).HAlign(HAlign_Center).VAlign(VAlign_Center)
[
SNew(STextBlock).Text(FText::FromString(TEXT("下拉选项")))
]
+ SGridPanel::Slot(1, 0).HAlign(HAlign_Fill).VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().FillWidth(3)
[
SAssignNew(Slider, SSlider)
]
+ SHorizontalBox::Slot().FillWidth(1)
[
SNew(STextBlock).Text_Lambda([this]()
{
return FText::FromString(FString::SanitizeFloat(Slider.IsValid() ? Slider->GetValue() : 0.f));
})
]
]
+ SGridPanel::Slot(1, 1).HAlign(HAlign_Fill).VAlign(VAlign_Fill)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().FillWidth(1)[
SAssignNew(CheckBoxes[0], SCheckBox)
.IsChecked(ECheckBoxState::Checked)
.OnCheckStateChanged(FOnCheckStateChanged::CreateLambda([this](ECheckBoxState NewState)
{
switch (NewState)
{
case ECheckBoxState::Checked:
for (int i = 0; i < CheckBoxes.Num(); ++i)
{
if (i != 0 && CheckBoxes[i].IsValid())
{
CheckBoxes[i]->SetIsChecked(ECheckBoxState::Unchecked);
}
}
break;
case ECheckBoxState::Unchecked:
CheckBoxes[0]->SetIsChecked(ECheckBoxState::Checked);
break;
}
}))[
SNew(STextBlock).Text(FText::FromString(TEXT("选项一")))]
]
+ SHorizontalBox::Slot().FillWidth(1)
[
SAssignNew(CheckBoxes[1], SCheckBox)
]
+ SHorizontalBox::Slot().FillWidth(1)
[
SAssignNew(CheckBoxes[1], SCheckBox)
.OnCheckStateChanged(FOnCheckStateChanged::CreateLambda([this](ECheckBoxState NewState)
{
switch (NewState)
{
case ECheckBoxState::Unchecked:
CheckBoxes[1]->SetIsChecked(ECheckBoxState::Checked);
break;
case ECheckBoxState::Checked:
for (int i = 0; i < CheckBoxes.Num(); ++i)
{
if (i != 1 && CheckBoxes[i].IsValid())
{
CheckBoxes[i]->SetIsChecked(ECheckBoxState::Unchecked);
}
}
break;
}
}))[
SNew(STextBlock).Text(FText::FromString(TEXT("选项二")))]
]
]
+ SGridPanel::Slot(1, 2).HAlign(HAlign_Fill).VAlign(VAlign_Center)
[
SNew(SComboBox<TSharedPtr<FString>>)
.OnGenerateWidget_Lambda(
[](TSharedPtr<FString> Item)
{
return SNew(STextBlock).Text(FText::FromString(*Item));
}
).OnSelectionChanged_Lambda(
[this](TSharedPtr<FString> Item, ESelectInfo::Type Type)
{
if (ComboText.IsValid())
{
ComboText->SetText(*Item);
}
}).OptionsSource(new TArray<TSharedPtr<FString>>({
MakeShareable(new FString(TEXT("0"))),
MakeShareable(new FString(TEXT("1"))),
MakeShareable(new FString(TEXT("2")))
}))[
SAssignNew(ComboText, STextBlock)
]
]
+ SGridPanel::Slot(1, 3).HAlign(HAlign_Center).VAlign(VAlign_Center)[
SNew(SButton)[
SNew(STextBlock).Text(FText::FromString(TEXT("确定")))
]
]
]);
}
最终的效果: