код игры три в ряд
Игры три-в-ряд. История создания и причины успеха у массового игрока
«Всё гениальное просто, и всё простое гениально» — считал Геббельс.
Многие из его суждений можно поставить под сомнение, но не это. Усложнение редко приводит к хорошему результату. Рассмотрим эту теорию на примере жанра игр «три в ряд»: взяв за основу простую и увлекательную головоломку, разработчики мобильных игр исказили ее до неузнаваемости, нагрузив дополнительными фичами. Как и зачем это произошло?
Однозначно сказать, какая игра является родоначальником жанра «три в ряд» сложно. Она одновременно напоминает едва ли не все настольные игры с механикой поиска одинаковых элементов, например маджонг, домино, различные виды пасьянсов и даже крестики-нолики.
Однако современную историю жанра принято отсчитывать с релиза Chain Shot! в 1985 году. Компьютерную игру, в которой на доске были расположены шарики разных цветов, придумал Куниаки Морибе.
Впервые Chain Shot! вышла на японских компьютерах Fujitsu. В 1992 году была написана версия для Unix, а в 1993-м — для Windows. С переходом на более популярные в мире платформы игра также сменила название: нам она знакома как SameGame.
В России в те же годы успели сделать собственную версию классической игры. Она получила название «Шарики» — на данный момент имя стало нарицательным. Автором и исполнителем идеи был российский программист Евгений Алемжин, игра вышла для DOS в 1994 году.
Правила игры «три в ряд» знают все: расположенные на поле одинаковые фигуры (в оригинале — тайлы одного цвета, например квадраты или шарики) можно группировать по три и более и убирать с поля. Цель — полностью освободить доску от изначально размещенных на ней элементов. Именно эта простая идея является основным крючком в этом типе головоломок.
Чем проще и популярнее идея, тем большее количество последователей она собирает вокруг себя. Так как повторить код любых «шариков» не составляло труда даже в девяностых, а авторское право на фишки и механики не распространяется до сих пор, к началу нулевых жанр «три в ряд» начал разрастаться. Сначала вышла Collapse, в которой основной отличающей особенностью был добавленный таймер, повышающий соревновательность. После появилась Bejeweled — в ней шарики были заменены на алмазы, а грамотная перестановка элементов могла привести к цепной реакции. Пожалуй, именно Bejeweled ознаменовала начало конца классического «три в ряд» и запустила конвейер безликих монстров казуальной ниши.
Bejeweled — одна из самых известных и популярных игр в жанре «три в ряд». Её создала студия PopCap игры в 2001 году, и по словам генерального директора студии, разработка заняла около трех месяцев. За основу идеи были взяты именно российские «Шарики», но программистам PopCap игра показалась слишком однообразной. Они добавили в нее уровни, более приветливую графику, а со временем даже подсказки. Кроме того, в игре сразу же появилось два режима — с бесконечным таймером и на время. К тому же каждый пользователь мог завершать игру в любой момент, а прогресс сохранялся и мог стать поводом для хвастовства перед товарищами.
И ведь действительно успехами в Bejeweled хвастались! Игра с космической скоростью распространялась по девайсам: по словам PopCap, в двухтысячных был момент, когда каждые 4,3 секунды продавалась одна копия тайтла. К 2013-му в Bejeweled сыграли более 500 миллионов человек по всему миру. Остальные же владельцы любых девайсов наверняка хотя бы раз пробовали поиграть в одну из ее многочисленных копий.
Авторы Bejeweled пошли по пути усложнения и нагромождения тайтла — со временем в обновлениях появлялись дополнительные анимации, по замыслу авторов направленные на более глубокое погружение геймеров в игру. Студия столкнулась с тем, что Bejeweled со временем все же начинала надоедать, как и простенькие «шарики». Авторы искали (ис успешно находили) новые способы вызывать у игроков привыкание. Кто играл в любую из существующих игр жанра «три в ряд» знают это чувство: когда всё поле взрывается после удачной манипуляции, дух захватывает. Классическое пропадание шариков с экрана и начисление очков по эффектности определенно проигрывает.
Со временем игр «три в ряд», созданных по классическим канонам, становилось все больше. Головоломки по лекалам плотно закрепились в чартах. Однако каждому разработчику приходилось выдумывать что-то свое: какую-то фишку, визуально или геймплейно отличающую один продукт от другого, ведь в ином случае авторов все-таки можно было бы преследовать по закону.
С течением времени в играх появились дополнительные способы удалить с поля шарики — бомбы, взрывающие все поле или какую-то его часть, магниты, световые мечи, луки, перевертыши. В режиме с таймером появились способы повернуть время вспять или замедлить его. К тому же каждый разработчик старался визуально изменить обычный шарик и сделать из него кристалл, кексик, цветочек, котика… вообще что угодно.
Три в ряд с рыбками — далеко не самое странное, что можно найти в сети
Игра жанра «три в ряд» настолько гибкие, что на них можно «натянуть» любой из доступных способов монетизации. Продавать косметику? Легко. Продавать подписку или игровое время? Пожалуйста. Уровни за просмотр рекламы, уровни за приглашение друзей, уровни за живые деньги постепенно или за единоразовую покупку. Каждый разработчик может монетизировать свою копию как пожелает. При этом и визуально у авторов нет никаких ограничений — хочешь по «Звездным войнам» делай «три в ряд», хочешь по «Гарри Поттеру».
По итогу, из разряда простой и понятной головоломки игры жанра «три в ряд» перешли в гиперказуальную нишу. Геймплей из-за огромного количества платного контента стал больше похож на pay-to-win с гига-мега-бомбами, подрывающими всё поле за несколько центов на счет авторов. Можно ли считать, что в Candy Crush Saga, Empires & Puzzles, Fishdom и других осталась та простота, которая была присуща SameGame и «Шарикам», ведь играть в них все также весело и легко? Пожалуй, нет.
Игры гиперказуального жанра, в частности «три в ряд» — это не о простоте и гениальности. Большинство тайтлов из этой категории имеют хорошую идею, на которую со временем повесили много мишуры и серпантина. Если отряхнуть всё это, будет годный скелет, ретеншн у которого куда меньше, но зато «на века» в долгосрочной перспективе.
Дизайн уровней для игр три в ряд
Три в ряд — один из самых популярных игровых жанров. Игр существует десятки тысяч, при этом в интернете довольно мало статей по дизайну уровней, а они — основная составляющая игры. Правим этот недостаток, размещая на Хабре свои мысли и опыт в виде тезисов, заметок по сути и по делу, с рассуждениями и картинками.
Общие положения
Суть три в ряд: каждый уровень для игры должен представлять собой полноценную головоломку — это самое важное. Если вам это удалось, тогда вы справились. Вы, наверняка, уже сделали сотню уровней на будущие обновления игры и с улыбкой на лице суете свой нос в задачи остальной части команды, раздавая бесплатные советы. Если нет — читайте дальше.
О головоломках: в каждой игре есть стандартный набор целей и препятствий и ими необходимо правильно распоряжаться. Уровни должны быть понятными, интересными и нести в себе ценность для игрока — вызов или фан.
Об интересе: например, вы сделали простой понятный уровень — это хорошо, но что если все уровни в вашей игре простые и понятные? Наверняка, игра станет скучной. Занимайтесь оценкой качества и разнообразия вашего контента — ищите неожиданные комбинации целей и препятствий, постоянно удивляйте игрока чем-то новым, требуйте от команды самый лучший редактор уровней и самое быстрое производство уникальных фич! Вам клепать сотни и сотни уровней!
Об игровых режимах: чем больше целей и препятствий есть в игре тем больше интересных уровней можно сделать. А если в вашей игре есть уникальные решения, то вам повезло. В жанре наблюдался застой, но появились хиты с новыми интересными решениями старых проблем. Экспериментируйте! Ищите новые игровые режимы и препятствия, вам это будет только на руку. Обилие уровней на сбор очков — это беда и деградация. Удивляйте игрока с самого начала игры.
О разнообразии: уровней в популярных играх зачастую 300+, поэтому необходимо понимать, что делать десять уровней подряд про разбивание клеточек или спуск каких-нибудь предметов — это глупо. Ваши игроки устанут после пары локаций. Чередуйте стандартные игровые режимы как можно чаще, уровни с новыми и уникальными фишками можно ставить несколько раз подряд в момент их появления в игре, но даже они должны чередоваться со знакомыми режимами.
О длительности: тут надо понимать кто и на каких устройствах и социальных сетях играет в вашу игру и подбирать наиболее удачную продолжительность уровней. Я предпочитаю основную массу уровней делать на 25-35 ходов, иногда включая очень короткие — меньше 20 ходов, и очень длинные — больше 45.
О качестве: уровни бывают сложными и легкими, но как оценить их качество? Очень просто! Смотрите на цифры проигрышей — насколько завершена цель уровня на момент провала. Например, если в уровне необходимо разбить 80 клеток, а среднее число выполнения условия при проигрыше около 10, то вы сделали плохой уровень. Скорее всего, игроки просто не понимают как его пройти. У каждого проекта свои цифры, но вы хотите, чтобы проигрыши всегда были при 90% выполнения условия уровня. Даже на самых сложных и мозговыносящих уровнях.
Начало работ
Самая важная часть при начале работы над проектом — это составить список требований к контенту, который вы ходите создавать. Необходимо определиться с критериями сложности, сильных и слабых сторон проекта, какие уровни стоит делать, а какие — нет, определите ценность контента для игрока и последовательность появления в игре новых фич и режимов.
Теперь непосредственно к дизайну. Представляю два уровня на разбивание ячеек, с двумя видами препятствий (сетка и блок) и сложной формой поля (серый — дырки), выпадает пять видов камней и игроку дается 45 ходов. Что вы для себя можете о них сказать?
Если вы хорошо знакомы с жанром, то уже заметили слабые стороны представленных уровней — сложная форма поля сама по себе серьезное препятствие. Разбить голубые ячейки в обоих случаях очень и очень сложно. 45 ходов — это много, более того — при особом невезении, в обоих уровнях можно потратить все ходы и так и не разбить ни одной ячейки. Препятствия находятся в труднодоступных местах и не выполняют своей функции качественно.
Главный принцип, которым я руководствуюсь при создании уровней — любой уровень по-умолчанию плохой, если я не знаю зачем я его сделал и не задумывался над тем, чтобы определить ему правильное место в игре.
Основные критерии при создании уровней
Можно сделать много хороших уровней без препятствий просто за счет хорошей формы игрового поля. Для этого необходимо понимать сильные и слабые стороны вашего проекта в области сбора комбинаций и бонусов. В общем, у всех проектов схожие правила — не стоит делать узкие коридоры и маленькие комнаты, где вероятность собрать комбинацию минимальна и нет возможности достать цель с помощью бонуса. Не злоупотребляйте отдельно стоящими ячейками, любите симметрию и удивляйте асимметричными решениями.
Кубик Рубика сложен для решения, но понятен в действии — крути во все стороны и будешь счастлив. Тут то же самое — поставьте цель прямо перед игроком и пусть он ей занимается без прочтения авторских заметок и тем на форуме. Если цели уровня нет на поле со старта — вы плохой человек. Исключайте вероятность того, что со старта уровня игрок должен совершать бесполезные ходы, пока ключевой элемент не явится на поле. Именно поэтому уровни на сбор очков весьма сомнительное удовольствие.
У каждого препятствия есть свои сильные и слабые стороны, их необходимо знать и использовать. Аккуратно прикрывайте цель уровня, чтобы ограничить доступ игрока к ней. Избегайте перенасыщения уровня препятствиями и оставляйте игроку точку входа — свободное место, где он начнет свою игру. Делайте уровни со смыслом, рисовать красивый смайлик и цветочек через все поле — дурной тон.
Как говорили мудрые: «Любой интерфейс можно упростить вдвое». Я им верю. Если уровень можно очистить от половины всего и пройти быстрее, сохранив интерес и сложность — делайте. Нет ни одной уважительной причины для затягивания уровней. Продолжительность уровней должна иметь смысл как в определенной точке игры, так и для проекта в целом. “Давно не делал длинных уровней”. — так себе обоснование при выборе количества ходов.
Используйте возможности вашей игры на максимум. Например пара простых примеров, цель уровня собрать фрукты заданного цвета — задайте их генерацию только в заданных участках и дайте игроку возможность легко создавать бонусы для выполнения задачи. Создайте большое количество предметов, которые нужно спустить до края поля и отделите их от точки сбора одним препятствием — игрок испытает дикое удовольствие собрав большое количество целей за раз.
Самые интересные и успешные уровни основаны на взаимодействии игровых элементов, а не из-за того, что ваш уровень похож на птичку.
В три в ряд очень многое определяет фактор удачи. Он зависит от формы поля, цели уровня и соотношения количества доступных комбинаций в текущий ход. Контролируйте удачу — это даст вам более предсказуемые результаты. У игрока должен быть шанс собрать самые мощные комбинации, но только там, где вы это запланировали.
Я люблю красивые вещи и трепетно отношусь ко внешнему виду своих уровней. Но бывают ситуации, например, ваша игра про сбор кристаллов, драгоценностей и борьбу с магией, а по самым необъяснимым причинам ваше основное препятствие — клетка в виде головы свиньи. В этом случае хорошо бы сбегать в отдел художников и хорошенько поскандалить, но до тех пор пока они осознают свои ошибки и внесут коррективы, неплохо бы смириться с текущим положением вещей и не превращать каждый уровень в свинарник, а аккуратно пользоваться тем, что есть.
Играйте в хорошие игры, вдохновляйтесь и превращайте эмоции в интересный контент. У меня все.
Код игры три в ряд
Главная » ActionScript 3: Создание игры «три-в-ряд» Bejeweled (первая часть)
ActionScript 3: Создание игры «три-в-ряд» Bejeweled (первая часть)
Bejeweled первая «современная» (первый релиз игры состоялся в 2001 году) игра, которая будет рассмотрена в этой книге. Я решил посвятить последние три главы этой книги современным играм, чтобы показать вам, как относительно легко можно создать код большинства современных успешных казуальных игр, а это значит, что в этой нише рынка хорошие идеи преобладают над сложностью игры. Миллионы копий Bejeweled были проданы, и в их Facebook версии играют миллионы людей каждый месяц.
Игра происходит на сетке размером 8×8 с 64 драгоценными камнями семи различных видов, расположенных на ней. Цель — обменивать местами драгоценный камень с соседним, чтобы сформировать горизонтальную или вертикальную линию из трех и более драгоценных камней. Эти камни затем исчезают и сверху падают новые, чтобы заполнить пробелы.
В этой главе вы напишете полностью рабочий код прототипа Bejeweled, изучив следующие методы:
В этот раз мы пропустим дизайн игры, потому что нужно сделать много другой работы. Во всяком случае, в этой книге вы уже видели, как сделать хороший дизайн-документ, поэтому вам не составит труда сделать это самостоятельно.
Создание документов и объектов
Все ресурсы нарисованы с точкой регистрации в координатах 0,0 и спроектированы так, чтобы вмещаться в тайл размером 60×60.
Затем нарисуйте что-то вроде этого:
Размещение драгоценных камней
Размещение драгоценных камней на сцене может показаться простым делом — добавить какие-то случайные объекты типа DisplayObject в список отображения (Display List), но вы увидите, что мы скоро столкнемся с некоторыми трудностями.
Идея: мы собираемся заполнить массив и физически разместить драгоценности в том же скрипте, поэтому мы заполним массив случайными целочисленными значениями между 0 и 6, чтобы представить каждый из возможных драгоценных камней. В то же время мы поместим драгоценный камень в соответствующее положение и покажем нужный кадр.
Разработка: вы уже знаете, что нам нужно поместить драгоценности, поскольку это та же концепция как и размещение карт, мин, ящиков и любых других ресурсов в игре, основанной на тайлах. В любом случае, давайте вспомним, что нужно:
Два цикла for будут заполнять массив случайными числами, когда объекты типа gem_mc будут помещаться на сцену.
Очевидно, что класс gem_mc будет управлять внешним видом драгоценностей и их позицией.
Я также дал имя этому экземпляру класса DisplayObject, чтобы сделать его выбор мышью более легким при необходимости. Имя состоит из номера, за которым стоит знак подчеркивания, далее идет номер столбца. Вы уже видели эту концепцию во время разработки игры «сапер».
Проверьте работу приложения, вы должны увидеть игровое поле заполненное драгоценными камнями. Вот когда стоит немного расстроиться. Проверьте приложение еще несколько раз, и вы поиграете вот в такую игру:
Что не так? У нас уже есть три и более смежных драгоценных камня одного типа в ряду и в столбце. В игре Bejeweled игровое поле начинается без более чем двух смежных драгоценных камней одного типа, поскольку это дело игрока перемещать драгоценные камни для создания комбинаций из них.
Проверка комбинаций драгоценных камней в начале игры
Предотвращение того, чтобы игровое поле начиналось с комбинациями означает кодирование всех необходимых функций с целью проверки является ли определенный драгоценный камень частью такой комбинации. Есть много способов сделать это, но хорошая новость в том, что функции для такой проверки, которые мы должны написать, точно такие же, что и функции, которые мы будем использовать, когда игрок начинает менять местами драгоценные камни между собой.
Идея: Когда наступает время разместить камень, проверьте сформирует ли камень, который мы собираемся установить, нужную комбинацию. В этом случае, продолжайте генерировать случайные драгоценные камни, пока они не станут частью линии из разных цветов. Наконец, разместите драгоценный камень.
Разработка: Проверка линии не отличается от проверки на победу в игре «Connect 4». Можно даже сказать, что это намного проще, поскольку нужно просматривать только по горизонтали и вертикали, не заботясь о диагонали.
Следуя концепциям, рассмотренным во время создания игры «Connect 4», давайте начнем с создания основных функций.
Первая необходимая функция та, которая сообщает имеется ли определенный драгоценный камень в данном ряду и столбце, даже при условии, что значения ряда и столбца могут быть неправильными.
Функция просто проверяет, чтобы значение элемента массива jewels[row][col] равнялось gem, как только мы проверили, что это не нулевое значение. Могут быть нулевые значения, когда мы пытаемся найти драгоценный камень в неверной позиции, которая находится за пределами заполненного массива.
Что можно сделать с этой функцией? Мы можем создать другие функции, чтобы проверить является ли данный драгоценный камень частью комбинации или нет.
Мы знаем, что драгоценный камень является частью линии-комбинации, когда есть хотя бы два смежных драгоценных камня одного типа по горизонтали или по вертикали. Так что легко создать функцию для определения является ли драгоценный камень горизонтальной линией-комбинацией.
Учитывая позицию ряда и столбца мы можем вести подсчет драгоценных камней слева и справа, пока мы не найдем драгоценный камень, который не совпадает, или имеет неправильную позицию.
Это функция rowStreak с аргументами row и col, представляющими позицию проверяемого драгоценного камня, которая возвращает количество совпадающих драгоценных камней.
Давайте кратко рассмотрим код:
Переменная streak будет сохранять счетчик (count) строки. Он установлен на 1 потому что начальный драгоценный камень сам по себе, который является частью линии.
tmp — это просто временная переменная для сохранения значения столбца, поскольку мы будем изменять его и должны сохранять его первичное значение для дальнейшего использования.
Этот цикл while сканирует драгоценные камни слева. Так как переменная tmp — это значение столбца, уменьшение которого означает перемещение одного столбца влево. Вот что мы делаем внутри цикла:
Значение переменной tmp уменьшается для проверки с левой стороны, а линия увеличивается так, как если бы мы находились внутри цикла, это означает, что мы нашли совпадающий по цвету драгоценный камень.
Как только мы выходим из цикла, значение переменной tmp сохраняется в col и мы начинаем сканирование справа таким же образом.
В это же время мы можем определить, когда драгоценный камень является частью линии-комбинации в ряду или в столбце.
Чтобы завершить набор функций, нам просто нужна еще одна функция, которая будет сообщать о том, что драгоценный камень в данной позиции является частью линии, не важно горизонтальной или вертикальной.
Как только мы узнаем, что есть линия-комбинация, то мы так же можем предотвратить функцию jewelsInit для ее генерации. Мы просто запустим генерацию случайного драгоценного камня, пока он не станет частью линии-комбинации.
Изменим функцию jewelsInit следующим образом:
Теперь проверьте приложение, и вы не увидите каких-либо линий-комбинаций.
Главная работа происходит в цикле do while, используемом в функции.
Цикл do while работает также как и цикл while, за исключением того, что код в цикле выполняется хотя бы раз, потому что условие while проверяется в конце блока цикла.
Теперь можно поиграть и проверить приложение.
Это первая часть урока по созданию игры «три-в-ряд» Bejeweled на ActionScript 3 из книги «Flash game development by example» Emanuele Feronato. Если вам понравился урок, то добавьте его в закладки социальных сетей и сохраните (значки внизу).
Создание игры Match-3 в Unity
Несколько лет назад на SeishunCon я заново открыл для себя игры match-3. Я играл в Dr. Mario детстве, но такие более соревновательные игры, как Magical Drop, Bust-A-Move и Tokimeki Memorial Taisen Puzzle-Dama, сильно отличаются от неё.
Dr. Mario
В результате я осознал, как много нейтральных решений связано с созданием игры match-3.
На следующем джеме Ludum Dare я решил поэкспериментировать, но сначала за неделю до этого для разогрева попробовал разработать алгоритм «Тетриса», обнаруживающий и удаляющий линии. Мне очень помог этот туториал Unity Plus. [Прим. пер.: у меня ссылка не открывается. Если вы знаете, как решить проблему, напишите мне, я дополню статью.] Разумеется, алгоритм «Тетриса» для поиска заполненных рядов гораздо проще, чем алгоритм, выискивающий разнообразные сочетания совпадающих тайлов.
Если вы хотите изучить эти примеры кода в контексте, то зайдите в мой репозиторий Ludum Dare 30. (Для бесстыдной саморекламы я снова использовал эту логику для игры Shifty Shapes.)
Два мира

Magical Drop 3 (источник: Kazuya_UK)
Самая мудрёная часть создания игры-головоломки в Unity заключается в том, что игра не живёт в пространстве мира. Во всяком случае, живёт не полностью.
В этом её отличие от других жанров. Платформеры, например, почти полностью живут в игровом мире Unity. Transform игрока сообщает о его положении. Коллайдеры (или, в некоторых случаях, raycast) говорят, находится ли игрок на земле, ударяется ли об потолок или столкнулся с врагом. Даже если вы не используете внутриигровую физику, то всё равно, скорее всего, добавляете силу или указываете скорость Rigidbody, чтобы обеспечить распознавание столкновений без затрат.
Игры-головоломки совсем другие. Если в вашей игре нужно щёлкать мышью, то она наверно получает какие-то координаты в пространстве мира, но их обычно преобразовывают в ячейку сетки, которая полностью живёт в коде. Для этого есть понятная причина: гораздо проще создать логику для такой игры как Tetris или Dr. Mario, когда работаешь не с отдельными пикселями, а с блоками или тайлами.
Блоки «Тетриса» определённо не должны прилипать к стенкам стакана
На самом деле, в своих экспериментах я стремился как можно больше придерживаться пространства мира. Я использовал физику для определения «приземления» тайлов и передавал данные в двухмерный массив только для определения заполнения строки. Это казалось более безопасным: в конце концов, то, что происходит в игровом мире реально. Именно это видит игрок, поэтому если хранить данные тут, то нет риска рассинхронизации, правда?
Я ошибался. Как бы я ни пытался настроить систему, она просто не работала правильно.
Туториал Unity Plus, ссылку на который я дал выше, оказал мне огромную помощь. Как минимум, он показал, что правильным подходом был полный перенос логики из игрового мира в абстрактную структуру данных. Если вы ещё этого не сделали, то хотя бы вкратце просмотрите его, потому что в этой статье я расширю логику «Тетриса» до логики match-3.
Преобразование из поля в пространство мира
Как только я понял, что этот переход удобен, остальное было простым. Я создал класс GameTile, отслеживающий цвет, строку и столбец тайла, и на его основании обновлял положение тайла. Вот его сокращённая версия:
Тайлы в сетке
Заметьте, что в этом случае TileSize является константой, определяющей размер тайла в единицах Unity. Я использую тайлы размером 64×64 пикселя, а спрайт в Unity имеет разрешение 100 пикселей на единицу, поэтому TileSize оказывается равным 0.64. Также я использую постоянное смещение, чтобы середина поля 7×7 находилась в координатах 0,0 пространства мира, а левый нижний угол являлся тайлом 0, 0 в игровом пространстве.
Также я создал массив, определяющий игровое поле как статичное поле (static field) в классе Board. (Board сначала был статичным классом, а потом превратился в синглтон (singleton), потому что мне нужно было изменять значения в редакторе, поэтому он неуклюже сочетает в себе черты игрового объекта и статичного класса.)
В туториале Unity Plus двухмерный массив использовался для хранения целых чисел, я же решил хранить в этом массиве ссылки на мои объекты GameTile. Это позволило мне передавать данные от тайлов и к ним напрямую (как вы увидите позже), что упростило удаление тайлов и создание анимации.
При внесении изменений в состояние игрового поля мне нужно было просто пройти циклом по всему массиву поля и сообщить каждому тайлу, где он должен находиться:
Заметьте, что в каждом случае мы выполняем преобразование из абстрактного игрового пространства в пространство мира. Игровые объекты Unity не хранят сами важную информацию о состоянии игры, они всегда являются только отображением этого состояния.
… и обратно
В моей игре был единственный случай, когда нужно было выполнять преобразование из мира в игровое пространство: когда игрок щёлкал мышью на пустое пространство, чтобы бросить на поле тайл. Для этой задачи я создал большой коллайдер под всем игровым полем и прикрепил к нем следующий скрипт:
Вот, собственно, и всё. Заметьте, что в сущности в нём выполняется действие, обратное UpdatePosition(), где игровое пространство преобразуется в пространство мира.
Распознавание и удаление совпавших тайлов
Удаление совпавших тайлов
Это самая хитрая часть. Наверно, именно ради этого вы читаете статью.
Горизонтальное совпадение (как в «Тетрисе») реализовать довольно просто: нужно всего лишь искать смежные тайлы в одной строке. Даже добавление горизонтальных или вертикальных совпадений (как в Dr. Mario) является просто вариацией этой темы. Однако отслеживание набора смежных тайлов в одновременно горизонтальном и вертикальном направлении потребует рекурсии.
При каждом действии, изменяющем игровое поле, мы запускаем проверку. Первое, что мы делаем — копируем весь массив поля в другой массив:
Зачем? Мы увидим позже, что так будет гораздо проще определить, какие тайлы мы проверяли.
Мы начинаем процесс с «грубого» перебора. Пройдём от ячейки к ячейке (сначала строки, потом столбцы), проверяя каждую ячейку. Для каждой проверки мы сбрасываем некоторые переменные, используемые для отслеживания проверки, а затем вызываем отдельную функцию (которую позже применим для рекурсии):
Давайте рассмотрим эту функцию TestTile:
Если функция обнаруживает, что ячейка имеет значение null, то пропускает её. Ячейка с null означает, что она или пуста, или мы уже тестировали её. (Именно поэтому мы скопировали её в отдельный массив — так проще произвольно манипулировать новым массивом.)
Если ячейка имеет значение, то мы делаем следующие действия. Во-первых, мы запоминаем её как «текущую» ячейку, ту, которая находится на вершине рекурсивной цепочки. Затем мы удаляем её из копии игрового поля, чтобы не проверять дважды. Также добавляем её в список (List), чтобы запомнить, сколько смежных тайлов одного цвета мы нашли.
Есть два состояния, которые могут возникнуть позже в рекурсии, но мы поговорим о них потом. Проверив ячейку, мы берём четыре ячейки вокруг неё и выполняем для них ту же проверку.
«Текущая» ячейка уже установлена, и это значит, что мы не на первом уровне рекурсии. В этих вызовах функций у нас есть три варианта для каждой ячейки.
Во-первых, ячейка может быть null, и это снова значит, что мы уже проверяли её, или она пуста. И в этом случае мы снова ничего не делаем.
Во-вторых, ячейка может не совпадать с «текущей» ячейкой. В этом случае мы не считаем её «проверенной». Наша рекурсия проверяет наличие одного набора смежных тайлов одного цвета. Только потому, что этот тайл не является частью текущего набора, не значит, что он не является частью какого-нибудь другого.
В-третьих, ячейка может быть того же цвета, что и «текущая» ячейка. В таком случае, она «проверена», поэтому мы устанавливаем ей значение null в копии игрового поля. Также мы добавляем её в List, который используем как накопитель. Это одно из состояний, которое мы пропустили в примере выше:
Функция продолжит выполнять рекурсию, пока не закончатся все варианты, добравшись или до пустой ячейки, или до конца поля. В этот момент мы возвращаемся в основной цикл грубого перебора для обработки результатов.
Если в накопителе есть больше трёх тайлов, то мы нашли совпадение. Если нет, то мы проверили один или два тайла, но нам не нужно выполнять никаких действий:
Здесь, как мы рассмотрим позже, я просто включаю анимацию. Простейший подход, однако, заключается в том, чтобы пройти циклом по нашему накопителю и вызвать DestroyObject для игрового объекта каждого совпадающего тайла. Так мы убьём двух зайцев одним выстрелом: избавимся от внутриигровых объектов и присвоим ячейкам в состоянии игрового поля значение null.
Падение тайлов
Падающий тайл
Определённые изменения — например, падение тайла или удаление тайлов, в этом случае — оставляют тайлы без опоры, и этот случай нужно разрешить (конечно, если это требуется по правилам вашей игры). И на самом деле это довольно простой алгоритм.
Теперь мы проходим столбец за столбцом, а затем строку за строкой. Порядок здесь важен.
В каждом столбце мы проходим снизу вверх, пока не находим пустую ячейку. Затем мы помечаем её. Следующий найденный тайл мы просто смещаем вниз, в это положение, и добавляем единицу к индексу «пустой ячейки»:
После завершения нужно не забыть снова вызвать функцию проверки совпадений. Очень вероятно, что падающие тайлы создали пустые строки.
На самом деле, если в игре ведётся счёт, то это упростит отслеживание комбо или множителей очков. Все эти повторения падений и удаления блоков являются рекурсиями того первого вызова, запущенного действием игрока. Мы можем понять, сколько всего совпадений возникло после действия игрока и какое количество уровней «цепочек» потребовалось для каждого действия.
Анимации
Игра уже работает, но пока она не понятна интуитивно, в основном из-за отсутствия анимаций. Тайлы исчезают, а затем появляются на нижних строках. Сложно понять, что происходит, если не следить внимательно.
Это тоже сложный момент. Игровые объекты всегда являются отражением состояния игры, поэтому тайлы постоянно расположены в сетке. Тайлы всегда занимают то или иное место: тайл может быть в строке 1 или 2, но никогда — в строке 1,5.
В чём заключается сложность? Нам нельзя одновременно манипулировать игровым полем и анимацией. Вспомните, как работает «Тетрис» или Dr. Mario — следующий тайл не падает, пока все тайлы на поле не «улягутся». Это даёт игроку короткую передышку, а также гарантирует отсутствие непредвиденных состояний и взаимодействий.
Кстати, при начале нового проекта я рекомендую создавать перечисление (enumeration) «игровых состояний». Мне никогда не приходилось писать игру, в которой не нужно было знать состояние игры: сам процесс игры, пауза, отображение меню, диалоговое окно, и так далее. Лучше всего спланировать состояния на ранних этапах разработки, таким образом можно сделать так, чтобы каждая написанная вами строка кода проверяла, должна ли она выполняться в текущем состоянии.
Признаюсь, что моя реализация неуклюжа, но в целом идея такова: при удалении или падении тайла мы задействуем изменение состояния. Каждый объект GameTile знает, как обрабатывать это изменение состояния, и, что более важно, знает, когда нужно сообщать игровому полю, что он завершил свою анимацию:
После завершения анимации удаления игра должна проверить наличие падающих тайлов:
После завершения анимации падения нужно проверить наличие совпадений:
Этот цикл повторяется, пока у нас не останется больше совпадений, после чего игра может возвращаться к своей работе.








