Today we took a closer look at the “parent-child” relationships of game objects, and how we could use this to coordinate our movement by “inheriting” a transform from the parent object.
Part 1: Enemy Movements (Inheritance & Coroutines)
A good Space Invaders game needs to feature rows and rows of aliens moving in perfect unison with one another. (That synchronization and slow beeping noise is super-creepy!) While we could spend time telling each and every one of our aliens to move to a new position every time we hit a new beat, there is a faster path – they can inherit the motion from a parent object.
We create an empty object that we will call “Mother”, and we make our enemy objects children of this by dragging their listing on the hierarchy onto the “Mother” object, thus making them child objects, which you can tell because “Mother” now has a collapsible arrow next to it.
Next, we create a new script – MotherShip.cs – that we will add to the Mother Object. The goal of this script is to move the fleet in that snaking pattern that we know so well. The enemy fleet will march in a pattern slowly downward until one of three things occurs which will end the current round:
- The player destroys all of the enemy objects
- An enemy object destroys the player
- An enemy object touches the ground
The downward march has four distinct moves, each happening in a series of rhythmic steps:
A) The objects move to the right in X number of steps
B) The objects move down on the next step
C) The objects move to the left in X number of steps
D) The objects move down on the next step
Since we do not know the precise measurements we will want to use yet, we expose the variables we will use to loop through these movements.
public int stepsToSide; public float sideStepUnits; public float downStepUnits; public float timeBetweenSteps;
We have stepsToSide which will determine how many steps will be used to travel parts A and C. We next have sideStepUnits and downStepUnits which we will use as a measure of how far to proceed when moving sideways or downward. And finally we have timeBetweenSteps, which will hold the interval in seconds between steps. We make this a variable instead of a hard coded value because later we will want to increase the speed of the attack as the alien force’s numbers are depleted.
Next, we are ready to create the coroutine MoveMother( ). This function will move laterally for the designated number of steps, then move down for one step, pausing in between each movement. For now, we will continue this cycle so long as our Mother object has children. This is for testing and debugging purposes. Later, we will perform a faster detection of how many objects remain as children and end the round as soon as that number hits zero, calling an end to the coroutines with the StopCoroutine( ) command.
Here is the script that we created:
public IEnumerator MoveMother() { // define our side step vector Vector3 moveVector = Vector3.right * sideStepUnits; // repeat this while (transform.childCount > 0) { // side move sequence for(int i = 0; i < stepsToSide; i++) { // move to the side transform.position = transform.position + moveVector; // wait for the next move yield return new WaitForSeconds(timeBetweenSteps); } // move down transform.position += (Vector3.down * downStepUnits); yield return new WaitForSeconds(timeBetweenSteps); // flip the direction moveVector *= -1; } }
First, we define a Vector3 object that will serve as the offset for our lateral step – defining how far we should move. We define this by multiplying a Vector3.right (which returns [1,0,0]) with our sideStepUnits value to create a our offset vector.
The reason we declare this vector is that it can be applied directly to the transform.position through addition. We cannot directly set a value inside the position, so we could not use a statement like:
transform.position.x += 1.0f; // this won't work
… but we can change the position by returning a Vector3 type, so this statement is possible:
transform.position += offsetVector; // this will work
Now that we have our offset, we set up the while( ) loop with a condition that the Mother transform maintains a child count greater than zero.
Inside that loop, we have a three step process:
- Move by the offset value stepsToSide times… which we do by creating a simple “for” loop that moves us and then waits timeBetweenSteps before moving on to the next step.
- Move the transform position down by adding a vector of Vector3.down * downStepUnits, and waiting another interval of timeBetweenSteps
- Reversing the direction of the offset vector by multiplying it against -1
Step 3 is our real shortcut here. Because we are simply reversing the movement, we don’t have to designate the left direction. Instead of writing this out as [A] -> [B] -> [C] -> [D], we made this [A] -> [B] -> [-A] -> [B].
Now our enemies advance, and we can adjust the values for what makes for the best gameplay.
Part 2: Dropping Bombs (Selection in a set)
Next, we create a behavior where our enemies periodically fire an object that could hit our player. To do this, we create a new cylinder primitive, size it to an appropriate “thing a ufo would drop” size, and turn it into a prefab. We don’t need to put any scripts on this object, as our enemy will use gravity to deliver the bomb. We DO need to put a Rigidbody on the prefab, so don’t forget to do that step!
Now we want to make each and every one of our enemy ships capable of firing a bomb. In our Enemy.cs script, we add a GameObject variable to hold the reference to the prefab so that we can instantiate it.
public GameObject enemyBombPrefab;
Next we create a new function that we call DropABomb( ). Don’t forget to declare this function public, as we will want the order for this to come from our MotherShip.
public void DropABomb() { // make the bomb Instantiate(enemyBombPrefab, (transform.position + Vector3.down), Quaternion.identity); }
This script is similar to our bullet firing script – we Instantiate( ) the prefab object at a position that is closely relative to this enemy (only 1 unit down thanks to the addition of Vector3.down). The Quaternion.identity gives the placement a rotation value of 0,0,0.
To test this we made a simple Input.GetKey( ) check in the Update( ) function and called DropABomb( ) to ensure this works. Sure enough, every enemy made a bomb.
We want to be a little more conservative with our bombs, so we will have our MotherShip script run another coroutine, where every certain number of seconds, a child object will be randomly chosen, and the command to drop a bomb will be sent.
public IEnumerator SendABomb() { float timeBetweenBombs = 0.5f; bool isRunning = true; while (isRunning) { // see how many child objects there are int enemyCount = transform.childCount; // if there are children... if (enemyCount > 0) { // pick one at random int enemyIndex = Random.Range(0, enemyCount); // get the object Transform thisEnemy = transform.GetChild(enemyIndex); // make sure it has the Enemy component Enemy enemyScript = thisEnemy.GetComponent<Enemy>(); if (enemyScript) { // drop the bomb enemyScript.DropABomb(); } } else { // .. and if no children, stop running isRunning = false; } yield return new WaitForSeconds(timeBetweenBombs); } }
Here we create the isRunning variable as a boolean, but it serves the same purpose as the childCount in the last coroutine. In fact, we set it the same way. Our script runs Transform.childCount in each iterations, and if the count is zero then we set isRunning = false and end the loop.
If there is still a fleet of intrepid fighters out there, however, our script chooses one lucky participant by running the Random.Range( ) function. A word of caution regarding Random.Range – there is both a float and an int version of this function and they behave slightly differently from one another. In the float version, the min and max values are inclusive, meaning that if I ask it to return a random number in the range of 0.0f and 10.0f, it could be either of those numbers or any number in between. With the integer version of the script the max value is exclusive, meaning a range of min: 0, max: 10 will only return values between 0-9. This means we don’t have to calculate our usual [count – 1] for a value.
Next we get the Enemy script by declaring an “Enemy” type object, and running GetComponent<Enemy>( ) on the lucky ufo. We then test this value to make sure that GetComponent did not return a null value, or empty value. If this is empty, we have tried to grab a component from an object that does not have it. This helps us error check and avoid making an unsupported call. If our Enemy object evaluates to true, it means our object is there, and we can go ahead and send it the DropABomb( ) command.
Finally, at the end of the while loop, we run a WaitForSeconds( ) before trying this again.
Part 3: Physics Layers
As you test this, you’ve probably noticed that physics is doing some… unusual things. Our enemy bomb objects dropped from attackers in the top row are landing on the row below them. We don’t want our enemy to destroy themselves with friendly fire, lest we give our player an unfair advantage. We also don’t want bombs to ride around on top of the enemy objects. How can we fix this? This is a pretty common problem in game engines – you want some things to collide with others, but not EVERYTHING to collide all the time. Thankfully, Unity has a solution for us – Layer Masks.
Layers are kind of like Tags. Every object has a Layer assignment which can be viewed and modified in a dropdown in the bottom-left of the object info, next to the tag designation.
Although we view these layers as strings, they are stored as integer values. (GameObjects are automatically assigned to Layer 0) . There are 32 possible layers, the first 8 are reserved by Unity, and the remaining 24 are available for your use. You can edit these by selecting Add New Layer… in the Layer Dropdown, or by going to Edit -> Project Settings -> Tags and Layers.
For this project, let’s define two new layers – “Enemy” and “Player”.
By defining these layers, we now have a method by which to “mask” (or block) physics events from processing between certain layers. Go to Edit -> Project Settings -> Physics and scroll to the bottom of the panel. There you will see the Layer Collision Matrix, a grid of checkboxes, each row and column sharing a name with a layer in our system.
This collision matrix tells the Unity engine which collisions to process, and which to ignore. When a box is checked, objects from the layers in that row and column will collide. When unchecked, they ignore one another – no collision event is created, and they simply pass through one another.
For our game, I’m going to assume that objects on the Player layer (which I will assign our player object and our bullets) should not collide with one another. And objects on the Enemy layer (which will be the enemy objects and their bullets) should not collide with themselves. I uncheck the boxes at the intersection of Enemy/Enemy, and Player/Player like so:
Now our enemy bombs drop right through the attackers in lower rows, but will interact with our Player object. (And if you’re a good shot, you can also hit them with your player bullets and knock them off course. You’ll be asked to address this as part of this week’s assignment).
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() { StartCoroutine(MoveMother()); StartCoroutine(SendABomb()); } // Update is called once per frame void Update() { } 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; // wait for the next move yield return new WaitForSeconds(timeBetweenSteps); } // now move down transform.position += (Vector3.down * downStepUnits); yield return new WaitForSeconds(timeBetweenSteps); // flip the direction moveVector *= -1.0f; } 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); } } }
Enemy.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { public GameObject enemyBombPrefab; private void Update() { if (Input.GetKeyDown(KeyCode.B)) { DropABomb(); } } private void OnCollisionEnter(Collision collision) { if (collision.transform.tag == "PlayerBullet") { // 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); } }