要在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

【UE4】实现自定义框选相关推荐

  1. 【UE4】UE4框选

    目录: 视窗操作篇 反转鼠标中键 自适应视窗缩放速度 解除对选中物体旋转 对屏幕外物体进行位移旋转缩放 物体吸附到模型顶点 屏幕编队(记录当前相机位置和旋转) 落到正下方物体 显示与隐藏 资源操作篇 ...

  2. element-ui 使用自定义复选框

    为什么不使用自带复选框? 自带复选框无法确定数据来源于回填还是用户手动选择,因为回填表格复选框时只能使用toggleRowSelection方法来遍历回填,而且会触发selection-change事 ...

  3. el-checkbox 自定义 复选框【多选框】

    1. 自定义 多选框 el-checkbox; 预览: 注意: 假如有全局字体引入会影响显示效果: 去除el-checkbox__inner附加字体css font-family: not speci ...

  4. CSS 自定义多选框样式

    今天看了一个做 Todo List 的视频,学了一招,可以自定义多选框的样式,只用 CSS 就可以做到. 整个的思路大概是:隐藏 input 元素,在 label 下用伪元素实现一个自定义的多选框. ...

  5. 基于华为云自定义模板的图片操作演示(框选、拖拽)

    掘金链接 华为云自定义模板识别是服务于AI领域的流程控制系统,我们一起了解下其中一个模块的界面化操作实例.如下图,针对图片的高精度识别,我们需要处理图片的样式以便获得更好的模型训练数据和高精度识别结果 ...

  6. django获取html复选框,扩展Django Admin tabular.html自定义复选框操作

    我有两个表被告和被告_Potential class Defendant(models.Model): fullname = models.CharField(max_length = 100, nu ...

  7. html制作复选框,html自定义复选框

    自定义复选框的素材: icon-check-circle.png icon-checked.png checkbox.html(为了方便起见,这里使用到了jQuery) 自定义复选框checkbox ...

  8. vue 自定义多选框组件

    自定义多选框组件 <template><div class="checkBox"><input v-model="isInherit&quo ...

  9. WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 下拉选 ...

最新文章

  1. 美国中学生被学校监控,实时位置、和谁接触一览无余
  2. 从Android中Activity之间的通信说开来
  3. linux c/c++ 判断是否为中文(不包括中文符号,非正则)
  4. SQL Server 2008数据库,显示正在恢复,想把它删了结果删除不了
  5. mysql中nvl_Mysql中类似于oracle中nvl()函数的ifnull()函数
  6. 一文读懂哈希和一致性哈希算法
  7. JavaScript常用算法
  8. 蚂蚁金服冯柯:下一个十年,核心自研技术将迎来黄金发展期
  9. 数据分析工具R和RStudio入门介绍
  10. json数据格式在javascript的读取与c#后台的赋值格式
  11. 广义线性模型解读必看文章
  12. 类似苹果数据线的android,除了常见的安卓、苹果、Type-c,还有哪些你不知道的手机数据线?...
  13. Java Web中乱码问题
  14. 四阶魔方邻角互换公式
  15. 常用java技术_java常用技术
  16. 计算机社团感恩节免费维修周策划书,社团感恩节活动策划书范文 .docx
  17. 适合婚礼上唱的歌曲 流行情歌大串烧
  18. 软件设计师学习笔记-数据库系统
  19. 关于将oracle11卸载干净及安装与配置
  20. 计算机毕业设计 SSM+Vue网上招投标管理系统 电子投标系统 项目投标管理系统Java Vue MySQL数据库 远程调试 代码讲解

热门文章

  1. php将jpg图片转换为png图片。去除白背景
  2. 吉哥系列故事——恨7不成妻 HDU - 4507
  3. Learning Log: 输入圆的半径,计算圆的面积
  4. 《打工人》关于打工人的段子合集!
  5. 说服别人的六种好方法
  6. java ssh2工具,SSH2代码生成工SSH2代码生成工具 PowerBy 清如许UnicodePowerBy
  7. android9.0与5g,vivo NEX 5G版?骁龙855+Android9.0,vivo这波
  8. USACO 2004 MooFest 奶牛集会
  9. 这个明星最爱的奢侈品旅行箱,如何在天猫618增长5000%?
  10. OEUF麻雀婴儿床 - 家长寻求新设计的幼儿园