Hello again and let me apologize for the incorrect tag - the problem has appeared in 4.9, not in 4.10 and here’s a detail description of a hacky workaround:
The breaking change was introduced in 4.9 and is the following:
Prior to 4.9 APlayerController::TickPlayerInput
looked like this:
...
if (LocalPlayer->ViewportClient->GetMousePosition(MousePosition))
{
bHit = GetHitResultAtScreenPosition(MousePosition, CurrentClickTraceChannel, true, /*out*/ HitResult);
}
UPrimitiveComponent* PreviousComponent = CurrentClickablePrimitive.Get();
UPrimitiveComponent* CurrentComponent = (bHit ? HitResult.Component.Get() : NULL);
UPrimitiveComponent::DispatchMouseOverEvents(PreviousComponent, CurrentComponent);
Post 4.9 the code is now:
// Only send mouse hit events if we're directly over the viewport.
if ( ViewportClient->GetGameViewportWidget().IsValid() && ViewportClient->GetGameViewportWidget()->IsDirectlyHovered() )
{
if ( LocalPlayer->ViewportClient->GetMousePosition(MousePosition) )
{
bHit = GetHitResultAtScreenPosition(MousePosition, CurrentClickTraceChannel, true, /*out*/ HitResult);
}
}
UPrimitiveComponent* PreviousComponent = CurrentClickablePrimitive.Get();
UPrimitiveComponent* CurrentComponent = (bHit ? HitResult.Component.Get() : NULL);
UPrimitiveComponent::DispatchMouseOverEvents(PreviousComponent, CurrentComponent);
This is a side-effect of a change listed in the release notes of 4.9:
We no longer ignore hit testing in APlayerController::GetHitResultAtScreenPosition
if the mouse isn’t directly over the viewport. That function can be used
regardless of the state of the mouse, so that logic has been moved elsewhere.
The change to APlayerController::TickPlayerInput
is to accomodate for the fact that
APlayerController::GetHitResultAtScreenPosition
no longer ignores hits outside the viewport.
Problem is, the new test is not equivalent to the previous one. Whereas the old one used
ULocalPlayer::CalcSceneView
which checks whether the mouse coordinates are inside the viewport, the new one
(ViewportClient->GetGameViewportWidget()->IsDirectlyHovered()
) instead checks whether the
viewport is the topmost widget under the mouse.
I was able to solve the issue by changing:
bool FSlateApplication::IsWidgetDirectlyHovered(const TSharedPtr<const SWidget> Widget) const
{
for( auto LastWidgetIterator = WidgetsUnderCursorLastEvent.CreateConstIterator(); LastWidgetIterator; ++LastWidgetIterator )
{
const FWeakWidgetPath& WeakPath = LastWidgetIterator.Value();
if( WeakPath.IsValid() && Widget == WeakPath.GetLastWidget().Pin() )
{
return true;
}
}
return false;
}
to
bool FSlateApplication::IsWidgetDirectlyHovered(const TSharedPtr<const SWidget> Widget) const
{
for( auto LastWidgetIterator = WidgetsUnderCursorLastEvent.CreateConstIterator(); LastWidgetIterator; ++LastWidgetIterator )
{
const FWeakWidgetPath& WeakPath = LastWidgetIterator.Value();
if( WeakPath.IsValid() &&
( Widget == WeakPath.GetLastWidget().Pin() ||
WeakPath.GetLastWidget().Pin()->GetTypeAsString() == "MyWidget") )
{
return true;
}
}
return false;
}
This way checking if a widget is on top will return true if either it, or the invisible
widget is on top. This introduces an inconsistency as the function will return
true for multiple widget but generally having my widget on top is the same
as having none at all so it shouldn’t matter.
Besides, searching for it in the engine’s source revealed that this is its only usage
(although it’s ■■■■■ and clients may call it in other places)
Another working solution is to remove the check from PlayerController::TickPlayerInput
:
// Change line 3842 in PlayerController.cpp as of 4.11 from
if ( ViewportClient && ViewportClient->GetGameViewportWidget().IsValid() && ViewportClient->GetGameViewportWidget()->IsDirectlyHovered() )
// to
if ( ViewportClient && ViewportClient->GetGameViewportWidget().IsValid())