How to fix mouse behaviour...is UE bugged?

What I want is this:

  • The mouse cursor is locked to the game window.
  • When right mouse button is held down:
    • The mouse cursor is hidden
    • Mouse movement affects camera angle.
  • When right mouse button is not held down:
    • The mouse cursor is shown.
    • Mouse movement affects cursor position.

Very normal really.

What I have is this…

DefaultInput.ini:

bCaptureMouseOnLaunch=False
DefaultViewportMouseCaptureMode=CaptureDuringRightMouseDown
DefaultViewportMouseLockMode=LockAlways

What happens is that when the game starts, the mouse cursor is shown, but not locked to screen. If I move it off the game screen and back again, then it becomes hidden within the game window. If I press the right mouse button while the cursor is visible, it is hidden, but not shown again when I release the right mouse button.

I changed DefaultInput.ini to have:

DefaultViewportMouseLockMode=LockInFullscreen

No difference.

I changed DefaultInput.ini to have:

DefaultViewportMouseLockMode=LockOnCapture

No difference.

I am a bit frustrated because earlier with, as far as I can remember, identical settings the cursor was initially locked to the game screen, just not after I pressed the right mouse button.

I have tried the code below, but it made no difference.

CustomPawn.cpp:

void CustomPawn::BeginPlay()
{
    APlayerController *controller;

    Super::BeginPlay();

    controller = Cast<APlayerController>(GetController());

    if (nullptr != controller)
    {
        FInputModeGameAndUI input_mode;
  
        input_mode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways);
        input_mode.SetHideCursorDuringCapture(true);
        controller->SetInputMode(input_mode);
    }
}

How do I get the mouse input to behave as I wish?

I have checked the code in Engine/Private/Slate/SceneViewport.cpp and can see what it is meant to be doing. The code all looks sensible at a brief glance. It just is not doing it.

This is fairly horrible code and probably a lot more than I need, but…

I registered call backs for RMB press and release and implemented them as follows…

Press:

    UGameViewportClient   *viewport_client = GetWorld()->GetGameViewport();
    TSharedPtr<SViewport>  viewport_widget = viewport_client->GetGameViewportWidget();
    ULocalPlayer          *player          = _GetLocalPlayer();

    if (!viewport_widget.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid widget!"));
    }
    else if (nullptr == player)
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid player!"));
    }
    else
    {
        TSharedRef<SViewport>  widget_ref = viewport_widget.ToSharedRef();
        FReply                &slate      = player->GetSlateOperations();

        Cast<APlayerController>(GetController())->SetShowMouseCursor(false);
        FSlateApplication::Get().SetAllUserFocusToGameViewport(EFocusCause::Cleared);
        FSlateApplication::Get().OnCursorSet();
        slate.UseHighPrecisionMouseMovement(widget_ref);
        slate.LockMouseToWidget(widget_ref);
        viewport_client->SetMouseLockMode(EMouseLockMode::LockAlways);
        viewport_client->SetIgnoreInput(false);
        viewport_client->SetMouseCaptureMode(EMouseCaptureMode::CapturePermanently);
    }

Release:

    UGameViewportClient   *viewport_client = GetWorld()->GetGameViewport();
    TSharedPtr<SViewport>  viewport_widget = viewport_client->GetGameViewportWidget();
    ULocalPlayer          *player          = _GetLocalPlayer();

    if (!viewport_widget.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid widget!"));
    }
    else if (nullptr == player)
    {
        UE_LOG(LogTemp, Warning, TEXT("Invalid player!"));
    }
    else
    {
        TSharedRef<SViewport>  widget_ref = viewport_widget.ToSharedRef();
        FReply                &slate      = player->GetSlateOperations();

        Cast<APlayerController>(GetController())->SetShowMouseCursor(true);
        FSlateApplication::Get().SetAllUserFocusToGameViewport(EFocusCause::Cleared);
        slate.LockMouseToWidget(widget_ref);
        slate.ReleaseMouseCapture();
        viewport_client->SetMouseLockMode(EMouseLockMode::LockOnCapture);
        viewport_client->SetMouseCaptureMode(EMouseCaptureMode::CaptureDuringRightMouseDown);
    }

Now I need to spend a few hours removing lines of code to see which ones are redundant.

The mouse is still not restricted to the viewport though.

1 Like

In C++ you could restrict cursor position to geometry (screen / player screen / widget space),
by snapping the cursor to the edges or center of the geometry on Tick.

	APlayerController* PC = GetOwningPlayerController();
	ULocalPlayer* LocalPlayer = IsValid(PC) ? PC->GetLocalPlayer() : nullptr;
	if (!IsValid(LocalPlayer)) {
		UE_LOG(LogUIAdditionsPlugin, Error, TEXT("Invalid LocalPlayer."));
		return;
	}

	const FVector2D NewPosition = USlateUtils::GetCenterOfPlayerScreen(GameViewportClient, LocalPlayer);
	PC->SetMouseLocation(NewPosition.X, NewPosition.Y);
FVector2D USlateUtils::GetCenterOfPlayerScreen(ULocalPlayer* InLocalPlayer) {
	FVector2D Position = FVector2D::ZeroVector;
	if (!IsValid(InLocalPlayer)) {
		return Position;
	}
	const UWorld* World = InLocalPlayer->GetWorld();
	const UGameViewportClient* GameViewportClient = IsValid(World) ? World->GetGameViewport() : nullptr;

	return USlateUtils::GetCenterOfPlayerScreen(GameViewportClient, InLocalPlayer);
}

FVector2D USlateUtils::GetCenterOfPlayerScreen(const UGameViewportClient* InGameViewportClient, ULocalPlayer* InLocalPlayer) {
	FVector2D Position = FVector2D::ZeroVector;
	if (!IsValid(InGameViewportClient)) {
		UE_LOG(LogUIAdditionsPlugin, Error, TEXT("Invalid GameViewportClient."));
		return Position;
	}
	const TSharedPtr<IGameLayerManager> GameLayerManager = InGameViewportClient->GetGameLayerManager();
	if (!GameLayerManager.IsValid()) {
		UE_LOG(LogUIAdditionsPlugin, Error, TEXT("Invalid GameLayerManager."));
		return Position;
	}

	Position = GameLayerManager->GetPlayerWidgetHostGeometry(InLocalPlayer).GetAbsolutePositionAtCoordinates(FVector2D(0.5f, 0.5f));
	return Position;
}

FVector2D USlateUtils::ClampAbsolutePositionToGeometry(const FGeometry& InGeometry, const FVector2D& InPosition) {
	return InGeometry.LocalToAbsolute(ClampLocalPositionToGeometry(InGeometry, InGeometry.AbsoluteToLocal(InPosition)));
}

FVector2D USlateUtils::ClampLocalPositionToGeometry(const FGeometry& InGeometry, const FVector2D& InLocalPosition) {
	FVector2D NewPosition = InLocalPosition;
	NewPosition.X = FMath::Clamp(NewPosition.X, 1.f, InGeometry.GetLocalSize().X - 1.f);
	NewPosition.Y = FMath::Clamp(NewPosition.Y, 1.f, InGeometry.GetLocalSize().Y - 1.f);
	return NewPosition;
}

In the past (and possibly currently) some engine code related to the viewport, player input mode and cursor visibility setting have been misbehaving, leading to all sorts of broken behaviors. I have put my UI framework online for free which deals with it all, you can browse through it and see if there is anything else you need.

For example, I don’t use “SetShowMouseCursor” anywhere random for it’s bugged (and misleading, as it influences other behaviors).

Controlled by the HUD, I found this to work in my environment without bad behavior:

void UHUDCorePlayerControllerComponent::ActivateInputMode_Implementation(E_PlayerControllerInputModes InInputMode) {
	APlayerController* PC = Cast<APlayerController>(GetOwner());
	if (!IsValid(PC)) {
		UE_LOG(LogUIAdditionsPlugin, Error, TEXT("Can't activate the input mode, because this component's owner is not a valid APlayerController."));
		return;
	}

	switch (InInputMode) {
	case (E_PlayerControllerInputModes::Game): {
		//UWidgetBlueprintLibrary::SetInputMode_GameOnly(this);
		FInputModeGameOnly InputModeStruct;
		PC->SetInputMode(InputModeStruct);
		PC->SetShowMouseCursor(false);
		UE_LOG(LogUIAdditionsPlugin, Verbose, TEXT("Activated input mode: Game, SetShowMouseCursor: false"));
		break;
	}
	case (E_PlayerControllerInputModes::GameAndUI): {
		//UWidgetBlueprintLibrary::SetInputMode_GameAndUIEx(this, nullptr, EMouseLockMode::LockAlways, false);
		// Prevent engine behavior that prevents widgets from responding to single click events: https://answers.unrealengine.com/questions/420047/an-lmb-event-is-only-triggered-with-a-double-click.html
		//UGameplayStatics::SetViewportMouseCaptureMode(this, EMouseCaptureMode::NoCapture);

		/** Workaround:
		* bShowMouseCursor does not, like the name suggest, just alter 'cursor visibility'. It influences input behavior and misbehaves when set true combined with "Input Mode Game / Game + UI" (as configured in UWidgetBlueprintLibrary).
		* The SceneViewPort releases focus if you release a mouse button in mode "GameAndUI" when bShowMouseCursor == true.This results in loss of mouse input axis data or loss of controls if not countered.Why that is implemented that way is unclear. As a workaround calling ActivateInputModeGameAndUI after mouse button release should be enough to counter that problem.
		*/
		FInputModeGameAndUI InputModeStruct;
		InputModeStruct.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways);
		InputModeStruct.SetHideCursorDuringCapture(false);
		PC->SetInputMode(InputModeStruct);
		PC->SetShowMouseCursor(true);
		UE_LOG(LogUIAdditionsPlugin, Verbose, TEXT("Activated input mode: GameAndUI, SetShowMouseCursor: true"));
		break;
	}
	case (E_PlayerControllerInputModes::UI): {
		//UWidgetBlueprintLibrary::SetInputMode_UIOnlyEx(this, nullptr, EMouseLockMode::LockAlways);
		FInputModeUIOnly InputModeStruct;
		InputModeStruct.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways);
		PC->SetInputMode(InputModeStruct);
		PC->SetShowMouseCursor(true);
		UE_LOG(LogUIAdditionsPlugin, Verbose, TEXT("Activated input mode: UI, SetShowMouseCursor: true"));
		break;
	}
	}

	InputMode = InInputMode;
}

I am using a software cursor to hide and show properly. A software cursor comes with a slight movement delay, but when you want to set the cursor position on tick, it won’t jump around like a hardware cursor does.

I let my HUD decide (based on context (is a menu open, is X happening)) how to manage input mode and cursor behavior. This centralizes the challenge to one solver and prevents users from manually calling bugged engine methods:

https://github.com/Seda145/Ferrefy-Plugin-UI-Additions/tree/master

void AHUDCore::UpdateInputMode() {
	APlayerController* PC = GetOwningPlayerController();
	if (!IsValid(PC)) {
		UE_LOG(LogUIAdditionsPlugin, VeryVerbose, TEXT("Can not update the current input mode without a valid owning player controller."));
		return;
	}	

	UHUDCorePlayerControllerComponent* HUDCorePCComponent = PC->FindComponentByClass<UHUDCorePlayerControllerComponent>();
	if (!IsValid(HUDCorePCComponent)) {
		UE_LOG(LogUIAdditionsPlugin, Error, TEXT("To update the input mode, the PlayerController requires a valid component of type HUDCorePlayerControllerComponent."));
		return;
	}

	if (GetIsAnyPlayerViewportHUDMenuVisible() || GetIsAnyPlayerScreenHUDMenuVisible()) {
		HUDCorePCComponent->ActivateInputMode(E_PlayerControllerInputModes::UI);
		UE_LOG(LogUIAdditionsPlugin, VeryVerbose, TEXT("PlayerScreenHUD or PlayerViewportHUD has visible menu panels and desires the cursor to be unfrozen. Input mode set to 'UI'."));
		SetFreezeCursorToCenterOfScreen(false);
	}
	else if (GetIsAnyPawnHUDMenuVisible()) {
		HUDCorePCComponent->ActivateInputMode(E_PlayerControllerInputModes::GameAndUI);
		UE_LOG(LogUIAdditionsPlugin, VeryVerbose, TEXT("PawnHUD has visible menu panels and desires the cursor to be unfrozen. Input mode set to 'Game + UI'."));
		SetFreezeCursorToCenterOfScreen(false);
	}
	else {
		HUDCorePCComponent->ActivateInputMode(E_PlayerControllerInputModes::Game);
		UE_LOG(LogUIAdditionsPlugin, VeryVerbose, TEXT("No Sub HUD has a visible menu, the cursor does not have to be unfrozen. Input mode set to 'Game'."));

		if (GetPawnDesiresCenteredWorldCursor()) {
			UE_LOG(LogUIAdditionsPlugin, VeryVerbose, TEXT("The pawn desires the cursor to be frozen to center of screen (World Cursor Modifier Component). This is allowed in input mode 'Game'."));
			SetFreezeCursorToCenterOfScreen(true);
		}
		else {
			UE_LOG(LogUIAdditionsPlugin, VeryVerbose, TEXT("Unfreezing cursor from center of screen."));
			SetFreezeCursorToCenterOfScreen(false);
		}
	}
}

Similar to my own code:

“AHUDCore::ActOnPawnDesiresCenteredWorldCursorChanged”

https://github.com/Seda145/Ferrefy-Plugin-UI-Additions/blob/master/UIAdditionsPlugin/Source/UIAdditionsPlugin/Private/UI/HUD/HUDCore.cpp

“UCursorWidget::TickUpdateCursorPosition”

https://github.com/Seda145/Ferrefy-Plugin-UI-Additions/blob/master/UIAdditionsPlugin/Source/UIAdditionsPlugin/Private/UI/UserWidgets/Cursor/CursorWidget.cpp

I documented any engine misbehaviors and how to deal with them when I encountered them, in my code.