Unity3D Tutorial #4

@Brandon

Note: This tutorial works best for SteamVR version 1.2.3, and no longer works for Steam 2.0 onward.

For the newer tutorial, go to Unity Tutorial #4.1

Requirements

Throw A Ball

Open up Unity and click "New" to create a new Unity Project. Make sure the 3D option is selected, and Create Project. Lets give it a name like, "Throw A Ball".


This should bring us to the project editor.

Hierarchy

On the left is the Hierarchy menu. Here is where all objects in the scene go. 

Click on anything in the Hierarchy menu, hover your mouse over the scene view, and type "f" to focus on the object.

The project editor's default view should look a little like this (If you aren't in default view, you can change the view by going to (Window>Layouts). I'll go over all the views we will be using and what they each do. It's okay if you don't understand it now, I'll go into more depth as we use each menu.

Project Window

On the bottom is the project window. This is where you organize all your files and folders as you work.

Inspector

To the right is the inspector menu.


When an object is selected, you can use this to change attributes specific to that object.

Let's do some organizing before we start. Save the scene (File>Save Scene), or (Ctrl+S) and give it a meaningful name (I named it Main). The default save folder should be the Assets folder. The saved scene should look like the Unity logo.

If we save everything in the Asset's folder, our projects will get pretty messy, so let's organize our files into folders. Navigate to the Assets folder, then right click the project view (shown below), and add a new folder. Let's call it "Scenes". Now drag the scene that we just created and drop it into the Scenes folder.

Before we add any virtual reality to our project, lets set up the scene. Go to the Hierarchy menu, right click, and in the popup menu do (3D Object>Plane). This will give us something to stand on when we start the project. Calling it plane sounds a little too...plain, right? To rename something in Unity, click it twice, but space out the clicks. Change the name to "Floor"

 Make sure that the floor's transform properties in the Inspector are all zero'ed and the scale is all set to 1. In addition, make sure the floor is not underneath anything in the hierarchy menu (If so, drag it out from under the parent object).

This will place the plane flat in the center. If not, to easily reset the properties, click the gear in the top right corner of the Transform box, and click "Reset"

Let's now add some things to interact with! Right click the Hierarchy menu and navigate 3D Object>Sphere.

Let's rename the Sphere to "Ball". Soon, we'll be able to throw this ball! Be sure that the ball's position is zero'ed out.

Scaling the ball at 1x1x1 makes it about the size of a beach ball. Let's shrink it down a bit. If we change the scale of x, y, and z to 0.25, that should make the ball about the size of a softball. Let's also raise it off the ground a bit. Type in (x:0, y:1.125, z:1) into position. These numbers are a little specific, but they'll come in handy in the future. 

Now let's add a little bit of physics to our project. One of the cool things about Unity is the fact that adding physics to a project is almost a click of a button away.

Find the "Add Component" Button at the bottom of the inspector menu. There, a search menu should pop up. Type in "Rigidbody" (note: not Rigidbody 2D) and hit Enter. Make sure Use Gravity is checked, and Is Kinematic is unchecked. Now if you click the "Play" button at the top of the scene menu, you won't have to wait until January to see the ball drop.

In the same way we created the Floor and Ball, add a Cube, and name it "Table", and adjust its transform to the properties below

The ball should now be resting comfortably on the table.

Now a little bit on the reasoning for the numbers: When we add in the virtual reality cameras, the player will start out facing the z-axis. By putting the table and ball 1 unit out, when the application starts, the table and ball will be right in front of the player. Since the "table" is 1 unit high, when its position is set to 0,0,0, you might have noticed that half of it was above, and half of it below (similar to the ball). Therefore, if we raise an object by half its height, we will put the object flat on the floor, along the x,z-plane. 

The other adjustments to scale are just to make it more table-y in the opinion of the esteemed author for this tutorial.

Let's make something to throw our ball at. Add a cylinder to the hierarchy menu, and name it "Target". 

The cylinder looks pretty big! Let's shrink it down a little and put it at the edge of the floor.

Now you must be thinking, "my scene looks whiter than a flock of doves eating rice-covered marshmallows in the middle of a blizzard," and you are absolutely right! Let's change that, shall we?

Create a new folder in "Assets" and name it "Materials" (refer to the "Save Scene" section for a refresher). Navigate to that folder and create a new Material (Create>Material). Let's name it "Ball," since we will use this to make our ball look extra sleek and schwifty. In the material's inspector, we can change all sorts of attributes relating to how something looks. There should be a dropper in the upper area of the inspector. Clicking that will allow us to pick a color. I picked red, but I encourage you to go as wild as you can when it comes to picking ball colors. In the inspector, there are also sliders that can make make the material look more shiny/metallic, or look smoother.

Once you are satisfied with how the material looks, drag the material from the Materials folder to the the desired object in the hierarchy menu (in this case, it would be ball). The object should now have the texture and color in the scene view. 

Let's make some materials for the rest of the objects in the scene to give the project some extra personality!

Now for the moment you've all been waiting for... it's time to add some VR!!!

A little warning though, if you don't have a headset system to test with, you won't be able to run the program until the end of class, when we run the program on the headset.

In the top toolbar, navigate to Window>Asset Store (or alternatively, Ctrl/Cmd + 9). In the search bar, look for "SteamVR plugin" (Should look like the picture on the top right.)

Clicking it should bring you to the SteamVR plugin import screen on the right below the first. Click import. There should be a popup asking if you want to "Import Complete Project." Click import again.

A popup should appear that says "Import Unity Package" on the top, and should have a bunch of checkboxes. This is where you can choose to selectively import parts of an asset package without importing the entire thing. Ignore the package and click import a third time.

After a minute or so, there should be a SteamVR folder in the Asset folder. Navigate into the SteamVR Prefabs folder (SteamVR>Prefabs). Now delete the "Main Camera" in the hierarchy (it will interfere with CameraRig), and drag "CameraRig" and "SteamVR" into the hierarchy.

CameraRig represents the player's point of view when the project is running, and controls the controllers and headset.

SteamVR helps smooth out the application at runtime.

There should be an arrow on the left that shows child elements of CameraRig. Click it, and it should reveal Controller (left), Controller (right), and Camera (head). Ctrl/Cmd + Click both controllers. This allows us to change both at the same time.

Since we want our hands to also be able to interact with physical objects, we also want our controllers to also have rigidbodies and box colliders. 

Add a rigidbody and box collider component in the inspector menu. In the rigidbody component, uncheck Use Gravity, and check Is Kinematic. After all, we don't want our hands falling down, do we? By checking Is Kinematic, we are telling Unity to treat the rigidbody as a physical object, but not have it abide by the laws of physics.

Collisions in Unity are not the same as real life collisions. As long as two objects are touching or overlap, they count as "colliding". Check Is Trigger. Adding a box collider and making it a trigger, we are basically telling Unity to notice every time the collider touches something. The default box collider is pretty big though, and it would be kind of weird if we could pick up things without touching them, right? To get a realistic size that doesn't break immersion as we grab objects, we'll need to fine tune the box collider a bit.

Now if we are planning on working with Vive controllers, then we want the following specifications:

If we are planning on working with Oculus controllers, then opt for the following specifications instead:

Now its time to code! Navigate to the Assets folder, and make a new folder called "Scripts". Now enter the folder and create a new C# Script (Create>C# Script), and name it "ControllerGrab". Double click the script, and it should open up an editor for C#.

The script default layout should look something like this:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class ControllerGrab : MonoBehaviour {


  // Use this for initialization

 void Start () {

  

 }

 

 // Update is called once per frame

 void Update () {

  

 }

}    

The "using" keyword works similar to the way the "package" keyword works in Java. It gives a class file access to all classes under the System.Collections namespace, and the System.Collections.Generic namespace. These statements are auto-generated, and should not need to be changed.

Next is the line  "public class ControllerGrab : MonoBehaviour {"

Unity uses the same keywords for levels of access as Java (Ex: public, private, protected, etc.). The colon represents inheritance, and can be used in place of both the "extends" and "implements" keywords. Multiple inheritance would looks something like this: "class ChildClass : ParentClass, Interface1, Interface2, Interface3". Lastly, MonoBehavior is a class that all Unity scripts must extend, we will go into a little more depth about the added functionality of this class in a bit.

Let's start out by adding the variables that we will be using in our project. In the line after the class declaration line, type the following variables and method:

private SteamVR_TrackedObject trackObj;

private GameObject collidingObject;

private GameObject held;

private SteamVR_Controller.Device Controller

{

    get { return SteamVR_Controller.Input((int)trackObj.index); }

}


void Awake()

{

    trackObj = GetComponent<SteamVR_TrackedObject>();

}

So many weird classes and objects right? What's a SteamVR_TrackedObject? Whats  GameObject? What's the weird variable-method looking thingy that even the Java programmers of our class don't recognize? Let's break this down to what actually happens when the code runs.

By following this syntax, this means that we are declaring "getter" and "setter" methods without having to type out private Integer getFoo() {return foo;}Which is something we would have to do in Java. Then we can just type in "foo" in place of the function "getFoo()"

So for the variable Controller, we are just declaring the getter function, and saying that we want to return a SteamVR_Controller.Device type related to a certain input. In other words, this getter allows us to distinguish when different buttons are pressed on a controller.

On a separate note, before we continue, a little on functions such as Start(), Awake(), Update(), etc. MonoBehavior has a couple specific built in functions that already have assigned significance to Unity. Awake(), for example, executes only once  as soon as the application starts up, before any other function is executed. After Awake methods are called, Start() is called. Any code written in the Update method body will be executed every frame of the program.

Delete the empty Start() function. We will not be needing it in ControllerGrab.

Now onto the next methods we will be needing- add these after the functions we wrote in the previous section.

    private void SetCollidingObject(Collider col)

    {

        if (!held && col.GetComponent<Rigidbody>())

        {

            collidingObject = col.gameObject;

        } 

    }


The SetCollidingObject method should be relatively straightforward. We basically for two things: are we already holding an object with the controller, and does the object we are colliding with have a RigidBody (Is it interactable)?  If it is, then store the reference to the game object associated with the collider for the object.

Something interesting to note here is that instead of writing if (held != null), we can just say if (!held). In C#, the system can infer a null check, treating null as false and not null as true.


Next, we add 3 functions related to the box collider trigger. Just like Start() and Update(), the "OnTrigger" methods also have associated significance (as in, you have to spell them exactly as is, and they have to take in the right types of parameters). These 3 methods are called whenever our controller box colliders, which we checked the box "Is Trigger" for, touches or overlaps with something. 

    public void OnTriggerEnter(Collider other)

    {

        SetCollidingObject(other);

    }


    public void OnTriggerStay(Collider other)

    {

        SetCollidingObject(other);

    }


    public void OnTriggerExit(Collider other)

    {

        collidingObject = null;

    }

We then call our SetCollidingObject method. So why are we calling it twice? This is purely to prevent bugs. If we touch something, we want Unity to know that were touching that object. But at the same time, if we keep touching an object, we don't want Unity to forget that we can still pick that object as long as we are in contact with it. Once we stop touching the other object, we don't want to be able to pick up the object anymore, so we remove the stored reference of the colliding object.

Finally, lets add in the grab functionality itself now. Add the following code immediately after the previous section code. Let's start out with the AddFixedJoint() function. Basically we are creating a FixedJoint game object, which will connect our grabbing controller rigidbody with the object we are grabbing.

The breakforce and breakTorque numbers are somewhat arbitrary, but we want to make the joints relatively strong, so that our grip on objects doesn't unexpectedly break.

private FixedJoint AddFixedJoint()

    {

        FixedJoint fj = gameObject.AddComponent<FixedJoint>();

        fj.breakForce = 65536;

        fj.breakTorque = 65536;

        return fj;

    }

So what's happening in the grab function? We want to take the object we're touching and we no longer want to simply "collide" with the object, we want to physically hold it. So we now store the colliding object in "held", and assign the collidingObject variable to null. We then connect the controller rigidbody to the held object's rigidbody with a FixedJoint object.

    private void Grab()

    {

        held = collidingObject;

        collidingObject = null;


        var joint = AddFixedJoint();

        joint.connectedBody = held.GetComponent<Rigidbody>();

    }

If we can pick something up, we should also be able to drop it right? First, we check to see if a FixedJoint exists. This means that we are, in fact, grabbing something. The next two lines of code serve to erase the FixedJoin from existence.

Then, we say, maintain the controller's velocity and angular velocity before releasing the object. This allows us throw our ball that we made way back when, instead of the ball just dropping straight to the ground.

Lastly, set the held variable to null to show that we no longer are holding the object.

    private void Drop()

    {

        if (GetComponent<FixedJoint>())

        {

            GetComponent<FixedJoint>().connectedBody = null;

            Destroy(GetComponent<FixedJoint>());


            held.GetComponent<Rigidbody>().velocity = Controller.velocity;

            held.GetComponent<Rigidbody>().angularVelocity = Controller.angularVelocity;

        }


        held = null;

    }

  

Now lets put our grab and drop functions into practice. Add the following code to the Update() function. Since we made our Grab and Drop functions, this makes our Update function look so much cleaner than it could have been. To summarize, whats happening is; If we press the trigger on a controller, and the controller is colliding with an interactable object, then grab the object. Then, if we lift the trigger on the controller, if the controller was holding an object, then execute the drop function. By putting this in the Update, we are telling Unity to execute this code every frame. In other words, on a Vive, that would mean this function checks for grabbing and dropping 90 times every second.

    // Update is called once per frame

    void Update()

    {

        if (Controller.GetHairTriggerDown())

        {

            if (collidingObject)

            {

                Grab();

            }

        }


        if (Controller.GetHairTriggerUp())

        {

            if (held)

            {

                Drop();

            }

        }

    }

Save the file, and build it (Should be Ctrl/Cmd + Shift + B). Building will allow us to check to see if maybe we made any sort of mistakes in our code. If the build succeeds and there are no errors, we should be good to go back to Unity. As soon as we save the code, it should also be saved in the Unity Project itself.

Let's add another simple script that will make the program a lot easier during runtime. Create a new script and call it BallController.

public class BallController : MonoBehaviour {


    private Vector3 position;


  // Use this for initialization

 void Start () {

        position = transform.position;

 }

 

 // Update is called once per frame

 void Update () {

  if (transform.position.y < 0)

        {

            var rb = GetComponent<Rigidbody>();

            transform.position = position;

            rb.velocity = Vector3.zero;

            rb.angularVelocity = Vector3.zero;

        }

 }

}

Since our floor is floating in the middle of nowhere, if we throw a ball and it falls off the edge, then we'll have nothing to throw anymore. This script here basically just checks to see if the ball is below the floor. If it is, then return it to its original position.

The notable syntax here includes:

Now all we need to do is take the scripts, and attach them to their related objects. To do this, all we need to do is select both controllers and drag the "ControllerGrab" script into their inspectors, and drag "BallController" to the Ball's inspector.

If we still have a little more time (like 5 minutes), go to the next section, otherwise, please skip the next section.

Having only one ball and once target to knock over seems kind of boring, doesn't it? Let's add some more of each!

Create a new folder in the assets folder called "Prefabs". Now drag "Target" and "Ball" from the hierarchy into the Prefabs folder. Delete them from the Hierarchy Menu.

Prefabs are basically blueprints for objects we make. If we make a Prefab, then we can make multiple copies of that object easily, and if we change one Prefab object, we have the option to change all others made from the same prefab in the same way.

Create two new Game Objects in the Hierarchy Menu (Create>Create Empty) (Be sure that their Transform properties are zero'ed out). Name one "Balls", and the other "Targets". Now drag the ball prefab to "Balls" and the "Target" prefab to Targets (This make the hierarchy menu a little more organized). Now right click the Ball (or Target), and select "duplicate". Now if we select the duplicate in the hierarchy menu and focus on it with "f", we should notice a colored cube with arrows. If we click the bottom face with the cube, you'll notice that we can drag the object around easily without having to manipulate any numbers. This way, we can duplicate a bunch more targets and balls to throw at them.

Once we've finished this, our project is now ready to go! Only one more step left. Go to the build settings with (File>Build Settings) or with (Ctrl/Cmd +Shift+B).

Make sure that the platform selected is "PC, Mac & Linux Standalone", then click Build. As for where to save this build, I would recommend navigating to the project directory folder (Where the .vs, Assets, Library, etc. folders are), creating a new folder called Builds, and saving the build there.

Once that's finished, all you need to do is find a Vive, and enter into a whole new reality!

Notes on tutorial execution: