• Register

Gamieon is a privately owned entertainment software development company located in Tampa, Florida. Since October of 2004, we have aimed to provide quality video game software which emphasizes both intellectual and action-driven challenge to the gaming community. Gamieon depends on the talent of individuals working as a team to develop video games and video game engines with a focus on exceptional game play and surrealism.

Image RSS Feed Latest Screens
Dominoze Construction Kit
Blog RSS Feed Report abuse Latest Blog: Pinball falling through flipper - Finally solved?

0 comments by Gamieon on Mar 9th, 2015

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!

Start a group Groups
Desura

Desura

Entertainment & Press group with 10,117 members, open to all members

Desura is a community driven digital distribution service for gamers, putting the best games, mods and downloadable content from developers at gamers...

Desura Publishing

Desura Publishing

Entertainment & Press group with 191 members, open to all members

Desura Publishing group is a group dedicated to all questions regarding game & mod publishing on Desura. Official features and updates on publishing progress...

Gamieon

Gamieon

Developer with 4 members, must apply to join

Gamieon is a small independently owned company devoted to developing innovative and high quality video games.

Gamieon, Inc.

Gamieon, Inc.

Developer Invitation only

Gamieon, Inc. is a privately owned entertainment software development company located in Tampa, Florida. Since October of 2004, Gamieon provides quality...

Unity Devs

Unity Devs

Hobbies & Interests group with 1,384 members, open to all members

For all Unity developers and developers-to-be, both beginners and professionals!

Post comment Comments
ArtisanCodesmith
ArtisanCodesmith Sep 24 2012 says:

I'm new to IndieDB and ModDB and wish to invite a few people (random people picked from the online list lol) to view my game and start generating a few opinions, do you mind taking a quick look? The game is called Arkanius. Thanks, Artisan Codesmith. :)

+2 votes     reply to comment
AlexVSharp
AlexVSharp Jan 30 2012 says:

Wow, well written. You should include this article in the Unity Dev's group. ;)

+3 votes     reply to comment
Post a Comment
click to sign in

You are not logged in, your comment will be anonymous unless you join the community today (totally free - or sign in with your social account on the right) which we encourage all contributors to do.

2000 characters limit; HTML formatting and smileys are not supported - text only

Level
Avatar
Avatar
Offline Since
Aug 28, 2015
Country
United States United States
Gender
Male
Member Watch
Track this member
Accolades
Desura
Statistics
Activity Points
1,029
Rank
2,105 of 523,680
Watchers
5 members
Time Online
16 hours
Comments
18
Site Visits
2,321
Profile Visitors
83,088 (4 today)
Contact
Homepage
Gamieon.com
Private Message
Send Now
Email
Members Only
ICQ
387263084
Twitter

Latest tweets from @gamieon

Argh I broke the gamepad controls of the Game Over menu in my last #hyperspacepinball update. Will put up a new one.

4hours 18mins ago

Making it in #hyperspacepinball so that the replay never expires when you defeat a boss until after the next round begins.

8hours 48mins ago

Beta testers: New #hyperspacepinball push in 30 mins. Fixed game controler navigation and removed Camera Angle setting.

Aug 29 2015

Special thanks to Derek for providing entertainment through mashing audio buttons in the hangout!

Aug 28 2015

Going back on the #reddit #gamedev hangout for a bit T.co

Aug 28 2015

Getting close to fixing #hyperspacepinball gamepad issues ... it's rather enjoyable playing it this way.

Aug 28 2015

#hyperspacepinball gamepad menu navigations are messed up. Fixing in the next build.

Aug 28 2015

@Windlikes @DeathbyBeta I believe you get to keep it. I definitely don't intend to remove it; but it won't get new updates post-release.

Aug 28 2015

RT @DeathbyBeta: Boom! More Hyperspace Pinball beta keys are here! Act fast! T.co @gamieon #gamedev #indiedev #beta http:…

Aug 28 2015

shaulhadar
shaulhadar friends since Nov 29, 2011
Original_Speeder
Original_Speeder friends since Jun 4, 2011
hermesdavidms
hermesdavidms friends since Jan 2, 2011
swiftseraph
swiftseraph friends since Jun 3, 2011
Namelessness
Namelessness friends since Apr 26, 2013
NullSoldier
NullSoldier friends since Jun 4, 2011