Help with controlling wheel turning with circular mouse movements

Trying to get things working so that the player can use circular mouse movements to control the turning of a wheel in a game. As an example, how turning the wheel works in Amnesia: The Dark Descent.

It’s semi-working in as far as I think I have been able to determine the magnitude of the player moving the mouse. I take the input each frame by using Mouse.current.delta.value and grab the X and Y values. Then I can compare the current values to the values from the last frame and do this to get the magnitude:

private double GetDistance(double x1, double y1, double x2, double y2)
{
    return Math.Sqrt(Math.Pow((x2 - x1), 2) + Math.Pow((y2 - y1), 2));
}

Where x2 and y2 are the current frame and x1 and y1 are the previous frame. I might be running into a problem with trying to determine if the mouse is moving clockwise or counter-clockwise by checking the current position plus the last TWO positions:

private RotateDirection GetDirection(double latestX, double middleX, double oldestX, double latestY, double middleY, double oldestY)
{
    var val = (middleY - oldestY) * (latestX - middleX) - (middleX - oldestX) * (latestY - middleY);

    Debug.Log($"val: {val}");

    if(Math.Abs(val) < 15)
        return RotateDirection.None;

    if (val > 0)
        return RotateDirection.Forward;

    return RotateDirection.Backward;
}

I have the absolute value check in case the mouse movements are very small, although this might be completely unnecessary. When I test it, the wheel is turning but it switches between Forward and Backward rotation rapidly, so it’s definitely not consistent. To actually rotate the wheel I’m just using a switch to test for forward and backward:

case RotateDirection.Forward:
    transform.Rotate(Vector3.forward, (float)result.Magnitude * Time.deltaTime, Space.Self);
    break;

case RotateDirection.Backward:
    transform.Rotate(-Vector3.forward, (float)result.Magnitude * Time.deltaTime, Space.Self);
    break;

I’m wondering if my method of getting the mouse position is completely wrong. I am using a Locked cursor so the mouse position doesn’t change, but I am checking the delta as shown above. This might be incorrect and causing erroneous values to be read as the current X and Y values of the mouse input?

Thanks.

Unlock the mouse cursor when turning the wheel but leave it hidden and use the cursor’s angle delta to rotate the wheel. To get the angle delta you subtract previous frame’s angle from the current angle. To get the angle you can use WorldToScreenPoint to get the screen position of the wheel’s center which you can then use to get the angle of the cursor relative to that center point.

Or you could leave the mouse locked and add the mouse delta to your own internal cursor position.

Thank you! Gonna try that tomorrow!

I’m still goofing something up I guess. Here’s what I’m doing:

UnityEngine.Cursor.lockState = CursorLockMode.None;
UnityEngine.Cursor.visible = false;
var tempPoint = Camera.main.WorldToScreenPoint(transform.position); // used to determine mouse location from wheel
worldToScreenPoint = new Vector3(tempPoint.x, tempPoint.y, 0);

private RotateInfo DetermineRotation()
{
    var currentX = Input.GetAxis("Mouse X");
    var currentY = Input.GetAxis("Mouse Y");
    //Debug.Log($"X: {currentX} Y: {currentY}");

    var angle = Vector3.Angle(worldToScreenPoint, new Vector3(currentX, currentY, 0));
    var angleDelta = angle - lastAngle;
    Debug.Log(angleDelta);

    var direction = angleDelta > 0 ? RotateDirection.Clockwise : RotateDirection.CounterClockwise;

    var rotateInfo = new RotateInfo(Math.Abs(angleDelta), direction);

    lastAngle = angle;

    return rotateInfo;
}

So I get the worldToScreenPoint of the wheel when the player begins interacting with it (the camera doesn’t move while interacting) then each frame I run DetermineRotation(). I use the RotateInfo here:

case RotateDirection.Clockwise:
    transform.Rotate(Vector3.forward, (float)result.Magnitude * Time.deltaTime, Space.Self);
    break;

case RotateDirection.CounterClockwise:
    transform.Rotate(-Vector3.forward, (float)result.Magnitude * Time.deltaTime, Space.Self);
    break;

Currently the wheel goes back and forth as the angleDelta switches back and forth between positive and negative even if I’m going in a constant clockwise motion. I’m probably misinterpreting something that you said?

This is closer:

private RotateInfo DetermineRotation()
{
    var currentX = Mouse.current.position.ReadValue().x;// + worldToScreenPoint.x;
    var currentY = Mouse.current.position.ReadValue().y;// + worldToScreenPoint.y;
    Debug.Log($"X: {currentX} Y: {currentY}");
    //Debug.Log($"World X: {worldToScreenPoint.x} World Y: {worldToScreenPoint.y}");
    var targetDir = new Vector2(currentX, currentY) - new Vector2(worldToScreenPoint.x, worldToScreenPoint.y);
    //Debug.Log($"Target dir: {targetDir}");
    var angle = Vector3.Angle(targetDir, transform.forward);
    Debug.Log($"Angle: {angle}");
    var angleDelta = angle - lastAngle;
    Debug.Log($"Angle delta: {angleDelta}");

    var direction = angleDelta > 0 ? RotateDirection.Clockwise : RotateDirection.CounterClockwise;

    var rotateInfo = new RotateInfo(Math.Abs(angleDelta * 20), direction);

    lastAngle = angle;

    return rotateInfo;
}

But still not quite there.

You should use Vector3.SignedAngle to get the angle. Here’s how I would do it using the old input system:

float lastAngle;

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            Vector3 center = Camera.main.WorldToScreenPoint(transform.position);
            center.z = 0;
            float angle = Vector3.SignedAngle(Input.mousePosition - center, Vector3.up, -Vector3.forward);
            if (Input.GetMouseButtonDown(0))
                lastAngle = angle;
            float deltaAngle = angle - lastAngle;
            transform.Rotate(0, 0, deltaAngle);
            lastAngle = angle;
        }
    }

I ended up needing to keep the cursor locked because when testing my cursor would end up on my other monitor, and I definitely don’t want the player experiencing that.

I ended up getting something working that I am quite happy with. Zulo3d’s suggestion about keeping an internal mouse position helped me so I appreciate that. I am starting the mouse position in the middle of the wheel, then each frame I’m grabbing the delta and then checking the location of the mouse position with the new delta and seeing if it falls inside a certain radius around the wheel. I’m doing this because I don’t want the internal mouse cursor to go too far from the middle of the wheel because it can end up in the corners of the screen which would make wheel rotation untenable. I ALSO don’t want the cursor to go through the midpoint 0,0 coordinate of the wheel because that would cause a sudden jerky movement when the cursor passes over it. So when the player moves the internal mouse position out of a certain distance from the center, or when it’s too close to the center, I set the position back into a comfortable radius inside the imaginary circle that we’re staying inside of.

So I ended up with this:

var tempPoint = Camera.main.WorldToScreenPoint(transform.position); // used to determine mouse location from wheel
mousePosition = worldToScreenPoint = new Vector2(tempPoint.x, tempPoint.y);

private void Update()
{
      var rotation = DetermineRotation();
      var currentRotation = transform.localRotation;

      transform.localRotation = Quaternion.Euler(0, 0.0f, Mathf.Clamp(currentRotation.eulerAngles.z - rotation, 0, 999f));
}

private float DetermineRotation()
{
    var delta = Mouse.current.delta.value;

    if (delta == Vector2.zero)
        return 0;

    var tempPos = delta + mousePosition;

    if (Vector2.Distance(tempPos, worldToScreenPoint) < (radius * 6) && Vector2.Distance(tempPos, worldToScreenPoint) > radius)
    {
        // player input is inside acceptable range, set mouse position
        mousePosition = tempPos;
    }
    else
    {
        // adjust mouse pos to center of radius when wandering outside of circle range
        var tempAngle = Mathf.Atan2(tempPos.y - worldToScreenPoint.y, tempPos.x - worldToScreenPoint.x);
        mousePosition.x = worldToScreenPoint.x + Mathf.Cos(tempAngle) * radius;
        mousePosition.y = worldToScreenPoint.y + Mathf.Sin(tempAngle) * radius;
    }

    var targetDir = mousePosition - new Vector2(worldToScreenPoint.x, worldToScreenPoint.y);
    var angle = Mathf.Atan2(targetDir.y, targetDir.x) * Mathf.Rad2Deg;
    var angleDelta = Mathf.Clamp(angle - lastAngle, -maxRotateSpeed, maxRotateSpeed); // control rotation speed
    lastAngle = angle;

    return angleDelta;
}

The Clamp in the DetermineRotation method is just because I want to limit how fast the wheel can spin. The Clamp in the Update function is just for my game where I want to have the wheel turn clockwise and counter-clockwise except once the clockwise rotation passes a certain threshold, I don’t want it to be able to go counter-clockwise back. So not necessarily needed for anyone that’s looking at this, but just something that I want to have.

Oh, and the radius that I’m using above is very vibes-based. I just needed to experiment with that a bit to get the right feel for how big the imaginary circle should be.

Hope this helps someone!