# 前言

嗯,前几天开始用 UE4 做第一个 VR 项目了,HTC Vive 平台的。主要功能就是些互动:比如用控制器开开水龙头、开开关关电视、捡捡小东西、换换模型材质什么的..... 功能很简单,所以想到目前处于学习阶段,就决定全用 C 来实现。其它就不多赘述了,这篇 Blog 主要来说说 UE4 中使用 C 操作 UMG 吧。

儿主要用到 UI 的地方,就是替换材质的功能了。
首先,对于替换材质这个功能来说,模型上可替换的材质数量是不定的;
其次,在 VR 中,UI 必须制作成 3D UI 才能进行具体交互的,相较 2D UI 来说,要麻烦了一些 — 特别是交互这块儿,所以当时花了大半天才琢磨好如何制作 UE4 中 3D UI 的交互(比如正确获取 3D UI 中按钮点击事件等)

然后,先说说 “材质不定” 这个问题吧。
在 Unity 中,一般直接可以把显示每个材质的 UI 做个 Prefab,然后根据传入数量直接进行实例化,更改其中的显示图片啊、点击事件调用的方法就可以了。
所以,在此我第一个想到的,也是这个方法。

# 实现

第一步一般是制作 UI 吧。
不过因为我打算将实际的代码放在 UI 自身上,所以在此之前,最好先建立一个 UUserWidget 的子类,方便管理 UI 操作的代码(其实代码写在 UI 之外也可以,不过据说那样不大推荐)

所以,首先打开项目的 “项目名.Build.cs” 文件,加入对 UI 的依赖:

p
public FJ_Project_CPP(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });
        PrivateDependencyModuleNames.AddRange(new string[] { });
        // Uncomment if you are using Slate UI
        PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");
        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }

然后就可以放心大胆地创建一个基于 UUserWidget 的子类,这儿我命名为 “UMyUserWidget”。
接着创建一个 WidgetBlueprint,并在菜单进行 Reparent, 选择刚才创建的 UMyUserWidget。

在 UI 设计器中,布局如下:

主要就两个组件:一个 Button,一个 ScrollBox。
Button 就是用于显示 “每个” 材质的容器,ScrollBox 则用于显示所有 UI 的容器,后面在代码中,会根据材质实时 Copy Button,设置信息,然后添加入 ScrollBox 中。

注意 ScrollBox 重命名为 “MatScrollBox”,Button 命名为 “ButtonPrototype”,这个名字可以用于代码中寻找这两个 UI 的引用,所以很重要。

接着打开 “UMyUserWidget.h”,在这个类中,主要有两个自定义的方法,因为 UserWidget 不带 “BeginPlay” 子类的自动初始化的重载方法,所以我定义了一个 Init () 方法用来初始化
,做一些诸如获取相关 UI (ButtonPrototype、ScrollBox) 引用的事儿。
其次是 ShowModifyMaterialUI 方法,接受两个参数,用于具体处理根据材质数量、信息实例化相应 UI 的功能。

p
public:
	// 初始化
	void Init();
	void ShowModifyMaterialUI(UStaticMeshComponent* mesh, TArray<FMaterialPair> materialDataList);

然后是一些私有变量或者函数:

p
private:
	UScrollBox* _scrollBox;
	UButton* _buttonPrototype;
	// 点击的 Button
	UButton* _clickTempButton;
	// 更换材质的模型引用
	UStaticMeshComponent* _mesh;
	TArray<FMaterialPair> _materialDataList;
	// 存储点击按钮的回调
	UFUNCTION()
	void OnClick();// 具体执行点击按钮方法
	UFUNCTION()
	void OnCheckClickButton();// 用以判断点中的按钮

然后进入 MyUserWidget.cpp,实现 Init 等方法:

p
void UMyUserWidget::Init()
{
	// 通过名字来获取显示材质的 ScrollBox
	_scrollBox = Cast<UScrollBox>(GetWidgetFromName(FName("MatScrollBox")));
	_buttonPrototype = Cast<UButton>(GetWidgetFromName(FName("ButtonPrototype")));
	if (_buttonPrototype)
		_buttonPrototype->SetVisibility(ESlateVisibility::Hidden);
	SetVisibility(ESlateVisibility::Hidden);
}

在这个方法中,获取了 Button 和 ScrollBox 的引用,并且隐藏了用于实例化的 Button 原型。

接着,就是重点的 ShowModifyMaterialUI 方法了:

p
void UMyUserWidget::ShowModifyMaterialUI(UStaticMeshComponent* mesh, TArray<FMaterialPair> materialDataList)
{
	// 缓存材质、Mesh
	_mesh = mesh;
	_materialDataList = materialDataList;
	_scrollBox->ClearChildren();
	for (auto mat = materialDataList.CreateIterator(); mat; ++mat)
	{
		FMaterialPair data = (*mat);
		UTexture* tex = NULL;
		// 获取显示的图片
		// 若指定了相应的图片,则直接显示相应图片
		// 否则,抓取材质中的图片进行显示
		if (data.Icon == nullptr)
		{
			TArray<UTexture*> texs;
			data.Material->GetUsedTextures(texs, EMaterialQualityLevel::Medium, true, ERHIFeatureLevel::SM5, true);// GetTextureStreamingData();
			if (texs.Num() > 0)
			{
				tex = texs[0];
			}
		}
		else tex = data.Icon;
		UButton* button = DuplicateObject<UButton>(_buttonPrototype, this);
		// 设置按钮的显示
		button->WidgetStyle.Normal.SetResourceObject(tex);
		button->WidgetStyle.Hovered.SetResourceObject(tex);
		button->WidgetStyle.Pressed.SetResourceObject(tex);
		// 绑定按钮的方法
		button->OnPressed.AddDynamic(this, &UMyUserWidget::OnCheckClickButton);
		button->OnClicked.AddDynamic(this, &UMyUserWidget::OnClick);
		button->SetVisibility(ESlateVisibility::Visible);
		_scrollBox->AddChild(button);
	}
	SetVisibility(ESlateVisibility::Visible);
}

在这个方法中,最主要就是通过复制开始的原型按钮实现功能。
另外因为按钮点击一类的回调使用了动态多播委托,在 UE4 中是这种的委托是不允许 Lambda 表达式的,所以必须定义两个方法:一个用来判断选择的按钮,一个用于当实际点击触发的时候执行。
其中判断选中哪个按钮用的是 “按下” 事件:

p
void UMyUserWidget::OnCheckClickButton()
{
	GEngine->AddOnScreenDebugMessage(-1, 3, FColor::Orange, TEXT("Check Click"));
	int buttonCount = _scrollBox->GetChildrenCount();
	for (size_t i = 0; i < buttonCount; i++)
	{
		UButton* button = Cast<UButton>(_scrollBox->GetChildAt(i));
		if (button->IsPressed())
		{
			_clickTempButton = button;
			break;
		}
	}
}

接着,当实际点击事件触发时,就可以根据按下按钮确定被点击按钮是谁?因为材质数量与按钮一一对应,所以就根据这个按钮的索引进行材质的替换了:

p
void UMyUserWidget::OnClick()
{
	if (_clickTempButton != nullptr)
	{
		int index = _scrollBox->GetChildIndex(_clickTempButton);
		GEngine->AddOnScreenDebugMessage(-1, 3, FColor::Orange, TEXT("Click " + _clickTempButton->GetName() + "  Index: " + FString::FromInt(index)));
		_mesh->SetMaterial(0, _materialDataList[index].Material);
	}
	SetVisibility(ESlateVisibility::Hidden);
}

其中,FMaterialPair 是一个自定义的结构体,存储了材质以及一个用于显示的 ICON:

p
USTRUCT(BlueprintType)
struct FMaterialPair
{
	GENERATED_USTRUCT_BODY()
	UPROPERTY(EditAnywhere, Category = Materials)
	UTexture* Icon;
	UPROPERTY(EditAnywhere, Category = Materials)
	UMaterialInstance* Material;
};

其规则是:若指定了材质的显示图片,那么 UI 上就直接显示这个图片,否则则抓取材质中的贴图进行显示。

编辑界面如下:

效果:

# 结语

恩,功能上是没问题的。
因为主要是介绍 UI 的,所以其它交互实现就不多提了,换材质的交互为了设置的方便,我是用组件方法实现的:差不多就是在一个 Mesh 上挂一个组件,然后就像上图那样设置下可替换材质就可以了。
另外有点不和谐就是:大概因为材质问题,3D UI 在世界空间中直接显示的时候,明显 “发白” 了(估计是受到了 UE4 各种附加特效的伤害~)