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;
}
}