This tutorial covers deterministic physics in Unity using trajectories, velocities, and forces. If you ever wanted to see if a trickshot was possible, simulate it first.
(This is the concept of digital twins. Feel free to look into it!)
Let's start out from the basics. Get an empty 3D Unity project.
Now, let's create a ground layer in our scene. On the left hierarchy, Right Click -> 3D Object -> Plane.
Scale it to be larger, and add a material if you want!
(To add a material, in assets folder, Right Click -> Create -> Material, set Albedo Color, and drag material to plane)
Now, let's add our physics ball.
On the left side again, Right Click -> 3D Object -> Sphere.
Now, on the sphere properties on the right, Add Component -> "rigid" -> "Rigidbody" Do NOT click 2D! We are in 3D!!
If everything went smoothly, when you click "Play", your ball should fall!
Easy enough. Now, let's make it fall into something. A cup? (Beer pong?) A hole? (Mini-golf?) Anything? We know that basically every game has some kind of receptor for this ball, so let's make one.
Make sure you're back in EDIT MODE ~ and not still in PLAY MODE. (Otherwise you will lose work)
On the left side, Right Click -> 3D Object -> Cylinder. Now, move it into position, scale it so a ball can fit inside.
Now, duplicate and scale for the "inside" of the cylinder.
Now, we want to "subtract" the inside cylinder from the outside one. This is called a boolean operation.
Can we do this in Unity?
It can be done.. "ProBuilder" "CSG"
Or.. we can just open Blender and do it almost instantly. (Don't worry if you don't have Blender, here's the model file.)
(Right click and save link as -> change the file extension from .png to .fbx! It's a trick to let me upload it here.)
In Blender, ADD -> Mesh -> Cylinder
Do it again, so we have two cylinders. Now, keybinds!! (Or use the tools, but keybinds are faster.)
Press S to scale the other cylinder, and just slightly drag your mouse inwards to make the other cylinder smaller.
Click to apply the operation.
Now, press G to position the other cylinder so it can reach the top. Notice it moves around! We don't want that! Press Z now to lock your movement to the Z-axis. Now, it should only move up and down. Move the cylinder up so that it goes through the top.
Now, finally, press S again, but this time, press Z to lock our scale to the Z-axis and scale the inner "hole" cylinder until are a good distance away from the bottom. We've now made a cup.. somewhat!
(You can play with locking axes if you like. Press X or Y or Z after a G (position) or S (scale) or R (rotation). You can also type numbers after to be super precise.. like try S -> Z -> 0.5, to scale your model by half in the Z-axis!)
Okay, time for our boolean operation. Click on the larger cylinder, and on the right side, find the wrench.
Now, add a modifier -> Boolean!
Make sure to have "Difference" selected, and use the Eyedropper tool to select our hole (the inner cylinder).
Finally, click the drop down arrow (in the modifier properties) and click Apply.
Now, delete your second (inner) cylinder! We don't need it anymore. It served its purpose.
We now have a cup!
File -> Export -> FBX
And remember to disable "camera" and other silly exports. We don't want those! Select only mesh.
And now just drag the FBX file into your Unity project, position a little under the ball.
It looks right and falls right in.. or does it?
We need a collider on our cup. Click the cup -> Add Component -> Mesh Collider.
Perfect.
Now, we can play with Physics properties, like bounciness, friction, and do trajectories..
Let's select our ball, go to Add Component -> "trickshot" -> and make a new script.
This should open as trickshot.cs, and we can simply do..
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class trickshot : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
// Apply a z velocity of 25.7f to the ball
// by rigidbody
GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 25.8f);
}
}
And our ball will go flying off!
This is fun.. but we have no idea where we're going!
Let's do some trajectories. I won't subject you to the math here, but it's just every physics equation ever.
Add Component -> TrajectoryController -> and make a new script.
This should open as TrajectoryController.cs, and we can simply do..
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrajectoryController : MonoBehaviour
{
[Header("Line renderer veriables")]
public LineRenderer line;
[Range(2, 30)]
public int resolution;
[Header("Formula variables")]
public Vector3 velocity;
public float yLimit;
private float g;
[Header("Linecast variables")]
[Range(2, 30)]
public int linecastResolution;
public LayerMask canHit;
private void Start()
{
g = Mathf.Abs(Physics.gravity.y);
}
private void Update()
{
RenderArc();
}
private void RenderArc()
{
line.positionCount = resolution + 1;
line.SetPositions(CalculateLineArray());
}
private Vector3[] CalculateLineArray()
{
Vector3[] lineArray = new Vector3[resolution + 1];
var lowestTimeValueX = MaxTimeX() / resolution;
var lowestTimeValueZ = MaxTimeZ() / resolution;
var lowestTimeValue = lowestTimeValueX > lowestTimeValueZ ? lowestTimeValueZ : lowestTimeValueX;
for (int i = 0; i < lineArray.Length; i++)
{
var t = lowestTimeValue * i;
lineArray[i] = CalculateLinePoint(t);
}
return lineArray;
}
private Vector3 HitPosition()
{
var lowestTimeValue = MaxTimeY() / linecastResolution;
for (int i = 0; i < linecastResolution + 1; i++)
{
RaycastHit rayHit;
var t = lowestTimeValue * i;
var tt = lowestTimeValue * (i + 1);
if (Physics.Linecast(CalculateLinePoint(t), CalculateLinePoint(tt), out rayHit, canHit))
return rayHit.point;
}
return CalculateLinePoint(MaxTimeY());
}
private Vector3 CalculateLinePoint(float t)
{
float x = velocity.x * t;
float z = velocity.z * t;
float y = (velocity.y * t) - (g * Mathf.Pow(t, 2) / 2);
return new Vector3(x + transform.position.x, y + transform.position.y, z + transform.position.z);
}
private float MaxTimeY()
{
var v = velocity.y;
var vv = v * v;
var t = (v + Mathf.Sqrt(vv + 2 * g * (transform.position.y - yLimit))) / g;
return t;
}
private float MaxTimeX()
{
if (IsValueAlmostZero(velocity.x))
SetValueToAlmostZero(ref velocity.x);
var x = velocity.x;
var t = (HitPosition().x - transform.position.x) / x;
return t;
}
private float MaxTimeZ()
{
if (IsValueAlmostZero(velocity.z))
SetValueToAlmostZero(ref velocity.z);
var z = velocity.z;
var t = (HitPosition().z - transform.position.z) / z;
return t;
}
private bool IsValueAlmostZero(float value)
{
return value < 0.0001f && value > -0.0001f;
}
private void SetValueToAlmostZero(ref float value)
{
value = 0.0001f;
}
public void SetVelocity(Vector3 velocity)
{
this.velocity = velocity;
}
}
Don't worry about understanding this code! It's physics.
Now, make a new "line" object in our scene, apply it to our "line" in the controller.
Click the line -> Line Renderer -> and turn on "World Space".
Now click the ball again, and under TrajectoryController, if you adjust the velocity slightly..
Now, let's rewrite trickshot.cs so the user can "swipe" on the ball and release it.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class trickshot : MonoBehaviour
{
bool isDragging = false;
Vector2 origPosition;
Vector2 newPosition;
float time1 = 0f;
float time2 = 0f;
void OnMouseDrag()
{
if(!isDragging) {
isDragging = true;
origPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
time1 = Time.time;
}
}
void OnMouseUp() {
Debug.Log("Up");
isDragging = false;
newPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
time2 = Time.time;
Vector2 distance = origPosition - newPosition;
float velocity = distance.magnitude / (time2 - time1);
GetComponent<Rigidbody>().isKinematic = false;
GetComponent<Rigidbody>().velocity = new Vector3(velocity*0.01f,0,0);
}
}
Now, when swiping, the ball should be thrown off in the direction depending on how fast you swiped and how far you swiped.
I challenge you to implement rotation to the swiping (think about what info we have from the swipe vector!), as well as dynamically update the "velocity" on our TrajectoryController to real-time show where we will land if we release the ball.
Note! The fact that a timer starts the instant we start holding the ball might turn out to be a problem.. how do you get around that?
(Does a ball go nowhere if a person holds it for too long? How to fix?)