Sunday, July 19, 2015

Unity: Rotate a 3D ball using 2D Physics

Unity is an awesome game engine with great 2D and 3D features. While working on a 2D game world with 3D game art, I ran into an unexpected challenge with rolling 3D spheres. Let's frame the problem in the context of a 2D soccer game with a top-down view.


TL;DR
Before (animated gif)
After (animated gif)
GitHub Project



Soccer Games

When building a 2D soccer game, a common problem is figuring out how to animate a rolling soccer ball. Soccer balls have 360 degrees of movement, so the conventional approach of animated sprite sheets does not look particularly realistic.

Sprite Sheets 

A google search of "soccer ball sprite sheet" is littered with incomplete solutions.

Often, game developers just don't animate the soccer ball at all:




Other times, the soccer ball animation is dramatically simplified:



Notice that the ball only rotates in 2 directions: clockwise or counter-clockwise. 

3D Soccer Ball

A 3D sphere made in Unity animates perfectly, because its RigidBody allows it to automatically roll around in 3D space. 



Unfortunately, using 3D physics for the ball means that your entire Unity game must use 3D physics. There are several reasons you may not want a 2D-looking game to have 3D game physics:
  1. Most collisions of the soccer ball can send it flying on the Z-axis. The ball could look like it is in the right place for a soccer player to hit the ball, but it could completely miss because the ball is 20 units above the player's head. 
  2. A dogpile of soccer players can stack in the Z-direction, messing up the look and feel of the game. 
  3. 3D physics feels very different from 2D physics
  4. 3D physics is just more complicated
Assuming you are dead-set on 2D physics, rotation of the 3D ball is no longer automatic. Lame. Let's use Unity scripting to manually rotate the ball as it moves in a 2D physics environment.

BallRoll Unity Project

The Unity 5.1 project BallRoll rotates a 3D ball using 2D physics. I have put source code on GitHub:



The main scene has a PlayerBall controlled by the arrow keys. There are also 3 PropBalls for you to bash into.




Note that the PlayerBall has a CircleCollider2D and a RigidBody2D, and that the RigidBody2D is constrained on the Z Rotation




PropBalls are the same as the player, with a different color, and minus the BallInput script.

Scripting Magic

First, we have a script that moves the ball with the arrow keys:

Next, we have a script that rotates the ball as it moves. The critical part is the rotateBall() method that utilizes Quaternions:

Quaternions

Unity uses Quaternions in all GameObjects to store their rotation information in 3D space. Specifically, GameObject.transform.rotation

There are many benefits to using Quaternions for 3D rotation, but everything that makes them powerful also makes them difficult to understand. I like to imagine a Quaternion as a hand crank:


Now stab the soccer ball straight through its center with the crank pole. That is the axis of rotation. Now turn the crank a little. That is the angle theta that the ball is rotated. A Quaternion literally stores these variables as the direction Vector [x, y, z], and angle w.

For the curious, I read through this article while implementing the ball rotation algorithm. It made for good bedtime reading.

Quaternions are pretty easy to handle in Unity. Let's figure out how they tick.

The rotateBall() Method

The rotateBall() method uses Vector Math and Geometry to build a rotation Quaternion. The Quaternion is then applied to the ball's GameObject.transform.rotation to rotate it.


We begin with transform.positionlastPosition, and the ball's radius. First, we determine currentToLast and segment.




 
// distance the ball traveled between the last Update and this one
// we subtract the vectors in this order to use it as a direction vector later
Vector3 currentToLast = lastPosition - transform.position;  

Vector Subtraction tells us that "lastPosition - transform.position" gives us a direction Vector currentToLast that points from transform.position to lastPosition.

// segment length that the ball rolled along its surface
float segment = currentToLast.magnitude;

if (segment == 0) 
{
 // no distance travelled, nothing to do
 return;
}

The Vector's magnitude tells us the distance the ball traveled, which we save as segment.

With currentToLast and segment calculated, we can determine the axis of rotation, and angle theta.

Axis of Rotation


We use the Cross Product to determine the axis of rotation. Given two Vector3s, the Cross Product returns a Vector3 that is perpendicular to both of them.




To use the Cross Product, we need two direction Vector3s. The first is currentToLast, the second is ballDown, which points directly at the ground.

// define the down direction vector for the ball
// the ball rolls in the x and y directions,
// and positive z points to the ground
// Important:  this is DIFFERENT from Vector3.down because Vector3 assumes 
//    a 3D world space where negative y is down
Vector3 ballDown = new Vector3(0, 0, 1); 


// use Cross Product to find the axis of rotation 
// https://www.mathsisfun.com/algebra/vectors-cross-product.html
Vector3 axis = Vector3.Cross(ballDown, currentToLast);

// Cross Product will fail if both vectors are parallel or perpendicular
if (axis == Vector3.zero) 
{
 // this should never happen because currentToLast.z is always 0
 // but who knows where this code will be copy-pasted to...
 return;
}

Now that we know where to rotate the ball, let's see how far we need to rotate it.

Angle Theta


The ball traveled segment (aka s) distance in between Updates, which means that the ball must roll exactly segment distance along the its surface. We can figure out theta (aka θ) using the Arc Length Formula:

// arc length formula
// s = r * theta
// theta = s / r
// http://www.mathopenref.com/arclength.html
float theta = segment / radius; // in radians
float thetaDegrees = theta * 180 / Mathf.PI;

With theta, we know exactly how many degrees to rotate the ball.

Putting It All Together


Now that we know theta and axis, we politely squeeze them into a Quaternion:

Quaternion q = Quaternion.AngleAxis(thetaDegrees, axis);

With our rotation Quaternion defined, we multiply it into the ball's transform.rotation

transform.rotation = q * transform.rotation;

Multiplication order is crucial. Quaternions, like Matrices, have Non-Commutative multiplication. Switch them up, and you'll get some weird results.

Finally

Let's see what the end result looks like:


BAM. DONE. YOU'RE WELCOME.


Thanks to OpenGameArt for:
  1. The soccer field by amon
  2. The neat repeating texture by nobiax


No comments: