Project

This project was developed using the original Oculus Rift CV1 through Unity 2020. After development, it has been successfully tested with the Oculus Quest and Oculus Quest 2. The game may work with other headsets, but full compatibility is not guaranteed.

Roadmap

Gantt Chart of project

Downloads

EntireProject.7z will have both my project folder from Unity and the folder containing the game. Next, HowTo.pdf explains how to play the game, as well as how to compile it. Lastly, CapstonePresentation.pptx is the presentation I used to present my project on May 1st, 2021.

Source Code

GameScript.cs

//Author:       Derek Huber (May, 2021)
//Filename:     GameScript.cs
//Purpose:      Controls game loop, UI navigation, and VR controller vibration

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;
using System.IO;
using TMPro;

public class GameScript : MonoBehaviour
{
    //Public objects viewable in the Inspector
    public GameObject mainMenu;
    public GameObject endMenu;
    public GameObject inGameMenu;
    public GameObject pauseMenu;
    public GameObject leftController;
    public GameObject rightController;
    public TMP_Dropdown songDropdown;
    public TMP_Text BPMTB;
    public TMP_Text FirstBeatDelayTB;
    public SoundManagerScript soundManagerScript;

    //Public variables available to other scripts
    public static float gameStartCubeSpawnDelay = 2.0f;
    public static int score = 0;
    public static int cubeSpeed = -3;
    public static bool bpmMode = false;
    public static bool gameBeingPlayed = false;
    public static string selectedSongFullPath;
    
    //Private variables
    private bool isPaused = false;
    private string[] filePaths;
    private List<string> songList;
    private List<UnityEngine.XR.InputDevice> controllers;
    private bool primaryButtonValue = false;
    private GameObject[] hideUIObjects;
    private GameObject[] frontObjects;


    void Start()
    {
        //Get the path of the game .exe folder, and then move into Songs folder
        string songsPath = Directory.GetCurrentDirectory();
        songsPath += "\\Songs\\";
        
        //Obtain full file path for songs in all audio formats supported by Unity
        string[] mp3Files = Directory.GetFiles(songsPath, "*.mp3");
        string[] wavFiles = Directory.GetFiles(songsPath, "*.wav");
        string[] oggFiles = Directory.GetFiles(songsPath, "*.ogg");
        string[] aiffFiles = Directory.GetFiles(songsPath, "*.aiff");
        
        //Convert to list, sort alphabetically, then convert back to array of full file paths for all songs
        List<string> filesList = new List<string>();
        filesList.AddRange(mp3Files);
        filesList.AddRange(wavFiles);
        filesList.AddRange(oggFiles);
        filesList.AddRange(aiffFiles);
        filesList.Sort();
        filePaths = filesList.ToArray();

        //List of song names only (exclude full path and extension) for dropdown menu
        List<string> songList = new List<string>();

        //Parse full song paths and add only song names to songList
        foreach(string file in filePaths)
        {
            string[] temp = file.Split('\\');
            string extension = System.IO.Path.GetExtension(file);
            string result = temp[temp.Length - 1].Substring(0, temp[temp.Length - 1].Length - extension.Length);
            songList.Add(result);
        }

        //Add song names to dropdown and select and load first song
        songDropdown.AddOptions(songList);
        selectedSongFullPath = filePaths[0];
        soundManagerScript.SetAudioClip("file:///" + selectedSongFullPath);

        //Get VR controllers
        controllers = GetControllers();

        //Get UI elements to be hidden/unhidden between mode switches and immediately hide BPM mode UI elements
        hideUIObjects = GameObject.FindGameObjectsWithTag("HideUI");
        for (int i = 0; i < hideUIObjects.Length; i++)
            hideUIObjects[i].SetActive(false);

        //Get Front Ropes of Boxing Ring and Front Punching Bags and immediately hide them
        frontObjects = GameObject.FindGameObjectsWithTag("Front Object");
        for (int i = 0; i < frontObjects.Length; i++)
            frontObjects[i].SetActive(false);
    }

    void Update()
    {
        if (controllers.Count == 0)
            controllers = GetControllers();
        
        if (gameBeingPlayed == true)
        {
            UpdateInGameScore();
            CheckForPause();
        }
    }

    //Song dropdown listener - loads audio file of song selected
    public void SetSelectedSong(int songIndex)
    {
        selectedSongFullPath = filePaths[songIndex];
        soundManagerScript.SetAudioClip("file:///" + selectedSongFullPath);
    }

    //Toggle group listener - changes game mode and hides/uhides respective UI elements
    public void SetGameMode(bool newToggleStatus)
    {
        //This listener function is called whenever the user hits the BPM Mode Toggle.  We only care if the toggle state changes
        if (newToggleStatus != bpmMode)
        {
            //Set game mode
            bpmMode = newToggleStatus;

            //Hide/unhide respective UI elements
            if (bpmMode == true)    //BPM Mode
            {
                for (int i = 0; i < hideUIObjects.Length; i++)
                {
                    hideUIObjects[i].SetActive(true);

                    //Preserve beat delay slider value (round to 2 decimal places)
                    if(hideUIObjects[i].name == "FirstBeatDelaySlider")
                        SpawnScript.firstBeatSpawnDelay = Mathf.Round(hideUIObjects[i].GetComponent<Slider>().value * 100f) / 100f;
                }
            }
            else    //Beat Detection Mode
            {
                //No delay since beat detection algorithm will take care potential delays
                SpawnScript.firstBeatSpawnDelay = 0f;
                for (int i = 0; i < hideUIObjects.Length; i++)
                    hideUIObjects[i].SetActive(false);
            }
        }
    }

    //Spawn range slider listener - sets cube spawn range
    public void SetSpawnRange(float newRange)
    {
        float floatValue = 0.75f;   //Need to initialize floatValue to something
        switch(newRange)
        {
            case 1: floatValue = 0.75f; break;
            case 2: floatValue = 1.00f; break;
            case 3: floatValue = 1.25f; break;
        }
        SpawnScript.spawnRange = floatValue;
    }

    //Cube speed slider listener - sets cube speed
    public void SetCubeSpeed(float newSpeed)
    {
        cubeSpeed = (int) newSpeed * -1;
    }

    //Song BPM slider listener - sets song BPM
    public void SetSongBPM(float newBPM)
    {
        BPMTB.text = newBPM + " BPM";
        SpawnScript.spawnTime = 60 / newBPM;
    }

    //First Beat Delay slider listener - sets first beat delay
    public void SetFirstBeatDelay(float newDelay)
    {
        newDelay = Mathf.Round(newDelay * 100f) / 100f; //Round to 2 decimal places
        FirstBeatDelayTB.text = newDelay + "s";
        SpawnScript.firstBeatSpawnDelay = newDelay;
    }

    //Start button listener - starts the game
    public void StartGame()
    {
        gameBeingPlayed = true;
        for (int i = 0; i < frontObjects.Length; i++)
            frontObjects[i].SetActive(true);
        leftController.GetComponent<XRInteractorLineVisual>().enabled = false;
        rightController.GetComponent<XRInteractorLineVisual>().enabled = false;
        mainMenu.SetActive(false);
        inGameMenu.SetActive(true);
    }

    //Exit button listener - quits the application, returning to the desktop
    public void ExitGame()
    {
        Application.Quit();
    }

    //Resume button listener - resumes game
    public void ResumeGame()
    {
        Time.timeScale = 1;
        AudioListener.pause = false;
        for (int i = 0; i < frontObjects.Length; i++)
            frontObjects[i].SetActive(true);
        leftController.GetComponent<XRInteractorLineVisual>().enabled = false;
        rightController.GetComponent<XRInteractorLineVisual>().enabled = false;
        pauseMenu.SetActive(false);
        isPaused = false;
    }

    //Restart button listener - restarts song from beginning
    public void RestartGame()
    {
        //Destroy all spawned boxes
        GameObject[] objects = GameObject.FindGameObjectsWithTag("BoxTag");
        for (int i = 0; i < objects.Length; i++)
            Destroy(objects[i]);
        
        //Restart game
        score = 0;
        Time.timeScale = 1;
        AudioListener.pause = false;
        soundManagerScript.StopSong();
        SpawnScript.ResetTimer();
        SpawnScript.firstBlockPlaced = false;
        SpawnScript.ok2spawn = true;
        for (int i = 0; i < frontObjects.Length; i++)
            frontObjects[i].SetActive(true);
        leftController.GetComponent<XRInteractorLineVisual>().enabled = false;
        rightController.GetComponent<XRInteractorLineVisual>().enabled = false;
        endMenu.SetActive(false);
        pauseMenu.SetActive(false);

        if (gameBeingPlayed == false)   //For Play Again Button
        {
            gameBeingPlayed = true;
            inGameMenu.SetActive(true);
        }
        isPaused = false;
    }

    //Quit button listener - quits the game, returns user to main menu
    public void QuitGame()
    {
        //Destroy all spawned boxes
        GameObject[] objects = GameObject.FindGameObjectsWithTag("BoxTag");
        for (int i = 0; i < objects.Length; i++)
            Destroy(objects[i]);

        //Quit game
        score = 0;
        Time.timeScale = 1;
        gameBeingPlayed = false;
        soundManagerScript.StopSong();
        SpawnScript.ResetTimer();
        SpawnScript.firstBlockPlaced = false;
        SpawnScript.ok2spawn = true;
        AudioListener.pause = false;
        leftController.GetComponent<XRInteractorLineVisual>().enabled = true;
        rightController.GetComponent<XRInteractorLineVisual>().enabled = true;
        inGameMenu.SetActive(false);
        pauseMenu.SetActive(false);
        mainMenu.SetActive(true);
        isPaused = false;
    }

    //Displays the end of song menu when the song is over
    public void EndOfSong()
    {
        GameObject textBox = GameObject.Find("FinalScoreDisplayTB");
        textBox.GetComponent<TMP_Text>().text = "Final Score: " + score;
        for (int i = 0; i < frontObjects.Length; i++)
            frontObjects[i].SetActive(false);
        leftController.GetComponent<XRInteractorLineVisual>().enabled = true;
        rightController.GetComponent<XRInteractorLineVisual>().enabled = true;
        inGameMenu.SetActive(false);
        endMenu.SetActive(true);
        gameBeingPlayed = false;
    }

    //Back to main menu button listener - brings user back to main menu
    public void BacktoMainMenu()
    {   
        score = 0;
        endMenu.SetActive(false);
        mainMenu.SetActive(true);
    }
    
    //Updates text box that displays the user's score during gameplay
    void UpdateInGameScore()
    {
        GameObject textBox = GameObject.Find("InGameScoreDisplayTB");
        textBox.GetComponent<TMP_Text>().text = "Score: " + score;
    }

    //Check primary button on either VR controller - if pressed during gameplay, pause game
    void CheckForPause()
    {
        foreach (var controller in controllers)
        {
            if (controller.TryGetFeatureValue(UnityEngine.XR.CommonUsages.primaryButton, out primaryButtonValue) && primaryButtonValue)
            {
                    if (!isPaused)
                    {
                        VibrateController(controller);
                        PauseGame();
                    }
            }
        }
    }

    //Pauses the game
    void PauseGame()
    {
        Time.timeScale = 0;
        AudioListener.pause = true;

        //Destroy existing particle effects
        GameObject[] objects = GameObject.FindGameObjectsWithTag("Particles");
        for (int i = 0; i < objects.Length; i++)
            Destroy(objects[i]);

        //Stop wrong arm sound if it is playing
        AudioSource[] audioSources = soundManagerScript.GetComponents<AudioSource>();
        if(audioSources[1].isPlaying)
            audioSources[1].Stop();

        //Hide punching bags + ropes
        for (int i = 0; i < frontObjects.Length; i++)
            frontObjects[i].SetActive(false);

        leftController.GetComponent<XRInteractorLineVisual>().enabled = true;
        rightController.GetComponent<XRInteractorLineVisual>().enabled = true;
        pauseMenu.SetActive(true);
        isPaused = true;
    }

    //Find available VR controllers
    List<UnityEngine.XR.InputDevice> GetControllers()
    {
        List<UnityEngine.XR.InputDevice> controllers = new List<UnityEngine.XR.InputDevice>();
        UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.LeftHanded, controllers);
        List<UnityEngine.XR.InputDevice> temp = new List<UnityEngine.XR.InputDevice>();
        UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.RightHanded, temp);
        controllers.AddRange(temp);
        return controllers;
    }

    //Since controller vibration isn't a necessary feature for the game, a try catch is utilized with an empty catch
    public void VibrateController(UnityEngine.XR.InputDevice controller)
    {
        try
        {
            uint channel = 1;
            float amplitude = 1.0f;
            float duration = 0.1f;
            controller.SendHapticImpulse(channel, amplitude, duration);
        }
        catch (System.Exception){}
    }
}

SoundManagerScript.cs

//Author:       Derek Huber (May, 2021)
//Filename:     SoundManagerScript.cs
//Purpose:      Loading, playing, and stopping songs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

public class SoundManagerScript : MonoBehaviour
{
    public static AudioSource songSelectedAudioSource;
    public static AudioSource beatDetectionAudioSource;
    private float timer;
    private bool beatDetectionSourcePlayed = false;
    private bool isPlaying;

    void Start()
    {
        AudioSource[] audioSources = GetComponents<AudioSource>();
        songSelectedAudioSource = audioSources[0];
        beatDetectionAudioSource = audioSources[2];
        isPlaying = false;
        timer = 0;
    }

    void Update()
    {
        if(GameScript.gameBeingPlayed == true)
        {
            timer += Time.deltaTime;

            if (GameScript.bpmMode == false && beatDetectionAudioSource.isPlaying == false && timer > GameScript.gameStartCubeSpawnDelay
                && beatDetectionSourcePlayed == false)   //Play silent beat detection audio ahead of time
            {
                beatDetectionAudioSource.Play();
                beatDetectionSourcePlayed = true;
            }
            if (timer >= (4.35f / GameScript.cubeSpeed * -1) +   //Time taken to be 0.65 units (arm's length) in front of player after delay
                    GameScript.gameStartCubeSpawnDelay && isPlaying == false)
            {
                songSelectedAudioSource.Play();
                isPlaying = true;
            }
        }
        else    //Not in play
        {
            timer = 0;
            isPlaying = false;
            beatDetectionSourcePlayed = false;
        }
    }

    //Starts the Coroutine to load selected audio file
    public void SetAudioClip(string selectedSongFullPath)
    {
        StartCoroutine(LoadSongCoroutine(selectedSongFullPath));
    }

    //Coroutine that does the loading of the audio file
    IEnumerator LoadSongCoroutine(string selectedSongFullPath)
    {
        UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(selectedSongFullPath, AudioType.UNKNOWN);  //Could be mp3, wav, ogg, aiff
        yield return www.SendWebRequest();
        songSelectedAudioSource.clip = DownloadHandlerAudioClip.GetContent(www);
        beatDetectionAudioSource.clip = DownloadHandlerAudioClip.GetContent(www);
        
        GetComponent<MyAudioProcessor>().SetSong();
    }

    //Stops audio sources and resets variables to default state
    public void StopSong()
    {
        timer = 0;
        isPlaying = false;
        beatDetectionSourcePlayed = false;
        songSelectedAudioSource.Stop();
        beatDetectionAudioSource.Stop();
    }
}

SpawnScript.cs

//Author:       Derek Huber (May, 2021)
//Filename:     SpawnScript.cs
//Purpose:      Spawns cubes to the beat (whether in BPM mode or Beat Detection mode)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnScript : MonoBehaviour
{
    public Transform spawnPosition;
    public GameObject redCube;
    public GameObject blueCube;
    public static float timer = 0f;
    public static double spawnTime = 0.5d;          //Time between each beat (60 / BPM)
    public static float firstBeatSpawnDelay = 0f;   //Time before first beat in audio file
    public static float spawnRange = 0.75f;         //Where in space cubes spawn (higher is farther apart)
    public static bool firstBlockPlaced = false;
    public static bool ok2spawn = true;
    public GameScript gameScript;

    //Timer-dependent so FixedUpdate is preferred to stay in sync with music
    void FixedUpdate()
    {
        if (GameScript.gameBeingPlayed == true)
        {
            timer += Time.deltaTime;
            if (firstBlockPlaced == false)
            {
                if (timer >= firstBeatSpawnDelay + GameScript.gameStartCubeSpawnDelay)
                {
                    if (GameScript.bpmMode == true)
                        SpawnObject();
                    ResetTimer();
                    firstBlockPlaced = true;
                }
            }
            else    //first block has been placed
            {
                if (timer >= spawnTime && ok2spawn == true)
                {
                    if (GameScript.bpmMode == true)
                        SpawnObject();
                    ResetTimer();
                    if (SoundManagerScript.songSelectedAudioSource.time > 
                        SoundManagerScript.songSelectedAudioSource.clip.length - (4.35f / GameScript.cubeSpeed * -1))
                        ok2spawn = false;
                }
                else if (timer >= 3 + (4.35f / GameScript.cubeSpeed * -1))   //Game done!
                {
                    ResetTimer();
                    gameScript.EndOfSong();
                    firstBlockPlaced = false;
                    ok2spawn = true;
                }
            }
        }
    }

    //Spawns the cube randomly (random color, side, height, and distance left/right of player (dependent on cube spawn range value from main menu))
    public void SpawnObject()
    {
        //Left or right side (0 == left, 1 == right)
        int side = Random.Range(0, 2);
        float x = Random.Range(0.25f, spawnRange);
        if (side == 0)
            x *= -1;
        
        //Height and distance away from player
        float y = Random.Range(0.75f, 1.75f);
        float z = spawnPosition.position.z;
        spawnPosition.position = new Vector3(x, y, z);

        //Red or blue cube (0 == red, 1 == blue)
        int redOrBlue = Random.Range(0, 2);
        if (redOrBlue == 0)
            Instantiate(redCube, spawnPosition.position, spawnPosition.rotation);
        else
            Instantiate(blueCube, spawnPosition.position, spawnPosition.rotation);
    }

    public static void ResetTimer()
    {
        timer = 0;
    }
}

MoveCube.cs

//Author:       Derek Huber (May, 2021)
//Filename:     MoveCube.cs
//Purpose:      Attached to each cube when they spawn in - moves cubes, tests for collision with VR controller, adjusts score accordingly

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveCube : MonoBehaviour
{
    public GameObject particles;
    private Vector3 cubeVector;
    private bool pastHitZone = false;

    void Start()
    {
        cubeVector = new Vector3(0, 0, GameScript.cubeSpeed);
    }

    void FixedUpdate()
    {
        //Move cube
        transform.Translate(cubeVector * Time.deltaTime);

        //If behind player (1 meter), leave behind particle effect, destroy cube, and reduce score
        if (transform.position.z < -1)
        {
            GameScript.score -= 25;
            Instantiate(particles, transform.position, transform.rotation);
            Destroy(this.gameObject);
        }
    }

    void OnTriggerEnter(Collider triggerObject) 
    {
        if (triggerObject.gameObject.name == "Fist")
        {
            //Find out which controller hit the cube, adjust score accordingly
            List<UnityEngine.XR.InputDevice> controllers = new List<UnityEngine.XR.InputDevice>();
            if (triggerObject.transform.parent.parent.parent.gameObject.name == "Left Controller")
            {
                UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.LeftHanded, controllers);
                if (gameObject.name.Contains("Red Cube"))
                    GoodHit();      //Red arm on red cube
                else
                    BadHit();       //Red arm on blue cube
            }
            else
            {
                UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.RightHanded, controllers);
                if (gameObject.name.Contains("Blue Cube"))
                    GoodHit();      //Blue arm on blue cube
                else
                    BadHit();       //Blue arm on red cube
            }
            
            //Make the controller that hit the cube vibrate
            foreach (var controller in controllers)
                GameObject.Find("GameController").GetComponent<GameScript>().VibrateController(controller);
            
            //Leave behind particle effect
            Instantiate(particles, transform.position, transform.rotation);

            //Destroy cube
            Destroy(this.gameObject);
        }
    }

    //Increases score and applies force to punching bags
    void GoodHit()
    {
        GameScript.score += 10;

        GameObject[] punchingBags = GameObject.FindGameObjectsWithTag("PunchingBagTag");
        Rigidbody rb;
        float force = 0.20f;
        for (int i = 0; i < punchingBags.Length; i++)
        {
            rb = punchingBags[i].GetComponent<Rigidbody>();
            if (punchingBags[i].name == "Punching BagL")
                rb.AddForce(-1f * force, 0, 0, ForceMode.Impulse);
            else if (punchingBags[i].name == "Punching BagR")
                rb.AddForce(force, 0, 0, ForceMode.Impulse);
            else
                rb.AddForce(0, 0, force, ForceMode.Impulse);
        }
    }

    //Plays error sound reduces score
    void BadHit()
    {
        GameObject.Find("Sound Manager").GetComponents<AudioSource>()[1].Play();
        GameScript.score -= 25;
    }
}

MyAudioProcessor.cs

//Author:       Derek Huber (May, 2021)
//Filename:     MyAudioProcessor.cs
//Purpose:      Used for Beat Detection (based on sample data)
//              Based on the "Simple sound energy algorithm #2" found on http://archive.gamedev.net/archive/reference/programming/features/beatdetection/index.html

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyAudioProcessor : MonoBehaviour
{
    //AudioSource attached to GameObject
	public AudioSource audioSource;

    //Sampling variables
    //~0.02 seconds of sound
    private int sampleSize = 1024;
    //Sample rate of audio file (prefer 44100 Hz)
    private int samplingRate;
    //Will be current 1024 samples (stereo)
    private float[] currentSampleSpectrumL;
    private float[] currentSampleSpectrumR;

    //Energy variables
    //Will be how many sample groups in ~1 second (should be 43 for 44100 Hz)
    private int energyGroupAmount;
    //Energy record of last second
    private float[] energyHistory;
    //Average of energyHistory
    private float pastEnergyAvg;
    //Energy from current sample group
    private float instantEnergy;

    //Formula variables
    private float constantC = 1.4f;

    //My other script to finally spawn the objects
    public SpawnScript spawner;

    //Timer variable for beat separation
    private float lastBeatTimer;
    private float MIN_BEAT_SEPARATION = 0.25f;
    

    void Start()
    {
        lastBeatTimer = Time.time;
    }

    void Update()
    {
        if(audioSource.isPlaying && GameScript.bpmMode == false)
        {
            //Get left and right channel data
            audioSource.GetOutputData(currentSampleSpectrumL, 0);
            audioSource.GetOutputData(currentSampleSpectrumR, 1);

            //Calculate instant energy
            instantEnergy = 0f;
            for(int i = 0; i < sampleSize; i++)
                instantEnergy += (currentSampleSpectrumL[i] * currentSampleSpectrumL[i]) + (currentSampleSpectrumR[i] * currentSampleSpectrumR[i]);

            //Compute average energy of the past
            pastEnergyAvg = 0f;
            for(int i = 0; i < energyGroupAmount; i++)
                pastEnergyAvg += energyHistory[i];

            //Total past energy becomes average past energy
            pastEnergyAvg /= (float)energyGroupAmount;

            //Out with the old!
            for(int i = energyGroupAmount - 1; i > 0; i--)
                energyHistory[i] = energyHistory[i-1];
            //In with the new!
            energyHistory[0] = instantEnergy;

            //Beat!
            if (instantEnergy > constantC * pastEnergyAvg && Time.time - lastBeatTimer > MIN_BEAT_SEPARATION)
            {
                lastBeatTimer = Time.time;
                spawner.SpawnObject();
            }
        }
    }

    //Set relevant variables for beat detection according to selected audio file
    public void SetSong()
    {
        samplingRate = audioSource.clip.frequency;
        energyGroupAmount = samplingRate / sampleSize;
        energyHistory = new float[energyGroupAmount];
        currentSampleSpectrumL = new float[sampleSize];
        currentSampleSpectrumR = new float[sampleSize];
        lastBeatTimer = Time.time;
    }
}

DropDownRayCast.cs

//Author:       Derek Huber (May, 2021)
//Filename:     DropDownRayCast.cs
//Purpose:      Rearranges UI sorting order so VR controller raycasts hit the song dropdown menu

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DropDownRayCast : MonoBehaviour
{
    void Awake()
    {
        Canvas canvas = gameObject.GetComponent<Canvas>();
        if (canvas != null)
            canvas.sortingOrder = 3;
    }
}