【UE4】实现自定义框选

要在UE4中实现自定义框选功能,首先我们来分析一下顶顶一框选插件需要些什么模块?

  • 绘制模块
  • 显示模块
  • 计算模块

嗯,大概分这么三个模块,好,现在我们一个个模块来分析实现。首先分析实现一下显示模块。

提示:

如果功能需要打包成插件,请先浏览第四章

一、显示模块

首先我们需要做一些准备

1.创建Wedgit作为显示载体

UE4绘制直线的方式很多,这里我使用DrawLine在RenderTarget里绘制,绘制的实现放在后面说。

然后我们需要让自定义框选的线显示在屏幕上,那么使用wedgit来显示是比较理想的,所以我们来创建一个widget,命名为CustomSelectUI,并为CustomSelectUI添加一个image作为显示的载体,命名为Background。

2.创建Material作为RenderTarget的显示载体

光一个image也是无法显示我们绘制的线的,因为我们的线是画在RenderTarget里的,而image没法直接使用RenderTarget,所以我们还需要创建一个Material来承载RenderTarget。这里创建一个Material命名为Mat_Paint。

3.为Material创建一个Texture

创建的Texture是有讲究的,Texture必须是存黑色的即RPG(0,0,0),然后分辨率可自定义。这里我使用PS制作了一个纯黑的PNG图片,并设置分辨率为2048x2048,并导入到UE4生成Texture,并命名为Mat_Transparent_Max。
在这里插入图片描述

使用纯黑色的原因在第四小节说明。

4.实现Material的作用

Material出来作为RenerTarget的载体外,还有设置笔刷的颜色,以及使背景透明的作用。

先来看一下Mat_Paint的蓝图

在这里插入图片描述

首先将Mat_Paint节点的Details/Material/Material Domain更换为User Interface,即将Material改为Material Interface。

并未Mat_Paint添加一个Texture,将之前创建的Mat_Transparent_Max拖入Mat_Paint中,右键节点选择Convert to Parameter将节点参数化,并取名RT_Texture,这是为了后面动态设置做的准备。

在这里插入图片描述

然后将RT_Texture连接到Mat_Paint中的Opacity上,Opacity节点是Material控制材质透明度的接口,在Opacity中RGB(0,0,0)表示全透明,RGB(1,1,1)表示不透明,即纯黑色表示全透明,纯白色表示不透明,这就是为什么我们需要一张纯黑色的Texture的原因。因为我们需要一个透明的材质赋予image这样我们才能看到Wedgit后面的场景,使场景不会被我们的image遮挡。

然后创建一个Constant3Vector,并且也将其参数化,命名为PaintColor,这为之后修改画笔颜色预留接口。将PaintColor连接到Mat_Paint的Final Color上。Final Color接口控制着材质最终显示的颜色。

到这里,擦材质我们就做好了。

5.显示

这里我在CustomSelectUI构造时为Background添加Mat_Paint动态材质。我们来看一下蓝图

在这里插入图片描述

  • RenderMat变量是Material Instance Dynamic类型用于存储动态创建的Mat_Paint,方便之后使用;
  • LineLinearColor变量是LinearColor类型,用于设置画笔颜色

到这里显示部分就完成了。

二、绘制模块

1.获取鼠标在屏幕中的位置坐标

线的绘制我使用DrawLine函数根据鼠标点击的点来绘制点与点之间的直线,绘制模块最终要的两个步骤就是获取鼠标点击的屏幕上的点和根据点集绘制多边形。

实现获取鼠标在屏幕上的位置,这里我们需要重写两个函数,OnMouseButtonUp和OnMouseButtonDown,我们来看一下蓝图。

在这里插入图片描述

在这里插入图片描述

  • MouseDown用于标识鼠标的按下与抬起,true表示按下,false表示抬起。

  • Setup控制是否开始绘制。

  • IsFirstPoint标识第一个点与其余点。

  • PolygonPoints是存储鼠标点击的点的数组,启动绘制之后鼠标每点击一次变向数组中添加一个Vector2D元素。

  • CurrentPoint存储鼠标当前点击的屏幕坐标。

  • StartPoint存储绘制直线的起点的屏幕坐标。

  • MousePositionAdaptDPI是自己封装的获取鼠标屏幕坐标的函数,之所以封装是为例修改方便。

在这里插入图片描述

到此,获取鼠标的屏幕坐标就实现了,接下来要根据鼠标点击确定的点集PolygonPoints绘制直线。

2.绘制直线

再绘制直线之前,需要做一些准备工作,即创建直线绘制的载体RenderTarget并用之前创建好的RenderMat承载,然后将RenderMat绑定到Background上显示。这里我绑定到Setup按钮的OnClicked事件下。

在这里插入图片描述

  • CreateCanvasRenderTarget2D函数负责创建RenderTarget,RenderTarget可以直接使用引擎默认的,也可以自己创建自定义的,这里我使用引擎默认的。

    在这里插入图片描述

    Width和Height控制着RenderTarget的长宽比例,超出这个比例的部分屏幕将无法绘制,如:

    在这里插入图片描述

    红框部分的屏幕比例就是1920:1080,超出着部分的屏幕将无法绘制,当然在全屏运行的情况下不会出现这种问题。出现这个问题是因为我的计算机屏幕尺寸就是1920:1080,运行时,UE4的实际运行窗口是蓝色部分,很明显由于windows菜单栏和UE运行窗口的菜单栏占据了屏幕的部分像素,所以UE的实际运行窗口是蓝色部分,其比例显然不是1920:1080,所以超出部分就没办法绘制了。这个RenderTarget的比例可以根据自己的实际需求更改。

  • SetupCustomSelect函数负责绘制的启动与关闭

在这里插入图片描述
准备工作结束后便可以开始绘制直线了,直线的绘制放在Tick函数下,每帧绘制。

在这里插入图片描述

  • LineThickness控制直线绘制时的粗细程度。

  • StartPaint是具体的直线绘制函数。

    在这里插入图片描述

    其中RenderColor必须设置纯白色,只有这样绘制出来的直线才是不透明的。

绘制直线这里有一点需要注意,即需要设置我们Background的锚点为左上角,因为RenderTarget的原点在左上角,只有这样鼠标点击的位置才会和绘制的位置匹配,否则会出现位置偏移的问题。

在这里插入图片描述

3.清除绘制内容

考虑到会有绘制出错的情况,所以添加一个清除绘制内容的功能。清除绘制内容原理比较简单,只需要清除RenderTarget缓存和PolygonPoints点击即可。

这里我绑定在Delete按钮的OnClicked事件下。

在这里插入图片描述

4.结束绘制

在这里插入图片描述

结束绘制之后就要开始计算框选内容了,SureSelect函数负责这方面的实现,计算后面讲解。

结束绘制之后需要将最后一个点和第一个点连接来,确保多边形是一个封闭的多边形。EndPaint函数负责这个功能的实现。

在这里插入图片描述

三、计算

计算这里需要用到C++了,在蓝图的SureSelect函数里调用C++的计算函数。

创建一个继承自Actor的C++类,并命名为CustomSelectActor,下面贴出C++源码:

.h

#pragma once

#define LeastPointNum 4
#define ActorSamplingPoints 9

#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "EngineUtils.h"
#include "GameFramework/PlayerController.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CustomSelectActor.generated.h"

USTRUCT()
struct FBoxPointSet
{
	GENERATED_USTRUCT_BODY()
public:
	TArray<FVector2D> points;
	FBoxPointSet()
	{
		points.Init(FVector2D(0, 0), 9);
	}
};

UCLASS()
class CUSTOMSELECT_API ACustomSelectActor : public AActor
{
	GENERATED_BODY()
	
public:	
	const FVector BoundsPointMapping[8] =
	{
		FVector(1, 1, 1),
		FVector(1, 1, -1),
		FVector(1, -1, 1),
		FVector(1, -1, -1),
		FVector(-1, 1, 1),
		FVector(-1, 1, -1),
		FVector(-1, -1, 1),
		FVector(-1, -1, -1)
	};

protected:

	virtual void BeginPlay() override;

public:	
	ACustomSelectActor();
	virtual void Tick(float DeltaTime) override;

	void GetMax(TArray<FVector2D>& points, float& max_x, float& max_y, int& len);
	void GetMin(TArray<FVector2D>& points, float& min_x, float& min_y, int& len);
	void SpwanVertArr(TArray<FVector2D>& polygonPoints, TArray<float>& vertx, TArray<float>& verty, int& len);
	bool PNPoly(int nvert, TArray<float> vertx, TArray<float> verty, float testx, float testy);
	UFUNCTION(BlueprintImplementableEvent)
	bool ProjectWorldLocationToWidgetPosition(APlayerController* player_ctrl, FVector worldLocation, FVector2D& screenPosition);
	void GetFBoxPointsSet(
		TArray<FBoxPointSet>& fboxPointsArr,
		TArray<AActor*>& actorArr,
		TSubclassOf<AActor>& classFilter,
		bool& bIncludeNonCollidingComponents,
		APlayerController* player_ctrl);
	void GetActorsRefByPointsSet(
		TArray<AActor*>& outActors,
		TArray<float>& vertx,
		TArray<float>& verty,
		TArray<FBoxPointSet>& fboxPointsArr,
		TArray<AActor*>& actorArr,
		TArray<FVector2D>& polygonPoints,
		int& len);
	UFUNCTION(BlueprintCallable, Category = "CustomSelect")
	bool CustomSelect(
		TArray<AActor*>& outActors,
		TArray<FVector2D> polygonPoints,
		TSubclassOf<AActor> classFilter,
		APlayerController * player_ctrl,
		bool bIncludeNonCollidingComponents);
	UFUNCTION(BlueprintCallable, Category = "CustomSelect")
	float CompuePolygonArea(const TArray<FVector2D> polygonPoints);
};

.cpp

#include "CustomSelectActor.h"

ACustomSelectActor::ACustomSelectActor()
{
	PrimaryActorTick.bCanEverTick = false;

}

void ACustomSelectActor::BeginPlay()
{
	Super::BeginPlay();
	
}

void ACustomSelectActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ACustomSelectActor::GetMax(TArray<FVector2D>& points, float& max_x, float& max_y, int& len)
{
	max_x = points[0].X;
	max_y = points[0].Y;
	for (int i = 1; i < len; i++)
	{
		if (max_x < points[i].X)
		{
			max_x = points[i].X;
		}
		if (max_y < points[i].Y)
		{
			max_y = points[i].Y;
		}
	}
}

void ACustomSelectActor::GetMin(TArray<FVector2D>& points, float& min_x, float& min_y, int& len)
{
	min_x = points[0].X;
	min_y = points[0].Y;
	for (int i = 1; i < len; i++)
	{
		if (min_x > points[i].X)
		{
			min_x = points[i].X;
		}
		if (min_y > points[i].Y)
		{
			min_y = points[i].Y;
		}
	}
}

void ACustomSelectActor::SpwanVertArr(TArray<FVector2D>& polygonPoints, TArray<float>& vertx, TArray<float>& verty, int& len)
{
	for (int i = 0; i < len; i++)
	{
		vertx.Add(polygonPoints[i].X);
		verty.Add(polygonPoints[i].Y);
	}
}

bool ACustomSelectActor::PNPoly(int nvert, TArray<float> vertx, TArray<float> verty, float testx, float testy)
{
	bool ret = false;
	for (int i = 0, j = nvert - 1; i < nvert; j = i++)
	{
		if (((verty[i] > testy) != (verty[j] > testy)) && (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]))
		{
			ret = !ret;
		}
	}
	return ret;
}

void ACustomSelectActor::GetFBoxPointsSet(
	TArray<FBoxPointSet>& fboxPointsArr,
	TArray<AActor*>& actorArr,
	TSubclassOf<AActor>& classFilter,
	bool& bIncludeNonCollidingComponents,
	APlayerController * player_ctrl)
{
	int i = 0;
	for (TActorIterator<AActor> Itr(GWorld->GetWorld(), classFilter); Itr; ++Itr)
	{
		AActor* EachActor = *Itr;
		const FBox EachActorBounds = Cast<AActor>(EachActor)->GetComponentsBoundingBox(bIncludeNonCollidingComponents);
		const FVector BoxCenter = EachActorBounds.GetCenter();
		const FVector BoxExtents = EachActorBounds.GetExtent();
		FBox2D ActorBox2D(ForceInit);
		fboxPointsArr.Add(FBoxPointSet());
		for (uint8 BoundsPointItr = 0; BoundsPointItr < 8; BoundsPointItr++)
		{
			FVector2D ScreenPos;
			if (ProjectWorldLocationToWidgetPosition(player_ctrl, BoxCenter + (BoundsPointMapping[BoundsPointItr] * BoxExtents), ScreenPos))
			{
				ActorBox2D += ScreenPos;
				fboxPointsArr[i].points[BoundsPointItr + 1] = ScreenPos;
			}
		}
		fboxPointsArr[i].points[0] = ActorBox2D.GetCenter();
		actorArr.Add(EachActor);
		i++;
	}
}

void ACustomSelectActor::GetActorsRefByPointsSet(
	TArray<AActor*>& outActors,
	TArray<float>& vertx,
	TArray<float>& verty,
	TArray<FBoxPointSet>& fboxPointsArr,
	TArray<AActor*>& actorArr,
	TArray<FVector2D>& polygonPoints,
	int& len)
{
	int fboxlen = fboxPointsArr.Num();
	int pointslen = polygonPoints.Num();
	float max_x = 0;
	float max_y = 0;
	float min_x = 0;
	float min_y = 0;
	GetMax(polygonPoints, max_x, max_y, len);
	GetMin(polygonPoints, min_x, min_y, len);
	for (int i = 0; i < fboxlen; i++)
	{
		for (int j = 0; j < ActorSamplingPoints; j++)
		{
			if (fboxPointsArr[i].points[j].X<min_x || fboxPointsArr[i].points[j].X>max_x ||
				fboxPointsArr[i].points[j].Y<min_y || fboxPointsArr[i].points[j].Y>max_y)
			{
				break;
				j = ActorSamplingPoints;
			}
			if (PNPoly(len, vertx, verty, fboxPointsArr[i].points[j].X, fboxPointsArr[i].points[j].Y))
			{
				outActors.Add(actorArr[i]);
				j = ActorSamplingPoints;
			}
		}
	}
}

bool ACustomSelectActor::CustomSelect(
	TArray<AActor*>& outActors,
	TArray<FVector2D> polygonPoints,
	TSubclassOf<AActor> classFilter,
	APlayerController * player_ctrl,
	bool bIncludeNonCollidingComponents)
{
	int len = polygonPoints.Num();
	if (len < LeastPointNum)
	{
		UE_LOG(LogTemp, Warning, TEXT("Polygon has too few points"));
		return false;
	}
	TArray<float> vertx;
	TArray<float> verty;
	SpwanVertArr(polygonPoints, vertx, verty, len);
	TArray<AActor*> actorArr;
	TArray<FBoxPointSet> fboxPointsArr;
	GetFBoxPointsSet(fboxPointsArr, actorArr, classFilter, bIncludeNonCollidingComponents, player_ctrl);
	GetActorsRefByPointsSet(outActors, vertx, verty, fboxPointsArr, actorArr, polygonPoints, len);
	return true;
}

float ACustomSelectActor::CompuePolygonArea(const TArray<FVector2D> polygonPoints)
{
	int point_num = polygonPoints.Num();
	if (point_num < 3)
	{
		UE_LOG(LogTemp, Warning, TEXT("The area is not polygon!"));
		return 0.0;
	}
	double s = polygonPoints[0].Y * (polygonPoints[point_num - 1].X - polygonPoints[1].X);
	for (int i = 1; i < point_num; ++i)
		s += polygonPoints[i].Y * (polygonPoints[i - 1].X - polygonPoints[(i + 1) % point_num].X);
	return fabs(s / 2.0);
}

  • BoundsPointMapping[8]用于确定场景中Actor的边界盒子的8个点。

  • 结构体FBoxPointSet是用来存储采样点集的数据结构,这里我取Actor边界盒子的8个点加中点一共9个点作为采样点集。

  • GetMax和GetMin计算多边形点集的横纵坐标的最大值和最小值。

  • SpawnVertArr负责将多边形点集分成横坐标点集和纵坐标点集。

  • PNPoly函数使用PNPoly算法判断一个点是否在多边形内部。

  • ProjectWorldLocationToWidgetPosition函数是一个由C++父类声明,由蓝图子类实现的函数,负责将场景中的Actor的边界盒子的点的空间坐标投影到屏幕坐标。之所以使用这种方式是因为ProjectWorldLocationToWidgetPosition蓝图节点没有C++版本,而必须使用ProjectWorldLocationToWidgetPosition蓝图节点的原因是ProjectWorldLocationToWidgetPosition蓝图节点投影出来的坐标会根据屏幕尺寸变化而自动适应,其他的空间坐标转屏幕坐标的蓝图节点在非全屏与全屏下会出现位置偏移。

    在这里插入图片描述
    所以这里需要创建一个继承自CustomSelectActor的蓝图子类来重写ProjectWorldLocationToWidgetPosition函数。

  • GetFBoxPointsSet函数负责获取世界中所有Actor的采样点集。

  • GetActorsRefByPointsSet函数负责使用PNPoly函数取在多边形内部的Actor的引用。

  • CustomSelect函数否则暴漏给蓝图提供数据输入输出的接口。

  • CompuePolygonArea函数负责计算多边形的面积,目前还有一些问题,暂时不用理睬。

至此多边形框选功能就完全实现了。来看一下效果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、将功能包成UE4插件

如果需要将功能打包成插件,那么就需要将CustomSelectActor的C++类创建在插件里。

1.创建一个空插件

在这里插入图片描述
在这里插入图片描述

创建之后需要在VS中编一下项目,然后关闭引擎,重新打开项目,以便引擎重新加载dll文件,因为插件不属于引擎的一部分,所以引擎没办法直接热加载插件内容。

2.在插件文件夹下创建C++类

我们需要将CustomSelectActor类创建在插件文件夹下,创建好空插件后,再创建C++类时可以选择创建文件夹。

在这里插入图片描述
然后按一、二、三的步骤实现功能即可。

3.打包插件

进入插件管理点集打包

在这里插入图片描述
至此插件就打包好了。

参考博客:
https://blog.csdn.net/weixin_36369675/article/details/88419361
https://www.cnblogs.com/anningwang/p/7581545.html
https://www.cnblogs.com/TenosDoIt/p/4047211.html

猜你喜欢

转载自blog.csdn.net/qq_39108291/article/details/106577178