Skip to content

Latest commit

 

History

History
380 lines (251 loc) · 12.2 KB

README.md

File metadata and controls

380 lines (251 loc) · 12.2 KB

3D Physics in Unity

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!)

Basically, beer pong.

image

Tutorial

Let's start out from the basics. Get an empty 3D Unity project.

image image

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)

image

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

image

If everything went smoothly, when you click "Play", your ball should fall!

ezgif com-video-to-gif

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.

image (Color added for effect)

Now, we want to "subtract" the inside cylinder from the outside one. This is called a boolean operation.

Can we do this in Unity?

Yes, but..

plugins..?

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

cup (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

image

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.

image

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.

image

Now, add a modifier -> Boolean!

image

Make sure to have "Difference" selected, and use the Eyedropper tool to select our hole (the inner cylinder).

image

Finally, click the drop down arrow (in the modifier properties) and click Apply.

image

Now, delete your second (inner) cylinder! We don't need it anymore. It served its purpose.

image

We now have a cup!

File -> Export -> FBX

image

And remember to disable "camera" and other silly exports. We don't want those! Select only mesh.

image

And now just drag the FBX file into your Unity project, position a little under the ball.

image

It looks right and falls right in.. or does it?

image

We need a collider on our cup. Click the cup -> Add Component -> Mesh Collider.

image

Perfect.

image

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".

image

Now click the ball again, and under TrajectoryController, if you adjust the velocity slightly..

image

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.

ezgif-4-214c307762

Challenges

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?)