Arkandroid: Let’s make a Breakout Game with Unity3D! – Part 4

Nope, not so easy 😏 We’ve got a game to finish, remember? 😛

So I know I lied in the last post, we didn’t really add more blocks to the scene. But we did something important, we prepared the blocks so they can be initialized from a central controller, the GridController! Which is the name of the next script you will have to make!

On the scene, create a new empty object and on the Inspector, click on Add Component, go to new script and create a script with the name GridController. Then open it and copy the following code:


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

public class BlockInfo {
    public int lives;
    public string color;
}

public class GridLineInfo {
    public BlockInfo[] blocks;

    public GridLineInfo(int blocksCount) {
        blocks = new BlockInfo[blocksCount];
        for (int i = 0; i < blocksCount; i++) {
            blocks[i] = new BlockInfo();
        }
    }
}

[Serializable]
public class GridData {
    public int linesCount;
    public int blocksCount;
    public GridLineInfo[] lines;

    public GridData(string filename) {
        // Load level file
        TextAsset levelRaw = Resources.Load(filename) as TextAsset;

        // Split into lines
        string[] linesRaw = levelRaw.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None);

        // Get number of grid lines and blocks per line
        int[] counts = linesRaw[0].Split(' ').Select(int.Parse).ToArray();
        linesCount = counts[0];
        blocksCount = counts[1];

        lines = new GridLineInfo[linesCount];

        for (int i = 1; i <= linesCount; i++) {
            lines[i - 1] = new GridLineInfo(blocksCount);

            // Split line into array of integers
            string[] array = linesRaw[i].Split(' ').ToArray();

            for (int j = 0; j < blocksCount; j++) {
                int currentLives = Int32.Parse(array[j][0].ToString());

                if (currentLives < 0) {
                    string[] data = array[j].Split(',').ToArray();
                    string blockColor = data[1];
                    lines[i - 1].blocks[j].color = blockColor;
                    lines[i - 1].blocks[j].lives = currentLives;
                } else {
                    lines[i - 1].blocks[j].lives = currentLives;
                }
            }
        }
    }
}

public class GridController : MonoBehaviour {
    public GameObject BlockPrefab;

    public string level;

    private Vector3 GridStartPosition;

    float spriteWidth;
    float spriteHeight;

    float viewportWidth;

    private void Awake() {
        viewportWidth = Math.Abs(Camera.main.ViewportToWorldPoint(new Vector2(1, 1)).x - Camera.main.ViewportToWorldPoint(new Vector2(0, 1)).x);

        // Instantiate object off to the distance
        GameObject newBlock = Instantiate(BlockPrefab, new Vector3(-1000, -1000, 0), new Quaternion(0, 0, 0, 0));

        // Get sprite width and height
        spriteWidth = (float)newBlock.GetComponent<Renderer<().bounds.size.x;
        spriteHeight = (float)newBlock.GetComponent<Renderer<().bounds.size.y;

        // We got all the info we need, now destory it
        Destroy(newBlock);

        GridStartPosition = Camera.main.ViewportToWorldPoint(new Vector3(0, 1, 0));
        GridStartPosition.y -= spriteHeight;
        GridStartPosition.z = 0;
    }

    // Use this for initialization
    void Start () {
        GridData grid = new GridData(level);

        // Calculate margin
        float margin = (viewportWidth - (grid.blocksCount * spriteWidth)) / 2;

        GridStartPosition.x += margin;
        Vector3 position = GridStartPosition;

        for (int i = 0; i < grid.linesCount; i++) {
            for (int j = 0; j < grid.blocksCount; j++) {
                if (grid.lines[i].blocks[j].lives < 0) {
                    SpawnNewBlock(grid.lines[i].blocks[j].color, grid.lines[i].blocks[j].lives, position, new Quaternion(0, 0, 0, 0));
                }
                position.x += spriteWidth;
            }
            position.x = GridStartPosition.x;
            position.y -= spriteHeight;
        }
    }

    GameObject SpawnNewBlock(string color, int lives, Vector3 position, Quaternion rotation) {
        // Instantiate object off to the distance
        GameObject newBlock = Instantiate(BlockPrefab, new Vector3(-1000, -1000, 0), rotation);

        // Move to the new location based on the objects width
        newBlock.GetComponent<BlockController<().Initialize(color, lives);
        newBlock.transform.position = new Vector3(position.x + (spriteWidth / 2), position.y + (spriteHeight / 2), position.z);
        return newBlock;
    }
}

Hey, don’t look at me like that, you know I always explain what is going on! And this is not going to be an exception so let’s get started!

First off we define three classes: BlockInfo, GridLineInfo and GridData.

BlockInfo holds the information for one block, the number of lives it has and it’s color.

GridLineInfo then holds the data of the blocks in one line of the grid. At its core, it has a dynamic array of BlockInfo objects where the number of the blocks for the line gets set and the blocks initialized on the constructor of the class.

Then we have GridData which has an array of GridLineInfo objects. Its initialization is a bit more interesting than the one for GridLineInfo as it loads information from a resource (a text file holding the lives and color information of each block and of each line of the grid) and stores them into the array.

Lastly, we have the most interesting part of this script, the GridController itself. The GridController has one job: load a level file and build it on the scene!

Well, yes, I know, these were two things but don’t forget that the whole loading process is done by the GridData constructor. 😛

On Awake() we are just calculating a few stuff, things like the width of the viewport in world units and the width and height of the block.

SpawnNewBlock() instantiates a new block with the color, lives and its top left corner starting at the position we pass it as parameters.

Finally, Start() we create a new GridData object and we place down the blocks described in the level file we loaded into it.

Which level files?

I know I’ve been talking about these “level files” for a while now but what are they really?

Instead of having to create each level by hand, why not store the blocks configuration of each level on a file and load them from there?

Alright, but what do they look like?

Here’s an example:


4 4
1,blue 0 0 2,red
1,blue 1,blue 2,red 2,red
2,red 2,red 1,blue 1,blue
2,red 0 0 1,blue

Noooo, not again! Come on, that’s simple!

Okay, let’s have GridController draw it for us:

And that’s the result of the code above!

So the first line has two numbers. The first one is the number of lines and the second on the number of blocks per line. The lines that follow contain the lives and color information of each block separated by a comma. Also, each block is separated with space. For example 1,blue describes a blue block with one life. If we want to keep a block empty, we just place a 0 at it’s position without a color

The good thing about this system is that we can get as complex as we want without much effort. For example:


15 10
0 0 1,blue 0 0 0 0 1,blue 0 0
0 0 1,blue 0 0 0 0 1,blue 0 0
0 0 0 1,blue 0 0 1,blue 0 0 0
0 0 0 1,blue 0 0 1,blue 0 0 0
0 0 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 0 0
0 0 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 0 0
0 1,blue 1,blue 1,red 1,blue 1,blue 1,red 1,blue 1,blue 0
0 1,blue 1,blue 1,red 1,blue 1,blue 1,red 1,blue 1,blue 0
1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue
1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue
1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue
1,blue 0 1,blue 1,blue 1,blue 1,blue 1,blue 1,blue 0 1,blue
1,blue 0 1,blue 0 0 0 0 1,blue 0 1,blue
1,blue 0 1,blue 0 0 0 0 1,blue 0 1,blue
0 0 0 1,blue 0 0 1,blue 0 0 0

generates the following level:

 

Cool, right?

 

But to the Editor, select the GridController object and on the script component, drag and drop the Block prefab from the Prefabs folder into the Block Prefab property and type “Level1” on the Level property. Now create a folder named Resources inside the Assets folder. Create a text file named Level1.txt and paste the following configuration:


8 11
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 1,blue 1,blue 1,blue 0 0 0 1,blue 1,blue 1,blue 0
1,blue 0 0 0 0 0 1,blue 0 0 0 0
1,blue 0 0 0 0 0 1,blue 0 0 0 0
1,blue 0 0 0 0 0 1,blue 0 0 0 0
1,blue 0 0 1,blue 1,blue 0 1,blue 0 0 1,blue 1,blue
0 1,blue 1,blue 1,blue 0 0 0 1,blue 1,blue 1,blue 0

Now run the game and give yourself a pat on the back!

 

On the next chapter, we are going to fix some gameplay aspects! Stay tuned!

If you have any questions, ideas or suggestions, feel free to leave a comment below!

You can also follow me on Twitter @KamaropoulosK for more updates on the #100DaysOfCode Challenge and more!

See you all on the next one!