2048 en Unity Tutorial - 2

El tablero empieza a moverse

Programa las reglas del juego 2048.

El juego guarda sus números en una matriz de 4 × 4. En cada movimiento calcula primero un tablero nuevo y, solo si algo cambió, genera una ficha aleatoria.

20 minutosMatriz + listasUn solo script nuevo

1. Separa los datos de la pantalla

Un número y su casilla visual son cosas distintas. values[x, y] almacena el valor de la celda; tileViews[x, y] apunta al objeto que lo dibuja. Las coordenadas usan x para la columna y y para la fila.

Matriz de datosLa regla del juego opera sobre números, sin mover objetos de Unity.
Vista UIAl final del movimiento, cada ficha muestra el valor de la matriz.

2. Entiende el movimiento y las fusiones

Para cada fila o columna, el controlador lee los valores en orden desde el borde hacia el que se mueve. Descarta los huecos, combina cada pareja igual y deja un cero al final. Por ejemplo:

Una fila antes y despuésIzquierda
[ 2, 0, 2, 2 ]  →  [ 4, 2, 0, 0 ]
[ 2, 2, 2, 2 ]  →  [ 4, 4, 0, 0 ]

El segundo ejemplo es importante: una ficha recién combinada no se puede volver a combinar en el mismo movimiento. La lista compacta garantiza esa regla sin animaciones ni objetos intermedios.

Mejora respecto a una búsqueda recursiva de vecinos: procesar cada fila en una sola pasada hace visible el orden de las operaciones y evita combinar dos veces una ficha por accidente.

3. Añade el controlador

Crea Assets/Scripts/Game2048.cs y pega el script completo. Incluye la lógica, entrada de teclado, puntuación, récord local, una oportunidad para deshacer y la detección del final de partida. No requiere ninguna biblioteca externa.

Assets/Scripts/Game2048.csC# · script completo
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public sealed class Game2048 : MonoBehaviour
{
    private const int Size = 4;
    private const string BestScoreKey = "2048_BestScore";

    [Header("Referencias de la escena")]
    [SerializeField] private TileView tilePrefab;
    [SerializeField] private Transform gridRoot;
    [SerializeField] private TMP_Text scoreLabel;
    [SerializeField] private TMP_Text bestLabel;
    [SerializeField] private TMP_Text messageLabel;
    [SerializeField] private Button undoButton;
    [SerializeField] private Button restartButton;

    private readonly int[,] values = new int[Size, Size];
    private readonly TileView[,] tileViews = new TileView[Size, Size];
    private int[,] previousValues;
    private int score;
    private int bestScore;
    private int previousScore;
    private bool previousHasWon;
    private bool hasWon;
    private bool gameOver;
    private bool canUndo;

    private void Awake()
    {
        if (tilePrefab == null || gridRoot == null ||
            scoreLabel == null || bestLabel == null || messageLabel == null)
        {
            Debug.LogError("Asigna prefab, cuadrícula y tres textos en Game2048.", this);
            enabled = false;
            return;
        }

        for (int y = 0; y < Size; y++)
        {
            for (int x = 0; x < Size; x++)
            {
                tileViews[x, y] = Instantiate(tilePrefab, gridRoot);
            }
        }

        bestScore = PlayerPrefs.GetInt(BestScoreKey, 0);

        if (undoButton != null)
            undoButton.onClick.AddListener(UndoMove);
        if (restartButton != null)
            restartButton.onClick.AddListener(StartNewGame);

        StartNewGame();
    }

    private void OnDestroy()
    {
        if (undoButton != null)
            undoButton.onClick.RemoveListener(UndoMove);
        if (restartButton != null)
            restartButton.onClick.RemoveListener(StartNewGame);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
            MoveLeft();
        else if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
            MoveRight();
        else if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W))
            MoveUp();
        else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S))
            MoveDown();
    }

    public void StartNewGame()
    {
        for (int y = 0; y < Size; y++)
            for (int x = 0; x < Size; x++)
                values[x, y] = 0;

        score = 0;
        hasWon = false;
        gameOver = false;
        canUndo = false;
        previousValues = null;
        AddRandomTile();
        AddRandomTile();
        RefreshUI();
    }

    public void MoveLeft()  { Move(new Vector2Int(-1, 0)); }
    public void MoveRight() { Move(new Vector2Int(1, 0)); }
    public void MoveUp()    { Move(new Vector2Int(0, -1)); }
    public void MoveDown()  { Move(new Vector2Int(0, 1)); }

    private void Move(Vector2Int direction)
    {
        if (gameOver)
            return;

        int[,] nextValues = new int[Size, Size];
        int gainedScore = 0;

        for (int lane = 0; lane < Size; lane++)
        {
            List<int> line = new List<int>(Size);
            for (int step = 0; step < Size; step++)
                line.Add(values[GetX(direction, lane, step), GetY(direction, lane, step)]);

            List<int> movedLine = ProcessLine(line, out int lineScore);
            gainedScore += lineScore;

            for (int step = 0; step < Size; step++)
                nextValues[GetX(direction, lane, step), GetY(direction, lane, step)] = movedLine[step];
        }

        if (!HasChanged(nextValues))
            return;

        previousValues = (int[,])values.Clone();
        previousScore = score;
        previousHasWon = hasWon;
        canUndo = true;

        CopyBoard(nextValues, values);
        score += gainedScore;
        AddRandomTile();

        if (!hasWon && Contains2048())
            hasWon = true;

        gameOver = IsGameOver();
        SaveBestScore();
        RefreshUI();
    }

    private static List<int> ProcessLine(List<int> line, out int gainedScore)
    {
        List<int> compact = new List<int>(Size);
        for (int i = 0; i < line.Count; i++)
            if (line[i] != 0)
                compact.Add(line[i]);

        List<int> result = new List<int>(Size);
        gainedScore = 0;

        for (int i = 0; i < compact.Count; i++)
        {
            if (i + 1 < compact.Count && compact[i] == compact[i + 1])
            {
                int mergedValue = compact[i] * 2;
                result.Add(mergedValue);
                gainedScore += mergedValue;
                i++;
            }
            else
            {
                result.Add(compact[i]);
            }
        }

        while (result.Count < Size)
            result.Add(0);

        return result;
    }

    private static int GetX(Vector2Int direction, int lane, int step)
    {
        if (direction.x == 0)
            return lane;
        return direction.x < 0 ? step : Size - 1 - step;
    }

    private static int GetY(Vector2Int direction, int lane, int step)
    {
        if (direction.y == 0)
            return lane;
        return direction.y < 0 ? step : Size - 1 - step;
    }

    private bool HasChanged(int[,] nextValues)
    {
        for (int y = 0; y < Size; y++)
            for (int x = 0; x < Size; x++)
                if (values[x, y] != nextValues[x, y])
                    return true;
        return false;
    }

    private static void CopyBoard(int[,] source, int[,] destination)
    {
        for (int y = 0; y < Size; y++)
            for (int x = 0; x < Size; x++)
                destination[x, y] = source[x, y];
    }

    private void AddRandomTile()
    {
        List<Vector2Int> emptyCells = new List<Vector2Int>();
        for (int y = 0; y < Size; y++)
            for (int x = 0; x < Size; x++)
                if (values[x, y] == 0)
                    emptyCells.Add(new Vector2Int(x, y));

        if (emptyCells.Count == 0)
            return;

        Vector2Int cell = emptyCells[Random.Range(0, emptyCells.Count)];
        values[cell.x, cell.y] = Random.Range(0, 10) == 0 ? 4 : 2;
    }

    private bool Contains2048()
    {
        for (int y = 0; y < Size; y++)
            for (int x = 0; x < Size; x++)
                if (values[x, y] >= 2048)
                    return true;
        return false;
    }

    private bool IsGameOver()
    {
        for (int y = 0; y < Size; y++)
        {
            for (int x = 0; x < Size; x++)
            {
                if (values[x, y] == 0)
                    return false;
                if (x + 1 < Size && values[x, y] == values[x + 1, y])
                    return false;
                if (y + 1 < Size && values[x, y] == values[x, y + 1])
                    return false;
            }
        }

        return true;
    }

    public void UndoMove()
    {
        if (!canUndo || previousValues == null)
            return;

        CopyBoard(previousValues, values);
        score = previousScore;
        hasWon = previousHasWon;
        gameOver = false;
        canUndo = false;
        previousValues = null;
        RefreshUI();
    }

    private void SaveBestScore()
    {
        if (score <= bestScore)
            return;

        bestScore = score;
        PlayerPrefs.SetInt(BestScoreKey, bestScore);
        PlayerPrefs.Save();
    }

    private void RefreshUI()
    {
        for (int y = 0; y < Size; y++)
            for (int x = 0; x < Size; x++)
                tileViews[x, y].SetValue(values[x, y]);

        scoreLabel.text = "Puntuación: " + score;
        bestLabel.text = "Récord: " + bestScore;
        messageLabel.text = gameOver
            ? "No quedan movimientos. ¡Prueba otra vez!"
            : hasWon ? "¡Has llegado a 2048! Puedes seguir jugando." : string.Empty;

        if (undoButton != null)
            undoButton.interactable = canUndo;
    }
}
Nota: el navegador representa los operadores < y > como parte del formato HTML; al leer el bloque, verás los caracteres habituales de C#.

4. Conecta el controlador en Unity

  1. Crea un objeto vacío llamado GameController y añade el componente Game2048.
  2. Asigna el prefab Tile del proyecto al campo Tile Prefab y arrastra BoardRoot al campo Grid Root.
  3. Arrastra los textos de puntuación, récord y mensaje a sus campos correspondientes. Asigna los botones de deshacer y reiniciar si los has creado; el controlador conecta sus acciones automáticamente.
  4. Para los botones de dirección en pantalla, usa su evento On Click y elige Game2048 → MoveUp, MoveDown, MoveLeft o MoveRight. El teclado funciona además de estos botones.

5. Prueba las reglas

Pulsa Play. Verás dos fichas iniciales. Prueba las flechas o WASD. Un movimiento que no cambia ninguna casilla no debe crear una ficha; dos pares alineados deben formar dos fichas, no una. Cada fusión suma su valor a la puntuación.

Animación de ejemplo del juego 2048 original en Unity
Demostración del proyecto de referencia · ver repositorio y licencia MIT.
Al terminar

El juego ya se puede jugar. La siguiente lección explica las mejoras añadidas y cómo probar el comportamiento completo.

No hay comentarios