top of page

Toybox Trouble : VR Wave Based Shooter

Members: 6
Duration: 4 weeks
Date: 2021

Brief

Toybox Trouble is a wave-based shooter game developed for the Oculus Go. Your objective is to use your nerf gun to shoot down all the toys back to their toyboxes before Mum comes to check on you. Since you play as a child, we took a spin on the game by portraying the world through a child's imagination.

Systems

Vacuum System

The first wacky system I developed for Toybox Trouble was the vacuum cleaner. The vacuum cleaner allows the player to suck up the bullets they have used. In order to get the vacuum cleaner to work, it would first need to detect the objects in front of it. I played around with this part by using Physics.OverlapCapsule, but found it clunky to use when testing it in the VR.

 

Since Unity doesn’t support any form of cone casting to detect collision, I went over to Blender and created a cone in Blender. Unfortunately, when the shape itself was exported over to Unity, there were already a few issues. The general complaint was that the vacuuming feature felt restricting and relied on the player to be accurate when aiming at the bullets. After trial and error, I had finally come across a video regarding the usage of dot products. 

 

With this knowledge, I created a class called, DetectableObject. The class checks if it’s within the vacuum cleaner’s range by sending its position over to the class, VacuumController using the CheckCollision function.

DetectableObject checkCollision.PNG

If the hasCollided boolean is true, then we add force to the object. 

addForce.PNG

As for the CheckCollision function, we first calculate the dot product of the distance between the object's position and the vacuum’s position with the vacuum's forward. Then, we compare the angle against the cutoff. If the angle is within the vacuum cleaner’s range, less than 45 degrees in this case, then the object can be sucked up. The system itself is more of a custom collision detector. With adjustable values such as the cutoff (cover angle of the vacuum cleaner), the feature is the most responsive out of the other attempts. 

CheckCollision.PNG
vacuum cleaner.png

Now with all of the maths out of the way, we can get back to getting the vacuum cleaner to work. The vacuum cleaner gameObject contains a child sphere collider. On this child object, we have a script called, VacuumStorage attached. The script is responsible for handling what is sucked up by the vacuum. If the object sucked up is a bullet, then the player recovers their bullet. Otherwise, if it’s a toy, then the toy gets destroyed and the player’s score is affected. 

 

Attached to the main vacuum cleaner gameObject is the VacuumController. This script is fairly straightforward. It does the dot product check to determine whether the object is within the range of the vacuum cleaner. It also detects the user’s input. When the player holds onto the trigger, it simply sets a boolean to determine whether the object gets vacuumed. 

Drawing System 

When the game ends, you are presented with an easel displaying your score and notifying you to proceed. Now, what do you expect to do when you come across a drawable object? Draw of course. I immediately accepted the task of implementing a drawing system. What have I gotten myself into?

 

Let’s start with what worked. On the easel, I placed a plane in front of it and attached a script called, DrawController. In this script, the player is constantly raycasting from their controller (a nerf gun in this case) while holding the trigger button. If the ray hits the plane, then two different types of functions get invoked. When the ray first contacts the plane, we call the CreateLine() function. 

CreateLine.PNG

Let’s break this one down a bit. We first create a line using linePrefab, a gameObject that holds a line renderer component. We then position currentLine in front of the easel. In order to update a lineRenderer’s position while drawing, we keep track of their position. We do this with fingerPositions, a list of vector3s, the list keeps track of every line’s positions. So, in this function, we are adding the contact position into the fingerPosition list. Then, we update the lineRenderer’s position. This will get the line started and create a point on the plane. You won’t be able to see this point since the start and end touch position is the same. To see the line renderer in action we’ll call another function. 

UpdateLine.PNG

Here, we add the updated position in the fingerPosition list and set the linerRenderer’s position with this new position. This particular function keeps on getting called, so that the line renderer updates and extends. 

 

The next part was quite a journey. I wanted to add a saving and loading mechanic for the drawing. So, when the player saves their drawing, it is loaded on the scoreboard immediately.  

 

The first attempt was the render texture method. A secondary camera is placed in front of the drawing, and when the player saves the drawing, the camera renders that drawing as a texture. The texture is then saved to a file. This method somewhat worked. The issue I encountered with this method was that the process was slow. The texture would require the scene to refresh in order for the texture to load. I performed some tests by restarting the scene. As a result, I had found that the texture sometimes doesn’t load after refreshing the scene. Therefore, this isn’t the ideal method to go for. 

 

The next method seemed more viable than the previous. Instead of taking a picture of the drawing, why not save every line’s positions to a text file and load it by creating a new set of lines with the saved positions? I created a class called, DrawManager, and created a save and load function. 

 

The way the save function worked was by iterating through every single line renderer present on the easel. I add each line’s positions into a list of vector3s called, positions.

save linerenderer.PNG

I then defined an array of strings called, content. What you might’ve noticed is that it holds three strings. The first index contains all of the positions. The second and third index holds the positions and rotations relative to the lines’ local space. This is to ensure the lines are positioned and facing correctly in the world. 

saving json.PNG

Then we simply save the data to file. 

file writeTo.PNG

As for the loading function, I first deserialize from the json file. Then, I iterate through the array and assign each new line renderer with the data’s positions. 

deserialising.PNG

With that all done and dusted. After testing all of this on the editor, I went over and made a build to test this feature. I didn’t see my drawing appear on the scoreboard. The saving and loading wasn’t working. After looking into this issue, I discovered something about android builds. They do not support json files. 

 

I wasn’t able to find a workaround for this problem. However, I was suggested to give xml saving and loading a shot. I attempted the method, but found out I was running short on time. Therefore, I never got around to getting it to work. 

 

It was disappointing to not see something as magical as your own drawing being presented over at the scoreboard. Well, at least you could draw after the round is over.

Power up system

The next cool mechanic I got to work was the power up system. The way the mechanic works is by random chance. Every time you put a toy back into their toybox, there’s a chance that a power up card will emerge from it. Once you shoot a power up card, you gain a temporary ability. There are a total of six power up abilities and they are:

power ups.PNG

Let’s start discussing the mechanic by first answering the question, how does one keep track of all of the power ups? As you can see right above this text, it is all done with scriptableObjects. I have first defined an abstract class deriving from scriptableObject, called, PowerUp. 

power up class.PNG

Derived under the PowerUp class, I have three child classes called, ObjectPowerUps, EventPowerUps, and ActivePowerUps.

 

The ObjectPowerUps class is a type of power up that enables and disables objects. For example, the vacuum net and the machine gun are objects that are the power ups themselves. So, when the player activates the machine gun, the object enables and then disables when the timer has run out. 

 

The EventPowerUps class are one-off power ups. Freeze is a type of EventPowerUp. When the player activates that ability, it is called through a Unity Event, which invokes that power up. 

 

The ActivePowerUps are passive modifiers that affect the player’s stats. AddTime, Increased Bullet Capacity, and Reduced Fire Rate are types of ActivePowerUps. Let’s say the player picks up a reduced fire rate power up. This power up has reduced the player’s fire rate for the remainder of the round. 

 

The PowerUp classes contains public functions that do the following:

  • Applying the power up 

  • Updating the power up (internal timer)

  • Removing the power up (when the timer is up)

 

Each of the power ups also hold a particular type of enum, so the GameManager can identify the type of power up. 

Table.PNG

I have the GameManager keep track of all the PowerUps 

power ups list.PNG

I then split the PowerUps into different dictionaries depending on their enum type: 

power up dictionary.PNG

With all the power up management all discussed about, the next question to answer is, how are the power ups spawned? Inside the GameManager class, I have a function called, PowerUpDropChance(), which is invoked by another class when a toy falls into the toybox. 

powerUpDropChance.PNG

In this function, I generate a random value and compare that against powerUpDropChance. num returns a value anywhere between 0 and 1, and powerUpDropChance is set anywhere between the range of 0 and 1. So, if powerUpDropChance is 0.5f, then we have a fifty percent chance of dropping a power up. Further into the function, I instantiated a card with a randomly selected power up. 

 

CardController handles the rest of the process. The class holds information such as the card’s visuals and PowerUp. When CardController gets instantiated, it moves towards its destination in a parabola shape. Once it is hit by the player, its power up then activates. 

Demo

bottom of page