Добро пожаловать в туториал по Unity 2D. В этом уроке мы пошагово
создадим базовую основу для игры «три в ряд». Предыдущий опыт работы с
Unity не требуется.
Установи
Unity 6.0 LTS (или любую другую версию с поддержкой 2D), если она ещё не
установлена — и двигаемся дальше.
На самом деле способов создания такой игры существует множество. Мы
создадим простую, но гибкую реализацию, используя 2D-пространство,
спрайты и их движение с помощью команды
transform.Translate
. Для определения выбранных элементов
будет применяться система Raycast 2D
. Игровую логику мы
разобьём на отдельные модули, чтобы код был понятным и легко
расширяемым.
Запусти Unity Hub и создай новый 2D-проект, выбрав шаблон 2D (Built-In render Pipeline). Для этого перейди во вкладку Projects и нажми New Project. Найди шаблон 2D (Built-In render Pipeline) — он уже включает базовую 2D-графику и физику. В зависимости от версии Unity название шаблона может отличаться — если нужного варианта нет, выбери 2D (URP) или обычный 2D.
Введи имя проекта, например MatchGame
(или задай своё) и
выбери папку, в которой он будет создан. Нажми
Create project и дождись завершения подготовки среды.
Для прототипа этой игры нам будет достаточно одного спрайта, внешний вид которого мы позже разнообразим с помощью изменения цвета.
Ты можешь использовать готовую картинку (например, ту, что ниже), или выбрать свою. Единственное требование — спрайт должен быть размером 100×100 пикселей, так как это будет базовый размер элементов на игровом поле.
Чтобы импортировать изображение в Unity: кликни правой кнопкой мыши в окне Assets, выбери Import New Asset..., найди скачанный файл и нажми Import.
💡 Совет: ты можешь просто перетащить файл с изображением прямо в окно
Assets — это может быть даже быстрее.
💡 Совет: начни с простого — визуальную часть всегда можно улучшить
позже.
Наша игра будет состоять из нескольких ключевых логических блоков. Сначала мы заполним игровое поле алмазами. При заполнении важно сразу проверять, чтобы не образовались линии из трёх и более одинаковых алмазов — таким образом мы исключим случайные совпадения в начальной расстановке.
После того как поле будет заполнено, мы передадим ход игроку. Для упрощения на этом этапе игрок сможет менять местами любые два соседних алмаза по вертикали или горизонтали, не ограничиваясь только ходами, которые приводят к совпадениям.
После хода игрока алмазы поменяются местами. Затем произойдёт проверка всего поля на наличие совпадений. Если совпадения из трёх и более одинаковых алмазов будут найдены, мы удалим их и заполним пустые клетки новыми алмазами. Если совпадений нет, ход снова перейдёт к игроку.
Для управления основными фазами игры мы создадим отдельный скрипт.
Создайте новый C# скрипт (MonoBehaviour) и назовите его
GameManager
. Этот скрипт будет определять текущую фазу
игры и запускать соответствующие логические блоки. Также он будет
хранить массивы для игрового поля и набор игровых объектов (например,
алмазов).
На данном этапе запуск логических блоков отключён — мы подключим их позже, когда реализуем соответствующие функции.
using UnityEngine;
public class GameManager : MonoBehaviour
{
// массив игрового поля
public GameObject[,] gemsGrid = new GameObject[6, 6];
// массив игровых объектов (алмазов)
public GameObject[] gemsArray;
// счётчик движущихся алмазов
public int gemCounter = 0;
// разрешение на ход игрока
public bool canMove = false;
// разрешение на проверку совпадений
public bool canCheck = false;
// разрешение на перезаполнение поля
public bool canRefill = false;
// флаг отсутствия совпадений
public bool noMatches = true;
private void Update()
{
// если нет движущихся алмазов и можно проверить совпадения
if (canCheck && gemCounter <= 0)
{
canCheck = false;
gemCounter = 0;
//GetComponent<GridChecker>().CheckForMatches();
}
// если нет движущихся алмазов и можно заполнить поле
if (canRefill && gemCounter <= 0)
{
canRefill = false;
gemCounter = 0;
//GetComponent<RefillGrid>().Refill();
}
// если нет совпадений и других процессов — передаём ход игроку
if (!canRefill && noMatches && !canCheck && gemCounter <= 0)
{
gemCounter = 0;
canMove = true;
}
}
}
Теперь создайте пустой игровой объект:
Переименуйте его в GameManager
и присвойте ему тег
GameManager
.
Добавьте скрипт GameManager
на этот объект. Для этого
выберите объект в иерархии и перетащите скрипт в поле инспектора или
воспользуйтесь кнопкой Add Component.
Теперь создадим заготовку алмаза для нашей игры. На втором шаге ты уже импортировал нужный спрайт. Перетащи его в игровую сцену — Unity автоматически создаст новый игровой объект.
В этой игре мы будем перемещать алмазы с помощью
transform.Translate
, изменяя их положение в пространстве.
Также мы задействуем встроенную физику 2D для регистрации
взаимодействий с игроком.
Часть игровой логики мы разместим прямо на объекте алмаза — чтобы не создавать отдельный управляющий скрипт. Каждый алмаз сам будет хранить часть нужной информации.
Для начала добавь на алмаз компонент Circle Collider 2D, чтобы можно было использовать события столкновений.
Теперь создай новый MonoBehaviour скрипт и назови его
GemData
:
using UnityEngine;
public class GemData : MonoBehaviour
{
// Позиция этого алмаза в массиве (grid),
// совпадает с позицией в мировом пространстве Unity
public int gridPosX, gridPosY;
// Ссылка на компонент коллайдера этого алмаза
private CircleCollider2D col;
// Флаг, разрешающий движение к целевой позиции
private bool canMoveToPos = false;
// Скорость движения
public float speed;
// Конечная точка, к которой движется алмаз
private Vector3 endPos;
// Ссылка на главный скрипт управления игрой
private GameManager manager;
private void Awake()
{
// Находим объект с тегом GameManager и получаем его скрипт
manager = GameObject.FindWithTag("GameManager").GetComponent<GameManager>();
// Получаем свой CircleCollider2D
col = GetComponent<CircleCollider2D>();
}
private void Update()
{
// Если разрешено двигаться
if (canMoveToPos)
{
// Вычисляем нормализованное направление до конечной позиции
Vector3 dir = (endPos - transform.position).normalized;
// Перемещаемся в этом направлении с заданной скоростью
transform.Translate(dir * speed * Time.deltaTime);
// Если уже почти достигли конечной точки
if ((endPos - transform.position).sqrMagnitude < 0.05f)
{
// Ставим точно в конечную позицию
transform.position = endPos;
// Включаем коллайдер обратно для регистрации столкновений
col.enabled = true;
// Уменьшаем счётчик движущихся алмазов в менеджере
manager.gemCounter--;
// Возвращаем порядок отрисовки на обычный
GetComponent<SpriteRenderer>().sortingOrder = 0;
// Отключаем флаг движения
canMoveToPos = false;
}
}
}
// Вызывается для запуска движения алмаза
public void Move()
{
// Обновляем конечную позицию по координатам сетки
endPos = new Vector3(gridPosX, gridPosY);
// Выключаем коллайдер, чтобы не ловить столкновения во время перемещения
col.enabled = false;
// Увеличиваем счётчик движущихся алмазов в менеджере
manager.gemCounter++;
// Ставим более высокий порядок отрисовки, чтобы этот алмаз рисовался поверх других
GetComponent<SpriteRenderer>().sortingOrder = 10;
// Разрешаем движение
canMoveToPos = true;
}
}
Обрати внимание на важный момент: хотя на алмазе установлен
2D-коллайдер, для движения мы используем
transform.Translate
без Rigidbody2D
. Такой
подход быстрее для простой логики, но требует вручную отключать
коллайдер на время движения.
При перемещении алмаза мы также меняем его порядок отрисовки (sorting order), чтобы он рисовался поверх остальных элементов. Когда движение заканчивается, возвращаем порядок обратно.
Для упрощения у нас совпадают координаты в массиве и в мировой сцене.
Так как по умолчанию в Unity одна единица мира равна 100 пикселям,
спрайт размером 100×100 пикселей занимает ровно одну клетку. Это
значит, что алмаз с позицией (1,4)
в массиве окажется в
той же точке на сцене.
Измени в инспекторе скорость движения speed
на
1 — позже можно будет подобрать оптимальное значение.
Готово: теперь у нас есть полноценная заготовка для алмазов. В следующем шаге добавим вариативность, чтобы они отличались друг от друга.
Теперь у нас уже есть заготовка алмаза в сцене, и самое время
превратить её в настоящие игровые префабы. При этом важно помнить о
балансе вариантов: если вариативность слишком мала, совпадения будут
происходить слишком часто, и игроку останется только наблюдать за
автоудалением и автозаполнением поля (хотя, может, это именно тот
эффект, который тебе нужен). А если вариантов слишком много,
совпадения станут редкостью, и игра может надолго застрять без
возможных ходов.
Для нашего поля 6×6 оптимально использовать
4–5 вариантов. Мы остановимся на пяти, чтобы
сохранить интересный баланс между случайностью и задачами игрока.
Выбери алмаз в сцене (или в окне иерархии) и переименуй его в
Gem-Red
.
Создай новый тег red
и присвой его объекту
Gem-Red
. После этого в компоненте
Sprite Renderer поменяй цвет алмаза на красный.
Теперь перетащи объект Gem-Red
из иерархии в папку
Assets — Unity автоматически создаст из него префаб.
Затем снова выбери объект алмаза в сцене и аналогичным образом создай ещё четыре префаба.
В итоге у меня получились следующие префабы:
Gem-Red
с тегом red
, Gem-Blue
с
тегом blue
, Gem-Yellow
с тегом
yellow
, Gem-Green
с тегом
green
и Gem-Fuchsia
с тегом
fuchsia
. Цвета у всех соответствующие.
После того как пять префабов созданы, можешь смело удалить объект алмаза из сцены — теперь все нужные заготовки хранятся в твоём проекте как префабы.
Первый логический шаг нашей игры — заполнить игровое поле случайными алмазами. Для начала подготовим массив, в котором будут храниться возможные варианты алмазов для спавна.
Выбери GameManager
в иерархии. В инспекторе найди скрипт
GameManager
, где есть массив GemsArray
.
Сейчас он пустой. Перетащи туда свои пять префабов алмазов.
Теперь создадим скрипт для начального заполнения поля. Создай новый
MonoBehaviour скрипт и назови его
SpawnController
. Добавь его на объект
GameManager
.
using UnityEngine;
// Для использования List
using System.Collections.Generic;
public class SpawnController : MonoBehaviour
{
private GameManager manager;
void Start()
{
// Получаем ссылку на GameManager
manager = GetComponent<GameManager>();
// При старте один раз заполняем поле
FillTheGrid();
}
public void FillTheGrid()
{
// Проходим по игровому полю по X и Y
for (int i = 0; i < 6; i++)
{
for (int j = 0; j < 6; j++)
{
Vector2 position = new Vector2(i, j);
// Делаем копию массива префабов для фильтрации
List<GameObject> candidateTypes = new List<GameObject>(manager.gemsArray);
// Проверяем соседа слева: если есть, убираем совпадающий тип
if (i >= 1)
candidateTypes.RemoveAll(gem => gem.tag == manager.gemsGrid[i - 1, j].tag);
// Проверяем соседа снизу: если есть, убираем совпадающий тип
if (j >= 1)
candidateTypes.RemoveAll(gem => gem.tag == manager.gemsGrid[i, j - 1].tag);
// Из оставшихся вариантов выбираем случайный
GameObject gem = Instantiate(
candidateTypes[Random.Range(0, candidateTypes.Count)],
position,
Quaternion.identity
);
// Указываем алмазу его позицию в сетке
GemData gemData = gem.GetComponent<GemData>();
gemData.gridPosX = i;
gemData.gridPosY = j;
// Сохраняем ссылку на объект в массиве поля
manager.gemsGrid[i, j] = gem;
}
}
// Разрешаем игроку сделать ход
manager.canMove = true;
}
}
Этот скрипт выполняется один раз при запуске игры. Он берёт массив доступных префабов алмазов, копирует его в динамический список и, проходя по каждой клетке поля, исключает из списка те типы, которые уже стоят рядом — слева и снизу. Так мы избегаем стартовых совпадений.
Затем выбирается случайный алмаз из оставшихся кандидатов, создаётся
объект в сцене, ему назначаются координаты в сетке, и он сохраняется в
массив gemsGrid
. После заполнения поле готово, и мы
разрешаем игроку сделать первый ход.
На этом этапе можно уже запустить игру и увидеть, как поле заполняется случайными алмазами.
GemsArray
, заполненным
префабами.
Теперь перед игроком уже есть заполненное поле, и он может сделать свой первый ход. Для этого создадим скрипт, который будет обрабатывать ввод мыши и движение выбранных алмазов.
Создай новый MonoBehaviour скрипт и назови его
MoveGem
. Добавь этот скрипт на объект
GameManager
.
using UnityEngine;
public class MoveGem : MonoBehaviour
{
// Минимальное расстояние для распознавания свайпа
public float swipeThreshold = 0.5f;
// Текстовое направление движения
private string direction;
// Выбранный алмаз
private GameObject selectedGem;
// Стартовая и конечная точки мыши
private Vector2 startMouse;
private Vector2 endMouse;
private GameManager manager;
void Start()
{
manager = GetComponent<GameManager>();
}
void Update()
{
// Если нажата левая кнопка мыши и ход разрешён
if (Input.GetMouseButtonDown(0) && manager.canMove)
{
startMouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);
RaycastHit2D hit = Physics2D.Raycast(startMouse, Vector2.zero);
if (hit.collider != null)
{
// Запоминаем выбранный алмаз
selectedGem = hit.collider.gameObject;
}
}
// Если отпущена левая кнопка мыши и есть выбранный алмаз
if (Input.GetMouseButtonUp(0) && selectedGem != null && manager.canMove)
{
endMouse = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Vector2 delta = endMouse - startMouse;
direction = "";
// Проверяем минимальное расстояние
if (delta.magnitude > swipeThreshold)
{
// Определяем направление
if (Mathf.Abs(delta.x) >= Mathf.Abs(delta.y))
direction = delta.x > 0 ? "right" : "left";
else
direction = delta.y > 0 ? "up" : "down";
}
if (direction != "")
{
// Запускаем смену алмазов
SwitchGems(direction);
}
}
}
void SwitchGems(string direction)
{
switch (direction)
{
case "right": GemSwap(Vector2Int.right); break;
case "left": GemSwap(Vector2Int.left); break;
case "up": GemSwap(Vector2Int.up); break;
case "down": GemSwap(Vector2Int.down); break;
}
}
void GemSwap(Vector2Int dir)
{
int x1 = selectedGem.GetComponent<GemData>().gridPosX;
int y1 = selectedGem.GetComponent<GemData>().gridPosY;
int x2 = x1 + dir.x;
int y2 = y1 + dir.y;
// Проверяем границы поля
if (x2 < 0 || x2 >= manager.gemsGrid.GetLength(0) || y2 < 0 || y2 >= manager.gemsGrid.GetLength(1))
return;
// Выбираем два алмаза для обмена
GameObject gem1 = manager.gemsGrid[x1, y1];
GameObject gem2 = manager.gemsGrid[x2, y2];
var data1 = gem1.GetComponent<GemData>();
var data2 = gem2.GetComponent<GemData>();
// Обновляем координаты в данных
data1.gridPosX = x2;
data1.gridPosY = y2;
data2.gridPosX = x1;
data2.gridPosY = y1;
// Меняем местами в массиве
manager.gemsGrid[x1, y1] = gem2;
manager.gemsGrid[x2, y2] = gem1;
// Даём команду двигаться
data1.Move();
data2.Move();
// Запрещаем дальнейшие ходы, пока не проверим совпадения
manager.canMove = false;
manager.canCheck = true;
}
}
Этот скрипт регистрирует и обрабатывает нажатия мыши. Когда игрок нажимает левую кнопку, запоминается стартовая точка и выбранный алмаз. При отпускании кнопки проверяется, насколько далеко переместился курсор — это защита от случайных кликов. Если движение достаточно большое, определяется его направление и вызывается функция для смены алмазов местами.
Далее мы находим пару алмазов (выбранный и соседний в нужном
направлении), меняем их позиции в массиве, обновляем координаты в
скриптах GemData
и отправляем оба двигаться к новым позициям. После этого отключается
возможность следующего хода до завершения движения и проверки
совпадений.
x = 2.5
и y = 2.5
, чтобы поле оказалось по
центру экрана.
Теперь можешь запустить игру и проверить, как алмазы меняются местами.
MoveGem
.
Следующим логическим шагом после хода игрока будет проверка поля на
совпадения. Для этого создадим новый
MonoBehaviour скрипт и назовём его
GridChecker
. Добавь этот скрипт на объект
GameManager
.
using UnityEngine;
using System.Collections.Generic;
public class GridChecker : MonoBehaviour
{
// Массив массивов, где храним группы совпавших алмазов (по 3+)
private List<List<GameObject>> matchedGems;
private GameManager manager;
GameObject[,] gemsGrid;
public void CheckForMatches()
{
manager = GetComponent<GameManager>();
gemsGrid = manager.gemsGrid;
matchedGems = new List<List<GameObject>>();
GameObject firstGemToCheck = null;
//динамический массив для обработки совпадений
List<GameObject> tmpGems = new List<GameObject>();
// Проверяем горизонтали
for (int y = 0; y < gemsGrid.GetLength(1); y++)
{
firstGemToCheck = null;
tmpGems.Clear();
for (int x = 0; x < gemsGrid.GetLength(0); x++)
{
GameObject current = gemsGrid[x, y];
// Если встретили пустое место (нет алмаза), то прерываем итерацию
if (current == null)
{
// проверяем, есть ли накопленные совпадения
if (tmpGems.Count >= 3)
//и сохраняем их
matchedGems.Add(new List<GameObject>(tmpGems));
tmpGems.Clear();
firstGemToCheck = null;
continue;
}
// Если это первый алмаз в линии или тег отличается от предыдущего
if (firstGemToCheck == null || current.tag != firstGemToCheck.tag)
{
//проверяем, есть ли уже совпавшие
if (tmpGems.Count >= 3)
//сохраняем их
matchedGems.Add(new List<GameObject>(tmpGems));
tmpGems.Clear();
//и сохраняем обрабатываемый алмаз для последующих сравнений
tmpGems.Add(current);
firstGemToCheck = current;
}
else
{
// Если тег совпадает, добавляем к текущей цепочке
tmpGems.Add(current);
}
}
// если накопилась серия одинаковых алмазов длиной 3+
if (tmpGems.Count >= 3)
// то сохраняем в массиве
matchedGems.Add(new List<GameObject>(tmpGems));
}
// Проверяем вертикали — по тому же принципу
for (int x = 0; x < gemsGrid.GetLength(0); x++)
{
firstGemToCheck = null;
tmpGems.Clear();
for (int y = 0; y < gemsGrid.GetLength(1); y++)
{
GameObject current = gemsGrid[x, y];
if (current == null)
{
if (tmpGems.Count >= 3)
matchedGems.Add(new List<GameObject>(tmpGems));
tmpGems.Clear();
firstGemToCheck = null;
continue;
}
if (firstGemToCheck == null || current.tag != firstGemToCheck.tag)
{
if (tmpGems.Count >= 3)
matchedGems.Add(new List<GameObject>(tmpGems));
tmpGems.Clear();
tmpGems.Add(current);
firstGemToCheck = current;
}
else
{
tmpGems.Add(current);
}
}
if (tmpGems.Count >= 3)
matchedGems.Add(new List<GameObject>(tmpGems));
}
// Удаляем все совпавшие алмазы из сетки и сцены
foreach (List<GameObject> group in matchedGems)
{
foreach (GameObject gem in group)
{
if (gem != null)
{
GemData data = gem.GetComponent<GemData>();
manager.gemsGrid[data.gridPosX, data.gridPosY] = null;
Destroy(gem);
}
}
}
// Проверяем: были ли совпадения на этом шаге
bool hadMatches = matchedGems.Count > 0;
matchedGems.Clear();
// Если были совпадения — запускаем refill
if (hadMatches)
{
manager.noMatches = false;
manager.canRefill = true;
manager.canCheck = false;
}
else
{
// Иначе возвращаем ход игроку, через GameManager
manager.noMatches = true;
manager.canRefill = false;
manager.canCheck = false;
}
}
}
Этот скрипт проверяет линии на поле — как по горизонтали, так и по
вертикали — чтобы найти группы из трёх и более одинаковых алмазов. Для
этого используется временный массив tmpGems
, который
собирает подряд одинаковые алмазы. Когда встречается другой тип или
пустая клетка, скрипт проверяет, есть ли там хотя бы три одинаковых —
и если да, сохраняет их в общий список совпадений
matchedGems
.
После проверки все совпавшие алмазы удаляются, а в зависимости от того, были ли совпадения, игра либо запускает заполнение пустых мест, либо возвращает ход игроку для следующего перемещения.
GetComponent<GridChecker>().CheckForMatches();
в
скрипте GameManager
и проверить, как работает поиск совпадений. Учитывай, что пока при
взаимодействии с пустыми полями может возникнуть ошибка — её позже мы
обработаем.
GridChecker
.
Теперь нам остался последний логический шаг игры. На этом этапе на поле уже появились пустые клетки — там, где ранее были удалены алмазы. Нам нужно: найти эти пустые места, сдвинуть находящиеся сверху алмазы вниз, а затем заспавнить новые алмазы для оставшихся пустых клеток.
Для этого создаём новый MonoBehaviour
скрипт и называем
его RefillGrid
. Добавляем этот скрипт на
GameManager
.
using UnityEngine;
using System.Collections;
public class RefillGrid : MonoBehaviour
{
private GameManager manager;
private GameObject[,] gemsGrid;
private GameObject[] gemsArray;
private void Start()
{
manager = GetComponent<GameManager>();
gemsArray = manager.gemsArray;
gemsGrid = manager.gemsGrid;
}
public void Refill()
{
// Запускаем последовательную цепочку: падение -> спавн -> ожидание -> разрешаем проверку
StartCoroutine(DoRefillSequence());
manager.canRefill = false;
}
IEnumerator DoRefillSequence()
{
// сначала падают уже существующие алмазы
yield return StartCoroutine(SlowFall());
// потом появляются новые на пустых местах
yield return StartCoroutine(SlowSpawn());
// даём чуть времени завершить движение
yield return new WaitForSeconds(0.2f);
// теперь можно искать совпадения
manager.canCheck = true;
}
IEnumerator SlowFall()
{
for (int x = 0; x < gemsGrid.GetLength(0); x++)
{
for (int y = 0; y < gemsGrid.GetLength(1) - 1; y++)
{
// Если находим пустое место в колонке
if (gemsGrid[x, y] == null)
{
// Проверяем ячейки выше, чтобы найти первый алмаз, который можно "уронить"
for (int aboveY = y + 1; aboveY < gemsGrid.GetLength(1); aboveY++)
{
if (gemsGrid[x, aboveY] != null)
{
// Перемещаем найденный алмаз в пустую ячейку
GameObject gem = gemsGrid[x, aboveY];
gemsGrid[x, y] = gem;
gemsGrid[x, aboveY] = null;
// Обновляем данные о позиции в скрипте алмаза и даём команду на движение
GemData data = gem.GetComponent<GemData>();
data.gridPosY = y;
data.Move();
// Делаем паузу, чтобы падение выглядело пошагово
yield return new WaitForSeconds(0.1f);
break;
}
}
}
}
}
}
IEnumerator SlowSpawn()
{
for (int x = 0; x < gemsGrid.GetLength(0); x++)
{
for (int y = 0; y < gemsGrid.GetLength(1); y++)
{
// Если клетка всё ещё пустая после падений — нужно создать новый алмаз
if (gemsGrid[x, y] == null)
{
// создаём чуть выше поля
GameObject newGem = Instantiate(
gemsArray[Random.Range(0, gemsArray.Length)],
new Vector2(x, gemsGrid.GetLength(1) + 1),
Quaternion.identity
);
gemsGrid[x, y] = newGem;
// Указываем целевые координаты в сетке и отправляем на движение
GemData data = newGem.GetComponent<GemData>();
data.gridPosX = x;
data.gridPosY = y;
data.Move();
// Задержка для пошаговой визуализации появления
yield return new WaitForSeconds(0.2f);
}
}
}
}
}
Этот скрипт выполняет две ключевые задачи: сначала ищет пустые места и сдвигает алмазы сверху вниз, а затем находит оставшиеся пустые клетки и спавнит туда новые алмазы.
На этом наша базовая игровая логика готова! Теперь
можешь раскомментировать вызовы
GetComponent<GridChecker>().CheckForMatches()
и
GetComponent<RefillGrid>().Refill()
в скрипте
GameManager
— и твоя игра заработает в полноценном цикле.
RefillGrid
.
Надеюсь, ты не забыл раскомментировать вызовы
CheckForMatches()
и Refill()
в скрипте
GameManager
— теперь твоя игра уже полностью работает!
А дальше всё зависит только от твоей фантазии 🤗 Добавляй спецэффекты, анимации, звуки, бонусы, таймеры, уровни — и пусть твоя match-3 превращается во что-то уникальное.