Board.cs
using UnityEngine;
using UnityEngine.Tilemaps;
[DefaultExecutionOrder(-1)]
public class Board : MonoBehaviour
{
public Tilemap tilemap { get; private set; }
public Piece activePiece { get; private set; }
public TetrominoData[] tetrominoes;
public Vector2Int boardSize = new Vector2Int(10, 20);
public Vector3Int spawnPosition = new Vector3Int(-1, 8, 0);
public RectInt Bounds
{
get
{
Vector2Int position = new Vector2Int(-boardSize.x / 2, -boardSize.y / 2);
return new RectInt(position, boardSize);
}
}
private void Awake()
{
tilemap = GetComponentInChildren<Tilemap>();
activePiece = GetComponentInChildren<Piece>();
for (int i = 0; i < tetrominoes.Length; i++)
{
tetrominoes[i].Initialize();
}
}
private void Start()
{
SpawnPiece();
}
public void SpawnPiece()
{
int random = Random.Range(0, tetrominoes.Length);
TetrominoData data = tetrominoes[random];
activePiece.Initialize(this, spawnPosition, data);
if (IsValidPosition(activePiece, spawnPosition))
{
Set(activePiece);
}
else
{
GameOver();
}
}
public void GameOver()
{
tilemap.ClearAllTiles();
}
public void Set(Piece piece)
{
for (int i = 0; i < piece.cells.Length; i++)
{
Vector3Int tilePosition = piece.cells[i] + piece.position;
tilemap.SetTile(tilePosition, piece.data.tile);
}
}
public void Clear(Piece piece)
{
for (int i = 0; i < piece.cells.Length; i++)
{
Vector3Int tilePosition = piece.cells[i] + piece.position;
tilemap.SetTile(tilePosition, null);
}
}
public bool IsValidPosition(Piece piece, Vector3Int position)
{
RectInt bounds = Bounds;
for (int i = 0; i < piece.cells.Length; i++)
{
Vector3Int tilePosition = piece.cells[i] + position;
if (!bounds.Contains((Vector2Int)tilePosition) || tilemap.HasTile(tilePosition))
{
return false;
}
}
return true;
}
public void ClearLines()
{
RectInt bounds = Bounds;
int row = bounds.yMin;
while (row < bounds.yMax)
{
if (IsLineFull(row))
{
LineClear(row);
}
else
{
row++;
}
}
}
public bool IsLineFull(int row)
{
RectInt bounds = Bounds;
for (int col = bounds.xMin; col < bounds.xMax; col++)
{
Vector3Int position = new Vector3Int(col, row, 0);
if (!tilemap.HasTile(position))
{
return false;
}
}
return true;
}
public void LineClear(int row)
{
RectInt bounds = Bounds;
for (int col = bounds.xMin; col < bounds.xMax; col++)
{
Vector3Int position = new Vector3Int(col, row, 0);
tilemap.SetTile(position, null);
}
while (row < bounds.yMax)
{
for (int col = bounds.xMin; col < bounds.xMax; col++)
{
Vector3Int position = new Vector3Int(col, row + 1, 0);
TileBase above = tilemap.GetTile(position);
position = new Vector3Int(col, row, 0);
tilemap.SetTile(position, above);
}
row++;
}
}
}
Ghost
using UnityEngine;
using UnityEngine.Tilemaps;
public class Ghost : MonoBehaviour
{
public Tile tile;
public Board mainBoard;
public Piece trackingPiece;
public Tilemap tilemap { get; private set; }
public Vector3Int[] cells { get; private set; }
public Vector3Int position { get; private set; }
private void Awake()
{
tilemap = GetComponentInChildren<Tilemap>();
cells = new Vector3Int[4];
}
private void LateUpdate()
{
Clear();
Copy();
Drop();
Set();
}
private void Clear()
{
for (int i = 0; i < cells.Length; i++)
{
Vector3Int tilePosition = cells[i] + position;
tilemap.SetTile(tilePosition, null);
}
}
private void Copy()
{
for (int i = 0; i < cells.Length; i++)
{
cells[i] = trackingPiece.cells[i];
}
}
private void Drop()
{
Vector3Int position = trackingPiece.position;
int current = position.y;
int bottom = -mainBoard.boardSize.y / 2 - 1;
mainBoard.Clear(trackingPiece);
for (int row = current; row >= bottom; row--)
{
position.y = row;
if (mainBoard.IsValidPosition(trackingPiece, position))
{
this.position = position;
}
else
{
break;
}
}
mainBoard.Set(trackingPiece);
}
private void Set()
{
for (int i = 0; i < cells.Length; i++)
{
Vector3Int tilePosition = cells[i] + position;
tilemap.SetTile(tilePosition, tile);
}
}
}
Piece
using UnityEngine;
public class Piece : MonoBehaviour
{
public Board board { get; private set; }
public TetrominoData data { get; private set; }
public Vector3Int[] cells { get; private set; }
public Vector3Int position { get; private set; }
public int rotationIndex { get; private set; }
public float stepDelay = 1f;
public float moveDelay = 0.1f;
public float lockDelay = 0.5f;
private float stepTime;
private float moveTime;
private float lockTime;
public void Initialize(Board board, Vector3Int position, TetrominoData data)
{
this.data = data;
this.board = board;
this.position = position;
rotationIndex = 0;
stepTime = Time.time + stepDelay;
moveTime = Time.time + moveDelay;
lockTime = 0f;
if (cells == null)
{
cells = new Vector3Int[data.cells.Length];
}
for (int i = 0; i < cells.Length; i++)
{
cells[i] = (Vector3Int)data.cells[i];
}
}
private void Update()
{
board.Clear(this);
lockTime += Time.deltaTime;
if (Input.GetKeyDown(KeyCode.Q))
{
Rotate(-1);
}
else if (Input.GetKeyDown(KeyCode.E))
{
Rotate(1);
}
if (Input.GetKeyDown(KeyCode.Space))
{
HardDrop();
}
if (Time.time > moveTime)
{
HandleMoveInputs();
}
if (Time.time > stepTime)
{
Step();
}
board.Set(this);
}
private void HandleMoveInputs()
{
if (Input.GetKey(KeyCode.S))
{
if (Move(Vector2Int.down))
{
stepTime = Time.time + stepDelay;
}
}
if (Input.GetKey(KeyCode.A))
{
Move(Vector2Int.left);
}
else if (Input.GetKey(KeyCode.D))
{
Move(Vector2Int.right);
}
}
private void Step()
{
stepTime = Time.time + stepDelay;
Move(Vector2Int.down);
if (lockTime >= lockDelay)
{
Lock();
}
}
private void HardDrop()
{
while (Move(Vector2Int.down))
{
continue;
}
Lock();
}
private void Lock()
{
board.Set(this);
board.ClearLines();
board.SpawnPiece();
}
private bool Move(Vector2Int translation)
{
Vector3Int newPosition = position;
newPosition.x += translation.x;
newPosition.y += translation.y;
bool valid = board.IsValidPosition(this, newPosition);
if (valid)
{
position = newPosition;
moveTime = Time.time + moveDelay;
lockTime = 0f;
}
return valid;
}
private void Rotate(int direction)
{
int originalRotation = rotationIndex;
rotationIndex = Wrap(rotationIndex + direction, 0, 4);
ApplyRotationMatrix(direction);
if (!TestWallKicks(rotationIndex, direction))
{
rotationIndex = originalRotation;
ApplyRotationMatrix(-direction);
}
}
private void ApplyRotationMatrix(int direction)
{
float[] matrix = Data.RotationMatrix;
for (int i = 0; i < cells.Length; i++)
{
Vector3 cell = cells[i];
int x, y;
switch (data.tetromino)
{
case Tetromino.I:
case Tetromino.O:
cell.x
cell.y -= 0.5f;
x = Mathf.CeilToInt((cell.x * matrix[0] * direction) + (cell.y * matrix[1] * direction));
y = Mathf.CeilToInt((cell.x * matrix[2] * direction) + (cell.y * matrix[3] * direction));
break;
default:
x = Mathf.RoundToInt((cell.x * matrix[0] * direction) + (cell.y * matrix[1] * direction));
y = Mathf.RoundToInt((cell.x * matrix[2] * direction) + (cell.y * matrix[3] * direction));
break;
}
cells[i] = new Vector3Int(x, y, 0);
}
}
private bool TestWallKicks(int rotationIndex, int rotationDirection)
{
int wallKickIndex = GetWallKickIndex(rotationIndex, rotationDirection);
for (int i = 0; i < data.wallKicks.GetLength(1); i++)
{
Vector2Int translation = data.wallKicks[wallKickIndex, i];
if (Move(translation))
{
return true;
}
}
return false;
}
private int GetWallKickIndex(int rotationIndex, int rotationDirection)
{
int wallKickIndex = rotationIndex * 2;
if (rotationDirection < 0)
{
wallKickIndex--;
}
return Wrap(wallKickIndex, 0, data.wallKicks.GetLength(0));
}
private int Wrap(int input, int min, int max)
{
if (input < min)
{
return max - (min - input) % (max - min);
}
else
{
return min + (input - min) % (max - min);
}
}
}
Data
using System.Collections.Generic;
using UnityEngine;
public static class Data
{
public static readonly float cos = Mathf.Cos(Mathf.PI / 2f);
public static readonly float sin = Mathf.Sin(Mathf.PI / 2f);
public static readonly float[] RotationMatrix = new float[] { cos, sin, -sin, cos };
public static readonly Dictionary<Tetromino, Vector2Int[]> Cells = new Dictionary<Tetromino, Vector2Int[]>()
{
{ Tetromino.I, new Vector2Int[] { new Vector2Int(-1, 1), new Vector2Int(0, 1), new Vector2Int(1, 1), new Vector2Int(2, 1) } },
{ Tetromino.J, new Vector2Int[] { new Vector2Int(-1, 1), new Vector2Int(-1, 0), new Vector2Int(0, 0), new Vector2Int(1, 0) } },
{ Tetromino.L, new Vector2Int[] { new Vector2Int(1, 1), new Vector2Int(-1, 0), new Vector2Int(0, 0), new Vector2Int(1, 0) } },
{ Tetromino.O, new Vector2Int[] { new Vector2Int(0, 1), new Vector2Int(1, 1), new Vector2Int(0, 0), new Vector2Int(1, 0) } },
{ Tetromino.S, new Vector2Int[] { new Vector2Int(0, 1), new Vector2Int(1, 1), new Vector2Int(-1, 0), new Vector2Int(0, 0) } },
{ Tetromino.T, new Vector2Int[] { new Vector2Int(0, 1), new Vector2Int(-1, 0), new Vector2Int(0, 0), new Vector2Int(1, 0) } },
{ Tetromino.Z, new Vector2Int[] { new Vector2Int(-1, 1), new Vector2Int(0, 1), new Vector2Int(0, 0), new Vector2Int(1, 0) } },
};
private static readonly Vector2Int[,] WallKicksI = new Vector2Int[,]
{
{ new Vector2Int(0, 0), new Vector2Int(-2, 0), new Vector2Int(1, 0), new Vector2Int(-2, -1), new Vector2Int(1, 2) },
{ new Vector2Int(0, 0), new Vector2Int(2, 0), new Vector2Int(-1, 0), new Vector2Int(2, 1), new Vector2Int(-1, -2) },
{ new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(2, 0), new Vector2Int(-1, 2), new Vector2Int(2, -1) },
{ new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(-2, 0), new Vector2Int(1, -2), new Vector2Int(-2, 1) },
{ new Vector2Int(0, 0), new Vector2Int(2, 0), new Vector2Int(-1, 0), new Vector2Int(2, 1), new Vector2Int(-1, -2) },
{ new Vector2Int(0, 0), new Vector2Int(-2, 0), new Vector2Int(1, 0), new Vector2Int(-2, -1), new Vector2Int(1, 2) },
{ new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(-2, 0), new Vector2Int(1, -2), new Vector2Int(-2, 1) },
{ new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(2, 0), new Vector2Int(-1, 2), new Vector2Int(2, -1) },
};
private static readonly Vector2Int[,] WallKicksJLOSTZ = new Vector2Int[,]
{
{ new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(-1, 1), new Vector2Int(0, -2), new Vector2Int(-1, -2) },
{ new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(1, -1), new Vector2Int(0, 2), new Vector2Int(1, 2) },
{ new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(1, -1), new Vector2Int(0, 2), new Vector2Int(1, 2) },
{ new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(-1, 1), new Vector2Int(0, -2), new Vector2Int(-1, -2) },
{ new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(1, 1), new Vector2Int(0, -2), new Vector2Int(1, -2) },
{ new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(-1, -1), new Vector2Int(0, 2), new Vector2Int(-1, 2) },
{ new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(-1, -1), new Vector2Int(0, 2), new Vector2Int(-1, 2) },
{ new Vector2Int(0, 0), new Vector2Int(1, 0), new Vector2Int(1, 1), new Vector2Int(0, -2), new Vector2Int(1, -2) },
};
public static readonly Dictionary<Tetromino, Vector2Int[,]> WallKicks = new Dictionary<Tetromino, Vector2Int[,]>
{
{ Tetromino.I, WallKicksI },
{ Tetromino.J, WallKicksJLOSTZ },
{ Tetromino.L, WallKicksJLOSTZ },
{ Tetromino.O, WallKicksJLOSTZ },
{ Tetromino.S, WallKicksJLOSTZ },
{ Tetromino.T, WallKicksJLOSTZ },
{ Tetromino.Z, WallKicksJLOSTZ },
};
}
Tetromino
using UnityEngine;
using UnityEngine.Tilemaps;
public enum Tetromino
{
I,
J,
L,
O,
S,
T,
Z
}
[System.Serializable]
public struct TetrominoData
{
public Tile tile;
public Tetromino tetromino;
public Vector2Int[] cells { get; private set; }
public Vector2Int[,] wallKicks { get; private set; }
public void Initialize()
{
cells = Data.Cells[tetromino];
wallKicks = Data.WallKicks[tetromino];
}
}