Ruben Bimmel

Game Development

Tetris Adventure

Assignment: Recreate a retro game and give it a twist
Duration: 2 months
Team size: Solo project
Role: Development
Skills:
Links: GitHub

For this game I created a unique twist to one of my favourite retro games, Tetris. The twist is that the game is also a platformer. A character needs to jump on top of the tetris blocks to stay alive. The player controls both the tetris blocks and the character. It is really hard to focus on both of them. The game turned out to be really fun to play and I actually still play this game every now and then.

The challenge in creating this game was to combine the grid based gameplay of tetris with the physics based gameplay of a platformer. There are a lot of rules to consider about how the falling blocks interact with other blocks, enemies and the player. Tetris is designed in a really clever way and you can break the gameplay really fast if you implement new mechanics in a wrong way.

Block class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Block : MonoBehaviour {

    private Collectable collectable;
    private Grid grid;

    // Set colour of block
    public void SetColour (Color colour) {
        transform.GetComponent<SpriteRenderer>().color = colour;
    }

    // Set grid referance of block
    public void SetGrid (Grid newGrid) {
        grid = newGrid;
    }

    // Add collectable to Block
    public bool AddCollectable(Collectable newCollactable) {
        if (!collectable) {
            collectable = newCollactable;
            collectable.transform.parent = transform;
            collectable.transform.localPosition = new Vector3(0, 0, -1);
            return true;
        }
        return false;
    }

    // Destroy block and release collectables
    public void Destroy () {
        if (collectable != null) {
            int column = grid.GetLocalPosition(transform.position)[0];
            int row = grid.getHeighestAvailableCellInColumn(column);
            collectable.transform.parent = grid.transform;
            collectable.transform.localPosition = new Vector3(column, row, -1);
            collectable.SetActive(true);
        }
        Destroy(gameObject);
    }

    // Gets called every time a block moves
    public void CheckCharacterCollision() {
        Collider2D characterCollider = Physics2D.OverlapBox(transform.position, transform.lossyScale, 0f, LayerMask.GetMask("Character"));
        if (characterCollider) {
            Vector2 characterPosition = characterCollider.transform.position;
            if (Mathf.Abs(characterPosition.x - transform.position.x) < .158f && characterPosition.y - transform.position.y < .158f && characterPosition.y - transform.position.y > -.63f) {
                characterCollider.GetComponent<ICharacterController>().HitByBlock();
            }
        }
    }
}

Tetromino class

Open »
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tetromino {

    private Grid grid;
    public enum TetrominoShape { I, J, L, O, S, T, Z };
    public TetrominoShape shape;
    private Color colour;
    public int[,] blockPositions;
    private int rotation;
    private Collectable[] collectables = new Collectable[4];

    // Constructor
    public Tetromino () {
        shape = (TetrominoShape)Random.Range(0, 6);
        
        switch (shape) {
            case TetrominoShape.I:
                colour = Color.red;
                break;
            case TetrominoShape.J:
                colour = Color.blue;
                break;
            case TetrominoShape.L:
                colour = new Color(1, .5f, 0);
                break;
            case TetrominoShape.O:
                colour = Color.yellow;
                break;
            case TetrominoShape.S:
                colour = Color.magenta;
                break;
            case TetrominoShape.T:
                colour = Color.cyan;
                break;
            case TetrominoShape.Z:
                colour = Color.green;
                break;
        }

        rotation = 0;
    }

    // Adds collactable to tetronimo list. When it is added to the grid it will be added to the newly created block
    public bool AddCollectable (Collectable newCollectable) {
        for (int i = 0; i < 4; i++) {
            if (collectables[i] == null) {
                collectables[i] = newCollectable;
                return true;
            }
        }
        return false;
    }

    // Add this tetromino to a grid. New Blocks will be constructed.
    public bool AddToGrid (Grid _grid) {
        grid = _grid;
        blockPositions = GetBlockPositions();

        for (int i = 0; i < 4; i++) {
            bool added = grid.AddBlock(blockPositions[i, 0], blockPositions[i, 1], colour);
            if (!added) {
                return false;
            }

            // Add collectables to blocks
            if (collectables[i]) {
                grid.GetBlock(blockPositions[i, 0], blockPositions[i, 1]).AddCollectable(collectables[i]);
                collectables[i].SetActive(false);
            }
        }

        return true;
    }

    // Public move function
    public bool Move (int xOffset, int yOffset) {
        int[,] targetPositions = (int[,])blockPositions.Clone();
        for (int i = 0; i < 4; i++) {
            targetPositions[i, 0] += xOffset;
            targetPositions[i, 1] += yOffset;
        }

        bool tetrominoCanMove = ((GameGrid) grid).TransformTetromino(blockPositions, targetPositions);

        if (tetrominoCanMove) {
            blockPositions = targetPositions;
        }

        return tetrominoCanMove;
    }

    // Public rotate function
    public bool Rotate () {
        //O can not rotate
        if (shape == TetrominoShape.O) {
            return true;
        }

        //calculate new positions for rotated tetromino
        int[,] targetPositions = new int[4, 2];
        int[,] relativePosition = new int[4, 2];

        int rotationBlock = GetRotationBlock();

        for (int i = 0; i < 4; i++) {
            relativePosition[i, 0] = blockPositions[i, 0] - blockPositions[rotationBlock, 0];
            relativePosition[i, 1] = blockPositions[i, 1] - blockPositions[rotationBlock, 1];

            targetPositions[i, 0] = blockPositions[rotationBlock, 0] + relativePosition[i, 1];
            targetPositions[i, 1] = blockPositions[rotationBlock, 1] - relativePosition[i, 0];
        }

        //I gets repositioned on rotation
        if (shape == TetrominoShape.I) {
            if (rotation % 2 == 1) {
                for (int i = 0; i < 4; i++) {
                    targetPositions[i, 0]--;
                }
            }
        }

        bool tetrominoCanMove = ((GameGrid)grid).TransformTetromino(blockPositions, targetPositions);

        if (tetrominoCanMove) {
            rotation = ++rotation % 4;
            blockPositions = targetPositions;
        }

        return tetrominoCanMove;

    }

    // Gets called on initialisation.
    private int[,] GetBlockPositions() {
        int[,] positions = new int[,] { };

        switch (shape) {
            case TetrominoShape.I:
                positions = new int[,] { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 2, 0 } };
                break;
            case TetrominoShape.J:
                positions = new int[,] { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 1, -1 } };
                break;
            case TetrominoShape.L:
                positions = new int[,] { { -1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 } };
                break;
            case TetrominoShape.O:
                positions = new int[,] { { 0, 0 }, { 0, -1 }, { 1, 0 }, { 1, -1 } };
                break;
            case TetrominoShape.S:
                positions = new int[,] { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 1, 0 } };
                break;
            case TetrominoShape.T:
                positions = new int[,] { { -1, 0 }, { 0, 0 }, { 0, -1 }, { 1, 0 } };
                break;
            case TetrominoShape.Z:
                positions = new int[,] { { -1, 0 }, { 0, 0 }, { 0, -1 }, { 1, -1 } };
                break;
        }

        for (int i = 0; i < 4; i++) {
            positions[i, 0] += grid.spawnPosition[0];
            positions[i, 1] += grid.spawnPosition[1];
        }

        return positions;
    }

    // Get anchor block for tetromino rotation, Tetrominos can't always rotate around its center
    // Returns the index of the block to rotate around
    private int GetRotationBlock() {
        switch (shape) {
            case TetrominoShape.I:
                if (rotation < 2) return 2;
                return 1;
            case TetrominoShape.J:
                return 1;
            case TetrominoShape.L:
                return 2;
            case TetrominoShape.S:
                if (rotation < 2) return 1;
                return 2;
            case TetrominoShape.T:
                return 1;
            case TetrominoShape.Z:
                if (rotation < 2) return 1;
                return 2;
        }

        return 0;
    }
}
« Back to portfolio

Contact