Stefan
Stefan

Reputation: 95

Unity. Rigidbody constraints ignored with attached child game object

I have a player character with a non-kinematic rigidbody that is being moved by pushing it along a flat ground with RigidBody.AddRelativeForce(). In the rigidbody's constraints section, I froze the X and Z rotation to keep it from falling over. This movement seems to work pretty well.

Character with Rigidbody

Now, there are many boxes on the ground. I made them (static) colliders (no rigidbodies) so they block player movement. The character is supposed to be able to pick up one of those boxes, carry it around, and put it down somewhere else. I want the carried box to collide with boxes on the ground, which is where the problems start. I found two different approaches to do this, but neither gets me all the way.

Approach 1. For pickup, I used SetParent() to link the box transform to the character dynamically (currently testing with setting the box as child of the character in the Editor hierarchy). This gives me the movement of the carried box and the collisions I want because the collider of the box (which was static) now acts as a rigidbody collider. But now I noticed that the rigidbody's rotation constraints are not working anymore, after the box has been made a child of the character game object. When applying force to the character's rigidbody (while carrying the box), it falls over (X-rotation and Z-rotation are not 0 anymore). I tried giving the box its own rigidbody component with the same constraints as the character parent, but this makes the box move independently. Am I doing something wrong here? Why are the constraints being ignored when the box is added as a child?

X/Z rotation constraints ignored.

The character is rotated in a script based on player input with the following line of code, but removing this code still produces the problem, i.e. the character still rotates around X and Z when carrying the box.

transform.RotateAround(transform.position, Vector3.up, deltaRot);

Not sure if it is relevant, but the character is a prefab of an imported (Blender/.blend) mesh. So in the editor hierarchy, the character game object (with rigidbody and collider) has a number of children (with mesh renderers, those have been created by Unity on import of the .blend file) and one child which is the carried box game object.

Approach 1 hierarchy

Approach 2. When the box is picked up, I create (or activate) a Parent Constraint component on the box, that has the character as a source. This seems to be a "cleaner" way than the first option because I am not messing with the game object hierarchy, but again, problems. First, the collider of the box now interacts with the collider of the character, pushing the character when it should be standing still. It seems weird that this would happen, but I can disable this interaction with Physics.IgnoreCollision(). Second, the carried box is still a static collider and therefore doesn't interact with the static colliders on the ground (and the level boundaries), meaning that the character can shove the box through obstacles. I don't know how to handle this. Adding a rigidbody to the box game object makes the carried box move around on its own again (the Parent Constraint is seemingly ignored). I don't want the boxes on the ground to be moved, so I don't want to give them a rigidbody. I could try to dynamically add a collider to the character game object that matches the collider of the carried box, but that seems tricky (the boxes are later supposed to have different sizes and shapes, so a single collider to enable/disable isn't a great solution).

Can one of those approaches work? Is there a better one I haven't considered, yet? Do I have a problem in my design?

Upvotes: 1

Views: 5036

Answers (1)

derHugo
derHugo

Reputation: 90813

You never want to nest/parent different Rigidbody objects. This leads to a lot of unexpected behavior.

That is what Joints are good for.

In your use case, you would probably use a FixedJoint

Fixed Joints restricts an object’s movement to be dependent upon another object. This is somewhat similar to Parenting but is implemented through physics rather than Transform hierarchy. The best scenarios for using them are when you have objects that you want to easily break apart from each other, or connect two object’s movement without parenting

So here is what I would use in your scenario:

  • As you already have Rigidbody on your player object
  • Rigidbody on all your Boxes
  • Setting the Rigidbody.constraints of these boxes on runtime (though this is probably not needed due to the next point)
  • Using the mentioned FixedJoint on the boxes. If no anchor body is set this fixes the boxes also to the current position in the scene

So on the boxes, you would have e.g.

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(FixedJoint))]
public class Pickable : MonoBehaviour
{
    [SerializeField] private Rigidbody _rigidbody;
    public Rigidbody Rigidbody => _rigidbody;

    [SerializeField] private FixedJoint _fixedJoint;
    public FixedJoint Joint => _fixedJoint;

    private void Awake ()
    {
        if(!_fixedJoint && ! TryGetComponent<FixedJoint>(out _fixedJoint)) _fixedJoint = gameObject.AddComponent<FixedJoint>();

        if(!_rigidbody && ! TryGetComponent<Rigidbody>(out _rigidbody)) _rigidbody = gameObject.AddComponent<Rigidbody>();

        _rigidbody.constraints = RigidbodyConstraints.FreezeAll;
    }
}

And your pickup might look somewhat like

public class PickupController : MonoBehaviour
{
    // Rigidbody of the player
    [SerializeField] private Rigidbody _rigidbody;
    private Pickable currentPickable;

    // However exactly you would call these in your scenario
    public void PickUp(Pickable pickable)
    {
        if(currentPickable)
        {
            Debug.LogError($"Have already picked up {currentPickable.name}", this);
            return;
        }

        currentPickable = pickable;
        pickable.Rigidbody.constraint = RigidbodyConstraints.None;
        pickable.Joint.connectedBody = _rigidbody;
    }

    public void Release()
    {
        // If have not picked anything or object was destroyed -> nothing to do
        if(! currentPickable) return;

        currentPickable.Rigidbody.constraint = RigidbodyConstraint.FreezeAll;
        currentPickable.Joint.connectedBody = null;    
        currentPickable = null;   
    }
}

Typed on a smartphone but I hope the idea gets clear

Upvotes: 2

Related Questions