What does our Application Manager do?

With the basic overview out of the way, let's get into the heart of our code. We will go through every line of code, though we will only lightly touch some areas.

First, start with the using statements:

using System; 
using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 
using HoloToolkit.Unity; 
using HoloToolkit.Unity.SpatialMapping; 
using HoloToolkit.Unity.InputModule; 

We can access additional functionality outside that of standard C# and .NET libraries through using statements.
Let's move forward with the declaration of ApplicationManager:

Class Declaration 
public class ApplicationManager : Singleton<ApplicationManager>, IInputClickHandler, ISpeechHandler 
{ 

This is the declaration of ApplicationManager. We declare it as a Singleton which, in this case, is a product of using HoloToolkit.Unity; . The HoloToolkit team took the time to implement the Singleton design pattern.

Normally, there is a construction process for creating a Singleton, but the HoloToolkit team decided to simplify it. This class also implements the IInputClickHandler and ISpeechHandler interfaces, which allow us to use air-tap and keyword controls.
The Singleton pattern restricts its own existence to a single instance. One effect this has is providing an access point on a global level for other classes to use. In our case, having a single Application Manager Singleton to control the flow of the whole process keeps our program easy to follow.

Next, we have our variable declarations. These are our references to various GameObjects in the scene and prefabs, followed by the string, ints, and bools that we will use throughout our application:

    public GameObject soundLayer1; 
    public GameObject soundLayer2; 
    public GameObject soundLayer3; 
    public GameObject soundLayer4; 
    public AudioSource skeeballSound; 

    public GameObject skeeballmachine; 
    public GameObject sharingPrefab; 
    public GameObject spatialPrefab; 
    public GameObject spawnPoint; 
    public GameObject skeeballPrefab; 
    GameObject currentBall; 

    public TextMesh debugTextMesh; 
    public TextMesh scoreBoard; 

    public string debugInfoString = ""; 

    public int score = 0; 
    public int throwCount = 0; 

    public bool moveButtonClicked; 
    public bool startButtonClicked; 
    public bool ThrowBall; 
    public bool ballThrown; 
    public bool skeeMachineClicked; 
    public bool hideMoveButton; 
    public bool destroySkeeCollider; 

Following our variables is our Enumerator type declaration AppState. In the declaration, we define ten different states, which will be set as 0-9, due to the first one being declared as 0:

    public enum AppState 
    { 
        SharingInit = 0, 
        ConnectingToServer, 
        SpatialMapping, 
        MapComplete, 
        PlaceSkeeMachine, 
        PlaceSkeeMachineComplete, 
        WaitForInput, 
        StartGame, 
        GameInProgress, 
        EndGame, 
    } 

Here, we declare our AppState variable, myAppState:

    public AppState myAppstate; 

Now it is time to create out methods; we will start with the two standard Unity methods: Start() and Update(). Unity has a collection of others, but these two will always be in any cs file we create from Unity. The Start() method is called on the frame that the associated object is enabled.

In our Start() method, we start simple. In order to accept an air-tap without the need to have a focused object, we are using PushModalInputHandler. This is so when the application finishes scanning the room, the player's air-tap does not require an object to be the focus:

void Start() 
    { 
     InputManager.Instance.PushModalInputHandler(this.gameObject); 
    } 

Next, we have our Update() method and the majority of this class. Update() is called by the Unity Engine every frame.

We will cover this block by block. If you look through the code, you will note #region statements. These statements make it easy to keep important code focused, while hiding code that is finished or of lower priority at the time:

void Update() 
    { 
        #region  IfStatements 
        if (scoreBoard != null) 
        { 
            scoreBoard.text = score.ToString(); 
        } 

Here, in the first if statement, we are checking to ensure that our scoreBoard object is not null and then we update the board with the current score. Convert the score integer to a string in the process:

if (moveButtonClicked && myAppstate != AppState.PlaceSkeeMachine)
{
myAppstate = AppState.PlaceSkeeMachine;
moveButtonClicked = false;
}

In the second if statement, we are checking to see whether the move button has been air-tapped and that the current myAppstate is not Appstate.PlaceSkeeMachine. If those conditions are not met, we move on. Otherwise, we move into the PlaceSkeeMachine state:

        if (startButtonClicked) 
        { 
            myAppstate = AppState.StartGame; 
            startButtonClicked = false; 
        } 

The final statement in the block is waiting on the startButtonClicked bool to be true, which occurs when the player air-taps the coin slot on the machine. When that happens, the AppState is changed to StartGame and the game begins:

        #endregion 

Now we have reached our switch statement; this is the biggest block of code in the project and we will cover it case by case.

It is worth noting that due to this switch statement being in the Update() function when a particular state is active, the code for that state will run repeatedly until the state is changed. This can cause issues. To resolve this, most of the states make a change and then change to a different state:

        #region  Switch 
        debugTextMesh.text = debugInfoString; 
          switch (myAppstate) 
        { 
            #region Connection and Spatial Mappng stuff 
            case AppState.SharingInit: 
                { 
                    sharingPrefab.SetActive(true); 
                    myAppstate = AppState.ConnectingToServer; 
                } 
                break; 

This state activates the sharingPrefab and then changes the state to AppState.ConnectingToServer:

            case AppState.ConnectingToServer: 
                { 
                    //need a test to determine that the user is connected. 
                    myAppstate = AppState.SpatialMapping; 
                } 
                break; 

After Chapter 8, Share What You Have Got, I stopped using the server due to testing issues and time, so currently the only thing this case does is to go to AppState.SpatialMapping:

            case AppState.SpatialMapping: 
                { 
                    spatialPrefab.SetActive(true); 
                } 
                break; 

This case is again simple. It sets the spatialPrefab to active. Having it constantly SetActive(true) is not optimal by any means, but does not interrupt the flow:

            case AppState.MapComplete: 
                { 
                    SyncSpawnSkee.Instance.SyncSpawnSkeeBallMachine(); 
                    skeeballmachine = GameObject.FindGameObjectWithTag("Skee"); 
                    spawnPoint = GameObject.FindGameObjectWithTag("SpawnPoint"); 
                    scoreBoard = GameObject.FindGameObjectWithTag("ScoreBoard").GetComponent<TextMesh>(); 
                    debugInfoString = "If you need to move the Skeeball 
 Machine, select the Move Machine menu 
 option. Otherwise, Air-Tap the 
 Red Coin Slot."; 
                    myAppstate = AppState.WaitForInput; 
                } 
                break; 
In a Unity scene, objects and prefabs cannot be linked to each other. So, in order to get references to a prefab that was just spawned, we have to use various types of search function.

In this state, which is activated when the user air-taps after the spatial map is finished:

  • We spawn the Skeeball machine
  • We assign the Skeeball machine a variable using the Skee tag we assigned to it to help find the variable
  • We assign the spawnPoint for the gameBall to the spawnPoint variable in the same manner
  • The scoreboard's TextMesh component is assigned to the scoreboard variable
  • debugStringInfo is updated with instructions for the user
  • Finally, the AppState is changed to AppState.WaitForInput
            
            case AppState.PlaceSkeeMachine: 
                { 
                    // Plae SkeeballMachine.cs should be working right now. 
                    debugInfoString = "Air-Tap on the Skeeball 
 Machine to move it. 
 Once you find a spot 
 you like Air-Tap again 
 to place the machine."; 
                    if (moveButtonClicked) 
                    { 
                        myAppstate = AppState.PlaceSkeeMachineComplete; 
                        moveButtonClicked = false; 
                    } 
                } 
                break; 

In this case, the user is actively moving the Skeeball machine around with their gaze. We change the variable debugInfoString to give the user instructions and then wait for the user to hit the moveButton a second time. When the user does hit the button, we change the state to PlaceSkeeMachineComplete, as shown in the following code:

            case AppState.PlaceSkeeMachineComplete: 
                { 
                    debugInfoString = ""; 
                SpatialMappingManager.Instance.GetComponent<ChangeLayerOfChildren>().enabled = true; 
                } 
                break; 

In the PlaceSkeeMachineComplete state, we set the debugInfoString to an empty string and then enable the ChangeLayerOfChildren component of the SpatialMappingManager :

            
            case AppState.WaitForInput: 
                { 
                } 
                break; 

There is nothing in the WaitForInput state; this is an idle state that is activated while we wait on the player to click on the start button, as shown in the following code:

             
            case AppState.StartGame: 
                { 
                    StartCoroutine(StartGame()); 
                } 
                break; 

The StartGame case runs the StartCoroutine(), as shown in the following code:

            
            case AppState.GameInProgress: 
                { 
                    if (ballThrown) 
                    { 
                        if (throwCount <= 10) 
                        { 
                            StartCoroutine(DeleteCurrentSpawnNew()); 
                            skeeballSound.Play(); 
                            ballThrown = false; 
                            throwCount += 1; 
                        } 
                        else 
                        { 

                            StartCoroutine(DeleteCurrent()); 
                            ballThrown = false; 

                            myAppstate = AppState.EndGame; 

                        } 
                    } 
                } 
                break; 
            #endregion 

This case is where the game logic happens, which is really simple. If the ballThrown bool becomes true, it verifies that the throwcount is not 10 or more. If that is the case, the ball is thrown in the ThrowBall class, so this case runs a Coroutine that counts to 3, deletes the ball, and spawns a new one. Then, it triggers the sound effect, increments the throwcount, and resets the ballThrown trigger. If ballThrown is 10 or greater, a Coroutine that counts to 3 and deletes the ball runs. This time, the ball is not respawned. The ballThrown trigger is reset and the AppState is set to EndGame:

 #region EndGame 
            case AppState.EndGame: 
            {
              soundLayer1.GetComponent<AudioSource>().volume = 0.2f;
              soundLayer2.GetComponent<AudioSource>().volume = 0.2f;
              soundLayer3.GetComponent<AudioSource>().volume = 0.2f;
              soundLayer4.GetComponent<AudioSource>().volume = 0.2f;
              debugInfoString = "Game Over";
              myAppstate = AppState.WaitForInput;
            }
            break;
            #endregion
           default:
           {

           }
           break;
        }

In the EndGame case, all the atmospheric sounds are set to a much lower volume, the debugInfoString is set to Game Over, and the AppState is set back to WaitfrInput:

#endregion
}

With Update(), the hard part is out of the way; the rest of this should be a breeze. SpawnBall() instantiates a ball into the game world and assigns it to the currentBall variable, as shown in the following code:

  void SpawnBall() 
    { 
        currentBall = Instantiate(skeeballPrefab, spawnPoint.transform.position, spawnPoint.transform.rotation); 

    } 

 The primary reason for this assignment is that the ball can be deleted when it is not needed. 
Let's call the SetSpatialMap() method:

    void SetSpatialMap() 
    { 
        SpatialMappingManager.Instance.DrawVisualMeshes = false; 
        InputManager.Instance.PopModalInputHandler(); 
        myAppstate = AppState.MapComplete; 

    } 

SetSpatialMap() is called when the user air-taps to confirm that the spatial map is complete. We set the visibility to false, then we pop the ModalInputHandler off the stack, and set the AppState to MapComplete.

Pop and Push are terms that come from assembly programming. In assembly, you are working directly with processor registers that work using a stack. Last In First Out -- LIFO is how a stack works. Push is adding something to the stack, Pop is taking something off the stack. One thing this does afford us, as developers, is the ability to order changes in the various input modes:

    IEnumerator DeleteCurrentSpawnNew() 
    { 
        yield return new WaitForSeconds(3.0f); 
        Destroy(currentBall); 
        Debug.Log("Push-DeleteCurrntSpawnNew"); 
        InputManager.Instance.PushModalInputHandler(this.gameObject); 

        yield return new WaitForSeconds(0.5f); 
        SpawnBall(); 
    } 

This method is an IEnumerator type, or a Coroutine as it is commonly referred to. What makes this different from your typical method is that it has the ability to be paused, a very useful ability in this type of programming. Here, we wait for 3 seconds and then destroy the currentBall. We then push a new ModalInputHandler onto the stack, wait for half a second, and call the SpawnBall() method, as shown in the previous code.

    IEnumerator DeleteCurrent() 
    { 

        yield return new WaitForSeconds(3.0f); 
        Destroy(currentBall); 

        yield return new WaitForSeconds(0.5f); 
        myAppstate = AppState.EndGame; 


    } 

This is similar to the preceding WaitForSeconds() method in that it waits for 3 seconds and deletes the currentBall. This time though, it waits for half a second and sets the AppState to EndGame, as shown in the following code:

    IEnumerator StartGame() 
    { 
        yield return new WaitForSeconds(1.5f); 
        debugInfoString = ""; 
        SpatialMappingManager.Instance.GetComponent<ChangeLayerOfChildren>().enabled = true; 
        if (currentBall != null) { DeleteSkeeBalls(); } 
        throwCount = 0; 
        score = 0; 

        if (soundLayer1.activeInHierarchy) 
        { 
            soundLayer1.GetComponent<AudioSource>().volume = 1f; 
            soundLayer2.GetComponent<AudioSource>().volume = 1f; 
            soundLayer3.GetComponent<AudioSource>().volume = 1f; 
            soundLayer4.GetComponent<AudioSource>().volume = 1f; 
        } 
        else 
        { 
            soundLayer1.SetActive(true); 
            soundLayer2.SetActive(true); 
            soundLayer3.SetActive(true); 
            soundLayer4.SetActive(true); 
        } 
        SpawnBall(); 
        Debug.Log("Push-StartGame"); 
        InputManager.Instance.PushModalInputHandler(this.gameObject); 
        myAppstate = AppState.GameInProgress; 

    } 

There is one last Coroutine to start the game: it waits for 1.5 seconds, sets the debugInfoString to empty, and then enables the ChangeLayerOfChildren. As there should not be any game balls active at the start of the game, it checks whether currentBall is not null and if that condition is true, it deletes the ball.

Throwcount and score are both set to 0. The ambient sounds are all turned to 1.0f and set to active. The SpawnBall() function is called. Once more, we push the PushModalInputHandler method and set the AppState to GameInProgress:

    public void DeleteSkeeBalls() 
    { 

        List<GameObject> skeeballs = new List<GameObject>(GameObject.FindGameObjectsWithTag("SkeeBall")); 
        if (skeeballs != null) 
        { 

            foreach (GameObject go in skeeballs) 
            { 
                Destroy(go); 

            } 
        } 

    } 

This method creates a collection known as a List, searches the scene for any active objects with the SkeeBall tag, and then iterates through the list deleting each object:

    public void OnInputClicked(InputClickedEventData eventData) 
    { 
        Debug.Log("OnInput"); 
        if (myAppstate == AppState.SpatialMapping) 
        { 
            SetSpatialMap(); 
        } 
        if (myAppstate == AppState.GameInProgress) 
        { 
            Debug.Log("Pop-DeleteCurrntSpawnNew"); 
            InputManager.Instance.PopModalInputHandler(); 
            ThrowBall = true; 
            ballThrown = true; 
            Debug.Log("ThrowBall is true"); 
        } 

    } 

Whenever there is an air-tap, this method is called. Depending on the current AppState, it will function differently. If this AppState is SpatialMapping, the method calls SetSpatialMap. If the AppState is Game In Progress, we pop the current PopModalInputHandler() method from its stack and then trigger both ThrowBall and ballThrown by setting them to true, as shown in the preceding code.

public void OnSpeechKeywordRecognized(SpeechKeywordRecognizedEventData eventData) 
    { 
        if (myAppstate == AppState.SpatialMapping) 
        { 
            switch (eventData.RecognizedText.ToLower()) 
            { 

                case "done": 
                    SetSpatialMap(); 
                    break; 
            } 
        } 
    } 
} 

In the final method, we are checking for speech; when the word done is recognized and the current AppState is SpatialMapping, the SetSpatialMap() method is called.

With that, we have covered the entire ApplicationManager class. My hope is that, even if you have no intention of ever writing code, now you can look at that code and have a solid understanding of what is going on under the hood. With some time and patience, you can eventually make changes that work and possibly even write your own code one day soon.

Always keep in mind that most programmers start off modifying other people's code. It's a modern rite of passage.

Now, it is time to learn how to fix the problems that arise when we can't see every consequence to the code we write. It is time to learn about debugging, what it is, how it can help us, and what tools are available to make it easier.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.162.247