目的:
如图,判定鼠标是否点击到红色和紫色圆形;
UE4做不到:
UE4点击会将整个图片的矩形区域做为判定条件,所以无法精确到透明位置不点击。
做法(两种):
一、直接在图片上监听点击事件,通过拿图片材质的透明的判断点击是否有效。参考可见实现UMG中自定义不规则形状按钮
(缺点1、只能实现一张图片透明判断 2、做不了点击穿透 )
(可另外参考 编写自定义控件 缺点:改源码,比较难(哈哈))
二、在外层做监听,通过换算坐标拿到图片材质相对位置的透明度来判断是否有效
(优点:能实现多张图的判断 半优点:做到伪点击穿透 缺点:对ui层级有要求)
以下是第二张方法的实现和大概逻辑流程说明:
先贴代码再说明
/**.h **/
//基于imageArr判断点击的位置是否是空白(透明度小于某个值)
//ui做法需要限制,image必须在接收点击事件界面的直接子节点,否则会导致位置换算有误
//@param imageArr 用来判断空白的贴图数组
//@return true:点击空白
UFUNCTION()
static bool IsClickTheBlank(int Alpha ,const FGeometry& MyGeometry, const FPointerEvent& MouseEvent,TArray<UImage*>imageArr);
/**.cpp*/
bool ULuaCallCppFunctionLib::IsClickTheBlank(int Alpha, const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, TArray<UImage*>imageArr)
{
if (imageArr.Num() <= 0)
{
return true;
}
bool bResult = true;
FVector2D LocalPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());//先对于MyGeometry所在slot的屏幕坐标
for (int32 i = 0; i < imageArr.Num(); ++i)
{
UImage* image = imageArr[i];
UTexture2D* AdvancedHitTexture = (UTexture2D*)image->Brush.GetResourceObject();
if (bResult && nullptr != AdvancedHitTexture)
{
UCanvasPanelSlot* canvasSlot = UWidgetLayoutLibrary::SlotAsCanvasSlot(image);
//计算出描点左上角,中心点alignment为(0,0)的相对父节点坐标
FVector2D parentSize = MyGeometry.GetLocalSize();//ui做法需要限制,image必须在接收点击事件界面的直接子节点,否则会导致位置换算有误
FVector2D relativeLUPos = FVector2D(0, 0);//相对父节点的左上角坐标
FVector2D relativeRDpos = FVector2D(0, 0);//右下角坐标
FVector2D pos = canvasSlot->GetPosition();
FMargin offset = canvasSlot->GetOffsets();
FVector2D size = canvasSlot->GetSize();
FAnchors abchors = canvasSlot->GetAnchors();
FVector2D alignment = canvasSlot->GetAlignment();
//判断锚点是一个点还是一个范围
//X Y要分开(难啊)
if (abchors.Minimum.X == abchors.Maximum.X)
{
float alignmentOffsetLU = size.X * -alignment.X;//中心点导致的左上角偏移
float alignmentOffsetRD = size.X * (1 - alignment.X);//中心点导致的右下角角偏移
//描点偏移
float abchorsOffset = parentSize.X * abchors.Minimum.X;
relativeLUPos.X = pos.X + alignmentOffsetLU + abchorsOffset;
relativeRDpos.X = pos.X + alignmentOffsetRD + abchorsOffset;
}
else
{
relativeLUPos.X = parentSize.X * abchors.Minimum.X + offset.Left;
relativeRDpos.X = parentSize.X * abchors.Maximum.X - offset.Right;
}
if (abchors.Minimum.Y == abchors.Maximum.Y)
{
float alignmentOffsetLU = size.Y * -alignment.Y;//中心点导致的左上角偏移
float alignmentOffsetRD = size.Y * (1 - alignment.Y);//中心点导致的右下角角偏移
//描点偏移
float abchorsOffset = parentSize.Y * abchors.Minimum.Y;
relativeLUPos.Y = pos.Y + alignmentOffsetLU + abchorsOffset;
relativeRDpos.Y = pos.Y + alignmentOffsetRD + abchorsOffset;
}
else
{
relativeLUPos.Y = parentSize.Y * abchors.Minimum.Y + offset.Top;
relativeRDpos.Y = parentSize.Y * abchors.Maximum.Y - offset.Bottom;
}
FVector2D imageSize = relativeRDpos - relativeLUPos; //图片大小
//适配旋转
FVector2D relativeMidPos = FVector2D(LocalPosition) - (relativeLUPos + relativeRDpos)/2; //转换为图片中心点为圆心的坐标系
float angle = image->GetRenderTransformAngle();
float cosV = FMath::Cos(-angle / 57.2958);
float sinV = FMath::Sin(-angle / 57.2958);
float px2 = cosV * relativeMidPos.X - sinV * relativeMidPos.Y;
float py2 = sinV * relativeMidPos.X + cosV * relativeMidPos.Y;
relativeMidPos.X = px2;
relativeMidPos.Y = py2;
//判断是否在image内
if (FMath::Abs(relativeMidPos.X) < imageSize.X/2 && FMath::Abs(relativeMidPos.Y) < imageSize.Y / 2)
{
//描点左上角,中心点alignment为(0,0)的相对父节点坐标
FVector2D clickRelativePos = relativeMidPos + (relativeLUPos + relativeRDpos) / 2 - relativeLUPos;
中心点旋转变换
//float px1 = clickRelativePos.X - (size.X / 2); //alignment为(0.5,0.5)时的坐标位置,
//float py1 = clickRelativePos.Y - (size.Y / 2);
//float angle = image->GetRenderTransformAngle();
//float cosV = FMath::Cos(-angle / 57.2958);
//float sinV = FMath::Sin(-angle / 57.2958);
//float px2 = cosV * px1 - sinV * py1;
//float py2 = sinV * px1 + cosV * py1;
旋转后坐标
//clickRelativePos.X = px2 + (size.X / 2);
//clickRelativePos.Y = py2 + (size.Y / 2);
FVector2D mapPos = clickRelativePos / (relativeRDpos - relativeLUPos);
float px = floor(mapPos.X * AdvancedHitTexture->PlatformData->SizeX);
float py = floor(mapPos.Y * AdvancedHitTexture->PlatformData->SizeY);
int BufferPosition = py * AdvancedHitTexture->PlatformData->SizeX + px;
FColor* ImageData = (FColor*)((AdvancedHitTexture->PlatformData->Mips[0]).BulkData.Lock(LOCK_READ_ONLY));
if (!ImageData) {
}
else {
if (ImageData[BufferPosition].A > Alpha)
{
//非空白
bResult = false;
}
}
//绘制材质透明度
/*UE_LOG(LogTemp, Log, TEXT("BufferPosition index:%d posX:%d,posY:%d ImageData[BufferPosition].A:%d Alpha:%d isBlank:%d"), i, px, py, ImageData[BufferPosition].A, Alpha, (ImageData[BufferPosition].A <= Alpha ? 1 : 0));
for (int32 i = 0; i < AdvancedHitTexture->PlatformData->SizeY; ++i)
{
FString lineStr = "";
for (int32 j = 0; j < AdvancedHitTexture->PlatformData->SizeX; j++)
{
int index = i * AdvancedHitTexture->PlatformData->SizeX + j;
if (ImageData[index].A <= Alpha)
{
lineStr += "0";
}
else
{
lineStr += "1";
}
}
UE_LOG(LogTemp, Log, TEXT("%s"), *lineStr);
}*/
AdvancedHitTexture->PlatformData->Mips[0].BulkData.Unlock();
}
}
}
return bResult;
}
直接提供一个静态函数用于查询点击是否空白,传入透明度阙值和UImage数组,通过换算点击点在UImage上的纹理信息,可以判断是否点击空白。
1、获取纹理信息
一张64*64的图片,就存有64*64个像素信息,如(0,0)存的就是左上角像素点的rgba。
通过 UTexture2D* AdvancedHitTexture = (UTexture2D*)image->Brush.GetResourceObject(); 可以获取图片的纹理。
FColor* ImageData = (FColor*)((AdvancedHitTexture->PlatformData->Mips[0]).BulkData.Lock(LOCK_READ_ONLY));获取纹理数据。
每个(x,y)点的数据 = y*size.x+ x;
2、将点击的坐标换算到UImage的相对坐标(比较耗时的地方)
如图,通过FVector2D LocalPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());可以计算出鼠标点击位置在整个监听界面的位置,
需要将位置转换为以图片为画布的坐标。
所以需要获取图片的UCanvasPanelSlot,通过一系列转换。
(由于屏幕坐标系为左上角为零,像右向下增长,需要将图片位置信息先转换为描点左上角,中心点alignment为(0,0),
所以有锚点是一个点还是一个范围,X Y要分开的判断)
3、
求出点击坐标在图片上的相对坐标后,通过缩放映射到对应纹理位置,便大功告成啦。
4、细节:
图1
图2
同一层级下的点击事件处理时,返回FReply::Unhandled() 是无法透传到下一个点击事件上的,所以图1上层的按钮1即使不处理点击事件,按钮2也接收不到点击事件。
图2子按钮不接受点击事件时父按钮会接收到点击事件。
总结1:点击事件的传递是父节点传递的。这也是为什么做法一做不到点击穿透的原因,除非按钮套按钮。方法二能做伪穿透因为监听层不在图片上,但也做不了与外界点击穿透(但已经够灵活了,至少能适用我们项目了)
总结2:ui里做位置转换挺麻烦的(可能因为我没找到aip),所以无法做到多层级转换,这也是方法二的弊端,也是为什么图片必须是监听层的直接子节点(或没位移变换可使用多层级)的原因。
END
希望对你有帮助~