5
\$\begingroup\$

I'm trying to program 2D mouse aim, where the arms and the gun in the arms rotates to point at the mouse. But for some reason, the arms and gun rotate and do not follow the mouse position.

The each of the arms and the guns are separate node that are parented to an orbit node that is located on at the shoulder of the arms.

The following code is script that is attached and running on the orbit.

    var mousePos = get_viewport().get_mouse_pos();
    var dir = get_pos();
    var angle = (atan2(dir.x,dir.y))+(2*PI);
    var arm_gun_dir = gravGun_Ref.get_pos()-get_pos();
    var arm_gun_angle = (atan2(arm_gun_dir.x,arm_gun_dir.y))+(2*PI);
    set_rot(angle-(PI/2));

Here is a GIF of the result of this setup: enter image description here

Here is an image showing where the positions of the upper arm and the gun.

animated clip of result

\$\endgroup\$
1
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$
    – user1430
    Commented Jan 3, 2017 at 17:18

1 Answer 1

2
\$\begingroup\$

There's a couple of assumptions to state based on the question + gif posted.

  • We have a static gun+arm that pivots around the player's shoulder
  • You want the arm and gun to rotate around the shoulder, such that the gun ends up pointing towards the cursor.
  • The gun does not point straight out from the pivot, meaning we can't calculate its trajectory simply using the location of the pivot and the gun. We need some way to know the direction the gun is pointing.

The first solution that comes to mind is to add a constant angle offset to the gun after making it rotate towards the target. However, due to the fact the gun is not centered at the pivot, this won't produce consistent results.

Imagine moving the cursor from the pivot point, then to the right in a straight line. this solution would cause the gun to point in a single particular direction, but you would expect the gun to move slightly to follow the cursor.

Let's approach the problem from a slightly different angle. See the following diagram: A diagram with a 2D character holding a gun.

We have the pivot, GunPoint, and your desired target. We need another point indicating the direction of the gun, so I put a point near the elbow behind the gun, labeled ElbowPoint. It doesn't necessarily need to be in the elbow, but it needs to be aligned in such a way the two points together provide the correct trajectory.

We draw a circle around the pivot intersecting with your desired target. We can see the gun's not pointing at the target, but some imaginary target. We can also see that the two targets form an angle theta from the pivot. If we rotate the arm by this angle, the gun will be correctly pointing at the target.

Here's what we'll need to do:

  • Provide two points that indicate the trajectory of the gun.
  • Calculate the points at which the trajectory line collides with the circle.
  • From the two collision points, pick the one closer to the gun (ignore the one behind the gun)
  • Find the angle theta between the current imaginary target, and the desired target.

First step is to calculate the Intersection of the gun's trajectory line and our target circle.

Luckily someone already implemented this in JS so I ported the solution to GDScript: https://stackoverflow.com/a/57892466/6549206

## Given position of a circle circle_pos, its radius r
## and two points on an infinite line p1 and p2, 
## returns the points of intersection, if any
func circle_line_intersection(circle_pos: Vector2, r: float, p1: Vector2, p2:Vector2):
    # Get two points' locations in respect to the circle.
    var x1: Vector2 = p1 - circle_pos
    var x2: Vector2 = p2 - circle_pos
    
    var dv: Vector2 = x2 - x1 # Vector of direction of line
    var dr: float = dv.length() # distance between points
    # var D: float = x1.x * x2.y - x2.x * x1.y # Equivalent to below:
    var D: float = x1.cross(x2) # Cross product of two points
    
    # Determinant goes in square root, determines how many real values there are
    var determinant: float = r * r * dr * dr - D * D
    if determinant < 0:
        # Negative in square root, no real solution
        # No collision with circle
        return []
    var root_det = sqrt(determinant)
    var collision_points = []
    var collision_1 = Vector2(
        D * dv.y + sign(dv.y) * dv.x * root_det,
        -D * dv.x + abs(dv.y) * root_det
    ) / (dr*dr) + (circle_pos)
    collision_points.append(collision_1)
    
    if determinant > 0.0:
        # There is a second collision point
        var collision_point_2 = Vector2(
            D * dv.y - sign(dv.y) * dv.x * root_det,
            -D * dv.x - abs(dv.y) * root_det
        ) / (dr*dr) + (circle_pos)
        collision_points.append(collision_point_2)
    
    return collision_points

If you're interested you can find some explanation on the math behind the intersection formula here: https://mathworld.wolfram.com/Circle-LineIntersection.html

This function takes the position and radius of the circle, and two points on the line. All positions global. It will return 0, 1, or 2 intersecting points. If the cursor is closer to the player than the gun, the circle won't collide with the line.

For the main case it's two. We'll pick the point closest to the Gun Point, since that will be in front of the gun. If there's one we'll use that one.

I happen to use this function to calculate the closest point between the two, since it's nicely reusable.

## Given an Array of Vector2s and a target Vector2
## Return the Vector2 in the list 
func find_closest_point(points: Array, target: Vector2) -> Vector2:
    var closest_point: Vector2
    var min_distance: float = INF
    for point in points:
        var distance = target.distance_to(point)
        if distance < min_distance:
            min_distance = distance
            closest_point = point
    return closest_point

Now we just need to compare the angles of the two targets, and we have this main method to calculate theta:

## Given the global positions of pivot, target, and two points on a line,
## Calculate theta to rotate the line so point_2 points towards target
func calculate_theta(
    pivot: Vector2, target: Vector2, point_1: Vector2, point_2: Vector2
):
    var radius = pivot.distance_to(target)
    var collisions = circle_line_intersection(
        pivot,
        radius,
        point_1,
        point_2
    )
    var imaginary_target: Vector2
    if len(collisions) == 0:
        # target is behind point_1
        # Add some special handling if you like
        return 0
    elif len(collisions) == 1: 
        imaginary_target = collisions[0]
    elif len(collisions) == 2:
        imaginary_target = find_closest_point(collisions, point_2)
    else:
        print("Shouldn't be mathematically possible")

    # Get the angle theta to have point_2 to rotate towards target 
    var local_imaginary = imaginary_target - pivot
    var local_target = target - pivot
    var theta = local_imaginary.angle_to(local_target)
    
    return theta

Then we can rotate the pivot/arm by theta, and it should work.

If you want the character to flip horizontally when the mouse passes behind them, you can flip the x scale and it should still work. Depending on node hierarchy may need to flip theta.

This is the script I used on the Pivot node, which is a parent to all the other nodes. (GunPoint, AimPoint, ElbowPoint). It rotates the gun towards the cursor when left click is held, and flips the arm horizontally when the cursor moves behind it.

# Note self is the Pivot Node!
func _process(delta):
    var theta = calculate_theta(
        self.global_position, # pivot
        get_global_mouse_position(), # target
        $ElbowPoint.global_position, # Elbow, trajectory guide
        $GunPoint.global_position
    )
    # Rotate when holding left click
    if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
        # Flip arm if targeting other way
        if get_global_mouse_position().x < self.global_position.x:
            scale.x = -1
        else:
            scale.x = 1
        rotate(theta)
    

Here's a GIF of my final result, with quite a few shapes drawn to illustrate what's happening:

enter image description here

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .