0
\$\begingroup\$

I have a cube that has been scaled to be rectangular (controlled by a remote Raspberry Pi gyro/accelerometer/magnetometer) and am translating its orientation into Unity via calls to transform.localEulerAngles = new Vector3(...). This works wonderfully (the cube object modeled to be a Pi rotates perfectly), however now I'm running into issues with the actual physics.

GIF of successful roll

Pi and Sphere

Initially the ball will land on and roll around the Pi object, however when I start to increase the speed of my movements the ball will sometimes fall through the Pi.

The cube has a Rigidbody (no gravity, is kinematic) and Box collider (not triggered), and the sphere has a Rigidbody (has gravity, not kinematic) and Sphere collider (not triggered) attached. I've tried varying the collision detection from discrete to continuous on both objects and have noticed similar behaviors.

The intent is to turn this into a marble maze type game, and the issue that I have is similar. If I roll the ball into the joint between a wall and the floor (both stretched cubes), rotate it down, and then rotate it up, the ball will fall through the joint. I have the same movement code attached to the GameArea object below. Again, same issue with various types of collision detection applied.

Marble maze Marble maze hierarchy

My assumption is that this is a quickness of movement thing, as the cubes overlap each other (there's not a space for the ball to sneak through).

For reference, in the physics colliders section of the project preferences, all relevant layers are supposed to collide with each other. I don't mind getting into code-based collision detection, but this seemed fairly trivial for Unity to take care of so I was wondering if I was missing something obvious.

Project settings

\$\endgroup\$
1
\$\begingroup\$

If you have an object with a Rigidbody, you should avoid moving it with its Transform.

That includes setting transform.position, transform.rotation, transform.localEulerAngles, or calling transform.Translate, transform.Rotate, etc. All of these rip the object out of the physics engine's control, and are treated like an instantaneous teleportation - allowing it to skip over collisions that should have happened in-between.

Basically, treat your Transform as read-only. Any and all changes should be applied via the Rigidbody.

You can replace your localEulerAngles code with something like...

Quaternion targetOrientation = transform.parent.rotation * Quaternion.Euler(inputEulerAngles);

(You can skip the multiplication by the parent rotation if you're already at the root level of the hierarchy)

Then you can either apply that rotation directly:

rigidbody.MoveRotation(targetOrientation);

Or, in FixedUpdate, you can apply a torque to reach that target for the next timestep:

(If I recall correctly, you'll need to make your body dynamic rather than kinematic for this approach, but it can play nicer with the rest of your physics world)

// Compute the change in orientation we need to impart.
Quaternion rotationChange = targetOrientation * Quaternion.Inverse(rigidbody.rotation);

// Convert to an angle-axis representation.
rotationChange.ToAngleAxis(out float angle, out Vector3 axis);
// I can't remember the range of the returned angle, so you might not need this.
if (angle > 180f)
    angle -= 360f;

// If you need to, you can enforce a cap here on the maximum rotation you'll
// allow in a single step, to prevent overly jerky movement from upsetting your sim.
// angle = Mathf.Clamp(angle, -90f, 90f);

angle *= Mathf.Deg2Rad;

// Compute an angular velocity that will bring us to the target orientation
// in a single time step.
var angularVelocity = axis * angle / Time.deltaTime;

// You can reduce this parameter to smooth the movement over multiple time steps,
// to help reduce the effect of sudden jerks.
float catchUp = 1.0f;

// Apply a torque to finish the job.
rigidbody.AddTorque(catchUp * angularVelocity - rigidbody.angularVelocity, ForceMode.VelocityChange);

Note that even when we do our best to give the physics engine full control over the situation, rotational collisions are kind of hard to detect efficiently. If you have a long skinny object, a small rotation can make its distant end jump all the way from one side of an object to another. So other things we can do to reduce tunnelling:

  • Expand the collider of your floor and walls to give them some extra padding out to the sides and underneath. You can use layers or Physics.IgnoreCollision() to make sure your expanded floor collider and wall colliders don't interfere with one another if they're not part of the same body (not sure if you move them as one unit, or if you have a moving floor inside stationary walls)

    As long as your ball stays on top, this extra padding won't be noticeable to the player, but it can give the collision resolution system more margin for error so it can still determine the side to push the object out through. The thinner your object is, the more chance that a collision ends up slightly past the middle plane and gets pushed out the wrong side.

    This change is very cheap, and surprisingly robust, so I'd make it your first option.

  • Set the ball's collision detection mode to Continuous Dynamic, and the moving part of the maze to use Continuous Speculative collision detection.

    These modes are much more expensive than the default discrete mode, but they catch more potential tunnelling cases for fast-moving objects. If you have only a small number of colliders interacting, it might not be too heavyweight.

  • Shorten your fixed timestep, so you simulate physics more frequently at smaller steps, making it less likely for one object to teleport through another.

    This too is expensive, so be sure to profile here.

A combination of these strategies should let you tame your rotational collisions and keep your ball behaving plausibly.

\$\endgroup\$
2
  • \$\begingroup\$ That worked quite well, thanks! Is there much of a difference between the two solutions other than having 'more control' with the latter? \$\endgroup\$ – the_e May 2 '20 at 2:23
  • 1
    \$\begingroup\$ If you have obstacles that should stop the movement, it's better to use forces/torques. MovePosition/MoveRotation can skip past a barrier in cases where your target is a long way off. \$\endgroup\$ – DMGregory May 2 '20 at 2:28

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.