MainGameScript


Here, we're going to be looking a bit more in depth at what the GameScript does and how it works.

The purpose of the game script is to control the game as a whole, which for this game involves triggering the moles and detecting when they have been 'whacked', amongst other things.

You can see the complete version of the script here, but for discussion purposes we will break it down into separate parts.

Overview

If you have previously looked at the MoleScript code, you will remember that most of the code had been written as Coroutines to manage the flow of the animation, and that we didn't use the Unity 'Update' function at all. In MainGameScript, we are still going to use Coroutines, but in addition to that we will be using the more traditional 'Update' to give us the behaviour we want.

Class Variables

private List moles

We need some way of holding our mole objects (or MoleScript objects to be exact). We could use an array, but there aren't that many moles and using a list will make things slightly easier.

private bool gameEnd

A simple boolean value which tells us if the game has ended.

private int score

Holds the player's score.

private int timeLimitMS

The upper limit, in milliseconds, before we trigger another mole.

private int moleLimit

The maximum amount of moles that can be active at any one time.

public Camera gameCam

Holds the game camera.

public tk2dSpriteAnimator dustAnimator

This holds the 'BigDust' animated sprite object we created earlier in the game.

private static MainGameScript instance

Finally, we have a static variable of type MainGameScript. If you aren't familiar with static variables it basically means that the variable will be created once and will remain for the duration of the program, and it belongs to the class rather than an instance of the class. We can use the variable to treat the class as a 'singleton'; we will only ever want one instance of this class to exist at any time.

We could make the MainGameScript instance variable public and allow other classes just to access it through 'MainGameScript.instance', but I like to keep variables private as much as possible. So we make the variable private and create an 'Instance' attribute, that checks to see if 'instance' and write to the error log if it hasn't - this should never happen so we need to know about it if it does. If the instance does exist however (which it should), then return it.

   public static MainGameScript Instance
    {
        get
        {
            if(instance == null)
            {
                Debug.LogError("MainGameScript instance does not exist");       
            }
            return instance;   
        }
    } 

Normally, we could ensure that only one instance of the class gets created by making the constructor private:

   private MainGameScript() { }

But as you are unable to create instances of classes that inherit from MonoBehaviour by use of the 'new' keyword, this isn't necessary.

Functions

void Awake()

The first thing we do in the class is to assign the instance of the class to the 'instance' variable. This will allow other classes to use access the instance through the 'Instance' attribute.

   void Awake()
    {
        instance = this; 
    }

IEnumerator Start()

The reason why we're making the Start function return an IEnumerator is because it is going to be ran as a Coroutine. And the reason we're going to do that is we want to ensure that all other game objects have been set up by the time we call the MainGameLoop function.

We start off by initializing a few of the member variables. We set the maximum time between triggering moles to be 3 seconds (timeLimitMS) and we set the maximum amount of moles that can be active at any one time to be 3. Both of these can be changed to what you prefer.

   IEnumerator Start () 
    {
        gameEnd = false;
        timeLimitMS = 3000;
        score = 0;
        moleLimit = 3;

Then we call the yield statement which passes control back, but also let's us continue from this point in the function next frame. By the next frame, all other game objects should have been set up and initialized so it will be safe to call our MainGameLoop.

       yield return 0;  // wait for the next frame!
        
        dustAnimator.gameObject.SetActive(false);
        StartCoroutine(MainGameLoop());
    }

void Update()

We are primarily using the Update function to ascertain whether a mole has been legitimately whacked, and to trigger the dust animation if it has been (not that there's any reason whacking a mole would result in a lot of dust, but we have the animation so might as well use it).

In order to see if a mole has been whacked, we are going to use Unity's Ray and RayCast functionality. We could do this ourselves by getting mouse positions, checking sprite positions, collision boxes etc. but the Ray stuff makes things a whole lot easier.

Firstly we check to see if the mouse button has been clicked as that's what we are using to whack the moles. If that is true then we continue on to see if the click was within the mole's collision box.

For more in depth details about the Ray functionality we're using, you can look view the various Unity documentation pages here, here, here, and here.

In a nutshell, however, we convert the point at which the mouse was clicked into a 'Ray' that fires into the screen, and check to see if that ray hits any colliders in the scene. If it does, we then check which mole the collider belongs to. We then call the 'Whack' function on that mole, which in turn changes the sprite, and then call the 'CallAnim' function which will move our dust animated sprite to the correct location on screen and trigger the animation.

   void Update()
    {
        if(Input.GetButtonDown ("Fire1"))
        {
            Ray ray = gameCam.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit = new RaycastHit();
            
            if(Physics.Raycast(ray, out hit))
            {
                foreach(MoleScript mole in moles)
                {
                    if(mole.sprite.gameObject.activeSelf && mole.ColliderTransform == hit.transform)
                    {
                        mole.Whack();
                        StartCoroutine(CallAnim(mole));
                    }
                }
            }
        }

private IEnumerator MainGameLoop()

Again, by harnessing the power of Coroutines, our main game loop is short and simple.

We start of by creating and initializing a variable which holds, in seconds, how long the player has to hit the mole. To make the game more challenging, we will be reducing this amount of time by one hundredth of a second for each mole that appears. We also set up a variable to hold which mole we have randomly picked to be triggered.

The while loop cycles around until the game has ended. It waits until it is okay to trigger a mole from its hole by checking that we aren't at our maximum amount of active moles. We wait for a random amount of time between 1 seconds and timeLimitMS/1000 seconds (which we had previously set to 3000), then randomly pick a mole, check that it isn't already active (and if it is, pick another one), and then trigger it. Finally, to make the game more difficult as it progresses we reduce the amount of time the player has to hit the mole.

   private IEnumerator MainGameLoop()
    {
        float hitTimeLimit = 1.0f;
        int randomMole;
        
        while(!gameEnd)
        {
            yield return StartCoroutine(OkToTrigger());
            yield return new WaitForSeconds((float)Random.Range(1, timeLimitMS) / 1000.0f);

            // Check if there are any free moles to choose from
            int availableMoles = 0;
            for (int i = 0; i < moles.Count; ++i) {
                if (!moles[i].sprite.gameObject.activeSelf) {
                    availableMoles++;
                }
            }

            if (availableMoles > 0) {           
                randomMole = (int)Random.Range(0, moles.Count);
                while(moles[randomMole].sprite.gameObject.activeSelf)
                {
                    randomMole = (int)Random.Range(0, moles.Count);
                }
                    
                moles[ randomMole ].Trigger(hitTimeLimit);
                hitTimeLimit -= hitTimeLimit <= 0.0f ? 0.0f : 0.01f;   // Less time to hit the next mole
            }

            yield return null;
        }
    }

private IEnumerator OkToTrigger()

There's not much to be said about this function, except that it loops around until we are under the limit for the amount of active moles we're allowed in the game, at which point it lets the MainGameLoop function continue on.

   private IEnumerator OkToTrigger()
    {
        int molesActive;

        do
        {
            yield return null;
            molesActive = 0;
            
            foreach(MoleScript mole in moles)
            {
                molesActive += mole.sprite.gameObject.activeSelf ? 1 : 0;
            }
        }
        while(molesActive >= moleLimit);

        yield break;
    }

private IEnumerator CallAnim(MoleScript mole)

This function handles the dust animation that is played after the mole is hit and goes back into its hole. We're making it a co-routine as we want to add a slight delay before it is played in order to see the change in the mole sprite ('normal' sprite to 'hit' sprite) and it also helps us when using the Instantiate functionality.

As we just created one dust animated sprite in our game and there may be more than one animation required to be on screen at any one time, we create a clone of our dust animated sprite using the Instantiate Unity function. Because we are calling this function as a Coroutine, the clones we create will all be independant of each other even though they have the same name (NewAnimator). This makes things much easier as we don't have to manage them at all.

After we've created the clone (using the position of the mole to place the new animated sprite), we set it to active and start to play it. A while loop is used to check when the animation has finished, again making good use of 'yield', then finally destroying the clone after the animation has finished.

Remember to destroy the gameObject of the clone you created, not just the object itself!

   private IEnumerator CallAnim(MoleScript mole)
    {
        yield return new WaitForSeconds(0.25f);
        
        tk2dSpriteAnimator newAnimator;
        newAnimator = Instantiate(dustAnimator, new Vector3(mole.transform.position.x, mole.transform.position.y,
                        dustAnimator.transform.position.z), dustAnimator.transform.rotation) as tk2dSpriteAnimator; 
        newAnimator.gameObject.SetActive(true);
        newAnimator.Play("DustCloud");
        
        while(newAnimator.IsPlaying("DustCloud"))
        {
            yield return null;  
        }
        
        Destroy(newAnimator.gameObject);
    }

public void RegisterMole(MoleScript who)

As mentioned in the MoleScript summary page, we are giving each mole object its own responsibility for registering itself with the MainGameScript class. It makes the code a lot simpler. Each MoleScript calls this function, and the object gets added to the list.

   public void RegisterMole(MoleScript who)
    {
        moles.Add(who);
    }