Adding rotate-to-mouse code causes issues with Relative Joint

Background

I’m trying to create a 2D character out of a few sprites. Each sprite has a Rigidbody2D and a PolygonCollider2D. The head and hands are tied to the body with RelativeJoint2Ds. What I want to create is a character that moves left and right, while the head rotates so that it’s always looking at the mouse.

The problem I’m having is that when I add the code to make the head rotate to look at the mouse, it seems to break the Relative Joint’s behaviour, and as the character moves back and forth, the head slowly slides downwards until it collides with the ground.


Visuals

You can see what movement should look like in this GIF: https://imgur.com/a/tnPUGnq. This is what the movement looks like before I add the code that rotates the head to look at the mouse (The last 5 lines of the Head’s code below). Ideally, movement should continue to look like this, except that the head should constantly rotate to look at the mouse.

You can see the bug occurring in this GIF: https://imgur.com/a/8Tq13ro. This is after adding the head rotation code. You’ll notice that the code seems to work and that the rest of the character moves regularly, but the head now moves downwards slowly as the character moves left and right.


What I’ve Tried

  • Changing the rotation code to affect the head’s RigidBody2D instead of the head’s transform. This instead just made the head rotate around the body as the mouse moved.
  • Moving the rotation code between Update() and ForcedUpdate(). No change.
  • Moving the AddForce code that moves the character from the head to the body. No change.
  • Trying different 2D Joints. None of them seemed to produce the effect I wanted.
  • Trying different colliders on the body and head. As long as the body is able to rotate even a bit, the bug seems to occur.
  • Messing around with the Relative Joint’s properties to see if Force, Torque, or Correction might fix the problem. They didn’t.
  • Freezing the body’s Z rotation in its RigidBody settings. This made the body stand perfectly upright at all times which did solve the problem, since the head remained stationary above the body. However, I don’t want the body to be perfectly upright at all times, I want it to be able to rotate, and any time it’s able to rotate, the bug occurs.

My hunch is that the problem is tied to the rotation code, the head’s Relative Joint, and/or the way the body rotates when it moves. Maybe the clamping of the body’s rotation isn’t translating properly to the head? Or maybe the rotation code can’t keep up with the head’s changing position?

Any suggestions of how to fix this? I also welcome suggestions for different ways to tie the sprites together that would avoid this problem all together.


Additional Code and Images

The head’s Components:
The head's Components


The body’s Components:
The body's Components


The head’s Code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerHeadMovementController : MonoBehaviour
{
    private float moveForce = 100;
    private float maxSpeed = 5;

    private Rigidbody2D rb2d;

    // Use this for initialization
    void Start()
    {
        //Get and store a reference to the Rigidbody2D component so that we can access it.
        rb2d = GetComponent<Rigidbody2D>();
    }
    
    //FixedUpdate is called at a fixed interval and is independent of frame rate. Put physics code here.
    void FixedUpdate()
    {
        //Store the current horizontal input in the float moveHorizontal.
        float moveHorizontal = Input.GetAxis("Horizontal");

        // Add force if current movement is less than maxSpeed
        if (moveHorizontal * rb2d.velocity.x < maxSpeed)
        {
            rb2d.AddForce(Vector2.right * moveHorizontal * moveForce);
        }

        // Cap force if current movement is greater than maxSpeed
        if (Mathf.Abs(rb2d.velocity.x) > maxSpeed)
        {
            rb2d.velocity = new Vector2(Mathf.Sign(rb2d.velocity.x) * maxSpeed, rb2d.velocity.y);
        }

        // Make the head track the mouse position
        // THE PROBLEM CODE, adding this causes the bug
        Vector3 mouse = Input.mousePosition;
        Vector3 screenPoint = Camera.main.WorldToScreenPoint(transform.localPosition);
        Vector2 offset = new Vector2(mouse.x - screenPoint.x, mouse.y - screenPoint.y);
        float angle = Mathf.Atan2(offset.y, offset.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.Euler(0, 0, angle);
    }
}

The body’s code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerBodyMovementController : MonoBehaviour
{
    private float minRot = -5;
    private float maxRot = 5;

    private Rigidbody2D rb2d;

    // Use this for initialization
    void Start()
    {
        //Get and store a reference to the Rigidbody2D component so that we can access it.
        rb2d = GetComponent<Rigidbody2D>();
    }

    //FixedUpdate is called at a fixed interval and is independent of frame rate. Put physics code here.
    void FixedUpdate()
    {
        // Clamp rotation
        if (rb2d.rotation < minRot || rb2d.rotation > maxRot)
        {
            rb2d.rotation = Mathf.Clamp(rb2d.rotation, minRot, maxRot);
        }

        // If we're not trying to move, rotate back to upright
        if (Input.GetAxis("Horizontal") == 0f)
        {
            if (rb2d.rotation > 1)
            {
                // Rotate upwards
                rb2d.rotation = rb2d.rotation * 0.2f;
            }
            else
            {
                // Set directly to 0 degrees to save needless arithmetic
                rb2d.rotation = 0;
            }
        }

        // Kill any low velocity
        if (Mathf.Abs(rb2d.velocity.x) < 0.1) rb2d.velocity = new Vector2(0, rb2d.velocity.y);
    }
}

I have an experiment for you to try. My reasoning follows the description of the experiment.


Create a child gameobject of what is now the head. Move the artwork, and only the artwork for the head, to the child. Change your code to apply the rotation only to the child, not what is currently the head. See what the results are.


My reasoning:


Joints are somewhat dysfunctional. If you use them exactly the way example tutorials show, they work. Once they’re used in situations for which one thinks they ought to work, joints can do strange things.


I have no experience with Relative Joint (I don’t do much in 2D). I have found similar issues in all 3D joints, especially hinge (I don’t find a Relative Joint in 3D, the spring is more like that). Hinge works for a door fairly well, but if you hit the door with moderate to heavy force, it can dislodge the artwork. It is as if the artwork is on a flange and is displace on the flange, even though the hinge otherwise continues working as it did before. I’ve used hinges for wheels (the motor makes this inviting), but if the vehicle is more than a few Kg, the result is as if the axle is bending and buckling under the load.


I think your observation is a version of this kind of issue, peculiar to relative joint. My thinking is that the transform.rotation changes you’re applying here affect the joint in a way that just drives it nuts. Why, I can’t say, but I believe if you apply that rotation to a child, which leaves the GameObject attached to the hinge ‘un-rotated’, the visible results should be similar (or identical), while the physics results should be the same as when your code is commented out. Let me know what you get.