Update #2: I fixed it through a number of methods:
- Forcing the ball out of the collider if it penetrates through a certain depth
My solution is to make the ball into a trigger and change its velocity to match the contact tangent to force it out. This is handled in OnCollisionStay and OnCollisionExit. I don’t believe it will handle multiple contact points properly.
- Restrict the ball to the world bounds
Imagine a rectangle that surrounds your playfield. The code in FixedUpdate will make sure it won’t leave the region.
- Ball won’t go through the flippers
This one took weeks of tweaking to figure out, and is handled in FixedUpdate. Every flipper in my simulation has a box collider; and immediately above it, another box collider that is a trigger which I call the “flipper tangent.” The “FlipperBuffer” objects are box colliders that surround the flippers and are triggers. My logic is this: If the ball is inside a flipper buffer, and behind a tangent; then it needs to bring itself back up to the tangent and start moving up and away. Of course there are sometimes false positives, but it’s better than the alternative of the ball going through.
- Nuclear option: Self-destruct if the ball doesn’t move for three seconds
This is handled in LateUpdate, and there’s one exception to this rule besides the ball at rest over the pin: The “FlipperBuffer” objects are boxes that surround the flippers. Pinball players often use flippers to steady the ball, so we want to make sure it won’t self destruct like that.
using UnityEngine;
using System.Collections;
public class BallPhysics : MonoBehaviour
{
float samePositionLastTime = 0;
Vector3 samePositionPos = Vector3.zero;
Transform myTransform; // Cache the transform (optimization)
Rigidbody myRigidbody;
bool restrictToWorldBounds;
FlipperBuffer[] flipperBuffers;
void OnBallReleaseLockTriggerExit()
{
Debug.Log("Restricting to world bounds");
restrictToWorldBounds = true;
}
void Awake()
{
rigidbody.mass = PinballPhysics.BallMass;
myRigidbody = rigidbody;
// Initialize our mandatory variables
flipperBuffers = (FlipperBuffer[])FindObjectsOfType(typeof(FlipperBuffer));
myTransform = transform;
samePositionPos = myRigidbody.position;
samePositionLastTime = Time.time;
restrictToWorldBounds = false;
}
void FixedUpdate()
{
// Restrict to world bounds
if (restrictToWorldBounds)
{
Vector3 adjustedPosition = myTransform.position;
bool mustAdjust = false;
if (adjustedPosition.x < -64.2f) {
adjustedPosition.x = -64.2f;
mustAdjust = true;
}
else if (adjustedPosition.x > 56 adjustedPosition.y < 42) {
adjustedPosition.x = 53.6f;
mustAdjust = true;
}
else if (adjustedPosition.y > 90.3f)
{
adjustedPosition.y = 90.3f;
mustAdjust = true;
}
if (mustAdjust)
{
#if UNITY_EDITOR
Debug.Log("Emergency adjustment!");
#endif
myTransform.position = adjustedPosition;
}
}
// If the ball has a collision layer, then we're OK to begin flipper correction checking
if (myRigidbody.velocity.y < 0 gameObject.layer > 0)
{
foreach (FlipperBuffer buffer in flipperBuffers)
{
// Begin by seeing if the ball is within the "flipper buffer" which is a rectangular region of space near the flipper.
// No point in doing correction calculations if the ball is far away.
if (buffer.collider.bounds.Contains(myTransform.position))
{
// We're near the flipper and it's in motion. Do a further check to see if the flipper is moving anywhere but to its resting position
if ((buffer.flipper.IsGoingToPressedRotation() || buffer.flipper.IsAtRest()) Mathf.Abs(buffer.flipper.rigidbody.angularVelocity.z) > 2.0f)
{
// Yes, conditions are right for doing corrections though we're not sure if we need to do one yet
// Cast a ray from behind the ball toward the tangent. If it hits, then try to put the ball above the flipper
RaycastHit hitInfo;
// if (Physics.Raycast(myTransform.position, Vector3.Normalize(-myRigidbody.velocity), out hitInfo, Mathf.Infinity, layerMask))
// if (Physics.Raycast(myTransform.position, Vector3.Normalize(-myRigidbody.velocity), out hitInfo, Mathf.Infinity, (1 << 14)))
if (buffer.flipperTangent.collider.Raycast( new Ray(myTransform.position, Vector3.Normalize(-myRigidbody.velocity)), out hitInfo, Mathf.Infinity))
{
// Move the ball up to the tangent point
// (c.haag 2011-02-28) - If you uncomment this out, sometimes the ball is jerked to a place it shouldn't
// be. This is because the ball may have already started going in the opposite direction by a regular
// Unity collision and the flipper tangent may not be in the direction it was when the ball actually penetrated
// it by this juncture.
//transform.position = hitInfo.point;
Debug.Log("Fixing flipper fall-through");
Vector3 surfaceNormal = -hitInfo.normal;
Vector3 ballRay = Vector3.Normalize(myRigidbody.velocity);
Vector3 angleOfReflection = Vector3.Reflect(ballRay, surfaceNormal);
// Also apply an extra velocity so it doesn't stick or loiter around the boundary of the flipper
Vector3 extraVelocity = (myRigidbody.velocity.y < 0 myRigidbody.velocity.y > -60) ? new Vector3(0,400,0) : new Vector3(0,100,0);
//Vector3 extraVelocity = Vector3.Normalize(surfaceNormal) * (( Vector3.Magnitude(rb.velocity) < 60 ) ? 100.0f : 500.0f);
myRigidbody.velocity = angleOfReflection * Vector3.Magnitude(myRigidbody.velocity);
// Ensure the current velocity is always positive
if (myRigidbody.velocity.y < 0) { myRigidbody.velocity = new Vector3(myRigidbody.velocity.x, -myRigidbody.velocity.y, myRigidbody.velocity.z); }
myRigidbody.velocity += extraVelocity;
}
break;
}
}
}
}
}
void OnCollisionStay(Collision collision)
{
foreach (ContactPoint contact in collision.contacts)
{
float f = Vector3.SqrMagnitude(contact.point - transform.position);
if (f < 2.0f) // Pick this "out of a hat"
{
#if UNITY_EDITOR
Debug.Log("Correcting " + f);
#endif
collider.isTrigger = true;
float av = rigidbody.velocity.magnitude;
rigidbody.velocity = contact.normal * av;
}
}
}
void OnCollisionExit(Collision collision)
{
if (collider.isTrigger)
{
collider.isTrigger = false;
#if UNITY_EDITOR
Debug.Log("Correction done");
#endif
}
}
void LateUpdate()
{
if (myRigidbody.useGravity)
{
// I got the ball stuck on a wall once. This should prevent that from happening again.
float e = 0.002f;
if (myRigidbody.position.x >= samePositionPos.x - e myRigidbody.position.x <= samePositionPos.x + e myRigidbody.position.y >= samePositionPos.y - e myRigidbody.position.y <= samePositionPos.y + e)
{
if (Time.time - samePositionLastTime > 3
!(myRigidbody.position.x > 59 myRigidbody.position.y < -44) /* isReadyToLaunch */
)
{
bool destroyBall = true;
foreach (FlipperBuffer buffer in flipperBuffers)
{
// Begin by seeing if the ball is within the "flipper buffer" which is a rectangular region of space near the flipper.
// If it's in there, don't destroy the ball because the player is just holding on to it.
if (buffer.collider.bounds.Contains(myTransform.position))
{
destroyBall = false;
break;
}
}
if (destroyBall)
{
// If we get here, the ball is probably stuck because it's well above the flippers
// and it hasn't moved in 4 seconds. We have no choice but to destroy it and credit
// the player a ball.
Debug.Log("Destroying stuck ball");
GameObject p = GameObject.Find("Player");
p.SendMessage("ReplayStuckBall", GetComponent("Ball"));
}
}
}
else
{
// Object is in motion
samePositionLastTime = Time.time;
samePositionPos = myRigidbody.position;
}
}
else
{
// This can't be the player's ball if it's not using graviry. It's probably
// a boss ball.
}
}
}