1
\$\begingroup\$

There are obstacles and other objects that are draggable. Colliders and dynamic rigidbodies are on the draggable object and there are colliders on the obstacles.

The problem is that with the following script, that was provided as an example in this answer, the draggable objects are colliding into each other, so the dragged object can push other draggable objects around.

How do I stop this from happening?

using System.Collections;
using UnityEngine;

public class DragTest : MonoBehaviour
{
    public enum DragMode {
        TransformPosition,
        MovePosition,
        Velocity,
        BoxCast
    }

    [Tooltip("Choose how to move the object to the cursor")]
    public DragMode mode;
    [Tooltip("Optional limit on how fast the object can follow")]
    public float maxSpeed = float.PositiveInfinity;
    [Tooltip("Select which layers should block the boxcast drag mode")]
    public LayerMask obstacleLayers;

    Rigidbody2D _body;
    BoxCollider2D _collider;

    delegate YieldInstruction dragMethod(Vector2 destination);    

    // Start a drag using the selected method when clicked.
    void OnMouseDown() {
        dragMethod method = null;
        switch (mode) {
            case DragMode.TransformPosition:
                method = TransformPosition;
                break;
            case DragMode.MovePosition:
                method = MovePosition;
                break;
            case DragMode.Velocity:
                method = Velocity;
                break;
            case DragMode.BoxCast:
                method = BoxCast;
                break;
        }
        // Start a function that will run each frame/physics step
        // to update our dragged position until the button is released.
        StartCoroutine(Drag(method));
    }

    // Update the dragged position as long as the mouse button is held.
    IEnumerator Drag(dragMethod dragTo) {
        // Turn off our gravity while we're being dragged.
        float cachedGravityScale = _body.gravityScale;
        _body.gravityScale = 0f;

        // Stash our current offset from the cursor, 
        // so we can preserve it through the move.
        var offset = transform.InverseTransformPoint(ComputeCursorPosition());

        while (Input.GetMouseButton(0)) {
            // Keep the object from accumulating velocity while dragging.
            _body.velocity = Vector2.zero;
            _body.angularVelocity = 0f;

            // Calculate desired drag position.
            var cursor = ComputeCursorPosition();
            var destination = cursor - transform.TransformVector(offset);

            var travel = Vector2.ClampMagnitude(
                destination - transform.position,
                maxSpeed * Time.deltaTime);

            // Let our chosen drag method choose how to get us there.
            yield return dragTo(_body.position + travel);
        }

        // Re-enable gravity as before.
        _body.gravityScale = cachedGravityScale;
    }

    // Using this method, the object will teleport through obstacles.
    YieldInstruction TransformPosition(Vector2 destination) {
        transform.position = destination;
        return null;
    }

    // Using this method, the object will stop at obstacles,
    // though it may penetrate for a frame before rebounding.
    YieldInstruction MovePosition(Vector2 destination) {
        _body.MovePosition(destination);
        return null;
    }

    // Effectively the same results as MovePosition.
    YieldInstruction Velocity(Vector2 destination) {
        var velocity = (destination - _body.position) / Time.deltaTime;
        _body.velocity = velocity;
        return new WaitForFixedUpdate();
    }

    // Using this method, the object will stop at the border of the obstacle.
    // It can "stick" to surfaces when dragged into them, because it keeps colliding.
    // Pull the cursor parallel or away from the surface to unstick it.
    YieldInstruction BoxCast(Vector2 destination) {
        // Compute the direction & distance to scan ahead.
        var travel = destination - _body.position;
        var distance = travel.magnitude;

        // Skip the query if we're not going anywhere.
        if (Mathf.Approximately(distance, 0f))
            return null;    


        // Find the center of our box.
        Vector2 origin = transform.TransformPoint(_collider.offset);

        // Check for any obstacles that our collider would clip on the way.
        var hit = Physics2D.BoxCast(
            origin,
            _collider.size, 
            _body.rotation, 
            travel, 
            distance, 
            obstacleLayers);

        // If we hit something, stop just a hair before the collision.
        if (hit.collider) {
            var direction = travel/distance;
            distance = hit.distance - Physics2D.defaultContactOffset * 2f;
            destination = _body.position + direction * distance;
        }       

        // Now it's safe to use any of our other methods without penetrating/tunneling,
        // since we took responsibility for avoiding collisions ourselves.
        transform.position = destination;
        return null;
    }    

    // Initialize component dependencies.
    void Start() {
        _body = GetComponent<Rigidbody2D>();
        _collider = GetComponent<BoxCollider2D>();
    }

    // Utility functions to compute dragged position.
    float GetDepthOffset(Transform relativeTo) {
        Vector3 offset = transform.position - relativeTo.position;
        return Vector3.Dot(offset, relativeTo.forward);
    }

    Vector3 ComputeCursorPosition() {
        var camera = Camera.main;
        var screenPosition = Input.mousePosition;
        screenPosition.z = GetDepthOffset(camera.transform);
        var worldPosition = camera.ScreenToWorldPoint(screenPosition);
        return worldPosition;
    }
}
\$\endgroup\$
  • \$\begingroup\$ I added _body.isKinematic = true; however this doesn't seem to work. \$\endgroup\$ – MContributor Aug 27 '19 at 22:28
  • \$\begingroup\$ Which drag mode are you using? The BoxCast mode should avoid causing these nudges. Where did you set the body to kinematic? Setting all of the draggable objects to be kinematic at all times should also stop them from being nudged. \$\endgroup\$ – DMGregory Aug 27 '19 at 23:47
  • \$\begingroup\$ I'm using BoxCast only. It is not avoiding causing these nudges. I set the body to kinematic in the code like so: { if (hit.collider) { _body.isKinematic = true; var direction = travel / distance; distance = hit.distance - Physics2D.defaultContactOffset * 2f; destination = _body.position + direction * distance; } } \$\endgroup\$ – MContributor Aug 27 '19 at 23:59
  • \$\begingroup\$ Why not set isKinematic to true in the inspector of the objects in your scene / prefabs in your assets folder, rather than messing with it in code? You can also increase the contact offset from the default to add a little more padding to keep the objects from touching. Large enough to exceed any edge radius you've set on your colliders would be ideal. \$\endgroup\$ – DMGregory Aug 28 '19 at 0:06
  • \$\begingroup\$ You mean switching body type to Kinematic? \$\endgroup\$ – MContributor Aug 28 '19 at 0:14
0
\$\begingroup\$

This script was only intended to demonstrate that positioning objects with physics methods was possible.

You can make a few improvements to handle some of the edge cases you're observing:

  1. Swap the dragged object to a different layer when dragging, so its own collider doesn't distract the physics query.

    (In my original example, the draggables and obstacles were already on separate layers, but if you want draggables to count as obstacles then you need to swap the layer of just the dragged object)

    public string draggingLayerName = "Dragging";
    
    IEnumerator Drag(dragMethod dragTo) {
        int cachedLayer = gameObject.layer;
        gameObject.layer = LayerMask.NameToLayer(draggingLayerName);
    
    
       // ...
    
       while (Input.GetMouseButton(0)) {
           // ...
       }
    
       // ...
    
       // Done dragging. Put the object back on the layer we found it.
       gameObject.layer = cachedLayer;
    
    }
  2. Adjust the padding around boxcast hits to be aware of the direction from which we approached. If we come in at a very shallow angle, we need to back up further to escape collision.

    if (hit.collider) {
        distance = hit.distance;
        distance += Physics2D.defaultContactOffset * 4f / Vector2.Dot(hit.normal, direction);
        destination = _body.position + distance * direction;
    }
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.