0
\$\begingroup\$

I have this body of code written for an object that needs to be an obstacle and another object that is draggable. I have colliders on both and a kinematic rigidbody on the draggable object, but the obstacle is still allowing the dragged object to be dragged through it. This is all 2d. I'm not sure what I'm doing wrong.

The part that I'm confused on is this - when you just add a collider to the obstacle sprite and a collider and rigidbody (all 2d) to the draggable sprite, if I don't add the script above, then the draggable sprite falls due to gravity on top of the obstacle and stop there, it doesn't fall through the obstacle sprite. However when I add the above script which has the OnMouseDrag function, and I move the draggable sprite around because now I can, it falls through the sprite.

With no script the obstacle acts like an obstacle. With this script the it does not.

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

public class Block : MonoBehaviour
{
    public GameObject Four_Object1;
    //public GameObject clone_Four_Object1;
    //private Vector3 screenPoint;
    //private Vector3 offset;
    private Rigidbody2D rb2d;
    private Vector3 screenPoint;
    private Vector3 offset;
    private Vector3 cursorPosition;

    public void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        GetComponent<Rigidbody2D>().isKinematic = true;
    }

    public void OnMouseDown()
    {
        screenPoint = Camera.main.WorldToScreenPoint(Four_Object1.transform.position);

        offset = Four_Object1.transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, screenPoint.z));
    }

    public void OnMouseDrag()
    {

        Vector3 cursorScreenPoint = new Vector3(Input.mousePosition.x, Input.mousePosition.y, screenPoint.z);
        cursorPosition = Camera.main.ScreenToWorldPoint(cursorScreenPoint) + offset;
        transform.position = cursorPosition;
    }

    public void FixedUpdate()
    {
        rb2d.MovePosition(cursorPosition);
        Debug.Log("Update!");
    }

}
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

Your issue here is the same as the one described in this answer: when you position an object with transform.position, you're telling the engine "I don't care about collisions, I figured out exactly where I want this thing to be, so just put it here."

The engine does the predictable thing, doing exactly what you told it to, even if it wedges two colliders clear through each other, or teleports one to the far side of the other without colliding in-between.

The next time the physics engine steps, it will find the resulting trainwreck and do its best to clean up the mess and push the colliders apart by the shortest route it can, but if you're still dragging the object that frame, then you'll just wedge them back together, or keep pulling it through the far side.

If you don't want it to move through collisions, then you need to say so:

  • You can use Rigidbody2D.MovePosition to ask the physics engine to move the object in a collision-aware fashion, or assign the body a velocity computed to bring it toward the target position, letting the physics engine integrate that motion and stop it at collision boundaries.

    These methods can still cause the object to penetrate a small amount when you try to cross a large distance in one frame, or even tunnel through if your delta is big enough to completely skip over the object in one step. But for low-speed movements they'll do a good job of keeping the colliders from overlapping.

    The Rigidbody2D must be dynamic (not kinematic) for these techniques to work. With a kinematic body, again, you're telling the engine "trust me, I know where I want this - put it exactly there and don't let collisions nudge it out of place."

  • You can take responsibility yourself for avoiding collisions, by using raycasts/shape casts/overlap queries to identify a safe location to place the object and then place it there directly.

    This method very thoroughly avoids tunneling/penetration. It can cause the object to seem to "stick" to a surface when your cursor tries to drag it along-and-slightly-into the obstacle: as long as the ray/shape cast hits the obstacle it'll put on the brakes, so you have to steer it away, or cast a second ray/shape query to slide along the first obstacle to the next-closest non-intersecting point you can find.

Here's a script that will let you play around with the various different methods of positioning an object to see how they compare. Just select from the drop-down either

  • TransformPoint which always passes through colliders.
  • MovePosition/Velocity which avoid overlaps at low speeds but can show some temporary penetration or tunneling at high speeds/offsets.
  • BoxCast which always stops at the first collision on the way toward the cursor.

Try it out!

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\$
13
  • \$\begingroup\$ DMGregory I thought that was the case but if I remove the script, the first object four_object1 doesn't fall through the second object. The collision works without the script. I thought it had to do with onmousedrag... \$\endgroup\$ Aug 20, 2019 at 20:59
  • \$\begingroup\$ Can you edit your question to clarify what you've tried? I'm not sure I understand how the comment relates to the answer above. "Without the script" means without the transform.position call, so we'd expect collision to work normally in that case, right? \$\endgroup\$
    – DMGregory
    Aug 20, 2019 at 21:53
  • \$\begingroup\$ Sure, I edited my question up above. \$\endgroup\$ Aug 20, 2019 at 23:13
  • \$\begingroup\$ The symptoms you describe sound consistent with the cause described in the answer. "When I move the object with a script that uses transform.position, it passes through obstacles. When I let the rigidbody have full control of its motion, without overriding its position directly, it does not pass through obstacles". So, the recommended fix is to not explicitly set the object's position in your script, but instead move the body using its velocity. Have I misunderstood you? \$\endgroup\$
    – DMGregory
    Aug 20, 2019 at 23:16
  • \$\begingroup\$ I have to explicitly set the object's position, that is the whole point of adding the block class above. Its purpose is to take an object from a panel on the left, to another panel on the right and place it on a 2d sprite which looks like a grid. The obstacles are another sprite which I place on some of the grid spaces that are open. The draggable object should go into spaces where there are no obstacles. Therefore I cannot move the body by its velocity, right? I have to move it using the OnMouseDrag function? \$\endgroup\$ Aug 20, 2019 at 23:22
0
\$\begingroup\$

if you want to be a little deep, in any any computer software and specially games, there are only calculations. unity and other tools only wrap detailed and low level calculations to let the developer concentrate on development.

when you have an active rigidBody on your object, you just tell the physics engine to controll the object. so when you want to make other element like animation or translation move it, it can cause problems.

if you want to drag this object, you have to make it kinematic. when the player left the dragging, you can turn off the kinematic again.

another approach can be velocity property of rigidbody. just turn the gravity off and make the object velocity to move toward mouse RayCast.

another guessing approach can be using joints with very short length that is attached to the GameObject. you can control the joint with mouse or touch dragging and ball should move with it, but it needs testing and work to make it behave properly

\$\endgroup\$

You must log in to answer this question.

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