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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Veggie Saber – Introduction to Unity Development with the Oculus Quest
30 mins
- Getting Started
- Building Your Virtual Body
- Tracking the Player’s Head Movements
- Setting Up the Right Hand
- Duplicating the Process for the Left Hand
- Positioning Objects Relative to Your Floor
- Grabbing Objects
- Saber Slicing
- Triggering the Slice
- Slicing the Vegetables in Half
- Flattening the Mesh
- Adding Sound Effects to the Slice
- Adding Controller Haptics
- Look Out for Flying Veggies!
- Making the Veggies Move With the Music
- Creating the Veggies
- What Happens When the Player Misses?
- Adding Some Awesome Music
- GameManager
- Updating the Scores
- Deploying on the Oculus Quest
- Enabling Developer Mode via App
- Building the APK Package
- Sideload the App to the Quest
- Install Veggie Saber on the Oculus Quest
- Where to Go From Here?
Making the Veggies Move With the Music
Next, you need to add a script that generates veggies that match the tempo of a background song. The provided song runs at 172 beats per minute, and with each beat, there’s a chance to create a veggie.
To do this, open RW/Scripts/VeggieGenerator.cs and add this to Update
:
void Update()
{
counter += Time.deltaTime;
float beatInterval = 60.0f / BPM;
// 1. Veggies appear with beats
if (counter > beatInterval)
{
counter = 0f;
if (Random.Range(0.0f, 1.0f) < cutoff)
{
CreateVeggie();
}
// 2. Move veggies as game progresses
cutoff += 0.01f;
}
}
Briefly, this script will:
- Attempt to create a veggie with each beat. There’s an infrequent chance that it will randomly create a veggie at the start.
- Increase the cutoff, which makes it more likely that it will create a veggie.
Creating the Veggies
Creating a veggie happens by instantiating GameObjects:
void CreateVeggie()
{
if (veggies.Length == 0) return;
// 1. Instantiate a random veggie model.
int randomVeggie = Random.Range(0, veggies.Length - 1);
GameObject veggie = Instantiate(veggies[randomVeggie]);
veggie.transform.position = transform.position;
// 2. Choose the lane the veggie will run in.
int pos = Random.Range(0, 5);
Vector3 destination = transform.position + new Vector3(startPositions[pos, 0], startPositions[pos, 1], startPositions[pos, 2]);
// 3. Add a VeggieBehaviour component.
VeggieBehaviour comp = (VeggieBehaviour) veggie.AddComponent(typeof(VeggieBehaviour));
comp.movement = new Vector3(0, 0, -6);
comp.destination = destination;
}
- Instantiate a random veggie from a list of possible veggies.
- Set the lane that the veggie will run in.
- Add the VeggieBehaviour component then set a speed and destination.
Create an empty GameObject in the scene named Level to hold any components of the music level. Add an empty child GameObject to it named VeggieSource. Set its transform position to X:0, Y:1.25, Z:12
to move the VeggieSource in front of the player. Add the VeggieGenerator component to VeggieSource.
Drag all veggies from RW/Prefabs/Veggies into the Veggies field of the VeggieGenerator to provide the models that will be randomly thrown at the player.
What Happens When the Player Misses?
You can play the scene at this point, but if the player misses some veggies, there aren’t any consequences yet.
To fix this, add another empty child GameObject to Level called EndWall. Set its transform position as X:0, Y:0, and Z:-2
to put it behind the player. Add a RigidBody component and enable Is Kinematic. Then add a Box Collider component and set its size as X:5, Y:5, Z:1
to make a large colliding plane to catch the veggies that pass the player.
Now open the RW/Scripts/TrapMisses.cs script and fill in OnTriggerEnter
:
void OnTriggerEnter(Collider other)
{
// 1. Create a message
GameObject textMessage = Instantiate(quickMessage);
textMessage.transform.position = gameObject.transform.position;
textMessage.GetComponent<TextMeshPro>().text = "Missed!";
// 2. Destroy the missed object
Destroy(other.gameObject);
}
When a VeggieObject collides with the EndWall, it will trigger this method that:
- Creates a TextMeshPro message saying “Missed!”
- Destroys the colliding VeggieObject to clear it from the scene.
You also want to be able to track how often a player misses, so attach TrapMisses to EndWall . Next, drag RW/Prefabs/QuickMessage into Quick Message in Trap Misses.
Adding Some Awesome Music
Finally, the level needs some rocking music! Attach an AudioSource component to the Level GameObject and set its AudioClip to RW/Sounds/FeelGood.wav. Enable Loop on the audio clip so that the song will repeat.
You can now play the scene from the Unity Editor.
Congratulations, you’ve achieved the basic Veggie Saber game!
GameManager
The last steps you need to take are to keep track of scores and to show a dialog screen. Start by opening RW/Scripts/GameManager.cs and adding the following to Update()
:
public void Update()
{
if (gameState == State.Menu && sabers[0].transform.parent && sabers[1].transform.parent)
{
ChangeState(State.Level);
}
if (misses > maxMisses)
{
if (score > highScore)
{
highScore = score;
}
ChangeState(State.Menu);
}
}
Then fill in the ChangeState(State state)
method:
public void ChangeState(State state)
{
gameState = state;
if (state == State.Menu)
{
menu.SetActive(true);
level.SetActive(false);
}
if (state == State.Level)
{
score = 0;
misses = 0;
menu.SetActive(false);
level.SetActive(true);
}
}
Drag the prefab RW/Prefabs/Menu into the scene to add a basic menu UI. The menu prefab provides a canvas that displays the latest and highest scores.
The Game Manager should exit the level after too many misses and switch to this menu screen. Create an empty GameObject in the scene and name it Game Manager, then attach RW/Scripts/GameManager.cs to it.
In the Inspector view:
- Drag the Menu GameObject into the
menu
slot. - Then drag the Level GameObject into level.
- Last, add both SaberBlue and SaberRed into sabers.
Updating the Scores
Next, you need to connect the Sabers to add points to the GameManager. Select both SaberBlue and SaberRed, find the component Slicer and drag the scene’s GameManager into the GameManager field.
Edit RW/Scripts/Slicer.cs and add the following to OnTriggerEnter(Collider other)
:
if (gameManager)
{
gameManager.GetComponent<GameManager>().score += 100;
}
Now, slicing a veggie adds to the game score.
Select the Level/EndWall GameObject and drag the scene’s GameManager into the GameManager field in Trap Misses.
Edit RW/Scripts/TrapMisses and add the following to OnTriggerEnter(Collider other)
:
// 3. Add to the tally
if (gameManager)
{
gameManager.GetComponent<GameManager>().misses += 1;
}
Likewise, misses are now tracked in GameManager.
Scores will update from RW/Scripts/MenuSetup.cs. Open this script and complete OnEnable
:
public void OnEnable()
{
// 1.
SetSaberLocation(sabers[0], leftStart);
SetSaberLocation(sabers[1], rightStart);
// 2.
SetScores();
}
Here, you’re:
- Setting the sabers back to start positions.
- Setting the score texts to show the high and last scores.
Next, add the following method to update the score texts:
public void SetScores()
{
// 1.
TextMeshProUGUI highscore = highScoreText.GetComponent<TextMeshProUGUI>();
highscore.text = gameManager.GetComponent<GameManager>().highScore.ToString();
// 2.
TextMeshProUGUI score = scoreText.GetComponent<TextMeshProUGUI>();
score.text = gameManager.GetComponent<GameManager>().score.ToString();
}
This sets the TextMeshProUGUI text to match the game’s scores.
Finally, fill in the SetSaberLocation(..)
method:
private void SetSaberLocation(GameObject saber, Vector3 position)
{
if (saber)
{
saber.transform.SetParent(null);
saber.transform.position = transform.position;
saber.transform.localPosition = position;
saber.transform.localRotation = Quaternion.identity;
}
}
Now select the Menu GameObject and expand the Menu Setup component. Drag SaberBlue and SaberRed into sabers. Drag the GameManager into Game Manager, High Score into High Score Text and also Score into Score Text to finish wiring up connections between the GameObjects in the scene.
Great! Save your scene then play it. Now, a menu should be visible before grabbing the sabers. Hitting objects scores points until the player misses too many veggies and the round ends.