![7c69e155d18672ad1c2933e2621cddf8.png](https://i-blog.csdnimg.cn/blog_migrate/8c3c0c9950f21117719f03c8cf30812c.jpeg)
通常我们都是用SetupPlayerInputComponent来绑定输入事件,而且它本身也是根据Actor类的InputPriority来判断输入的优先级,确保不会同一个输入响应两个地方的绑定。不仅仅是这样Actor的输入监听,包括界面,比如鼠标左键如果点击了一个按钮,它这次输入就会被按钮吞掉了,并不会触发你其他地方绑定的左键按下事件。为什么会这样呢?看看源码就知道了,在源码里所有的输入监听都是一个个IInputProcessor,存储在InputPreProcessorList中,在FSlateApplication::InputPreProcessorsHelper中会遍历所有输入监听,当有一个响应了这次的输入,就不会触发其他的输入监听了:
![19e3402d60b3ee5446a68acb58c6f2ba.png](https://i-blog.csdnimg.cn/blog_migrate/f6509a8179942910dfd0e864385c389f.jpeg)
可能很多人都没用过IInputProcessor,看到FSlateApplication类你就知道了,这已经是一个接近底层的类的,我们这里的需求是想做一个响应任意点击时,播一个动效,但不吞掉输入,该点击按钮还是响应按钮事件。
所以我也来继承实现了IInputProcessor类:
![df5d804079fa4600a8bc8bb0928c6e2e.png](https://i-blog.csdnimg.cn/blog_migrate/c60a476ca2f0debe2e2db3fce17b6f4d.jpeg)
这些都是必须实现的接口,这里我们的输入是不想吞掉,很简单,所有这些函数都返回false,这样它既响应了自己,也不会阻断输入往下的传递。
FTouchInputProcessor类需要我们注册一下,我们就在PlayerController类的BeginPlay和EndPlay里进行注册和注销:
![6fccaa1b9c6e1f0499215967b24fb14a.png](https://i-blog.csdnimg.cn/blog_migrate/dc2033b6cec3379e80faaa96242e42fe.jpeg)
这样就把我们的输入监听注册进去了。下面再讲一下FTouchInputProcessor的应用:
声明几个成员:
![19a8962c3c3a790a51cb9b0f8d1e02d5.png](https://i-blog.csdnimg.cn/blog_migrate/1d386766a1d01f803effb68230966677.jpeg)
TouchEffectImage是点击下去播放动效的Image控件,动效是使用UI材质,里面主要是flipbook函数来播放序列帧,为了保证每次点击下去,序列帧是从第一帧开始播放,材质的刷新由这边CPU控制。当我们的鼠标按下的时候,如果TouchEffectImage还没有就NewObject一个,并且工具鼠标点击处设置坐标添加到视口上,并且重置这次的计时器,计时器重新算,每次都是从第一帧播放点击动效。我这里的Lua部分不用在意,因为我们的界面主要是在Lua里,我从Lua里获取全屏的那个UCanvasPanel,这里要注意的是,第一次创建的控件,才添加到视口中,是拿不到正确的FGeometry的,GetCachedGeometry是拿到上一帧缓存的数据,这里调用SlateApp.Tick(ESlateTickType::All);进行手动的Tick,提前计算了TouchEffectImage的FGeometry。
bool FTouchInputProcessor::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
UnLua::FLuaRetValues RetVals = LuaStateMgr::GetLuaStateMgr().CallLuaFunctionTable("UIManager", "GetRootCanvasForCPP");
if (RetVals.IsValid())
{
if (UNCanvasPanel* RootPanel = RetVals[0].Value<UNCanvasPanel*>())
{
if (TouchEffectImage)
{
if (UCanvasPanelSlot* CanvSlot = Cast<UCanvasPanelSlot>(TouchEffectImage->Slot))
{
CalcWidgetPosition(SlateApp, MouseEvent, CanvSlot, TouchEffectImage);
SetTouchEffectBrush(EffectConfig.TouchEffectBrush);
TouchEffectImage->SetVisibility(ESlateVisibility::SelfHitTestInvisible); /*计算好Geo后再显示出来*/
TouchTimer = EffectConfig.TouchDuration;
}
}
else
{
TouchEffectImage = NewObject<UImage>();
TouchEffectImage->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
if (UCanvasPanelSlot* CanvasSlot = RootPanel->AddChildToCanvas(TouchEffectImage))
{
CanvasSlot->SetAnchors(FAnchors());
CanvasSlot->SetAlignment(FVector2D(0.0f, 0.0f));
CanvasSlot->SetAutoSize(true);
SetTouchEffectBrush(EffectConfig.TouchEffectBrush);
SlateApp.Tick(ESlateTickType::All); /*手动Tick,获取最新的Geo*/
CalcWidgetPosition(SlateApp, MouseEvent, CanvasSlot, TouchEffectImage);
TouchTimer = EffectConfig.TouchDuration;
}
}
if (TouchEffectImage)
{
if (UMaterialInstanceDynamic* DIM = Cast<UMaterialInstanceDynamic>(TouchEffectImage->Brush.GetResourceObject()))
{
/*设置动效的时间*/
DIM->SetScalarParameterValue(TEXT("PlayTime"), EffectConfig.TouchDuration);
}
}
TouchPlayTime = 0.0f;
bDrag = true;
}
}
return false;
}
然后我们在看看怎么设置Image对应的坐标:
![14eda3570dc6b23a81d99cf0b2f31d99.png](https://i-blog.csdnimg.cn/blog_migrate/c922e63784269badb56c2a6cb77951bf.jpeg)
首先是拿到Image的FGeometry,还有Image的大小WidgetSize。游戏视口在屏幕的坐标也要知道,因为FPointerEvent只能拿到的是屏幕控件的坐标,和视口坐标做差才能得到视口空间下点击处的坐标。如果是编辑器下,还要减去边框的大小。然后视口的ViewportGeo也要拿一下,用USlateBlueprintLibrary::TransformVectorLocalToAbsolute将视口坐标从局部坐标转换成绝对坐标,到这里还没得到Image的坐标,还要减去控件大小的一半,前面得到的是控件中心的坐标,但控件的锚点是左上角,所以减掉大小的一半得到左上角的坐标,然后就可以设置Image的CanvasSlot的坐标了。
然后还有就是TouchEffectImage的材质表现要在Tick里去更新:
![afc8474ecd6505c6eeb4f643bcb78a6d.png](https://i-blog.csdnimg.cn/blog_migrate/b0d9f0bba68b6de49b452c9b66e7abea.jpeg)
这里也贴一下材质的连连看吧:
![c0cb8236f5337587cf0276cd3dc61ec8.png](https://i-blog.csdnimg.cn/blog_migrate/300dbc6474b667b831a297bf82998a77.jpeg)
PlayTime是这个动效需要播放的时长,在点击的时候初始化设置。Order是序列帧是几乘几的,然后CurrentTime就是我们在C++里进行更新的播放时间了。
好了,任意点击的动效讲完了,下面在讲一下在屏幕上拖拽时的拖尾效果怎么做。这里建一个UTrailingPointImage类继承UImage和FTickableGameObject,这样它除了是一个Image,也有Tick逻辑。主要成员变量是这些:
![c49b95e9ce7d33ccdb7bed010cc790b9.png](https://i-blog.csdnimg.cn/blog_migrate/d2d81fe9864282a657dc0fb4b6bb54d5.jpeg)
大概就是在自己的Tick里对透明度进行渐变:
![d73bc882636bf54d58e6bcf460091776.png](https://i-blog.csdnimg.cn/blog_migrate/4c7bdec498a4f1d984b560657b321937.jpeg)
在FTouchInputProcessor的构造函数里对拖尾的Image数组TrailingArray进行初始化,提前都添加到视口的UCanvasPanel上,默认不透明度是0,虽然添加到屏幕上且是SelfHitTestInvisible的,但是是看不到的。前面我们在HandleMouseButtonDownEvent里已经标记了bDrag为true,记录自己当前是否在拖拽滑动,在HandleMouseButtonUpEvent里又将bDrag设为false。然后在HandleMouseMoveEvent里就判断是否在拖拽,如果在拖拽,按TrailingIndex序号取拖尾的Image控件设置到当前坐标坐标处,并将Opacity设为1,然后拖尾控件就会开始自己渐隐了,为了有些变化,对控件的角度进行了随机,也可以对缩放进行随机,TrailingIndex还要加1,如果超出了变回0重新开始。
![a8df8d4947547efc62c263fab68041d2.png](https://i-blog.csdnimg.cn/blog_migrate/cb8f322ffabe84eb5ae0098b917eb98d.jpeg)
好了,这里就完成了不会被吞噬的输入监听了,并做了两个应用例子,下面看看效果:
知乎视频www.zhihu.com