Download

Dragging a lever based on orientation & viewing angle

In my project, I have a lever mesh sticking out of the floor. Holding the left mouse button on this lever and moving the mouse initiates a dragging operation. This dragging operation is responsible for calculating the 2D delta mouse movements, and utilizing this data to rotate the lever, which can only rotate in a single axis (negative or positive, but still only one axis) in local space.

But what if, instead of being in front of the lever, I’m actually behind it? What if I’m on one of its sides? What if the lever is actually on a wall instead of the floor?..
How do I make it so that mouse movements actually rotate the lever appropriate to the angle at which it is viewed from?

Perhaps I’m not explaining myself well enough…
So here’s a list of scenarios, and how I’d like the mouse to control them:

When the lever’s on the FLOOR and you are in FRONT of it:

  • If you move the mouse UP (-Y), it should rotate away from the camera
  • If you move the mouse DOWN (+Y), it should rotate toward the camera

When the lever’s on the FLOOR and you are BEHIND it:

  • If you move the mouse UP (-Y), it should rotate away from the camera
    (which is the opposite world-space direction of when you are in front of it)
  • If you move the mouse DOWN (+Y), it should rotate toward the camera
    (which is the opposite world-space direction of when you are in front of it)

When the lever’s on the FLOOR and you are BESIDE it:

  • If you move the mouse LEFT (-X), it should rotate to the LEFT of the camera
    (which is the opposite direction of when you are on the other side of it)
  • If you move the mouse RIGHT (+X), it should rotate to the RIGHT of the camera
    (which is the opposite direction of when you are on the other side of it)

When the lever’s on a WALL and you are in FRONT of it:

  • If you move the mouse UP, it should rotate UP (toward the sky)
  • If you move the mouse DOWN, it should rotate DOWN (toward the floor)

When the lever’s on the WALL and you are BESIDE it:

  • Same as when it’s on the wall and you are in front of it

From what I understand, simple vector math should be able to solve this, but I just can’t figure out the details.
If it helps at all, I know the world-space (XYZ) position of:

  • Lever Pivot Point (Base)
  • Lever Handle (Top)

… And of course I know the 2D mouse coordinates, and have the ability to convert any screen-space coordinates to world-space (and vice-versa).

Can anyone help me fill in the blanks, here?

Either way you can get DotProducts of lever activation vector (vector connecting handle positions of on\off state) and forward\right player vectors.
Or you can just add some physics constrains to handle and add Physics Handle Component/ to your pawn.

Sadly, adding a physics component isn’t an option, as the handle itself is a custom component and is to be used for various types of devices (not just rotatable switches/levers, but also rotatable dials & translatable levers).

And something else to note is that some of these devices can even have multiple states, or bi-directional movement.
Take the following lever for example:
52e38e7d22f746f4b9739eed2d8a8d41.png
This lever, by default, is centered within it’s base. It can be pulled all the way down, or all the way up before “activating” something.
For the “translatable” version of this lever, picture it’s base being flat and the handle moving along it’s track (as opposed to rotating from it’s pivot).

So what I really need is to determine which direction the device needs to be rotated (or translated, in the case of translatable levers), and allow the delta mouse movements to tie into how much the device moves within that direction. Something like this really calls for a mathematical approach, but I’m having trouble figuring out the exact algorithm that I should be using to figure out which direction these devices should move.

For the actual rotations, I’m using USceneComponent::AddRelativeRotation and then clamping the angles to user-defined values within the blueprint’s class defaults.
As for the dot product, I’m not sure what to do with it. Here’s basically what I have:


auto MouseDirection = MouseEnd - MouseStart; // Direction (2D) the mouse has moved since the last frame
MouseDirection.Normalize();

auto GrabSocketLocation = HandleMesh->GetSocketLocation(TEXT("Handle"));	// Location (3D, World-Space) of handle
auto PivotSocketLocation = HandleMesh->GetSocketLocation(TEXT("Pivot"));	// Location (3D, World-Space) of pivot/base
auto PC = CastChecked<AMyPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
auto MouseWorldLocation = FVector::ZeroVector;
auto MouseWorldDirection = FVector::ZeroVector;
PC->DeprojectMousePositionToWorld(MouseWorldLocation, MouseWorldDirection);

auto V1 = PivotSocketLocation - GrabSocketLocation;
auto V2 = PivotSocketLocation - MouseWorldLocation;
V1.Normalize();
V2.Normalize();
auto DotProduct = FVector::DotProduct(V1, V2);	// Not sure what to do with this value
auto ArcCos = FMath::Acos(DotProduct);			// Not sure what to do with this value

//...

HandleMesh->AddRelativeLocation(/* Magic Numbers Based On Direction and Scaled By MouseDelta */);

Ok, lets assume you have something like this:

Lever.jpg

You have:
DeltaMouseX, DeltaMouseY.

Then you calculate:



dotPForward = DotProduct(LeverActivationVector.SafeNormal(), ForwardVec);
dotPRight = DotProduct(LeverActivationVector.SafeNormal(), RightVec);
dotPUp = DotProduct(LeverActivationVector.SafeNormal(), UpVec); // UpVec not presented on picture


Then you have this massive if statement:



bool isActivating;
float ActivationThreshold = 0.1; // how much you should move mouse to activate\deactivate
// in front
if (dotPForward > 0.5 && abs(dotPRight) < 0.5 &&  abs(dotPUp ) < 0.5)
{
    isActivating = (DeltaMouseY < 0 && abs(DeltaMouseY) > ActivationThreshold );
}
// behind
if (dotPForward < -0.5 && abs(dotPRight) < 0.5 &&  abs(dotPUp ) < 0.5)
{
    isActivating = (DeltaMouseY > 0 && abs(DeltaMouseY) > ActivationThreshold );
}
// from right side
if (dotPRight > 0.5 && abs(dotPForward) < 0.5 &&  abs(dotPUp ) < 0.5)
{
    isActivating = (DeltaMouseX < 0 && abs(DeltaMouseX) > ActivationThreshold );
}
// from left side
if (dotPRight < -0.5 && abs(dotPForward) < 0.5 &&  abs(dotPUp ) < 0.5)
{
    isActivating = (DeltaMouseX > 0 && abs(DeltaMouseX) > ActivationThreshold );
}
// on wall
if (abs(dotPUp) > 0.5)
{
    isActivating = (DeltaMouseY < 0 && abs(DeltaMouseY) > ActivationThreshold );
}

if (isActivating)
{
    // move handle to On State
}
else
{
    // move handle to Off State
}


I could mess up with DeltaMouseX\Y checks, but overall idea is that.

Thank you for your help! No seriously, I appreciate it immensely.
What exactly is LeverActivationVector? Is it a normalized direction which defines which way the lever is allowed to move? Or does the location/length matter?
What about bi-directional levers, where rotation/translation in both (+/-) directions along one axis are valid in order to “activate” something?

Perhaps my main problem is calculating what LeverActivationVector should be?

LeverActivationVector is normalized direction in which it is in On State (pulled towards +X local space direction for example)

BTW you can define it as a difference between lever handle Off and On location, because I suggested using “LeverActivationVector.SafeNormal()”.

Looks like I’m getting pretty close:
[video]https://youtu.be/ops__oWtFf0[/video]

Here’s the code I used in the video
(USERotatingDevice is a subclass of USEDeviceActivator, which handles all the mouse input and event delegates for the component):


/* Called whenever a mouse drag operation occurs */
void USERotatingDevice::OnDeviceDragged_Callback(UActorComponent *Device, FVector2D DeltaMouse, float DeltaTime)
{
    // Only allow changes to occur on the device being interacted with
    if (Device != this)
        return;

    // Only allow changes to occur if this component is attached to a base component
    TArray<USceneComponent*> Parents;
    GetParentComponents(Parents);
    if (Parents.Num() < 1)
        return;

    // Find the base component
    USceneComponent *Base = nullptr;
    for (auto Parent : Parents) {
        if (Parent->GetName().Contains(TEXT("Base"))) {
            Base = Parent;
            break;
        }
    }

    // Exit if no valid base component could be found
    if (!IsValid(Base))
        return;

    // Exit if no activation nodes have been defined
    auto NodeCount = ActivationNodes.Num();
    if (NodeCount < 1)
        return;

    // Get the handle and pivot socket locations
    auto GrabSocketLocation = FVector::ZeroVector;
    auto PivotSocketLocation = FVector::ZeroVector;
    if (ActivationMesh->HasAnySockets()) {
        if (MeshSocketName != NAME_None) {
            GrabSocketLocation = ActivationMesh->GetSocketLocation(MeshSocketName);
        }
        PivotSocketLocation = ActivationMesh->GetSocketLocation(TEXT("PivotPoint"));
    }

    // Get the mouse location & directional vector (in world coordinates)
    auto PC = CastChecked<AMyPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
    auto MouseWorldLocation = FVector::ZeroVector;
    auto MouseWorldDirection = FVector::ZeroVector;
    PC->DeprojectMousePositionToWorld(MouseWorldLocation, MouseWorldDirection);

    // Get the device's normalized directional vectors
    auto BaseUpVector = Base->GetUpVector();
    auto BaseRightVector = Base->GetRightVector();
    auto BaseForwardVector = Base->GetForwardVector();

    // Get the camera's normlized directional vectors
    auto Character = CastChecked<ASEDeviceTestCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
    auto CharacterCamera = Character->GetFirstPersonCameraComponent();
    auto CameraUpVector = CharacterCamera->GetUpVector();
    auto CameraRightVector = CharacterCamera->GetRightVector();
    auto CameraForwardVector = CharacterCamera->GetForwardVector();

    // Get the direction vectors between Pivot & Handle, and Pivot & Mouse
    auto HandleUpVector = -(PivotSocketLocation - GrabSocketLocation); // Not sure why, but without the negation this is actually a "down" vector
    auto PivotToMouseVector = PivotSocketLocation - MouseWorldLocation;
    HandleUpVector.Normalize();
    PivotToMouseVector.Normalize();

    auto LeverActivationVector = BaseForwardVector;
    auto dotPUp = FVector::DotProduct(LeverActivationVector.SafeNormal(), CameraUpVector);
    auto dotPRight = FVector::DotProduct(LeverActivationVector.SafeNormal(), CameraRightVector);
    auto dotPForward = FVector::DotProduct(LeverActivationVector.SafeNormal(), CameraForwardVector);

    auto StrDotPUp = FString(TEXT("[UP]: ")).Append(FString::SanitizeFloat(dotPUp));
    auto StrDotPRight = FString(TEXT("[RIGHT]: ")).Append(FString::SanitizeFloat(dotPRight));
    auto StrDotPForward = FString(TEXT("[FORWARD]: ")).Append(FString::SanitizeFloat(dotPForward));

    FlushPersistentDebugLines(GetWorld());
    DrawDebugDirectionalArrow(GetWorld(), PivotSocketLocation, PivotSocketLocation + (BaseForwardVector * 40.f), 6.f, FColor::Red, false, 3.f, 0, 1.5f);
    DrawDebugDirectionalArrow(GetWorld(), PivotSocketLocation, PivotSocketLocation + (BaseRightVector * 40.f), 6.f, FColor::Green, false, 3.f, 0, 1.5f);
    DrawDebugDirectionalArrow(GetWorld(), PivotSocketLocation, PivotSocketLocation + (BaseUpVector * 40.f), 6.f, FColor::Blue, false, 3.f, 0, 1.5f);
    DrawDebugDirectionalArrow(GetWorld(), PivotSocketLocation, GrabSocketLocation + (HandleUpVector * 40.f), 6.f, FColor::Yellow, false, 3.f, 1, 2.f);

    auto color = FColor::White;
    auto message = FString(TEXT("N/A"));

    // in front
    if (dotPForward > 0.5f && FMath::Abs(dotPRight) < 0.5f &&  FMath::Abs(dotPUp) < 0.5f)
    {
        color = FColor::Blue;
        message = TEXT("Floor: FRONT");
    }
    // behind
    if (dotPForward < 0.5f && FMath::Abs(dotPRight) < 0.5f &&  FMath::Abs(dotPUp) < 0.5f)
    {
        color = FColor::Cyan;
        message = TEXT("Floor: BEHIND");
    }
    // from right side
    if (dotPRight > 0.5f && FMath::Abs(dotPForward) < 0.5f &&  FMath::Abs(dotPUp) < 0.5f)
    {
        color = FColor::Emerald;
        message = TEXT("Floor: RIGHT");
    }
    // from left side
    if (dotPRight < 0.5f && FMath::Abs(dotPForward) < 0.5f &&  FMath::Abs(dotPUp) < 0.5f)
    {
        color = FColor::Green;
        message = TEXT("Floor: LEFT");
    }
    // on wall
    if (FMath::Abs(dotPUp) > 0.5f)
    {
        color = FColor::Magenta;
        message = TEXT("Wall");
    }

    FlushDebugStrings(GetWorld());
    DrawDebugString(GetWorld(), GrabSocketLocation + (HandleUpVector * 40.f), message, nullptr, color);
    DrawDebugString(GetWorld(), GrabSocketLocation + (HandleUpVector * 32.f), StrDotPUp, nullptr, dotPUp < 0.f ? FColor::Red : FColor::Green);
    DrawDebugString(GetWorld(), GrabSocketLocation + (HandleUpVector * 24.f), StrDotPRight, nullptr, dotPRight < 0.f ? FColor::Red : FColor::Green);
    DrawDebugString(GetWorld(), GrabSocketLocation + (HandleUpVector * 16.f), StrDotPForward, nullptr, dotPForward < 0.f ? FColor::Red : FColor::Green);
}

Any suggestions?



// behind
    if (**dotPForward < 0.5f** && FMath::Abs(dotPRight) < 0.5f &&  FMath::Abs(dotPUp) < 0.5f)
...
 // from left side
    if (**dotPRight < 0.5f** && FMath::Abs(dotPForward) < 0.5f &&  FMath::Abs(dotPUp) < 0.5f)


Here change to -0.5f. I messed up in original post and edited that message afterwards. Sorry.



auto dotPUp = FVector::DotProduct(LeverActivationVector.SafeNormal(), CameraUpVector);
    auto dotPRight = FVector::DotProduct(LeverActivationVector.SafeNormal(), CameraRightVector);
    auto dotPForward = FVector::DotProduct(LeverActivationVector.SafeNormal(), CameraForwardVector);


Try using Character directional vectors. It should fix wall condition, when you tilt camera down.

Thanks for very clearly video. Hope my suggestions works.

Looks like I’m still having trouble getting this properly implemented.
I did however manage to figure out how to get the exact rotation around the object you are at on the XY plane using the following code:


auto PivotSocketLocation = ActivationMesh->GetSocketLocation(TEXT("PivotPoint"));
auto CharacterToPivotVector = Character->GetActorLocation() - PivotSocketLocation;
auto ArcTangent = FMath::Atan2(
    Character->GetActorLocation().Y - Base->GetComponentLocation().Y,
    Character->GetActorLocation().X - Base->GetComponentLocation().X
);
​
auto DegreesAroundPivot = ((ArcTangent + M_PI) / M_PI2) * 360.f;

That spits out a value from 360 to 0, going CCW around the object when it is placed directly in front of you. Paired with some conditional statements that check if you are within -22.5 to +22.5 degrees, you know if you are S, SE, E, NE, N, NW, W or SW of the object, and can update the rotation based upon this information.

Here’s a video showing this in action:
[video]https://youtu.be/LhU45xwh6qM[/video]

And here’s the full code I’m using in the video:


void USERotatingDevice::OnDeviceDragged_Callback(UActorComponent *Device, FVector2D DeltaMouse, float DeltaTime)
{
    // Only allow changes to occur on the device being interacted with
    if (Device != this)
        return;

    // Only allow changes to occur if this component is attached to a base component
    TArray<USceneComponent*> Parents;
    GetParentComponents(Parents);
    if (Parents.Num() < 1)
        return;

    // Find the base component
    USceneComponent *Base = nullptr;
    for (auto Parent : Parents) {
        if (Parent->GetName().Contains(TEXT("Base"))) {
            Base = Parent;
            break;
        }
    }

    // Exit if no valid base component could be found
    if (!IsValid(Base))
        return;

    // Exit if no activation nodes have been defined
    auto NodeCount = ActivationNodes.Num();
    if (NodeCount < 1)
        return;

    // Get the handle and pivot socket locations
    auto GrabSocketLocation = FVector::ZeroVector;
    auto PivotSocketLocation = FVector::ZeroVector;
    if (ActivationMesh->HasAnySockets()) {
        if (MeshSocketName != NAME_None) {
            GrabSocketLocation = ActivationMesh->GetSocketLocation(MeshSocketName);
        }
        PivotSocketLocation = ActivationMesh->GetSocketLocation(TEXT("PivotPoint"));
    }

    // Get the mouse location & directional vector (in world coordinates)
    auto PC = CastChecked<AMyPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
    auto MouseWorldLocation = FVector::ZeroVector;
    auto MouseWorldDirection = FVector::ZeroVector;
    PC->DeprojectMousePositionToWorld(MouseWorldLocation, MouseWorldDirection);

    // Get the device's normalized directional vectors
    auto BaseUpVector = Base->GetUpVector();
    auto BaseRightVector = Base->GetRightVector();
    auto BaseForwardVector = Base->GetForwardVector();

    // Get the camera's normlized directional vectors
    auto Character = CastChecked<ASEDeviceTestCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
    auto CharacterUpVector = Character->GetActorUpVector();
    auto CharacterRightVector = Character->GetActorRightVector();
    auto CharacterForwardVector = Character->GetActorForwardVector();

    // Get the direction vectors between Pivot & Handle, and Pivot & Mouse
    auto HandleUpVector = -(PivotSocketLocation - GrabSocketLocation); // Not sure why, but without the negation this is actually a "down" vector
    auto PivotToMouseVector = PivotSocketLocation - MouseWorldLocation;
    HandleUpVector.Normalize();
    PivotToMouseVector.Normalize();

    auto CharacterToPivotVector = Character->GetActorLocation() - PivotSocketLocation;
    auto ArcTangent = FMath::Atan2(
        Character->GetActorLocation().Y - Base->GetComponentLocation().Y,
        Character->GetActorLocation().X - Base->GetComponentLocation().X
    );

    auto Angle = ArcTangent;
    static auto LastAtan = 0.f;
    if (LastAtan < -3.0f && ArcTangent > 3.0f) {
        Angle += M_PI2;
    } else if (LastAtan > 3.0f && ArcTangent < -3.0f) {
        Angle -= M_PI2;
    }
    LastAtan = ArcTangent;
    auto Degrees = ((Angle + M_PI) / M_PI2) * 360.f;
    auto Radians = FMath::DegreesToRadians(Degrees);

    auto StrQuadrant = FString(TEXT("[Quadrant]: "));
    auto StrDegrees = FString(TEXT("[Degrees]: ")).Append(FString::SanitizeFloat(Degrees));
    float Move = 0.f;

    if (Degrees > 22.5f && Degrees < 67.5f) {
        StrQuadrant.Append(TEXT("Southwest"));
    } else if(Degrees > 67.5f && Degrees < 112.5f) {
        StrQuadrant.Append(TEXT("West"));
    } else if (Degrees > 112.5f && Degrees < 157.5f) {
        StrQuadrant.Append(TEXT("Northwest"));
    } else if (Degrees > 157.5f && Degrees < 202.5f) {
        StrQuadrant.Append(TEXT("North"));
    } else if (Degrees > 202.5f && Degrees < 247.5f) {
        StrQuadrant.Append(TEXT("Northeast"));
    } else if (Degrees > 247.5f && Degrees < 292.5f) {
        StrQuadrant.Append(TEXT("East"));
    } else if (Degrees > 292.5f && Degrees < 337.5f) {
        StrQuadrant.Append(TEXT("Southeast"));
    } else {
        StrQuadrant.Append(TEXT("South"));
        Move = DeltaMouse.Y;
    }

    ActivationMesh->AddRelativeRotation(FQuat(FRotator(0.f, 0.f, -Move)));
    FlushDebugStrings(GetWorld());
    DrawDebugString(GetWorld(), GrabSocketLocation + (HandleUpVector * 18.f), StrQuadrant);
    DrawDebugString(GetWorld(), GrabSocketLocation + (HandleUpVector * 10.f), StrDegrees);
}

The only real issue here is that it doesn’t take character rotation into the equation… So if you are, for example, West of the object (and facing it), dragging the mouse left & right only makes sense until you rotate the camera to the left, placing the device at your side (at which point you’d actually want mouse up & down to affect the lever, NOT left & right).