Summer Break – Block Breaker Game in Unity, Part 2

Home / Technology / Game Development / Summer Break – Block Breaker Game in Unity, Part 2
It's time for a few more levels, audio, improved gameplay experience, and all the other finishing touches for the Summer Break block breaker game. Read more at blissfullemon.com/summer-break-block-breaker-2

Well, this week did not go accordingly to plan. Despite such a whirlwind of a week, I managed to finish my Summer Break block breaker game in Unity. It has a functional start menu, 5 unique playable levels, and a game over screen. So, what’s not to like? Plus it’s colorful, which you know I can always get behind.

Let me first start by addressing the fact that I am posting a lot later than I have been in previous weeks. I planned on moving my posts back to Saturday so I had the entire work week to complete my game development projects. I thought it would equate to more time to work without sacrificing my business and blog projects. My plan might have been good, had it not been for the sheer number of issues I had in finishing my game.

This is not a gripe post. I am extremely happy with how things went this week. Sure, I was mentally over this project by the time I finished, but I also learned and accomplished so freaking much!

What I Learned

  • How to use prefabs to create additional levels using the existing elements as building blocks
  • The effects of modifying friction of a game object (the ball)
  • How to use audio components, including listeners, audio source, and clips, to play audio when an event takes place
  • Using an array to create a variation of sound effects for a single collision
  • Using the PlayClipAtPoint() method to play audio even if the game object is destroyed
  • How to count the number of breakable blocks remaining and use that information to determine the score, when to change levels, etc.
  • How to modify the game’s speed using the Time.timeScale property
  • Implementing a singleton pattern
  • Triggering particle effects when the ball breaks a block
  • How to destroy the particle effect after 1-2 seconds (once you no longer need the object instance)
  • Using tags in Unity to be able to access and perform actions on multiple related objects in the script
  • Updating the sprite of an object to show how damaged it is
  • Using extreme tuning to experiment with the overall game experience
  • Automating (through script) for easy playtesting

The Summer Break Block Breaker Game

I wanted to figure out a way to embed the game, and believe me I spent a lot of time trying to figure it out. Unfortunately, I wasn’t able to really figure out how to make it work properly. It would be easy to do if my site was a normal HTML/CSS website, but things get a little screwy when you start adding things to WordPress. That’s why so many people (myself included) have to rely on plugins for adding functionality. There is no WebGL plugin that I could find, so that makes embedding the game a bit more difficult. I’ll try to add it later on when I finish and polish the game for my portfolio. In the meantime, here’s a little video demonstrating the first three levels of the game.

If you read Part 1 of this or saw the screenshot for the first level, you might have noticed that this version of the game looks different. As much as I would love to pretend that this was intentional, it actually was a last-minute change that I hated making.

Here – just in case you forgot, this is what it looked like last week.

It's week 3 of Summer of Code 2019. This week, I started developing a summer-themed classic arcade block breaker game in Unity. It features 2D colliders, triggers, rigidbody components, and more! Read more at blissfullemon.com

So, What Happened To Level 1?!

I learned a very important lesson about doing too much, too soon, when it comes to creating those initial levels and building up my game objects. I should have spent more time figuring things out before I spent so long building a level because guess what happened? My blocks changed.

My original blocks were prefabs of prefabs, meaning that I created a prefab of a block, then created prefab children for each color. Then I did something really stupid and accidentally deleted my parent block prefab. 🤦‍♀️

Long story short, it did not end well. I ended up having to use my unbreakable block prefab (seen later in the 3rd level) to duplicate and create additional breakable prefabs. Then I had to redo the layout of my first two levels. By this point, I was too defeated to go through the trouble of creating that “Summer” lettering pattern. Besides, it was really difficult for level 1 anyway, right?

The Final Scripts

Level.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    private void Start()
    {
        sceneLoader = FindObjectOfType<SceneLoader>();
    }

    public void CountBlocks()
    {
        breakableBlocks++;
    }

    public void BlockDestroyed()
    {
        breakableBlocks--;

        if (breakableBlocks <= 0)
        {
            sceneLoader.LoadNextScene();
        }
    }

}

GameSession.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class GameSession : MonoBehaviour
{
    // Configuration Params
    [Range(0.1f, 10f)] [SerializeField] float gameSpeed = 1f;
    [SerializeField] int pointsPerBlockDestroyed = 83;
    [SerializeField] TextMeshProUGUI scoreText;
    [SerializeField] bool isAutoPlayEnabled;

    // State Vars
    [SerializeField] int currentScore = 0;

    private void Awake()
    {
        int gameStatusCount = FindObjectsOfType<GameSession>().Length;
        if (gameStatusCount > 1)
        {
            gameObject.SetActive(false);
            Destroy(gameObject);
        }
        else
        {
            DontDestroyOnLoad(gameObject);
        }
    }

    private void Start()
    {
        scoreText.text = "Score: " + currentScore.ToString();
    }

    // Update is called once per frame
    void Update()
    {
        Time.timeScale = gameSpeed;
    }

    public void AddToScore()
    {
        currentScore += pointsPerBlockDestroyed;
        scoreText.text = "Score: " + currentScore.ToString();
    }

    public void ResetGame()
    {
        Destroy(gameObject);
    }

    public bool IsAutoPlayEnabled()
    {
        return isAutoPlayEnabled;
    }
}

Block.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Block : MonoBehaviour
{
    // Configuration Params
    [SerializeField] AudioClip breakSound;
    [SerializeField] GameObject blockSparklesVFX;
    [SerializeField] Sprite[] hitSprites;

    // Cached Reference
    Level level;

    // State Vars
    [SerializeField] int timesHit; // TODO only serialized for debug purposes

    private void Start()
    {
        CountBreakableBlocks();
    }

    private void CountBreakableBlocks()
    {
        level = FindObjectOfType<Level>();
        if (tag == "Breakable")
        {
            level.CountBlocks();
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (tag == "Breakable")
        {
            HandleHit();
        }
    }

    private void HandleHit()
    {
        timesHit++;
        int maxHits = hitSprites.Length + 1;

        if (timesHit >= maxHits)
        {
            DestroyBlock();
        }
        else
        {
            ShowNextHitSprite();
        }
    }

    private void ShowNextHitSprite()
    {
        int spriteIndex = timesHit - 1;
        if (hitSprites[spriteIndex] != null)
        {
            GetComponent<SpriteRenderer>().sprite = hitSprites[spriteIndex];
        }
        else
        {
            Debug.LogError("Block sprite is missing from array" + gameObject.name);
        }
    }

    private void DestroyBlock()
    {
        PlayBlockDestroySFX();
        FindObjectOfType<GameSession>().AddToScore();
        Destroy(gameObject);
        level.BlockDestroyed();
        TriggerSparklesVFX();
    }

    private void PlayBlockDestroySFX()
    {
        AudioSource.PlayClipAtPoint(breakSound, Camera.main.transform.position);
    }

    private void TriggerSparklesVFX()
    {
        GameObject sparkles = Instantiate(blockSparklesVFX, transform.position, transform.rotation);
        Destroy(sparkles, 1f);
    }
}

Ball.cs

using UnityEngine;

public class Ball : MonoBehaviour
{
    // Configuration parameters
    [SerializeField] Paddle paddle1;
    [SerializeField] float xPush = 2f;
    [SerializeField] float yPush = 15f;
    [SerializeField] AudioClip[] ballSounds;
    [SerializeField] float randomFactor = 0.2f;

    // State
    Vector2 paddleToBallVector;
    bool hasStarted = false;

    // Cached component references
    AudioSource myAudioSource;
    Rigidbody2D myRigidBody2D;

    // Start is called before the first frame update
    void Start()
    {
        paddleToBallVector = transform.position - paddle1.transform.position;
        myAudioSource = GetComponent<AudioSource>();
        myRigidBody2D = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        if (!hasStarted) 
        {
            LockBallToPaddle();
            LaunchOnMouseClick();
        }
    }

    private void LockBallToPaddle()
    {
        Vector2 paddlePosition = new Vector2(paddle1.transform.position.x, paddle1.transform.position.y);
        transform.position = paddlePosition + paddleToBallVector;
    }

    public void LaunchOnMouseClick()
    {
        if (Input.GetMouseButtonDown(0)) // 0 = primary button (left)
        {
            hasStarted = true;
            myRigidBody2D.velocity = new Vector2(xPush, yPush); 
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        Vector2 velocityTweak = new Vector2
            (Random.Range(0f, randomFactor), 
            Random.Range(0f, randomFactor));

        if(hasStarted)
        {
            AudioClip clip = ballSounds[UnityEngine.Random.Range(0, ballSounds.Length)];
            myAudioSource.PlayOneShot(clip);
            myRigidBody2D.velocity += velocityTweak;
        }
    }
}

Paddle.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Paddle : MonoBehaviour
{
    // Configuration Params
    [SerializeField] float screenWidthInUnits;
    [SerializeField] float minX = 1f;
    [SerializeField] float maxX = 15f;

    // Cached References
    GameSession theGameSession;
    Ball theBall;

    // Start is called before the first frame update
    void Start()
    {
        theGameSession = FindObjectOfType<GameSession>();
        theBall = FindObjectOfType<Ball>();
    }

    // Update is called once per frame
    void Update()
    {
        Vector2 paddlePosition = new Vector2(transform.position.x, transform.position.y);
        paddlePosition.x = Mathf.Clamp(GetXPos(), minX, maxX);
        transform.position = paddlePosition;
    }

    private float GetXPos()
    {
        if (theGameSession.IsAutoPlayEnabled())
        {
            return theBall.transform.position.x;
        }
        else
        {
            return Input.mousePosition.x / Screen.width * screenWidthInUnits;
        }
    }
}

LoseCollider.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class LoseCollider : MonoBehaviour
{

    private void OnTriggerEnter2D(Collider2D collision)
    {
        SceneManager.LoadScene("Game Over");
    }

}

SceneLoader.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;


public class SceneLoader : MonoBehaviour
{

    public void LoadNextScene()
    {
        int currentSceneIndex = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(currentSceneIndex + 1);
    }

    public void LoadStartScene()
    {
        SceneManager.LoadScene(0);
        FindObjectOfType<GameSession>().ResetGame();
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

What do you think?

If you can forgive me for posting so late, I’d love to know what you think about the finished Summer Break block breaker game. Also, if you enjoy reading content like this, be sure to subscribe below to get alerts in your inbox when I post something new. ❤


Join 1,467 other subscribers
It's time for a few more levels, audio, improved gameplay experience, and all the other finishing touches for the Summer Break block breaker game. Read more at blissfullemon.com/summer-break-block-breaker-2

Let's chat!

%d bloggers like this: