Day 15: Building

Today we took care of some outstanding issues with our game – the use of TextMeshPro was revisited (this time working properly on my machine), we added the explosion effect prefab to the enemy objects, we finished the missing implementation of our “SwapFrames( )” function, and we looked at the build process for Unity, which turns our games from projects in an editor into standalone applications.

Read more: Day 15: Building

Part 1: Telling the children to do things…

“Children never listen…” – it’s a common thought amongst parents, and while not entirely true, can certainly feel that way sometimes. But in the case of game objects, children will listen, if you know how to talk to them.

Today in class we implemented our missing “Swap Frames” function. Each of our enemy objects has the “Enemy” script component, and within that exists SwapFrames( ). We made that function public so that this can be accessible from the outside, and then we looked at 3 different methods to tell our children to do something, which we placed inside of a void function in our MotherShip script named “TellTheChildrenSwapFrames( )”.

Method 1 : Use a “foreach” call

        Enemy[] enemies = GetComponentsInChildren<Enemy>();

        foreach (Enemy enemy in enemies)
        {
            enemy.SwapFrames();

        }

We used this method previous to apply our explosion to each object. First, we declare an array to hold our Enemy script components, and use GetComponentsInChildren<Enemy>( ) to fetch all instances of the Enemy component that are contained in the child objects of this. Note that this function is plural ( components will return an array of all children including grandchildren. “component” singular will return the first instance that it finds)

Next, we call foreach and tell each enemy object to swap frames. Simple enough, and we know these are all valid references to enemy component because that’s what the array is made of.

Method 2 : Send a Message

        int enemies_left = transform.childCount;

        for (int i = 0; i < transform.childCount; i++)
        {
            transform.GetChild(i).SendMessage("SwapFrames");
        }

Let’s say we ONLY want to grab the children, not the grandchildren. In that case, accessing the children through Transform.GetChild( ) makes the most sense. Similar to our bomb dropping script, we use Transform.childCount to fetch the number of current children (which is going to change as we destroy these game objects). We then execute a for loop to get each child instance.

At this point we have some decisions to make. We could execute a GetComponent<Enemy> on each child object, and tell it to run SwapFrames( ). But what if there are child objects that do not have the Enemy script component? We would have to run a check for null, otherwise we may hit a Null Reference Exception at which point our script just stops executing and moves on.

A better method here might be to use the SendMessage( ) function, which allows us to tell all components of a game object to execute the named method, if they have it. This solves our null problem because objects without the Enemy script will hear the message and ignore it, as none of their components have public functions with that name.

NOTE: Be careful when naming a functions that you intend to call with SendMessage( ) – you want to make sure the name is distinct / unique. A generic function name like “Stop” may activate your Play method, but it will also activate any Stop method in other components like AudioSource.

Method 3 : Broadcast a Message

BroadcastMessage("SwapFrames");

The final method that we looked at was also the simplest – what if we could avoid the search for children and just run SendMessage( ) on every child and grandchild? For this we used the GameObject.BroadcastMessage( ) method, which lets us do that, sending our message to every object in our children and their children, and so on. This method is convenient, but also potentially expensive as the more children you have in your object’s lineage, the more calls it must make, so it is recommended to use this sparingly. But in situations like this, where I need x number of child objects to do a thing, this can be quite the time saver.


Part 2: Build Process

When we “build” the game, Unity will combine all of our code, all of the assets and packages that we use in the game, and all of it’s underlying systems and combine them into an application (if you are running MacOS) or an executable and a series of support files and folders (if you are running on Windows). Unity is also capable of building to other platforms such as Linux, Android, iOS, PS5, XBox, and HTML5. That’s one of the advantages of working with Unity – the platform specific instructions and abstractions are already built in, making it easy to publish cross platform games.

When you are ready to publish your game, you will want to open the Build Settings panel (File > Build Settings).

The first thing to do is to add your current scene to the build, by clicking the Add Open Scenes button. This will bring your current scene into the build process. Games can have many scenes, but the build process will only include those that are listed in the Scenes in Build panel above. Also note the number to the right – these scenes have an index number defined by order in this process, that can be used to jump to the next or previous scene. We will see this in the next project when we get into Scene Management.

Next, check your Platform Settings to make sure you are building towards the appropriate target. This will be set to the platform you targeted when opening the Editor (the Target setting in Unity Hub). If you change this now, Unity may need to reload the project. For this project, use whatever platform you are currently on – Windows or Mac. While it is possible for one platform to build for the other, we will not get into that in this class.

One final step – open the Player Settings panel by clicking the button in the lower left. This will open up the Project Settings panel to the Player Settings tab. The important items to know about in here are the Company and Product name, the Icon, and the Resolution. By default, applications are set to run as Fullscreen.

Once you are ready to publish your game and your settings are correct, click the Build or Build and Run button. Unity will ask you for a location to save this. If you are building for the Windows platform, you will want to create a new folder to contain the various files.

Then you’ll have to wait for a few moments while Unity does its thing.  Now is the time where your game engine takes all of your code and objects and compiles them into raw game code and zipped binaries of data.  Often this process is short, mostly just compressing files, but if you generating baked lighting and light/reflection probes as part of the build process this may extend the time required to complete this.

The end result will be either an Application (Mac) or an Executable (Windows) .  If you publish for Windows, you will get an “.exe” file and a “Data” folder. (And a few other folders… and a UnityCrashHandler.exe)   You need to include these files and folders as they contain contain relevant components – for instance, “Data” holds all of your assets.   (Apple users don’t have to worry about this, as both their executable and data are both inside the “application” container.)

All of these files must be included in your build submission. Zip up the parent folder.

And that’s it!  Now you have a self contained game.  Congratulations!


Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour
{
    public GameObject enemyBombPrefab;

    // animated frames
    public GameObject FrameA;
    public GameObject FrameB;
    public GameObject FrameExplode;

    private void Awake()
    {
        // set up the frames
        FrameA.SetActive(true);
        FrameB.SetActive(false);
        FrameExplode.SetActive(false);
    }

    private void Update()
    {
        /*
        if (Input.GetKeyDown(KeyCode.B))
        {
            DropABomb();
        }

        if (Input.GetKeyDown(KeyCode.F))
        {
            SwapFrames();
        }

        if (Input.GetKeyDown(KeyCode.E))
        {
            Explode();
        }

        */

    }

    public void SwapFrames()
    {
        // invert the frame states
        FrameA.SetActive(!FrameA.activeSelf);
        FrameB.SetActive(!FrameB.activeSelf);
    }

    private void Explode()
    {
        // turn off parent collider
        GetComponent<Collider>().enabled = false;

        // activate the explosion frame
        FrameA.SetActive(false);
        FrameB.SetActive(false);
        FrameExplode.SetActive(true);

        // find all of the rigidbodies in FrameExplode
        Rigidbody[] cubes; // define the array
        cubes = FrameExplode.GetComponentsInChildren<Rigidbody>();

        // apply an explosive force to each
        foreach (Rigidbody rb in cubes)
        {
            rb.AddExplosionForce(650f, (FrameExplode.transform.position + (2 * Vector3.back)), 10f);
        }

        // set the explosion frame to the world.
        FrameExplode.transform.parent = null;

        Destroy(FrameExplode, 5.0f);
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.transform.tag == "PlayerBullet")
        {
            // make the explosion noise
            SoundManager.S.MakeEnemyExplosionSound();

            // call the explosion
            Explode();

            // destroy myself
            Destroy(this.gameObject);

            // destroy the thing that hit me
            Destroy(collision.gameObject);
        }
    }

    public void DropABomb()
    {
        // make the bomb
        Instantiate(enemyBombPrefab, transform.position + Vector3.down, Quaternion.identity);

    }

}

MotherShip.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MotherShip : MonoBehaviour
{
    public int stepsToSide;
    public float sideStepUnits;
    public float downStepUnits;
    public float timeBetweenSteps;

    public float timeBetweenBombs;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    
    public void StartTheAttack()
    {
        StartCoroutine(MoveMother());
        StartCoroutine(SendABomb());
    }

    public void StopTheAttack()
    {
        StopAllCoroutines();
    }

    public IEnumerator MoveMother()
    {
        // define the side step vector
        Vector3 moveVector = Vector3.right * sideStepUnits;

        // repeat the movement while this object has children
        while (transform.childCount > 0)
        {
            // move to the side (sequence)
            for (int i = 0; i < stepsToSide; i++)
            {
                // move to the side
                transform.position = transform.position + moveVector;

                // swap the frames
                TellTheChildrenSwapFrames();

                // wait for the next move
                yield return new WaitForSeconds(timeBetweenSteps);
            }

            // swap the frames
                TellTheChildrenSwapFrames();

            // now move down
            transform.position += (Vector3.down * downStepUnits);
            yield return new WaitForSeconds(timeBetweenSteps);

            // flip the direction
            moveVector *= -1.0f;

        }

        SoundManager.S.StopTheMusic();

        Debug.Log("End of MoveMother script reached");

    }

    public IEnumerator SendABomb()
    {
        // set the running flag
        bool isRunning = true;

        while(isRunning)
        {
            // see how many child objects exist
            int enemyCount = transform.childCount;

            // if there are children
            if (enemyCount > 0)
            {
                // pick one at random
                int enemyIndex = Random.Range(0, enemyCount);

                // get the chosen object
                Transform thisEnemy = transform.GetChild(enemyIndex);

                // make sure this has the Enemy component
                Enemy enemyScript = thisEnemy.GetComponent<Enemy>();

                if (enemyScript)
                {
                    // drop a bomb
                    enemyScript.DropABomb();
                }


            }
            else
            {
                // there are no children
                isRunning = false;
            }


            // wait for that time
            yield return new WaitForSeconds(timeBetweenBombs);
        }

    }

    private void TellTheChildrenSwapFrames()
    {
        /*
        // get all the children
        Enemy[] enemies = GetComponentsInChildren<Enemy>();

        foreach (Enemy enemy in enemies)
        {
            enemy.SwapFrames();

        }
        */
        /*
        int enemies_left = transform.childCount;

        for (int i = 0; i < transform.childCount; i++)
        {
            transform.GetChild(i).SendMessage("SwapFrames");
        }
        */

        BroadcastMessage("SwapFrames");

    }

}