Hola a todos, si alguno llego hasta acá, ¡felicidades!
Como a todos, tuve mis stoppers, pero ahora que tenemos la IA podemos profundizar más en esos detalles donde fallamos y mejorar, esto lo comento porque tuve problemas con el código, hasta lo de los matches ya no me funciono y me frustre, pero en vez de quedarme de brazos, empecé a hacer mi propia investigación y llegue a una solución junto con la IA, les dejo el código, espero les sirva para que puedan seguir con el curso, estúdienlo a detalle para que puedan ver y entender los cambios:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using DG.Tweening;
public class Board : MonoBehaviour
{
[Header("Visual")]
public float timeBetweenPieces = 0.05f;
[Header("Board")]
public int width;
public int height;
public GameObject tileObject;
[Header("Camera")]
public float cameraSizeOffset;
public float cameraVerticalOffset;
[Header("Pieces")]
public GameObject[] availablePieces;
private Tile[,] Tiles;
private Piece[,] Pieces;
private Tile startTile;
private Tile endTile;
private bool swappingPieces = false;
[Header("Spawn")]
[SerializeField] private int spawnYOffset = 2; // cuánto arriba spawnear
[SerializeField] private int maxSpawnIterations = 50; // anti-loop
void Start()
{
Tiles = new Tile[width, height];
Pieces = new Piece[width, height];
SetupBoard();
PositionCamera();
// Llenado inicial con "juice" (timeBetweenPieces) y evitando matches iniciales
StartCoroutine(FillBoardCoroutine());
}
// =========================
// Setup (tiles/cámara)
// =========================
private void SetupBoard()
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
var o = Instantiate(tileObject, new Vector3(x, y, -5), Quaternion.identity);
o.transform.parent = transform;
Tiles[x, y] = o.GetComponent<Tile>();
Tiles[x, y]?.Setup(x, y, this);
}
}
}
private void PositionCamera()
{
float newPosX = (float)width / 2f;
float newPosY = (float)height / 2f;
Camera.main.transform.position = new Vector3(
newPosX - 0.5f,
newPosY - 0.5f + cameraVerticalOffset,
-10f
);
float horizontal = width + 1;
float vertical = (height / 2) + 1;
Camera.main.orthographicSize =
horizontal > vertical ? horizontal + cameraSizeOffset : vertical + cameraSizeOffset;
}
// =========================
// Input desde Tile
// =========================
public void TileDown(Tile tile_)
{
if (swappingPieces) return;
startTile = tile_;
}
public void TileOver(Tile tile_)
{
if (swappingPieces) return;
endTile = tile_;
}
public void TileUp(Tile tile_)
{
if (swappingPieces) return;
if (startTile != null && endTile != null && IsCloseTo(startTile, endTile))
{
swappingPieces = true;
StartCoroutine(SwapTiles());
}
else
{
startTile = null;
endTile = null;
}
}
// =========================
// Swap + Match + Resolve
// =========================
private IEnumerator SwapTiles()
{
var a = Pieces[startTile.x, startTile.y];
var b = Pieces[endTile.x, endTile.y];
if (a == null || b == null)
{
ResetSwapState();
yield break;
}
// Mover y esperar fin REAL del tween
Tween t1 = a.MoveTween(endTile.x, endTile.y);
Tween t2 = b.MoveTween(startTile.x, startTile.y);
// swap lógico inmediato
Pieces[startTile.x, startTile.y] = b;
Pieces[endTile.x, endTile.y] = a;
yield return DOTween.Sequence().Join(t1).Join(t2).WaitForCompletion();
var m1 = GetMatchByPiece(startTile.x, startTile.y, 3);
var m2 = GetMatchByPiece(endTile.x, endTile.y, 3);
var allMatches = m1.Union(m2).Where(p => p != null).Distinct().ToList();
if (allMatches.Count == 0)
{
// Revertir y esperar fin REAL del tween
Tween r1 = a.MoveTween(startTile.x, startTile.y);
Tween r2 = b.MoveTween(endTile.x, endTile.y);
Pieces[startTile.x, startTile.y] = a;
Pieces[endTile.x, endTile.y] = b;
yield return DOTween.Sequence().Join(r1).Join(r2).WaitForCompletion();
ResetSwapState();
yield break;
}
yield return StartCoroutine(ResolveMatchesRecursively(allMatches));
ResetSwapState();
}
private void ResetSwapState()
{
startTile = null;
endTile = null;
swappingPieces = false;
}
// =========================
// Resolver: Clear -> Collapse -> Refill -> Cascadas (solo piezas afectadas)
// + Refill con timeBetweenPieces (estilo profesor)
// =========================
private IEnumerator ResolveMatchesRecursively(List<Piece> matches)
{
ClearPieces(matches);
var columns = GetColumns(matches);
// Collapse (con lista de tweens y piezas movidas)
var collapse = CollapseColumns(columns);
yield return WaitForTweens(collapse.tweens);
// Refill escalonado (timeBetweenPieces) evitando matches instantáneos
var spawnedPieces = new List<Piece>();
yield return StartCoroutine(FillColumnsCoroutine(columns, spawnedPieces));
// Piezas candidatas para revisar cascadas (lo del profesor: solo afectadas)
var affectedPieces = collapse.movedPieces
.Union(spawnedPieces)
.Where(p => p != null)
.Distinct()
.ToList();
while (true)
{
var newMatches = new List<Piece>();
foreach (var p in affectedPieces)
{
if (p == null) continue;
var m = GetMatchByPiece(p.x, p.y, 3);
if (m != null && m.Count > 0)
newMatches = newMatches.Union(m).ToList();
}
newMatches = newMatches.Where(p => p != null).Distinct().ToList();
if (newMatches.Count == 0)
yield break;
ClearPieces(newMatches);
columns = GetColumns(newMatches);
collapse = CollapseColumns(columns);
yield return WaitForTweens(collapse.tweens);
spawnedPieces = new List<Piece>();
yield return StartCoroutine(FillColumnsCoroutine(columns, spawnedPieces));
affectedPieces = collapse.movedPieces
.Union(spawnedPieces)
.Where(p => p != null)
.Distinct()
.ToList();
}
}
// =========================
// Fill inicial (todo el board) con timeBetweenPieces
// =========================
private IEnumerator FillBoardCoroutine()
{
var allColumns = Enumerable.Range(0, width).ToList();
var spawned = new List<Piece>();
yield return StartCoroutine(FillColumnsCoroutine(allColumns, spawned));
}
// Refill escalonado + anti-match (similar a SetupPieces del profesor, pero con tween)
private IEnumerator FillColumnsCoroutine(List<int> columns, List<Piece> spawnedPiecesOut)
{
List<Tween> tweens = new List<Tween>();
for (int i = 0; i < columns.Count; i++)
{
int x = columns[i];
for (int y = 0; y < height; y++)
{
if (Pieces[x, y] != null) continue;
yield return new WaitForSeconds(timeBetweenPieces);
int iterations = 0;
while (iterations < maxSpawnIterations)
{
// 1) Crear pieza (sin tween todavía)
var piece = CreatePieceAtSpawn(x, y);
// 2) Si genera match inmediato, destruirla SIN haber creado tween
if (HasPreviousMatches(x, y))
{
// ClearPieceAt ya mata tweens si existieran
ClearPieceAt(x, y);
iterations++;
continue;
}
// 3) Ya es válida: ahora sí, animar caída
var t = piece.MoveTween(x, y);
if (t != null) tweens.Add(t);
spawnedPiecesOut.Add(piece);
break;
}
}
}
// esperar que terminen los tweens reales del relleno
yield return WaitForTweens(tweens);
}
private Piece CreatePieceAtSpawn(int x, int y)
{
int spawnY = height + spawnYOffset;
var selected = availablePieces[UnityEngine.Random.Range(0, availablePieces.Length)];
var o = Instantiate(selected, new Vector3(x, spawnY, -5f), Quaternion.identity);
o.transform.parent = transform;
var piece = o.GetComponent<Piece>();
piece.Setup(x, y, this);
Pieces[x, y] = piece;
return piece;
}
// =========================
// Clear / Collapse helpers
// =========================
private void ClearPieces(List<Piece> piecesToClear)
{
foreach (var p in piecesToClear.Where(p => p != null).Distinct())
{
ClearPieceAt(p.x, p.y);
}
}
private void ClearPieceAt(int x, int y)
{
if (x < 0 || x >= width || y < 0 || y >= height) return;
var pieceToClear = Pieces[x, y];
if (pieceToClear == null) return;
// mata tweens antes de destruir
if (pieceToClear.transform != null)
pieceToClear.transform.DOKill();
Pieces[x, y] = null;
Destroy(pieceToClear.gameObject);
}
private List<int> GetColumns(List<Piece> pieces)
{
var result = new List<int>();
foreach (var p in pieces.Where(p => p != null))
{
if (!result.Contains(p.x)) result.Add(p.x);
}
return result;
}
private (List<Piece> movedPieces, List<Tween> tweens) CollapseColumns(List<int> columns)
{
List<Piece> moved = new List<Piece>();
List<Tween> tweens = new List<Tween>();
for (int i = 0; i < columns.Count; i++)
{
int x = columns[i];
for (int y = 0; y < height; y++)
{
if (Pieces[x, y] != null) continue;
for (int yPlus = y + 1; yPlus < height; yPlus++)
{
if (Pieces[x, yPlus] == null) continue;
var falling = Pieces[x, yPlus];
Tween t = falling.MoveTween(x, y);
tweens.Add(t);
Pieces[x, y] = falling;
Pieces[x, yPlus] = null;
if (!moved.Contains(falling)) moved.Add(falling);
break;
}
}
}
return (moved, tweens);
}
private IEnumerator WaitForTweens(List<Tween> tweens)
{
if (tweens == null || tweens.Count == 0) yield break;
var seq = DOTween.Sequence();
foreach (var t in tweens)
{
if (t != null && t.active) seq.Join(t);
}
if (seq.active)
yield return seq.WaitForCompletion();
}
// =========================
// Match logic
// =========================
public bool IsCloseTo(Tile start, Tile end)
{
if (Math.Abs(start.x - end.x) == 1 && start.y == end.y) return true;
if (Math.Abs(start.y - end.y) == 1 && start.x == end.x) return true;
return false;
}
private bool HasPreviousMatches(int posx, int posy)
{
var down = GetMatchByDirection(posx, posy, new Vector2(0, -1), 2) ?? new List<Piece>();
var left = GetMatchByDirection(posx, posy, new Vector2(-1, 0), 2) ?? new List<Piece>();
return (down.Count > 0 || left.Count > 0);
}
public List<Piece> GetMatchByDirection(int xpos, int ypos, Vector2 direction, int minPieces = 3)
{
Piece startPiece = Pieces[xpos, ypos];
if (startPiece == null) return null;
List<Piece> matches = new List<Piece> { startPiece };
int maxVal = width > height ? width : height;
for (int i = 1; i < maxVal; i++)
{
int nx = xpos + ((int)direction.x * i);
int ny = ypos + ((int)direction.y * i);
if (nx < 0 || nx >= width || ny < 0 || ny >= height) break;
var next = Pieces[nx, ny];
if (next != null && next.pieceType == startPiece.pieceType) matches.Add(next);
else break;
}
return matches.Count >= minPieces ? matches : null;
}
public List<Piece> GetMatchByPiece(int xpos, int ypos, int minPieces = 3)
{
var up = GetMatchByDirection(xpos, ypos, new Vector2(0, 1), 2) ?? new List<Piece>();
var down = GetMatchByDirection(xpos, ypos, new Vector2(0, -1), 2) ?? new List<Piece>();
var right = GetMatchByDirection(xpos, ypos, new Vector2(1, 0), 2) ?? new List<Piece>();
var left = GetMatchByDirection(xpos, ypos, new Vector2(-1, 0), 2) ?? new List<Piece>();
var vertical = up.Union(down).Where(p => p != null).Distinct().ToList();
var horizontal = left.Union(right).Where(p => p != null).Distinct().ToList();
var found = new List<Piece>();
if (vertical.Count >= minPieces) found = found.Union(vertical).ToList();
if (horizontal.Count >= minPieces) found = found.Union(horizontal).ToList();
return found.Where(p => p != null).Distinct().ToList();
}
}
/////////////////////////////////////////////////////////////
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Tile : MonoBehaviour
{
public int x;
public int y;
public Board board;
public void Setup(int x_, int y_, Board board_)
{
x = x_;
y = y_;
board = board_;
}
public void OnMouseDown()
{
board.TileDown(this);
}
public void OnMouseEnter()
{
board.TileOver(this);
}
public void OnMouseUp()
{
board.TileUp(this);
}
}
/////////////////////////////////////////////////////////////
using UnityEngine;
using DG.Tweening;
public class Piece : MonoBehaviour
{
public int x;
public int y;
public Board board;
[SerializeField] private float moveDuration = 0.25f;
public enum type
{
Block_Red,
Block_Green,
Block_Blue,
Block_Yellow,
Block_Purple,
Block_Orange,
Block_Cyan,
Block_Pink,
Block_Brown,
Block_Peach,
Tile_Ice,
Tile_IceCracked,
Tile_Frozen,
Tile_Stone,
Tile_Sand,
Tile_Grass_Light,
Tile_Grass_Dense
}
public type pieceType;
public void Setup(int x_, int y_, Board board_)
{
x = x_;
y = y_;
board = board_;
}
public Tween MoveTween(int desX, int desY)
{
if (!this || transform == null) return null;
// mata cualquier tween previo de este transform
transform.DOKill(false);
return transform
.DOMove(new Vector3(desX, desY, -5f), moveDuration)
.SetEase(Ease.InOutCubic)
// clave: si el GO se destruye, el tween se mata automáticamente
.SetLink(gameObject, LinkBehaviour.KillOnDestroy)
.OnComplete(() =>
{
x = desX;
y = desY;
});
}
public void Move(int desX, int desY) => MoveTween(desX, desY);
private void OnDestroy()
{
// respaldo extra
if (transform != null) transform.DOKill();
}
}
Yo trabajé con mis propios assets, adecuénelo a como lo estén trabajando ustedes.
Saludos.
PD: al parecer hay problemas de traduccion con algunos caracteres especiales logicos, arreglen esos detalle para que no tengan problema con el codigo, al parecer es un problema con la caja de comentarios de Platzi.