Home Unity Tutorials

Unity Tutorial: How to Make a Game Like Space Invaders

In this Unity tutorial, you’ll learn how to make a classic 2D space shooting game similar to Space Invaders.

Version

  • C# 7.3, Unity 2020.3, Unity

Space Invaders, known in Japan as Supēsuinbēdā (スペースインベーダー), is one of the most well known retro games in the world. Released to arcades in 1978 by the Japanese game company Taito, it quickly became a massive hit.

The classic Invaders arcade game.

The laser bullet-shooting cannon represents the player. The player can hide behind any of four torchka (トーチカ), also known as a pillboxes.

There are three types of invaders: crab, squid and octopus. They appear in a swarm of multiple rows and move from the top to the bottom of the screen. The crab invader has become an iconic symbol universally associated with arcades and games in general.

The game’s objective is to eliminate the invaders before they reach the bottom of the screen while dodging and hiding behind torchkas. In addition to the swarm, there’s also a UFO that appears from time to time. However, you’ll focus on the swarm.

In this tutorial, you’ll replicate the core gameplay features of Space Invaders using Unity. Along the way, you’ll learn how to:

  • Spawn and move the invader swarm.
  • Make the invaders at the swarm’s head shoot laser bullets.
  • Move and shoot back as the player.
  • Change the music tempo and swarm speed based on kill-count.

Time to get started!

Note: This tutorial assumes you already have basic experience with Unity and an intermediate knowledge of C#. Additionally, this tutorial assumes you’re using C# 7 and Unity 2020.3.

Getting Started

Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial.

Extract the zip file to a convenient location. After extracting, you’ll notice Starter and Final project folders. Open Starter in Unity.

The project contains some folders to help you to get started. Open Assets/RW and you’ll find the following directories:

  • Animations: Contains pre-made animations and the animator for the bullet and explosion prefabs.
  • Prefabs: Has all the prefabs the project uses.
  • Resources: Contains fonts and textures.
  • Scenes: Contains the main scene you’ll work with.
  • Scripts: Has all the project scripts. Most of them are empty shells which you’ll fill.
  • Sounds: Has all the sound effects and the music file.
  • Sprites: Contains all the pixel art for the project.

Navigate to RW/Scenes and open Main. Click Play and you’ll see the red player cannon in the bottom center of the screen and hear the music loop. In addition, you’ll see the Score and Lives UI labels.

You can’t interact with the game yet, but you’ll add the interactivity in this tutorial.

The starting point shows the cannon, the score and the number of lives.

Stop the game and select Edit ▸ Project Settings ▸ Physics 2D. Take a look at the Layer Collision Matrix. Notice this project has two custom layers: Hero and Enemy.

The Physics2D Layer Collision Matrix

The Physics 2D collision detection for GameObjects in the Enemy layer only work with GameObjects on the Hero layer. Hero layer GameObjects work for both Enemy and other Hero layered GameObjects. This information will be important later.

Now, take a look at the GameObjects in the Hierarchy. Some have custom components attached that you’ll work on in this tutorial.

The project's Hierarchy window

Game Controller has Invader Swarm, Torchka Manager and Game Manager components attached. In addition to these, there’s an Audio Source component you’ll use for the sound effects.

The Game Controller GameObject's Inspector window

Music has Music Control and an Audio Source. The Audio Source plays Beats located in RW/Sounds. This is the game’s main music. Currently, the music has a fixed tempo, but later you’ll make it dynamic.

The Music GameObject in the Inspector window

CANNON has Cannon Control and a Box Collider 2D for collision detection. Notice its layer is set to Hero. It also has two immediate children: Sprite, which is its associated sprite, and Muzzle, an empty GameObject that represents the position for bullet instantiation while shooting.

The Inspector for the CANNON component

Main Camera is the game’s main and only camera and has the Audio Listener. It’s orthographic with a position offset of -10 along the Z-axis.

All the other GameObjects have Z set to 0. In fact, for the others, assume the Z-axis doesn’t exist since this is a 2D game, and you’ll stick to positioning them only on the X and Y axes.

INVADERS Spawn Start and TORCHKA Spawn Center are two empty helper GameObjects parented to Helpers. Their positions will help you spawn the invader swarm and torchkas later on.

Canvas and Event System are for the UI. Canvas has the following children:

  • Score Text: This displays the score.
  • Lives Text: You’ll use this to display the remaining player lives.
  • Game Over Panel: This panel displays the Game Over message when a player runs out of lives.
  • All Clear: You’ll use this panel to display the All Clear message when the player eliminates all the invaders.
  • Restart Button: This button restarts the game.

Now that you’re done with the tour, it’s time to add the good stuff. :] In the next section, you’ll work on the player controls.

Implementing the Player Controls

Select the CannonControl script attached to CANNON. Open it in your code editor. Paste the following code inside the class:

[SerializeField] 
private float speed = 500f;

private void Update()
{
    if (Input.GetKey(KeyCode.D)) 
    {
        transform.Translate(speed * Time.deltaTime, 0, 0);
    }
    else if (Input.GetKey(KeyCode.A)) 
    {
        transform.Translate(-speed * Time.deltaTime, 0, 0);
    }
}

Here’s a code breakdown:

  • The variable speed controls the cannon’s speed.
  • Inside Update, you check if the player is holding down keys D or A. If the player holds down D, the cannon moves right by speed * Time.deltaTime every frame. If they hold down A, the cannon moves left by the same amount.

Save the file. Return to Unity and select Play inside the editor. Now you can move the ship using D and A.

The cannon is now able to move via the A and D keys.

Next, you’ll add the shooting mechanics by using the Bullet prefab inside RW/Prefabs.

Select Bullet and take a look at the Inspector.

The prefab has a Kinematic Rigidbody 2D component. It’s Kinematic because you won’t rely on physics to move the bullet, but instead translate it via a script.

The Rigidbody 2D‘s layer is set to Hero and it has the Box Collider 2D component for collision detection. There’s also a Bullet component which doesn’t do anything yet.

Open the Bullet script inside your code editor. Add the following code inside the class:

[SerializeField] 
private float speed = 200f;

[SerializeField] 
private float lifeTime = 5f;

internal void DestroySelf()
{
    gameObject.SetActive(false);
    Destroy(gameObject);
}

private void Awake()
{
    Invoke("DestroySelf", lifeTime);
}

private void Update()
{
    transform.Translate(speed * Time.deltaTime * Vector2.up);
}

private void OnCollisionEnter2D(Collision2D other)
{
    DestroySelf();
}

In the code above, you:

  • Use Update to move the bullet by speed * Time.deltaTime every frame toward the top.
  • Use DestroySelf to destroy the bullet GameObject. Before calling Destroy, you disable the bullet because destruction takes some frames to process but disabling is almost instantaneous.
  • Awake invokes DestroySelf to destroy the bullet automatically after lifetime seconds. DestroySelf is also called when a bullet collides with another GameObject.

You’ll need sound effects during shooting. Inside RW/Scripts, open GameManager and add the following inside the class:

internal static GameManager Instance;

[SerializeField] 
private AudioSource sfx;

internal void PlaySfx(AudioClip clip) => sfx.PlayOneShot(clip);

private void Awake()
{
    if (Instance == null) 
    {
        Instance = this;
    }
    else if (Instance != this) 
    {
        Destroy(gameObject);
    }
}

The above code turns GameManager into a singleton, which ensures it only has a single instance at any given time. It also adds a utility method, PlaySfx, which accepts an Audio Clip and plays it using the Audio Source sfx.

To use the bullet, you need to instantiate it. Open CannonControl again and add the following after the declaration for speed:

[SerializeField] 
private Transform muzzle;

[SerializeField] 
private AudioClip shooting;

[SerializeField] 
private float coolDownTime = 0.5f;

[SerializeField] 
private Bullet bulletPrefab;

private float shootTimer;

Then, after the earlier movement code inside Update, paste:

shootTimer += Time.deltaTime;
if (shootTimer > coolDownTime && Input.GetKey(KeyCode.Space))
{
    shootTimer = 0f;

    Instantiate(bulletPrefab, muzzle.position, Quaternion.identity);
    GameManager.Instance.PlaySfx(shooting);
}

This code increments shootTimer every frame until it reaches coolDownTime. If the player holds down the Space key, the code resets shootTimer and instantiates the bulletPrefab at muzzle.position. It also plays the shooting sound effect by using the PlaySfx inside GameManager.

Save everything and return to Unity.

Go back to the Bullet prefab. Notice the new fields for the Bullet component.

The Inspector window for the Bullet prefab.

Now, select Game Controller from the Hierarchy. Set Game Manager‘s Sfx to the Audio Source attached to the Game Controller.

Inspector window showing Game Controller's Audio Source component.

Select CANNON from the Hierarchy. In Cannon Control, first set Muzzle to the Muzzle Transform that’s CANNON‘s child. Then set Bullet Prefab to the Bullet found in RW/Prefabs. Finally, set Shooting to CannonBullet found under RW/Sounds.

The Cannon Control component in the Inspector window

Save and Play. Now you can move the cannon and shoot laser bullets by holding down the Space key.

The cannon can now shoot laser bullets.

Your hero is ready! Now it’s time to add villains. In the next section, you’ll add the invader swarm.

Creating the Invaders

How do all antagonists start out their lives in games? Spawning of course! You’ll do that next.

Spawning the Invaders

Navigate to RW/Scripts to examine the SimpleAnimator script. You’ll use it to animate the invaders, but you don’t need to worry about its inner workings for this tutorial.

When you add SimpleAnimator to a GameObject you also add a SpriteRenderer because it’s a required component for the script to function.

SimpleAnimator also requires an array of sprites. It’ll keep updating the SpriteRenderer‘s sprite cyclically using the sprites inside the array, essentially animating it.

Now, select Invader Swarm attached to Game Controller. Open the associated script inside your code editor. Add the following code the inside the class:

[System.Serializable]
private struct InvaderType
{
    public string name;
    public Sprite[] sprites;
    public int points;
    public int rowCount;
}

Here’s a code breakdown:

  • This code defines an InvaderType struct.
  • The sprites array stores all the sprites associated with the invader type, which SimpleAnimator uses to animate the invader.
  • rowCount is the number of rows the invader type will have in the swarm.
  • name stores the type name for the invader and points stores the score contribution if the player eliminates an invader of this type. These will come in handy later.

Now add the following right below InvaderType:

[Header("Spawning")]
[SerializeField] 
private InvaderType[] invaderTypes;

[SerializeField] 
private int columnCount = 11;

[SerializeField] 
private int ySpacing;

[SerializeField]
private int xSpacing;

[SerializeField] 
private Transform spawnStartPoint;

private float minX;

You’ll use all of these fields for the spawning logic:

  • invaderTypes: Represents all the invader types in use.
  • columnCount: Total number of columns for the swarm.
  • ySpacing: The spacing between each invader in the swarm along the Y-axis.
  • xSpacing: The spacing between each invader in the swarm along the X-axis.
  • spawnStartPoint: Spawning point for the first invader.
  • minX: Stores the minimum X position value for the swarm.

Next, paste the following code after all the variable declarations:

private void Start()
{
    minX = spawnStartPoint.position.x;

    GameObject swarm = new GameObject { name = "Swarm" };
    Vector2 currentPos = spawnStartPoint.position;

    int rowIndex = 0;
    foreach (var invaderType in invaderTypes)
    {
        var invaderName = invaderType.name.Trim();
        for (int i = 0, len = invaderType.rowCount; i < len; i++)
        {
            for (int j = 0; j < columnCount; j++)
            {
                var invader = new GameObject() { name = invaderName };
                invader.AddComponent<SimpleAnimator>().sprites = invaderType.sprites;
                invader.transform.position = currentPos;
                invader.transform.SetParent(swarm.transform);

                currentPos.x += xSpacing;
            }

            currentPos.x = minX;
            currentPos.y -= ySpacing;

            rowIndex++;
        }
    }
}

Here’s a code breakdown:

  1. This code sets up minX inside Start. Then it creates an empty GameObject named Swarm.
  2. xSpacing and ySpacing update currentPos along the X and Y-axis, respectively.
  3. currentPos.x increments after every invader iteration in the row. After a row is complete, currentPos.y decrements.
  4. Looping over the members of invaderTypes, for each invaderType, you iterate row-wise to create individual invader GameObjects at the currentPos position.
  5. xSpacing and ySpacing update currentPos along the X and Y axis, respectively.
  6. Each created invader GameObject has its name set to invaderName. You then add a SimpleAnimator component, and assign its sprite array to the sprites associated with the invaderType.
  7. Finally, the invader becomes a child of Swarm and its position is set to currentPos.

Save everything and go back to Unity.

Select Game Controller from the Hierarchy. On Invader Swarm, set:

  • Y Spacing to 25
  • X Spacing to 25
  • Spawn Start Point to INVADERS Spawn Start, which is a child of Helpers.

Then, set the Invader Types size to 3 and set the member fields as follows:

  • 0: Set Name to SQUID, Points to 30 and Row Count to 1.
  • 1: Set Name to CRAB, Points to 20 and Row Count to 1.
  • 2: Set Name to OCTOPUS, Points to 10 and Row Count to 2.

Now, navigate to RW/Sprites and look at the INVADERS spritesheet. These aren’t the original Space Invaders but they’ll work for this tutorial.

Go back to Invader Swarm and set it up as follows, using the spritesheet:

For the SQUID entry, set the Sprites list to contain the following sprites from the spritesheet in this order:

  • bugs_invaders_0
  • bugs_invaders_5
  • bugs_invaders_9
  • bugs_invaders_4

Perform the same exercise, but this time for CRAB using the following sprites:

  • bugs_invaders_13
  • bugs_invaders_18

Lastly, assign the sprites for OCTOPUS using:

  • bugs_invaders_7
  • bugs_invaders_2

Here is a visual reference of what things should look like now:

Set up for Game Controller's Invader Swarm component's Invader Types field.

Save and Play. Notice the swarm spawns, and the invaders animate in one place.

The game now has spawned invaders.

Wonderful! But they’re not moving. You’ll fix that in the next section.

Moving the Invaders

Go back to the InvaderSwarm.cs script and add the following after the existing variable declarations:

[Space]
[Header("Movement")]
[SerializeField] 
private float speedFactor = 10f;

private Transform[,] invaders;
private int rowCount;
private bool isMovingRight = true;
private float maxX;
private float currentX;
private float xIncrement;

All of these variables will help move the swarm:

  • speedFactor for now represents the speed at which the invaders move along the X-axis. Later, speed will involve the music tempo so the actual speed is the product of the two.
  • invaders stores the Transforms of all the created invader GameObjects.
  • rowCount stores the total row count of the swarm.
  • isMovingRight represents the direction of movement and is set to true by default.
  • maxX is the maximum X position for the swarm movement.
  • currentX represents the overall X position of the swarm.
  • xIncrement is the value per frame that moves the invaders along the X-axis.

Now, in Start, add the following code right above int rowIndex = 0;:

foreach (var invaderType in invaderTypes)
{
    rowCount += invaderType.rowCount;
}
maxX = minX + 2f * xSpacing * columnCount;
currentX = minX;
invaders = new Transform[rowCount, columnCount];

This code calculates the total row count and stores it inside rowCount. Then, you calculate maxX based on the total columns and the spacing between each invader. Initially, currentX is set to the spawnStartPoint‘s X position.

You declared the invaders array. To populate it, you’ll need one more line of code.

Paste the following line inside the innermost for loop, right above currentPos.x += xSpacing;:

invaders[rowIndex, j] = invader.transform;

This line takes care of populating invaders.

Finally, right after Start, paste:

private void Update()
{
    xIncrement = speedFactor * Time.deltaTime;
    if (isMovingRight)
    {
        currentX += xIncrement;
        if (currentX < maxX) 
        {
            MoveInvaders(xIncrement, 0);
        }
        else 
        {
            ChangeDirection();
        }
    }
    else
    {
        currentX -= xIncrement;
        if (currentX > minX) 
        {
            MoveInvaders(-xIncrement, 0);
        }
        else 
        {
            ChangeDirection();
        }
    }
}

private void MoveInvaders(float x, float y)
{
    for (int i = 0; i < rowCount; i++)
    {
        for (int j = 0; j < columnCount; j++)
        {
            invaders[i, j].Translate(x, y, 0);
        }
    }
}

private void ChangeDirection()
{
    isMovingRight = !isMovingRight;
    MoveInvaders(0, -ySpacing);
}

Here's a code breakdown:

  • MoveInvaders accepts two float values: x and y. It moves each Transform in invaders by the same value along the X and Y axis respectively.
  • ChangeDirection toggles isMovingRight and moves the swarm down by the ySpacing amount.
  • Inside Update, you calculate xIncrement and update currentX based on the direction every frame.
  • You use currentX to check whether the swarm X position is approaching a threshold. If yes, you call ChangeDirection. If not, you move the swarm using MoveInvaders.

Save everything. Go back to Unity and click Play. You'll see the invader swarm moving. While in Play Mode, try different values for the Invader Swarm's Speed Factor and see how it affects the swarm's speed.

The invaders are now moving at increasing speed across and then down the screen.

The invaders are moving, but they don't shoot bullets yet. You'll work on that in the next section.

Making the Invaders Shoot Lasers

You'll use a variant of the Bullet prefab for the invaders. Navigate to RW/Prefabs and find the EnemyBullet. It's the same as Bullet, except the sprite points in the opposite Y direction and its layer is set to Enemy.

Select EnemyBullet. Notice the Speed of Bullet is set to -200. This ensures the bullet moves in the opposite direction of the cannon bullets but with the same magnitude.

The Inspector window for the EnemyBullet prefab

In the game, only invaders at the front of the swarm shoot laser bullets. To achieve this, you'll use the BulletSpawner prefab located in RW/Prefabs.

You'll instantiate as many of them as the invader column count. You'll also make the bullet spawners follow the invader Transforms at the front of the swarm.

This will ensure that when firing, the bullets will appear to be coming from the front row invaders.

Before you do that, you need a way to get an invader Transform at a specific row and column from the invaders array inside InvaderSwarm.

Open the InvaderSwarm.cs script and add the following line after the InvaderType struct declaration:

internal static InvaderSwarm Instance;

This helps turn the InvaderSwarm into a Singleton.

Then paste the following right above Start:

internal Transform GetInvader(int row, int column)
{
    if (row < 0 || column < 0
        || row >= invaders.GetLength(0) || column >= invaders.GetLength(1)) 
    {
        return null;
    }

    return invaders[row, column];
}

private void Awake()
{
    if (Instance == null) 
    {
        Instance = this;
    }
    else if (Instance != this) 
    {
        Destroy(gameObject);
    }
}

GetInvader returns the invader Transform at the row and column index of invaders. Awake turns the InvaderSwarm into a Singleton by ensuring that when the game starts, only one instance of InvaderSwarm will be alive.

Now, select the BulletSpawner prefab and take a look at the Inspector. Notice it has Bullet Spawner attached to it.

There's also a Kinematic Rigidbody 2D, a Box Collider 2D and the layer is set to Enemy. You won't add colliders to the invaders, but rather use this collider to detect hits from the cannon bullets.

The Inspector window for BulletSpawner.

In your code editor, open the BulletSpawner.cs script attached to BulletSpawner and add the following inside the class:

internal int currentRow;
internal int column;

[SerializeField] 
private AudioClip shooting;

[SerializeField] 
private GameObject bulletPrefab;

[SerializeField] 
private Transform spawnPoint;

[SerializeField] 
private float minTime;

[SerializeField]
private float maxTime;

private float timer;
private float currentTime;
private Transform followTarget;

internal void Setup()
{
    currentTime = Random.Range(minTime, maxTime);
    followTarget = InvaderSwarm.Instance.GetInvader(currentRow, column);
}

private void Update()
{
    transform.position = followTarget.position;

    timer += Time.deltaTime;
    if (timer < currentTime) 
    {
        return;
    }

    Instantiate(bulletPrefab, spawnPoint.position, Quaternion.identity);
    GameManager.Instance.PlaySfx(shooting);
    timer = 0f;
    currentTime = Random.Range(minTime, maxTime);
}

Here's a code breakdown:

  • currentTime represents the time to wait until shooting the next bullet. It's set to a random value between minTime and maxTime.
  • currentRow and column link a bullet spawner with an invader. The column is set once and won't change. But, as you'll see later, currentRow updates if the player's bullets hit this spawner.
  • Inside Setup(), you set followTarget by calling GetInvader from the InvaderSwarm instance using currentRow and column. You also set an initial value to currentTime.
  • Inside Update, you update the position of the bullet spawner to match the followTarget.position. In addition, you increment the timer until it reaches currentTime. When this happens, you create a bullet at spawnPoint.position while playing the shooting sound effect, followed by resetting timer and currentTime.

Save everything. Return to Unity and open BulletSpawner in Prefab Mode. Ensure the following values are set for Bullet Spawner:

  • Shooting to InvaderBullet located at RW/Sounds.
  • Bullet Prefab to EnemyBullet located at RW/Prefabs.
  • Spawn Point to the SpawnPoint Transform which is the only child of BulletSpawner.
  • Min Time to 1 and Max Time to 10.

The Inspector showing the Bullet Spawner script.

To use the BulletSpawner, you need to go back to the InvaderSwarm.cs script.

First, paste the following line at the end of all the variable declarations:

[SerializeField] 
private BulletSpawner bulletSpawnerPrefab;

Then, inside Start, add the following lines at the end:

for (int i = 0; i < columnCount; i++)
{
    var bulletSpawner = Instantiate(bulletSpawnerPrefab);
    bulletSpawner.transform.SetParent(swarm.transform);
    bulletSpawner.column = i;
    bulletSpawner.currentRow = rowCount - 1;
    bulletSpawner.Setup();
}

In this code, you create a bullet spawner and set it up. You instantiate bulletSpawner for each column of the swarm and set its column and currentRow followed by calling its Setup method. You also parent the bulletSpawner to the Swarm to prevent clutter in the Hierarchy.

Save everything and go back to Unity. Select Game Controller from the Hierarchy and set the Bullet Spawner Prefab for Invader Swarm to BulletSpawner, located in RW/Prefabs.

Save and Play. You now have invaders who shoot bullets at the player.

The swarm is now shooting laser bullets at the cannon.

Notice that both bullets disappear when an invader bullet and cannon bullet collide. The invader bullet disappears when it hits the cannon and the cannon bullet disappears when it hits a bullet spawner. They disappear because OnCollisionEnter2D calls DestroySelf inside Bullet.

There's one thing missing, however, and that's explosions. :] You'll add them next.

Adding Explosions

Navigate to RW/Prefabs. Notice the Explosion prefab. It's a simple GameObject with a pre-made sprite animation you'll use for the explosion visuals.

Now, open GameManager.cs again. Add the following after declaring sfx:

[SerializeField] 
private GameObject explosionPrefab;

[SerializeField] 
private float explosionTime = 1f;

[SerializeField] 
private AudioClip explosionClip;

internal void CreateExplosion(Vector2 position)
{
    PlaySfx(explosionClip);

    var explosion = Instantiate(explosionPrefab, position,
        Quaternion.Euler(0f, 0f, Random.Range(-180f, 180f)));
    Destroy(explosion, explosionTime);
}

CreateExplosion creates an explosion at position with a random rotation along the Z-axis, and destroys it after explosionTime seconds.

To use CreateExplosion, add the following line at the end of DestroySelf inside the Bullet class:

GameManager.Instance.CreateExplosion(transform.position);

Save all. Return to Unity and select Game Controller from the Hierarchy. For the Game Manager set:

  • Explosion Prefab to Explosion located in RW/Prefabs.
  • Explosion Clip to Explosion located in RW/Sounds.

The Game Manager component in the Inspector window

Save the scene and Play. You now have explosions. :]

Firing bullets now yields explosions

Currently, the bullets don't affect the invaders or the cannon. In the following sections, you'll add scores and lives.

Adding Scores and Lives

Did you notice those super retro UI labels in the game view? Right now they're just deadweight. It's time to get them working so that there are goals and consequences to give this game meaning.

Implementing Player Lives

Open GameManager.cs and add the following code after the variable declarations:

[SerializeField] 
private int maxLives = 3;

[SerializeField] 
private Text livesLabel;

private int lives;

internal void UpdateLives()
{
    lives = Mathf.Clamp(lives - 1, 0, maxLives);
    livesLabel.text = $"Lives: {lives}";
}

Calling UpdateLives decrements the lives variable by one and updates the UI label to reflect the change. Currently, nothing happens when lives reaches zero but you'll change that later.

Add the following at the end of Awake:

lives = maxLives;
livesLabel.text = $"Lives: {lives}";

This code sets the default value for lives and also updates the UI label.

Now, open CannonControl and paste the following lines after the variable declarations:

[SerializeField] 
private float respawnTime = 2f;

[SerializeField] 
private SpriteRenderer sprite;

[SerializeField] 
private Collider2D cannonCollider;

private Vector2 startPos;

private void Start() => startPos = transform.position;

Then, add the following lines after Update:

private void OnCollisionEnter2D(Collision2D other)
{
    GameManager.Instance.UpdateLives();
    StopAllCoroutines();
    StartCoroutine(Respawn());
}

System.Collections.IEnumerator Respawn()
{
    enabled = false;
    cannonCollider.enabled = false;
    ChangeSpriteAlpha(0.0f);

    yield return new WaitForSeconds(0.25f * respawnTime);

    transform.position = startPos;
    enabled = true;
    ChangeSpriteAlpha(0.25f);

    yield return new WaitForSeconds(0.75f * respawnTime);

    ChangeSpriteAlpha(1.0f);
    cannonCollider.enabled = true;
}

private void ChangeSpriteAlpha(float value)
{
    var color = sprite.color;
    color.a = value;
    sprite.color = color;
}

Here's what's happening:

  • ChangeSpriteAlpha changes the opacity of the cannon sprite.
  • When a bullet hits the cannon, GameManager.UpdateLives decrements the total lives and the Respawn coroutine starts.
  • Respawn first disables the cannonCollider and makes the cannon sprite invisible. After a few moments, it makes the cannon sprite slightly transparent and sets cannon's position back to startPos. Finally, it restores the opacity of the sprite and enables the collider again.

Save everything and return to Unity. Select Game Controller and set the Game Manager's Lives Label to Lives Text, which is a child of Canvas.

The Inspector for Game Manager

For the Cannon Control on CANNON, set Sprite to the Sprite Renderer on Sprite, a child GameObject of CANNON. Set Collider to the Box Collider 2D on the CANNON.

The Inspector for the Cannon Control component

Now, save and Play. You'll see the respawn sequence as well as the lives update whenever a bullet hits the cannon.

The cannon now loses lives.

The bullets don't seem to affect the invaders. You'll work on that in the next section.

Implementing Score and Game Over

Before you do anything else, open the MusicControl.cs script. You want to stop the music at game over, so paste the following code inside the class:

[SerializeField] 
private AudioSource source;

internal void StopPlaying() => source.Stop();

StopPlaying stops the audio source when called.

Now, open the GameManager.cs script and add the following after the variable declarations:

[SerializeField] 
private MusicControl music;

[SerializeField] 
private Text scoreLabel;

[SerializeField] 
private GameObject gameOver;

[SerializeField] 
private GameObject allClear;

[SerializeField] 
private Button restartButton;

private int score;

internal void UpdateScore(int value)
{
    score += value;
    scoreLabel.text = $"Score: {score}";
}

internal void TriggerGameOver(bool failure = true)
{
    gameOver.SetActive(failure);
    allClear.SetActive(!failure);
    restartButton.gameObject.SetActive(true);

    Time.timeScale = 0f;
    music.StopPlaying();
}

Then, paste the following lines at the end of UpdateLives:

if (lives > 0) 
{
    return;
}

TriggerGameOver();

Finally, add the following at the end of Awake:

score = 0;
scoreLabel.text = $"Score: {score}";
gameOver.gameObject.SetActive(false);
allClear.gameObject.SetActive(false);

restartButton.onClick.AddListener(() =>
{
    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    Time.timeScale = 1f;
});
restartButton.gameObject.SetActive(false);

Here's what this code does:

  • allClear stores a reference to the All Clear panel, which displays when the player eliminates all the invaders. gameOver references the Game Over panel that shows when the player runs out of lives.
  • UpdateScore increments score by the value passed to it and updates the UI label to reflect the changes.
  • TriggerGameOver shows the Game Over panel if failure is true. Otherwise, it shows the All Clear panel. It also enables the restartButton, pauses the game and stops the music.
  • Awake handles the onClick event for the restart button. It reloads the scene when clicked.

Open InvaderSwarm.cs and add the following inside the class after the variable declarations:

private int killCount;
private System.Collections.Generic.Dictionary<string, int> pointsMap;

internal void IncreaseDeathCount()
{
    killCount++;
    if (killCount >= invaders.Length)
    {
        GameManager.Instance.TriggerGameOver(false);
        return;
    }
}

internal int GetPoints(string alienName)
{
    if (pointsMap.ContainsKey(alienName)) 
    {
        return pointsMap[alienName];
    }
    return 0;
}

Then paste the following line right above int rowIndex = 0; inside Start:

pointsMap = new System.Collections.Generic.Dictionary<string, int>();

Below the line right under var invaderName = invaderType.name.Trim(); add the following:

pointsMap[invaderName] = invaderType.points;

Here's a code breakdown:

  • pointsMap is a Dictionary (a map of string to integer). It maps the invader type name with its points value.
  • IncreaseDeathCount keeps track of and updates killCount when the player eliminates an invader. When the player eliminates all of the invaders, TriggerGameOver receives false and displays the All Clear panel.
  • GetPoints returns the points associated with an invader type by passing in its name as the key.

Finally, open BulletSpawner.cs to handle collision detection for the invaders. Paste the following right after Update:

private void OnCollisionEnter2D(Collision2D other)
{
    if (!other.collider.GetComponent<Bullet>()) 
    {
        return;
    }

    GameManager.Instance.
        UpdateScore(InvaderSwarm.Instance.GetPoints(followTarget.gameObject.name));

    InvaderSwarm.Instance.IncreaseDeathCount();

    followTarget.GetComponentInChildren<SpriteRenderer>().enabled = false;
    currentRow = currentRow - 1;
    if (currentRow < 0) 
    {
        gameObject.SetActive(false);
    }
    else 
    {
        Setup();
    }
}

Here's what this code does:

  • OnCollisionEnter2D returns without doing anything if the object that hit the bullet spawner wasn't of type Bullet.
  • If the cannon bullet hit the spawner, the score and kill count update. Also, the current followTarget's Sprite Renderer disables, then updates the currentRow.
  • If there aren't any rows left, the GameObject is disabled. Otherwise, you call Setup to update the followTarget.

Wow! That was a lot of work. Save everything and jump back into Unity to finish this step.

Select Music and set Source of Music Control to the Audio Source component on the same GameObject.

The Inspector for the Music Control component

Then, select Game Controller. For Game Manager, set:

  • Music to Music Control of Music.
  • Score Label to Score Text.
  • Game Over to Game Over Panel.
  • All Clear to All Clear.
  • Restart Button to Restart Button.

The Inspector showing the Game Manager component

Save and Play. Now you can kill the invaders! Enable Gizmos in the Game View and select the Swarm to see how the bullet spawners update.

The laser bullets now hit and remove the invaders.

Kill all the invaders to see the All Clear, or let the lives run out to see the Game Over. Then, you can use the Restart button to reload the scene.

The Game Over panel
The All Clear panel

The invaders are a bit slow and have the same speed throughout. In the next section, you'll update their speed with the tempo of the music.

Adding Simple Dynamic Audio

Go to MusicControl.cs. Open it inside your code editor. Add the following line at the top of the class:

private readonly float defaultTempo = 1.33f;

This line represents the music's default beats per second. You can calculate this value by considering the music has four beats and is three seconds long.

Now, paste the following above StopPlaying:

[SerializeField] 
internal int pitchChangeSteps = 5;

[SerializeField] 
private float maxPitch = 5.25f;

private float pitchChange;

internal float Tempo { get; private set; }

Then, add the following lines after the definition for StopPlaying:

internal void IncreasePitch()
{
    if (source.pitch == maxPitch) 
    {
        return;
    }

    source.pitch = Mathf.Clamp(source.pitch + pitchChange, 1, maxPitch);
    Tempo = Mathf.Pow(2, pitchChange) * Tempo;
}

private void Start()
{
    source.pitch = 1f;
    Tempo = defaultTempo;
    pitchChange = maxPitch / pitchChangeSteps;
}

Here's how the code works:

  • Inside Start, source.pitch and Tempo are set to their default values.
  • IncreasePitch increments the the source audio's pitch by an amount dictated by pitchChange, which in turn is the ratio of maxPitch and pitchChangeSteps. maxPitch also puts an upper limit on the pitch.
  • After changing the pitch, you can calculate the tempo based on the following formula:

pitchchange = -log(2)(tempo1/tempo2) where tempo1 denotes the tempo before the change and tempo2 the tempo after the change.

Now open the InvaderSwarm.cs script and add the following at the end of variable declarations:

[SerializeField] 
private MusicControl musicControl;

private int tempKillCount;

In IncreaseDeathCount, paste the following lines at the end:

tempKillCount++;
if (tempKillCount < invaders.Length / musicControl.pitchChangeSteps) 
{
    return;
}

musicControl.IncreasePitch();
tempKillCount = 0;

Now IncreaseDeathCount tracks the variable tempKillCount to check whether it exceeds invaders.Length / musicControl.pitchChangeSteps. If it does, it calls IncreasePitch and tempKillCount resets.

This means when the player eliminates almost invaders.Length / musicControl.pitchChangeSteps invaders, both audio pitch and tempo increase. The variable Tempo inside MusicControl keeps track of the updated tempo.

Finally, inside Update, replace xIncrement = speedFactor * Time.deltaTime; with:

xIncrement = speedFactor * musicControl.Tempo * Time.deltaTime;

This line ensures the xIncrement considers the music tempo, and the invaders move faster as the music gets faster as the player eliminates more and more invaders.

Save everything. Return to Unity and select Game Controller. Set the Invader Swarm's Music Control to Music.

Save and Play.

The Inspector showing the Invader Swarm component

Try shooting down the invaders. You'll notice they start moving faster.

The invaders getting shot, and the swarm moving at increasing speed.

There's still one minor issue: If you miss any invaders, they continue moving once they reach the bottom of the screen. It would be better to trigger Game Over if the swarm reaches the bottom.

To do this, open InvaderSwarm.cs and add the following at the end of variable declarations:

[SerializeField] 
private Transform cannonPosition;

private float minY;
private float currentY;

Then, paste the following lines at the beginning of Start:

currentY = spawnStartPoint.position.y;
minY = cannonPosition.position.y;

Then add the following at the end of ChangeDirection:

currentY -= ySpacing;
if (currentY < minY) 
{
    GameManager.Instance.TriggerGameOver();
}

Here's what this code does:

  • Inside Start, you set minY to the Y position of the cannon and currentY to the Y position of spawnStartPoint.
  • Whenever called, ChangeDirection decrements currentY until it becomes less than minY, at which point the game ends and shows Game Over.

Save everything. Return to Unity and select Game Controller. Set Cannon Position of Invader Swarm to the Transform of CANNON.

The Inspector window showing the Invader Swarm component

Save and Play. Now, you'll see that Game Over panel is triggered if the swarm goes below the cannon's position.

The Game Over panel showing after invaders reached below the cannon's position.

And you're done!

Where to Go From Here?

You can use the Download Materials button at the top and bottom of this tutorial to download both the starter and final projects.

You may have noticed you didn't get to add any torchkas. Try adding them as a challenge.

Everything you need is already there. You may need to examine the code inside TorchkasManager and the Torchka scripts. If you get stuck, take a look at the Final project for the solution. Good luck!

The full game, with Torchkas!

Thanks for taking the time to read this article. I hope you had fun and learned something new. Please feel free to join the forum below for any questions or comments.

Special thanks to opengameart user jlunesc for some of the CC-BY assets used in the project.

Add a rating for this content

More like this

Contributors

Comments