Veggie Saber – Introduction to Unity Development with the Oculus Quest

In this tutorial, you’ll learn how to develop and deploy a game for Oculus Quest in Unity by developing Veggie Saber, a Saber VR game featuring tasty veggies! By Matt Larson.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Saber Slicing

Things are about to get a bit dicey, because you need to implement veggie slicing with your sabers! You want your players to be able to slice a vegetable in half with a satisfying sound effect.

Triggering the Slice

Select SaberBlue and SaberRed in the scene and add a RigidBody component to both. Enable the field IsKinematic. This will allow another colliding RigidBody to trigger events, such as the slicing event.

To implement that slicing effect, add a Slicer component to both sabers. Open its script to fill OnTriggerEnter as follows:

private void OnTriggerEnter(Collider other)
{
    SplitMesh(other.gameObject);
    Destroy(other.gameObject);
}

Slicing the Vegetables in Half

Other GameObjects that are “triggers” can collide with the saber to begin a slicing event. That slicing event splits a mesh in two and removes the original object.

Fill in SplitMesh(..) like so:

    
void SplitMesh(GameObject go)
{
    // 1.
    GameObject leftHalf = MakeHalf(go, true);
    GameObject rightHalf = MakeHalf(go, false);
    // 2.
    GetComponent<AudioSource>().Play();
}

This is what the code is doing here:

  1. Splitting makes two copies of the mesh. This will give the illusion of slicing through the mesh.
  2. Slicing triggers a snare hit sound for feedback.

Now, complete MakeHalf(..):

GameObject MakeHalf(GameObject go, bool isLeft)
{
    // 1.
    float sign = isLeft ? -1 : 1;
    GameObject half = Instantiate(go);
    MeshFilter filter = half.GetComponent<MeshFilter>();

    // 2.
    Plane cuttingPlane = GetPlane(go);
    filter.mesh = CloneMesh(cuttingPlane, filter.mesh, isLeft);

	// 3.
    half.transform.position = go.transform.position + transform.rotation * new Vector3(sign * 0.05f, 0, 0);
    half.GetComponent<RigidBody>().isKinematic = false;
    half.GetComponent<RigidBody>().useGravity = true;

	// 4.
    half.GetComponent<Collider>().isTrigger = false;
    Destroy(half, 2);
	return half;
}

Walking through this code:

  1. This creates a copy of the source GameObject using Instantiate.
  2. Next, it determines a cutting plane that matches the angle of the saber blade. It then clones the mesh using this cutting plane as a slice, and assigns the new mesh to the copy of the GameObject.
  3. The halves separate slightly, then you reenable physics by disabling isKinematic and enabling useGravity.
  4. You disable further sub-slicing by turning isTrigger to false. This prevents rapid cloning of too many sub-slices, which could easily crash the game.

So… how do you determine the cutting plane? With the following code:

private Plane GetPlane(GameObject go)
{
    // 1.
    Vector3 pt1 = transform.rotation * new Vector3(0, 0, 0);
    Vector3 pt2 = transform.rotation * new Vector3(0, 1, 0);
    Vector3 pt3 = transform.rotation * new Vector3(0, 0, 1);

    // 2.
    Plane rv = new Plane();
    rv.Set3Points(pt1, pt2, pt3);
    return rv;
}
  1. Unity has an easy helper function that creates a plane from three points that are on the plane. By multiplying two unit vectors (0, 1, 0) and (0, 0, 1) by the Quaternion rotation of the saber, you find the points that represent the slice in the matching saber cut.
  2. Now, you create a plane and use Set3Points(pt1, pt2, pt3) to turn it into an idealized cutting plane.

Flattening the Mesh

The last bit of magic lets you use the plane to flatten a mesh in a cheap slicing effect. You do this in CloneMesh:

private Mesh CloneMesh(Plane p, Mesh oMesh, bool halve)
{
    // 1.
    Mesh cMesh = new Mesh();
    cMesh.name = "slicedMesh";
    Vector3[] vertices = oMesh.vertices;
    for (int i = 0; i < vertices.Length; i++)
    {
        bool side = p.GetSide(vertices[i]);

        if (side == halve)
        {
            vertices[i] = p.ClosestPointOnPlane(vertices[i]);
        }
    }

    // 2.
    cMesh.vertices = vertices;
    cMesh.triangles = oMesh.triangles;
    cMesh.normals = oMesh.normals;
    cMesh.uv = oMesh.uv;
    return cMesh;
}

Here’s what this code does:

  1. Creates a new Mesh object, then passes over each vertex of the original Mesh. With Plane.GetSide, you check to determine which side the vertex falls on. If it’s not on the side of the half you want, you map it back to the cutting plane with Plane.ClosestPointOnPlane. This results in a new Mesh that appears to represent only half of the object.
  2. Reassigns the cloned mesh vertices, then triangles, normals and UV(s). Returns this mesh as the half.

Adding Sound Effects to the Slice

Now, it’s time to add that satisfying snare effect when the player slices a veggie. So select both SaberBlue and SaberRed and add an AudioSource component. Set the AudioClip as RW/Sounds/SnareHit.wav and disable Play On Awake.

If you have a PC VR headset, you can now test chopping by adding RW/Prefabs/ChoppingTable into the scene hierarchy below the two sabers. Press play on the Unity Editor, put on your headset and grab your sabers. It time to chop ’em up!

Chop it up

Adding Controller Haptics

Providing visual and haptic feedback during the experience will add to the player’s immersion. When the player’s saber slices the veggie, you can make them feel the contact by rumbling their Oculus controllers.

To start adding this feature, open RW/Scripts/ControllerHaptics.cs and add:

public void HapticEvent()
{
	// 1.
	InputDevice device = InputDevices.GetDeviceAtXRNode(hand);
	// 2.
	HapticCapabilities capabilities;
	if (device.TryGetHapticCapabilities(out capabilities))
	{
		if (capabilities.supportsImpulse)
		{
			uint channel = 0;
			float amplitude = 0.5f;
			float duration = 0.1f;
			// 3.
			device.SendHapticImpulse(channel, amplitude, duration);
		}
	}
}

Walking through this script:

  1. This script provides a haptic feedback event to either the left or right hand’s controller. Configure this by setting the property of the InputHand in the inspector.
  2. Before sending an event, check to make sure the device supports haptic rumbles.
  3. Use SendHapticImpulse with an amplitude and duration to change the feeling of the haptic event. You might use short and intense bumps when hovering over menus, like in the Oculus Home screen. But here, you use longer and less intense rumbles to feel like a distant hit between the saber and a veggie.

Attach the ControllerHaptics to each saber and be sure to set whether the event is for the left or right InputHand in the inspector.

Edit RW/Scripts/Slicer.cs and insert the following at the top of OnTriggerEnter:

// 1. Generate haptic event
ControllerHaptics haptics = GetComponentInParent<ControllerHaptics>();
if (haptics)
{
    haptics.HapticEvent();
}

Now, if you play the scene again in the Unity editor, you’ll get a haptic feedback response when you hit a veggie with your saber.

Look Out for Flying Veggies!

Just chopping veggies on a table is boring. Instead, you want the veggies fly towards the player in time with some great music.

To get started, inactivate the test asset ChoppingTable to hide it in the scene. Then, open RW/Scripts/VeggieBehaviour.cs in your editor and fill in Update() as follows:

void Update()
{
    // 1 If not using physics, this will update the movements of the veggies.
    if (rb.isKinematic)
    {
        // 2 Move from the origin to the position for the note in the first second.
        dTime += Time.deltaTime;

        if (dTime < 1.0f)
        {
            // 3 Lerp along the X/Y axis to the correct lane.
            Vector3 position = transform.position;
            float z = position.z;
            position.z = destination.z;

            position = Vector3.Lerp(position, destination, dTime);
            position.z = z;
            transform.position = position;
        }

		// 4 Move along Z
        transform.Translate(movement * Time.deltaTime);
    }
}

This script controls the movements of veggies before and after collisions:

  1. Each veggie GameObject has a RigidBody component. When the RigidBody sets isKinematic==true, it ignores the forces of physics. Instead, the GameObject’s movements are controlled by this simple script.
  2. The veggie starts out at the origin position of the VeggieGenerator, then moves sideways into the lane of the music.
  3. A lerp from the origin X/Y to the lane’s X/Y causes the veggies to move smoothly into their lane.
  4. Move the veggies along the Z axis towards the player every frame.