using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// MatchBoard — a complete, minimal match-3 core loop for Unity 6.
///
/// HOW TO USE:
/// 1. Create a new 3D (URP or Built-in) project in Unity 6.3 LTS.
/// 2. In the Hierarchy: Create Empty -> rename it "Board".
/// 3. Drag this script onto "Board" (or Add Component -> Match Board).
/// 4. Make sure there is a "Main Camera" in the scene (there is by default).
/// 5. Press Play. Click one tile, then an adjacent tile, to swap.
///
/// This intentionally has NO backend, NO pooling, NO special candies yet.
/// It is the honest foundation. Build fun on top of this, not infrastructure
/// underneath it. The grid is pure 2D logic; the cubes are just the renderer.
///
public class MatchBoard : MonoBehaviour
{
[Header("Board size")]
public int width = 8;
public int height = 8;
public int colorCount = 6; // 3..6
[Header("Layout / feel")]
public float spacing = 1.1f; // world distance between tile centers
public float fallSpeed = 14f; // how fast tiles slide into their slot
// ---- state ----
int[,] grid; // color id per cell (0..colorCount-1), -1 = empty
Transform[,] tiles; // the visible cube for each cell
Vector3[,] target; // where each cube should slide to
Color[] palette;
Camera cam;
bool busy; // locks input while the board resolves
Vector2Int? selected;
void Start()
{
cam = Camera.main;
BuildPalette();
grid = new int[width, height];
tiles = new Transform[width, height];
target = new Vector3[width, height];
BuildBoard();
PlaceCamera();
}
void Update()
{
// Every frame, slide each cube toward its logical slot. Decoupling the
// animation from the logic like this keeps the cascade code simple.
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
if (tiles[x, y] != null)
tiles[x, y].position = Vector3.MoveTowards(
tiles[x, y].position, target[x, y], fallSpeed * Time.deltaTime);
if (!busy && Input.GetMouseButtonDown(0))
HandleClick();
}
// ---------------------------------------------------------------- build
void BuildPalette()
{
palette = new Color[]
{
new Color(0.90f, 0.22f, 0.27f), // red
new Color(0.98f, 0.75f, 0.05f), // yellow
new Color(0.49f, 0.23f, 0.93f), // purple
new Color(0.96f, 0.49f, 0.00f), // orange
new Color(0.26f, 0.63f, 0.28f), // green
new Color(0.12f, 0.53f, 0.90f), // blue
};
colorCount = Mathf.Clamp(colorCount, 3, palette.Length);
}
void BuildBoard()
{
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
grid[x, y] = PickNonMatching(x, y);
SpawnTile(x, y, grid[x, y]);
}
}
// Avoid creating a 3-in-a-row as we fill left->right, bottom->top.
int PickNonMatching(int x, int y)
{
var banned = new HashSet();
if (x >= 2 && grid[x - 1, y] == grid[x - 2, y]) banned.Add(grid[x - 1, y]);
if (y >= 2 && grid[x, y - 1] == grid[x, y - 2]) banned.Add(grid[x, y - 1]);
int c;
do { c = Random.Range(0, colorCount); } while (banned.Contains(c));
return c;
}
void SpawnTile(int x, int y, int color)
{
var go = GameObject.CreatePrimitive(PrimitiveType.Cube); // includes a BoxCollider for raycasts
go.name = $"Tile_{x}_{y}";
go.transform.SetParent(transform);
go.transform.localScale = Vector3.one * 0.92f;
go.GetComponent().material.color = palette[color];
Vector3 slot = WorldPos(x, y);
go.transform.position = slot + Vector3.up * (height * spacing); // start above and fall in
target[x, y] = slot;
tiles[x, y] = go.transform;
}
Vector3 WorldPos(int x, int y) => new Vector3(x * spacing, y * spacing, 0f);
void PlaceCamera()
{
if (cam == null) return;
float cx = (width - 1) * spacing / 2f;
float cy = (height - 1) * spacing / 2f;
cam.orthographic = true; // flat 2D-style view of 3D cubes
cam.orthographicSize = (height * spacing) / 2f + 0.6f;
cam.transform.position = new Vector3(cx, cy, -10f);
cam.transform.rotation = Quaternion.identity;
// For a true 3D look later: cam.orthographic = false; then tilt the camera and add a light.
}
// ---------------------------------------------------------------- input
void HandleClick()
{
Ray ray = cam.ScreenPointToRay(Input.mousePosition);
if (!Physics.Raycast(ray, out RaycastHit hit)) return;
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
if (tiles[x, y] != null && tiles[x, y] == hit.transform)
{
OnTileClicked(new Vector2Int(x, y));
return;
}
}
void OnTileClicked(Vector2Int cell)
{
if (selected == null) { selected = cell; Highlight(cell, true); return; }
Vector2Int a = selected.Value;
Highlight(a, false);
selected = null;
if (a == cell) return; // tapped same tile -> deselect
if (IsAdjacent(a, cell)) StartCoroutine(TrySwap(a, cell));
else { selected = cell; Highlight(cell, true); } // tapped far tile -> reselect
}
bool IsAdjacent(Vector2Int a, Vector2Int b)
=> Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y) == 1;
void Highlight(Vector2Int c, bool on)
{
if (tiles[c.x, c.y] != null)
tiles[c.x, c.y].localScale = Vector3.one * (on ? 1.08f : 0.92f);
}
// ---------------------------------------------------------------- core loop
IEnumerator TrySwap(Vector2Int a, Vector2Int b)
{
busy = true;
SwapCells(a, b);
yield return new WaitForSeconds(0.18f);
if (FindMatches().Count == 0)
{
SwapCells(a, b); // illegal move -> swap back
yield return new WaitForSeconds(0.18f);
}
else
{
yield return ResolveCascades();
}
busy = false;
}
void SwapCells(Vector2Int a, Vector2Int b)
{
(grid[a.x, a.y], grid[b.x, b.y]) = (grid[b.x, b.y], grid[a.x, a.y]);
(tiles[a.x, a.y], tiles[b.x, b.y]) = (tiles[b.x, b.y], tiles[a.x, a.y]);
target[a.x, a.y] = WorldPos(a.x, a.y);
target[b.x, b.y] = WorldPos(b.x, b.y);
}
// Returns every cell that is part of a run of 3+ (horizontal or vertical).
HashSet FindMatches()
{
var matched = new HashSet();
for (int y = 0; y < height; y++) // horizontal runs
{
int run = 1;
for (int x = 1; x <= width; x++)
{
bool same = x < width && grid[x, y] != -1 && grid[x, y] == grid[x - 1, y];
if (same) run++;
else { if (run >= 3) for (int k = 1; k <= run; k++) matched.Add(new Vector2Int(x - k, y)); run = 1; }
}
}
for (int x = 0; x < width; x++) // vertical runs
{
int run = 1;
for (int y = 1; y <= height; y++)
{
bool same = y < height && grid[x, y] != -1 && grid[x, y] == grid[x, y - 1];
if (same) run++;
else { if (run >= 3) for (int k = 1; k <= run; k++) matched.Add(new Vector2Int(x, y - k)); run = 1; }
}
}
return matched;
}
IEnumerator ResolveCascades()
{
while (true)
{
var matches = FindMatches();
if (matches.Count == 0) yield break; // <-- hook a combo multiplier / score here later
foreach (var c in matches)
{
if (tiles[c.x, c.y] != null) Destroy(tiles[c.x, c.y].gameObject);
tiles[c.x, c.y] = null;
grid[c.x, c.y] = -1;
}
yield return new WaitForSeconds(0.12f);
ApplyGravity();
Refill();
yield return new WaitForSeconds(0.20f);
}
}
void ApplyGravity()
{
for (int x = 0; x < width; x++)
{
int write = 0; // lowest empty slot in this column
for (int y = 0; y < height; y++)
{
if (grid[x, y] != -1)
{
if (write != y)
{
grid[x, write] = grid[x, y];
tiles[x, write] = tiles[x, y];
target[x, write] = WorldPos(x, write);
grid[x, y] = -1;
tiles[x, y] = null;
}
write++;
}
}
}
}
void Refill()
{
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
if (grid[x, y] == -1)
{
grid[x, y] = Random.Range(0, colorCount);
SpawnTile(x, y, grid[x, y]);
}
}
}