Physically correct dragging (pickup) objects (like Portal or Half-Life 2)

I want to apologize in advance for some mistakes, I used a translator.

After a month of searching for the perfect and physically correct mechanics for picking up and releasing objects, I finally found it. Initially I had a negative opinion about making an object child and kinematic, because it would not take collisions into account and could be easily thrown under the world map. At first I tried to go this way:

private void MovePickedUpObject()
{
    Vector3 directionToTarget = _pickupTarget.position - PickedUpObjectRigidbody.position;
    PickedUpObjectRigidbody.AddForce(directionToTarget * _forceSpeed, ForceMode.VelocityChange);
    PickedUpObjectRigidbody.linearVelocity *= Mathf.Min(1.0f, Vector3.Distance(_pickupTarget.position, PickedUpObjectRigidbody.position));
}

But it did not give the right result, because when the player moves the object started to twitch and it looked bad.
I also tried the method of changing the velocity of the object:

private void MovePickedUpObject()
{
    Vector3 directionToTarget = _pickupTarget.position - PickedUpObjectRigidbody.position;
    PickedUpObjectRigidbody.linearVelocity = directionToTarget * _forceSpeed;
}

But the result was the same.
These were not the only ways to solve the problems, but they were the basis for many others.
After a few weeks break, I decided to come back to the project and try to make a symbiosis of what I had and what I didn’t want to do in the beginning.
After only a few hours of testing, I got the code I needed:

[SerializeField] private Camera _playerCamera;

[Space]
[SerializeField] private float _interactDistance = 3.0f;
[SerializeField] private Transform _pickupTarget;
[SerializeField] private float _forceSpeed = 25.0f;
[SerializeField] private PhysicsMaterial _zeroFrictionMaterial;
[SerializeField] private LayerMask _interactableLayer;
[SerializeField] private LayerMask _raycastExcludeLayers;

private PhysicsMaterial _originalFrictionMaterial;

public Rigidbody PickedUpObjectRigidbody { get; private set; }

private void Update()
{
    if (PickedUpObjectRigidbody != null)
    {
        MovePickedUpObject();
    }
}

private void PickupObject(InputAction.CallbackContext callbackContext)
{
    if (PickedUpObjectRigidbody == null)
    {
        TryPickupObject();
    }
    else 
    {
        ReleasePickedUpObject();
    }
}

private void TryPickupObject()
{
    if (Physics.Raycast(_playerCamera.transform.position, _playerCamera.transform.forward, out RaycastHit hit, _interactDistance, ~_raycastExcludeLayers))
    {
        Rigidbody rb = hit.collider.GetComponent<Rigidbody>();

        if (rb != null)
        {
            PickedUpObjectRigidbody = rb;

            if (PickedUpObjectRigidbody.TryGetComponent(out Collider collider))
            {
                _originalFrictionMaterial = collider.material;
                collider.material = _zeroFrictionMaterial;
            }

            PickedUpObjectRigidbody.useGravity = false;
            PickedUpObjectRigidbody.transform.parent = _pickupTarget;
            PickedUpObjectRigidbody.interpolation = RigidbodyInterpolation.None;
            PickedUpObjectRigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;

            PickedUpObjectRigidbody.linearVelocity = Vector3.zero;
            PickedUpObjectRigidbody.excludeLayers = _interactableLayer;
            PickedUpObjectRigidbody.constraints = RigidbodyConstraints.FreezeRotation;
            Physics.IgnoreCollision(PickedUpObjectRigidbody.GetComponent<Collider>(), GetComponentInParent<Collider>(), true);
        }
    }
}

private void ReleasePickedUpObject()
{
    if (PickedUpObjectRigidbody != null)
    {
        if (PickedUpObjectRigidbody.TryGetComponent(out Collider collider))
        {
            collider.material = _originalFrictionMaterial;
        }

        PickedUpObjectRigidbody.useGravity = true;

        PickedUpObjectRigidbody.transform.parent = null;
        PickedUpObjectRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
        PickedUpObjectRigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete;

        PickedUpObjectRigidbody.excludeLayers = 0;
        PickedUpObjectRigidbody.constraints = RigidbodyConstraints.None;
        Physics.IgnoreCollision(PickedUpObjectRigidbody.GetComponent<Collider>(), GetComponentInParent<Collider>(), false);

        PickedUpObjectRigidbody = null;
    }
}

private void MovePickedUpObject()
{
    Vector3 directionToTarget = _pickupTarget.position - PickedUpObjectRigidbody.position;
    PickedUpObjectRigidbody.linearVelocity = directionToTarget * _forceSpeed;
}

It’s based on the most primitive pick up and release method, but with a few changes:

  • I don’t make the object kinematic, as this disables collision handling;
  • I disable gravity so that the object can move freely;
  • in the Update() method I use a method to make the object strive for the desired position.

Using this approach we get a behavior where the object will always stay exactly at a given point, collision handling will also take place, which will prevent the object from moving outside the game world. After a collision with another object, it will aim at the desired point.

If you have any questions or suggestions on how to better optimize this code, I would be very happy to message you.

Are you familiar with the concept of a “PID Controller”? As in “Proportional-Integral-Derivative”. Code or examples are readily available online, including one someone posted on these boards quite a few years ago.

I find them really useful to make stuff like this feel good. Rather than the object always moving precisely towards the target location, you can give it a small amount of variability / slack / play, while still having great control over how responsive it feels to the player.

Generally, I definitely agree that leaving the object non-kinematic and applying force is the way to go. Keep it as consistent as you can with any other physics objects in your game, if for no other reason than it’ll help avoid a whole bunch of edge cases later on.

3 Likes

I read about “PID Controller” in the wiki, but I’m not sure if that’s what it’s about