Skip to content
Adam Graham edited this page Nov 9, 2023 · 12 revisions

Links

FAQs


Is the source project free to use?

Yes, you are free to use the project for any purpose. However, keep in mind that copyright and/or trademark laws can still apply to the original material since many of the games in my tutorials were not originally authored by me. I open source my own code for the community to learn from, but the project is intended for educational purposes only.


Why use a tilemap?

Many implementations of Tetris made with Unity don't use a tilemap. They instead create prefabs for each of the pieces and move them using the object's transform. This is a perfectly fine way to implement Tetris, but it requires manually creating a grid system to align the pieces and ensure they don't overlap with each other.

A tilemap already has a built-in grid system which simplifies some of the logic of the game, but it of course makes other aspects more difficult. There's no right or wrong implementation, just different pros and cons to each. Using a tilemap allowed me to focus on the game logic in a more intuitive way. It also helped me more easily implement some of the complex, data-driven features like wall-kicks.


Which rotation system does the tutorial use?

The version of Tetris in the tutorial is based on the "Super Rotation System" which has become the standard today. You can read more about it here: https://tetris.fandom.com/wiki/SRS. The wall-kick tables I used are pulled directly from that website.

My version of Tetris is not an exact replica of the game, nor are any of my tutorials intended to be. Although it is close, there are some edge cases that are not accounted for, such as certain "twists" any other advanced mechanics of Tetris.


How can I implement hold/swap/preview?

Source code: https://github.com/zigurous/unity-tetris-tutorial/blob/hold-swap/Assets/Scripts/Board.cs#L4-L115

First, we need to add a few new properties to our Board.cs script to keep track of the next piece and saved piece as well as the positions on the board where these pieces will be drawn.

public Piece activePiece { get; private set; }
public Piece nextPiece { get; private set; }  // <-- new property
public Piece savedPiece { get; private set; } // <-- new property

public Vector3Int spawnPosition = new Vector3Int(-1, 8, 0);
public Vector3Int previewPosition = new Vector3Int(-1, 12, 0); // <-- new property
public Vector3Int holdPosition = new Vector3Int(-1, 16, 0);    // <-- new property

In the Awake function, we add new components to our board for the next piece and saved piece. They will be disabled initially.

private void Awake()
{
    //...

    nextPiece = gameObject.AddComponent<Piece>();
    nextPiece.enabled = false;

    savedPiece = gameObject.AddComponent<Piece>();
    savedPiece.enabled = false;
}

Next, we need to refactor the SpawnPiece function to initialize the active piece with the data of the next piece. But first, we need a way to set the data on the next piece, so we'll add a new function for this. The logic is very similar to the existing spawn function, but we also draw the next piece at the "preview" position on the board.

private void SetNextPiece()
{
    // Clear the existing piece from the board
    if (nextPiece.cells != null) {
        Clear(nextPiece);
    }

    // Pick a random tetromino to use
    int random = Random.Range(0, tetrominoes.Length);
    TetrominoData data = tetrominoes[random];

    // Initialize the next piece with the random data
    // Draw it at the "preview" position on the board
    nextPiece.Initialize(this, previewPosition, data);
    Set(nextPiece);
}

Now, we can revisit the SpawnPiece function to initialize the data of the active piece with the next piece. After spawning the piece, we call SetNextPiece to initialize the next random piece.

public void SpawnPiece()
{
    // Initialize the active piece with the next piece data
    activePiece.Initialize(this, spawnPosition, nextPiece.data);

    // Only spawn the piece if valid position otherwise game over
    if (IsValidPosition(activePiece, spawnPosition)) {
        Set(activePiece);
    } else {
        GameOver();
    }

    // Set the next random piece
    SetNextPiece();
}

To handle saving and swapping pieces, we add an entirely new function.

public void SwapPiece()
{
    // Temporarily store the current saved data so we can swap
    TetrominoData savedData = savedPiece.data;

    // Clear the existing saved piece from the board
    if (savedData.cells != null) {
        Clear(savedPiece);
    }

    // Store the next piece as the new saved piece
    // Draw this piece at the "hold" position on the board
    savedPiece.Initialize(this, holdPosition, nextPiece.data);
    Set(savedPiece);

    // Swap the saved piece to be the next piece
    if (savedData.cells != null)
    {
        // Clear the existing next piece before swapping
        Clear(nextPiece);

        // Re-initialize the next piece with the saved data
        // Draw this piece at the "preview" position on the board
        nextPiece.Initialize(this, previewPosition, savedData);
        Set(nextPiece);
    }
}

And finally, we check for input in Update to actual perform the swap.

private void Update()
{
    if (Input.GetKeyDown(KeyCode.LeftShift) || Input.GetKeyDown(KeyCode.RightShift)) {
        SwapPiece();
    }
}