Gamieon is a one-developer part-time studio focused on creating video games for the desktop and mobile platforms. Since 2010 Gamieon has developed and released six games as well as six more prototypes. Including beta distributions, Gamieon's products have netted a combined 300,000 downloads! Below this bio are images from all my games and prototypes. Check them out and don't forget to follow me on Twitter @[Gamieon](members:gamieon:321769)

Report RSS Pinball falling through flipper - Finally solved?

Posted by on

A major issue that has long plagued Hyperspace Pinball is that sometimes the ball would fall right through the flipper as the flipper is moving. This is because the ball and flipper are both moving so fast that the physics solver doesn't think they made contact when they actually did. In early development it happened maybe 1 out of every 5 times. After making a series of little changes (like decreasing the fixed time step, using thicker box colliders and writing a script that detected the ball going under the flipper and making it shoot back up), I managed to get it to happen 1 out of 30 times. All those things were done from various hacks I threw together, but I never took a real methodical approach to solving it until now.

Step 1: Build a clean test environment

This is what I should have done all along. I created a new scene with nothing but a ball and a flipper that repeatedly rotates back and forth as if someone were hammering a button. Surrounding them both are walls of triggers; if the ball touches one it gets teleported back to its starting point. If the ball touches the bottom trigger, the scene turns red meaning the test failed. The ball has no script and collisions are discrete. Here's how it looks in action:

Step 2: Make non-script changes

I cloned the test scene from step 1 and made two changes: I changed the ball and flipper colliders to be Continuous Dynamic and I added a new collider below the flipper that resembles a 90 degree pie slice to stop the ball if it falls through the flipper. I call this the "backup collider."

These changes prevented the ball from touching the bottom, but the ball would apparently stick to the flipper.

I found this post Forum.unity3d.com to help explain why this happens.

I decided that the backup collider would have to be a trigger, and I would have to do corrective math myself.

Step 3: Make scripting changes

I cloned the test scene from step 2, made the backup collider a trigger, and wrote two new scripts. The first is a script for the ball that tracks where it was over the last five calls to FixedUpdate(). The second is a script for the GameObject containing the backup collider that tracks where the tip of the flipper was over the last five calls to FixedUpdate(). The second script detects when the ball enters the backup collider, then tries to "rewind the simulation" to when the ball -really- entered the backup collider, and then sets the ball back to that position and launches it away by its angle of reflection against the flipper's up plane.

I won't explain all this in further detail here; but I include the scripts in this post for you to review if you like.

This satisfied the test environment but created a problem in the game: If the left flipper was held in the up position and the right flipper was down, and the ball was rolling down the right flipper...the ball would eventually touch the left backup collider and shoot away from the flipper.

In such a case I don't want the backup collider to do anything because the ball isn't darting toward it from above.

I considered two ways to solve this:

1. Reduce the arc length of the backup collider. I decided not to do this because the shorter the arc length, the less chance there is for the backup collider to detect the ball going into it.

2. Ignore the ball if it's not coming from above. I made it so the ball is ignored if the absolute value of its X velocity is greater than the absolute value of its Y velocity. It's not the best solution I think, but it seems to work. It's also practical because the flipper is wider than it is tall; if the ball is coming diagonally toward the flipper, it will have to pass through more of the box collider before it can get to the backup collider. I think therefore there is a better chance that Unity's solver will detect that and send the ball away without the backup collider script's intervention.

Outcome

After seeing the first test scene fail after only a few seconds, and the final test scene with all my changes not fail after several minutes; I concluded that my changes are good enough to send to QA (if I had a QA department). I applied the new scripts to my game, and it played out just fine.

If you are writing a Unity pinball simulator and are having problems with the ball passing through the flippers, feel free to study the scripts I wrote.

Scripts

FallThroughPreventerBall

csharp code:
using UnityEngine;
using System.Collections;

public class FallThroughPreventerBall : MonoBehaviour
{
  /// <summary>
  /// The previous ball positions
  /// </summary>
  Vector3[] prevBallPositions;
 
  /// <summary>
  /// The previous ball velocities
  /// </summary>
  Vector3[] prevBallVelocities;
 
  /// <summary>
  /// The number of positions to track
  /// </summary>
  const int positionsToTrack = 5;
 
  /// <summary>
  /// The number of actively tracked positions
  /// </summary>
  int trackedPositions = 0;
 
  /// <summary>
  /// Cached transform for faster access
  /// </summary>
  Transform myTransform;
 
  /// <summary>
  /// Gets the radius.
  /// </summary>
  /// <value>
  /// The radius.
  /// </value>
  public float Radius
  {
    get
    {
      return myTransform.localScale.x * 0.5f;
    }
  }
 
  /// <summary>
  /// Determines the ball position at a specified collision time.
  /// </summary>
  /// <returns>
  /// The ball position.
  /// </returns>
  /// The collision time expressed as the number of fixed updates that have elapsed since now.
  /// A value of 0 will not tell you where the ball is now; it will tell you where it was in the most recent
  /// call to FixedUpdate.</param>
  public Vector3 GetTrackedPosition(float t)
  {
    int recentOrdinal, distantOrdinal;
    if (t < 0) t = 0;
    else if (t >= (float)trackedPositions) t = trackedPositions - 1;
    recentOrdinal = Mathf.FloorToInt(t);
    distantOrdinal = (recentOrdinal == trackedPositions-1) ? recentOrdinal : (recentOrdinal+1);  
    t -= Mathf.Floor(t);
   
    // Lerp from the newer ordinal to the older ordinal
    return Vector3.Lerp(prevBallPositions[recentOrdinal], prevBallPositions[distantOrdinal], t);
  }  
 
  /// <summary>
  /// Determines the ball velocity at a specified collsion time.
  /// </summary>
  /// <returns>
  /// The ball position.
  /// </returns>
  /// The collision time expressed as the number of fixed updates that have elapsed since now
  /// A value of 0 will not tell you where the ball is now; it will tell you where it was in the most recent
  /// call to FixedUpdate.</param>
  public Vector3 GetTrackedVelocity(float t)
  {
    int recentOrdinal, distantOrdinal;
    if (t < 0) t = 0;
    else if (t >= (float)trackedPositions) t = trackedPositions - 1;
    recentOrdinal = Mathf.FloorToInt(t);
    distantOrdinal = (recentOrdinal == trackedPositions-1) ? recentOrdinal : (recentOrdinal+1);  
    t -= Mathf.Floor(t);
   
    // Lerp from the newer ordinal to the older ordinal
    return Vector3.Lerp(prevBallVelocities[recentOrdinal], prevBallVelocities[distantOrdinal], t);
  }    
 
  #region MonoBehaviour

  void Start ()
  {
    prevBallPositions = new Vector3[positionsToTrack];
    prevBallVelocities = new Vector3[positionsToTrack];
    myTransform = transform;
   
    prevBallPositions[0] = myTransform.position;
    prevBallVelocities[0] = myTransform.rigidbody.velocity;
    trackedPositions = 1;
  }
   
  void FixedUpdate ()
  {
    for (int i = prevBallPositions.Length - 1; i >= 1; i--)
    {
      prevBallPositions[i]= prevBallPositions[i - 1];
    }

    for (int i = prevBallVelocities.Length - 1; i >= 1; i--)
    {
      prevBallVelocities[i]= prevBallVelocities[i - 1];
    }
   
    prevBallPositions[0] = myTransform.position;
    prevBallVelocities[0] = myTransform.rigidbody.velocity;
    trackedPositions = Mathf.Min(trackedPositions+1, positionsToTrack);  
  }
 
  #endregion
}
 

FallThroughPreventerFlipper

csharp code:
using UnityEngine;
using System.Collections;

public class FallThroughPreventerFlipper : MonoBehaviour
{
  /// <summary>
  /// The transform representing the hinge of the flipper
  /// </summary>
  public Transform hinge;
  /// <summary>
  /// The transform representing the tip of the flipper
  /// </summary>
  public Transform tip;
  /// <summary>
  /// The flipper collider.
  /// </summary>
  public Collider flipperCollider;
 
  /// <summary>
  /// The previous positions of the tip of the flipper
  /// </summary>
  Vector3[] prevTipPositions;
 
  /// <summary>
  /// The number of positions to track
  /// </summary>
  const int positionsToTrack = 5;
 
  /// <summary>
  /// The number of actively tracked positions
  /// </summary>
  int trackedPositions = 0;
 
  #region Private Methods
 
  /// <summary>
  /// Gets the collision time expressed as a value in [0,1] where 0 is the time of the
  /// previous fixed update call, and 1 is now.
  /// </summary>
  /// The ball</param>
  /// <returns>The collision time expressed as the number of fixed updates that have elapsed since now</returns>
  float GetCollisionTime(FallThroughPreventerBall ball)
  {
    int iFirstOrdinalNotIntersecting = 0;
    if (1 == trackedPositions)
    {
      // We only have one position to work with, so we're stuck with it whether or not we actually intersected
      return 0.0f;
    }
    else
    {
      for (; iFirstOrdinalNotIntersecting < trackedPositions - 1; iFirstOrdinalNotIntersecting++)
      {
        if (!Intersects(ball, (float)iFirstOrdinalNotIntersecting))
        {
          break;
        }
      }
    }
   
    if (0 == iFirstOrdinalNotIntersecting)
    {
      // If we get here, the intersection took place between the most recent fixed update and now
      return 0.0f;
    }
    else
    {
      // Try to better estimate exactly when this happened
      const int precisionLevel = 3;
      float tNotIntersect = (float)(iFirstOrdinalNotIntersecting); // We are not intersecting at tNotIntersect
      float tIntersect = (float)(iFirstOrdinalNotIntersecting - 1); // We are intersecting at tIntersect
      float t = tIntersect;
     
      for (int i=0; i < precisionLevel; i++)
      {
        if (Intersects(ball, t))
        {
          // bring t closer to tNotIntersect
          t = (t + tNotIntersect) * 0.5f;
        }
        else
        {
          // bring t closer to tIntersect
          t = (t + tIntersect) * 0.5f;
        }
      }
     
      return t;
    }
  }
 
  /// <summary>
  /// Determines whether the ball is intersecting with the fallback collider at a specified collision time
  /// </summary>
  /// The ball</param>
  /// The collision time expressed as the number of fixed updates that have elapsed since now</param>
  bool Intersects(FallThroughPreventerBall ball, float t)
  {
    Vector3 ballPosition = ball.GetTrackedPosition(t);
    Plane collisionPlane = GetTrackedCollisionPlane(t);
   
    // Get the distance from the ball center to the plane
    float d = collisionPlane.GetDistanceToPoint(ballPosition);
   
    // Return true if the distance is less than the radius of the ball
    return (d < ball.Radius);
  }
 
  /// <summary>
  /// Determines the flipper's collision plane (cuts through the center of the flipper) at a specified collsion time.
  /// </summary>
  /// <returns>
  /// The collsion plane
  /// </returns>
  /// The collision time expressed as a value in [0,1] where 0 is the time of the
  /// previous fixed update call, and 1 is now.</param>
  Plane GetTrackedCollisionPlane(float t)
  {
    int recentOrdinal, distantOrdinal;
    if (t < 0) t = 0;
    else if (t >= (float)trackedPositions) t = trackedPositions - 1;
    recentOrdinal = Mathf.FloorToInt(t);
    distantOrdinal = (recentOrdinal == trackedPositions-1) ? recentOrdinal : (recentOrdinal+1);  
    t -= Mathf.Floor(t);   
   
    // Find the vectors from the hinge to the tip
    Vector3 v0 = (prevTipPositions[recentOrdinal] - hinge.position).normalized;
    Vector3 v1 = (prevTipPositions[distantOrdinal] - hinge.position).normalized;
    Vector3 va = Vector3.RotateTowards(v0, v1, Vector3.Angle(v0, v1) * Mathf.Deg2Rad * t, 0.0f);
    return new Plane( Vector3.Cross(new Vector3(0,0,1),va), hinge.position );
  }
 
  #endregion
 
  #region MonoBehaviour
 
  // Use this for initialization
  void Start ()
  {
    prevTipPositions = new Vector3[positionsToTrack];
    prevTipPositions[0] = tip.position;
    trackedPositions = 1;
  }
 
  // Update is called once per frame
  void FixedUpdate ()
  {
    for (int i = prevTipPositions.Length - 1; i >= 1; i--)
    {
      prevTipPositions[i]= prevTipPositions[i - 1];
    }
       
    prevTipPositions[0] = tip.position;
    trackedPositions = Mathf.Min(trackedPositions+1, positionsToTrack);
  }
 
  void OnTriggerEnter(Collider other)
  {
    FallThroughPreventerBall ball = other.gameObject.GetComponent<FallThroughPreventerBall>();
    if (null != ball
      // Only do the fall through prevention if the ball is travelling faster "vertically" than "horizontally"
      // WISHLIST: Find a better way to discern balls coming from the top from balls coming from the sides.
      // Another option is to make this collider an eighth of a pie slice rather than a full quarter slice
      // though I prefer the additional coverage of a full quarter slice.
      &amp;&amp; Mathf.Abs(ball.rigidbody.velocity.x) < Mathf.Abs(ball.rigidbody.velocity.y))
    {
      // Get the approximate time of collision
      float t = GetCollisionTime(ball);
 
      // Calculate the ball and collision plane at the time of contact
      Vector3 ballPositionAtImpact = ball.GetTrackedPosition(t);
      Plane collisionPlaneAtImpact = GetTrackedCollisionPlane(t);
     
      // Calculate the point of contact between the ball and collision plane
      Vector3 contactPoint = ballPositionAtImpact - collisionPlaneAtImpact.normal * ball.Radius;
     
      // Calculate the velocity of the ball at the time of contact
      Vector3 contactVel = ball.GetTrackedVelocity(t);
     
      // Calculate the velocity the ball would be going had it properly collided with and bounced off the flipper
      Vector3 reflectVel = Vector3.Reflect(contactVel, collisionPlaneAtImpact.normal);
         
      // WISHLIST: A perfect reflection isn't realistic, but it's pretty good for our purposes
      // WISHLIST: Calculate the velocity of the flipper at the contact point and add it to the ball velocity
 
      Debug.Log("t=" + t + " vOld=" + contactVel + " vNew=" + reflectVel);
      //Debug.Break();
     
      // Move the ball back to the point of contact and assign its new velocity.
      ball.transform.position = contactPoint;
      ball.rigidbody.velocity = reflectVel;
     
      Physics.IgnoreCollision(flipperCollider, other, true);
    }
  }
 
  void OnTriggerExit(Collider other)
  {
    FallThroughPreventerBall ball = other.gameObject.GetComponent<FallThroughPreventerBall>();
    if (null != ball)
    {
      Physics.IgnoreCollision(flipperCollider, other, false);
    }
  }
 
  #endregion
}
 

Check out my homepage and social feeds

And my projects!

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: