#### Desura

Official group with 10,118 members, open to all membersDesura is a community driven digital distribution service for gamers, putting the best games, mods and downloadable content from developers at gamers...

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.

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:

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.

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.

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.

**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

}

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.

&& 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

}

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.

&& 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

}

Groups

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

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

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

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

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

Post a Comment

Profile

Statistics

Contact

Twitter

Latest tweets from @gamieon

Having a mental block? Lost the inspiration to work on your game? You need to watch T.co

Jul 25 2015, 10:59am

I think I have a winner for the first #hyperspacepinball banner logo... T.co

Jul 24 2015, 8:04pm

Experimenting with #hyperspacepinball logos...not a fan of this one. Might give volumetric light a shot. T.co

Jul 23 2015, 8:35pm

Put some helpful marketing links in my #tigsource post T.co #indiedev #gamedev

Jul 23 2015, 6:24pm

#hyperspacepinball Steam leaderboard integration 1.0 working! Needs more players... #indiedev #gamedev T.co

Jul 21 2015, 5:06pm

#hyperspacepinball internal beta bug fix list posted on #tigsource T.co

Jul 20 2015, 9:17am

#hyperspacepinball screenshots posted on #indiedb T.co

Jul 20 2015, 9:16am

Just posted #hyperspacepinball on Reddit #screenshotsaturday T.co #gamedev

Jul 18 2015, 12:15am

New #hyperspacepinball screenshots on T.co #indiedev #gamedev #indiegames T.co

Jul 17 2015, 5:26pm

#hyperspacepinball time dilation and a mini-challenge T.co #indiedev #gamedev #indiegames

Jul 16 2015, 10:58am

Friends
(become friends)

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. :)

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