Добро пожаловать в туториал по 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 — это может быть даже быстрее.
💡 Совет: начни с простого — визуальную часть всегда можно улучшить
позже.
Наша игра будет состоять из нескольких ключевых логических блоков. Сначала мы заполним игровое поле алмазами. При заполнении важно сразу проверять, чтобы не образовались линии из трёх и более одинаковых алмазов — таким образом мы исключим случайные совпадения в начальной расстановке.
После того как поле будет заполнено, мы передадим ход игроку. Для упрощения на этом этапе игрок сможет менять местами любые два соседних алмаза по вертикали или горизонтали, не ограничиваясь только ходами, которые приводят к совпадениям.
После хода игрока алмазы поменяются местами. Затем произойдёт проверка всего поля на наличие совпадений. Если совпадения из трёх и более одинаковых алмазов будут найдены, мы удалим их и заполним пустые клетки новыми алмазами. Если совпадений нет, ход снова перейдёт к игроку.
Для управления основными фазами игры мы создадим отдельный скрипт.
Создайте новый MonoBehaviour
скрипт и назовите его
GameManager
. Этот скрипт будет определять текущую фазу
игры и запускать соответствующие логические блоки. Также он будет
хранить массивы для игрового поля и набор игровых объектов (например,
алмазов).
На данном этапе запуск логических блоков отключён — мы подключим их позже, когда реализуем соответствующие функции.
using UnityEngine;
public class GameManager : MonoBehaviour
{
// Двумерный массив, представляющий игровое поле (6×6 ячеек)
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();
}
// Проверка: если все алмазы на месте и разрешено заполнение — запускаем refill
if (canRefill && gemCounter <= 0)
{
canRefill = false;
gemCounter = 0;
//GetComponent<RefillGrid>().Refill();
}
// Если нет процессов (проверка, 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;
// Компонент 2D-коллайдера для регистрации столкновений
private CircleCollider2D col;
// Флаг: можно ли двигаться к целевой позиции
private bool canMoveToPos = false;
// Скорость движения алмаза
public float speed;
// Целевая позиция, к которой должен переместиться алмаз
private Vector3 endPos;
// Ссылка на главный управляющий скрипт GameManager
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;
// Сообщаем GameManager, что один алмаз завершил движение
manager.gemCounter--;
// Возвращаем порядок отрисовки на стандартный
GetComponent<SpriteRenderer>().sortingOrder = 0;
// Отключаем флаг движения
canMoveToPos = false;
}
}
}
// Метод для запуска движения алмаза к его новой позиции в сетке
public void Move()
{
// Вычисляем целевую позицию в мировом пространстве по координатам сетки
endPos = new Vector3(gridPosX, gridPosY);
// Отключаем коллайдер на время движения, чтобы избежать ложных столкновений
col.enabled = false;
// Увеличиваем счётчик активных перемещений в GameManager
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;
// Направление движения ("left", "right", "up", "down")
private string direction;
// Алмаз, который выбрал игрок
private GameObject selectedGem;
// Начальная и конечная позиции мыши
private Vector2 startMouse;
private Vector2 endMouse;
private GameManager manager;
void Start()
{
// Получаем ссылку на GameManager на том же объекте
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;
// Подключаем список (List)
using System.Collections.Generic;
// Класс для проверки совпадений на игровом поле
public class GridChecker : MonoBehaviour
{
// Список всех групп совпавших алмазов (по 3 и более)
private List<List<GameObject>> matchedGems;
// Ссылка на GameManager
private GameManager manager;
// Ссылка на массив игрового поля
private GameObject[,] gemsGrid;
// Метод, запускающий проверку совпадений
public void CheckForMatches()
{
// Получаем ссылку на GameManager и игровое поле
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)
{
// Если в цепочке было 3 и более — сохраняем её
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));
}
// === Проверка вертикальных совпадений ===
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();
// === Обновляем состояние игры ===
if (hadMatches)
{
// Были совпадения — запускаем перезаполнение
manager.noMatches = false;
manager.canRefill = true;
manager.canCheck = false;
}
else
{
// Совпадений не было — передаём ход игроку
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()
{
// Получаем ссылки из GameManager
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 превращается во что-то уникальное.