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: BuildingPart 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.)
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"); } }