0

Тетрис фигуры

Все или почти все великие вещи создаются случайно. Дальше эта случайность (рано или поздно) становится мировым достоянием и меняет жизнь многих людей.

Тетрис — одна из таких случайностей. Несложная логическая головоломка, написанная в 1985 году сотрудником Вычислительного центра при Академии наук СССР Алексеем Пажитновым для себя и своих коллег, за короткий срок обрела мировую известность, спровоцировала крупный скандал, череду судебных разбирательств и, в конечном счете, осталась в истории как самая популярная компьютерная игра всех времен.

Содержание

Буря в стакане

Идея тетриса родилась у Алексея Пажитнова в 1984 году после знакомства с головоломкой американского математика Соломона Голомба Pentomino Puzzle. Суть этой головоломки была довольно проста и до боли знакома любому современнику: из нескольких фигур нужно было собрать одну большую. Алексей решил сделать компьютерный вариант пентамино.

Алексей Пажитнов.

Пажитнов не просто взял идею, но и дополнил ее — в его игре собирать фигурки в стакане предстояло в реальном времени, причем сами фигурки состояли из пяти элементов (от греч. «п ента» — пять) и во время падения должны были проворачиваться вокруг собственного центра тяжести. Но компьютерам Вычислительного центра это оказалось не под силу — электронному пентамино попросту не хватало ресурсов. Тогда Алексей принимает решение сократить количество блоков, из которых состояли падающие фигурки, до четырех. Так из пентамино получился тетрамино (от греч. «т етра» — четыре). Новую игру Алексей нарекает тетрисом — от слов «тетрамино» и «теннис».

Первый вариант игры Пажитнов написал быстро, взяв за основу семь фигурок, ставших впоследствии стандартным набором тетриса. В той версии в стакан падали даже не графические изображения фигур, а их текстовые аналоги, в которых квадратики были составлены из открывающей и закрывающей скобки. Сделано это было не от хорошей жизни, а вынужденно: у компьютера «Электроника-60», на котором создавался тетрис, был даже не монитор, а дисплей, умеющий выводить только буквы и цифры (никакой графики!) и только в 24 строки по 80 символов в каждой.

Та самая Pentomino Puzzle.

«Несколько месяцев это была такая непонятная работа, которую фактически было даже не видно: на экране что-то меняется, Леша сопит, Леша ходит там, курит огромное количество папирос… — вспоминает Михаил Кулагин, один из сотрудников Вычислительного центра. — И вдруг он позвал нас посмотреть на игру. И говорит: вот, ребят, смотрите, получается вроде вот так. На экране появился знаменитый стакан, в который падали какие-то фигурки. Я, честно говоря, сразу даже и не понял в чем суть…»

Первая версия тетриса создавалась на популярном в те времена языке Pascal и выглядела достаточно примитивно. Но зато игра работала, да еще как работала! Такая вот нехитрая идея, когда фигурки тетрамино падают, а заполненные ряды исчезают, и дала впоследствии удивительные результаты.

Месяцев через восемь Пажитнов решил портировать игру на РС. В то время это было большой проблемой, потому что сети были еще дохленькие, а совместимых носителей не существовало (то есть, чтобы обмениваться данными между разными компьютерами, надо было искать специальные диски, которые читали формат на физическом уровне). Сам Алексей опыта работы на РС не имел, поэтому для портирования игры он привлек шестнадцатилетнего школьника Вадима Герасимова, который в Вычислительном центре слыл за юного гения и к нему все бегали за консультациями.

Алексей Пажитнов со своими друзьями из Вычислительного центра: Михаилом Кулагиным (слева) и Михаилом Потемкиным (справа).

Перенос игры на РС занял всего три-четыре дня, еще несколько дней ушло на отладку таймера, налаживание работы с экраном и тому подобные моменты. Но это было только начало, потом Алексей и Вадим еще около полугода возились с тем, чтобы сделать тетрис цветным, добавить таблицу рекордов (они воспользовались уже готовой программой для вывода на экран, написанной Дмитрием Павловским, коллегой Пажитнова) и систему защиты, чтобы можно было потом доказать свое авторство (любой софт в СССР распространялся бесплатно, и ничего зазорного в этом не видели). Еще много сил понадобилось на то, чтобы добавить поддержку разных типов дисплеев (!). Сейчас это звучит смешно, но тогда единых стандартов не было и под каждый дисплей игру надо было адаптировать, а это сильно портило код. На все это ушло полгода, но не из-за большого объема работ, а из-за того, что и у Алексея, и у Вадима были свои дела и тетрисом они занимались лишь от случая к случаю.

Много позже свой вклад в тетрис внес еще Михаил Потемкин, тоже сотрудник Вычислительного центра. Он портировал игру на компьютер «Электроника» следующей версии и первым добавил автоматическую загрузку мусора (это когда начинаешь партию, а стакан уже наполовину полон).

Распространялся тетрис на набиравших тогда популярность 5,25-дюймовых дискетах путем банального копирования у друзей. За две недели игра расползлась по всей Москве, а потом и по всему СССР. Успех был просто феноменальным. Игра была полностью бесплатной, о том, чтобы извлечь из нее какую-то выгоду, Пажитнов даже не думал: права на тетрис были у Вычислительного центра (как и на любую программу, написанную в его стенах), так что Алексей скорей бы оказался в тюрьме, чем за клавиатурой компьютера. Продажа подобных вещей была уже в компетенции государства.

Так тетрис выглядел на «Электронике-60».

А так — на PC под управлением MS-DOS.

И вот так — на Game Boy.

За кордон

Первыми иностранцами, познакомившимися с тетрисом, стали будапештцы из Института проблем кибернетики, с которыми сотрудничал Вычислительный центр (это случилось в 1986-м). Игра им понравилась, и они быстренько портировали ее на компьютер Commodore 64, производившийся компанией Commodore International с августа 1982 года, и на Apple 2, первый компьютер, серийно выпускавшийся Apple Computer с 1977 года. Как раз в это время в Институте гостил Роберт Штайн, венгр английского происхождения, владелец британской компании Andromeda Software, занимавшейся разработкой программного обеспечения. Штайн хорошо разбирался в играх, поэтому когда он увидел тетрис, то сразу же решил выкупить права на него. Роберт связался с Пажитновым, договорился о покупке прав, не называя, правда, никаких конкретных чисел, получил первоначальное «добро» и пообещал в течение пары дней прислать официальное соглашение. Но из-за железного занавеса переписка затянулась на многие недели.

Роберт Штайн.

Тем временем Штайн, понимая, какие деньги можно сделать на тетрисе, находится весь в нетерпении и, не выдерживая и не имея на то абсолютно никаких официальных прав, предлагает игру своим партнерам из британской компании Mirrorsoft. Те усомнились в привлекательности игры, но отослали ее на альтернативную пробу своим американским коллегам из Spectrum Holobyte. Американцы сразу увидели, какой огромный потенциал таится в тетрисе, и отрапортовали в Великобританию о том, что нужно как можно скорее получить права на продажу этой чудо-игры. Результатом этого стал контракт между Andromeda Software и Mirrorsoft на сумму всего лишь в 3000 фунтов стерлингов и на 7-15% (в зависимости от количества проданных копий) от прибыли с продаж. Алексей обо всем этом даже не знал.

Штайну надо было как-то все это дело легализовать, и уже зимой 1985 года он отправляется в Москву с твердым намерением заключить контракт с настоящими владельцами прав на игру. Однако такими вещами, как официальные переговоры с иностранцами и заключение договоров с зарубежными компаниями, занимались уже не сотрудники Вычислительного центра, а государственные органы, в данном случае — люди из верхушки Академии наук. А людям этим предложение Штайна оказалось неинтересно — то ли сумма показалась маленькой, то ли они просто отнеслись к нему с недоверием. Венгру пришлось уехать ни с чем.

Между тем американцы из Spectrum Holobyte даже и не подозревали, что ни они, ни кто-либо еще, кроме Пажитнова, на самом деле не владеют правами на тетрис. Холодная война между СССР и США еще в самом разгаре, всякий русский продукт, пусть даже с первого взгляда ничем не примечательный, тут же вызывает интерес у американцев. Что уж говорить о такой необычной игре, как тетрис. Пиар-подразделение Spectrum Holobyte не дремлет и перекраивает внешне игру в соответствии с самыми распространенными американскими стереотипами: добавляют коммунистических зарисовок, портреты известных русских, пускает в качестве музыкального сопровождения русские народные песни вроде «Калинки-малинки» и «Эх, ухнем!». Нетронутой остается только игровая механика. В общем, тетрис на глазах превращается в полноценный коммерческий продукт, у которого должен быть и разработчик, и обладающий соответствующими правами издатель.

На дворе был уже 1987 год, в Америке и Британии Spectrum Holobyte уже вовсю готовили PC-версию тетриса, а у Штайна по-прежнему не было прав на игру, то есть релиз, по сути, был незаконным. Штайн никак не мог получить права и в то же время не знал, как сказать своим коллегам из Европы и США, что запуск игры необходимо отложить. В итоге он так ничего не сделал и никому ничего не сказал.

В 1988 году состоялся релиз западной PC-версии тетриса.

Первая коммерческая версия тетриса от Mirrorsoft.

На троих

На Западе тетрис стал популярен еще быстрее, чем в СССР. Игра расходится приличными тиражами, ей присуждают несколько престижных наград Американской ассоциации разработчиков программного обеспечения: «Лучшая развлекательная программа», «Лучшая динамическая и стратегическая программа», «Лучшая оригинальная игровая разработка», «Лучшее потребительское программное обеспечение». До этого ни одной игре не удавалось достичь такого признания. Это был большой успех. Ходила даже байка, что тетрис специально разработали в КГБ, чтобы парализовать западную экономику: все, у кого в офисе были компьютеры, играли в него днями напролет.

Хэнк Роджерс.

Тем временем Алексей покинул Вычислительный центр и перешел в организацию «Электроноргтехника» (или просто ЭЛОРГ), которая была закреплена за Академией наук и которой теперь предстояло отстаивать международные права на тетрис.

В это время на Штайна навалились со всех сторон: русские требовали урегулировать вышедшую из-под контроля ситуацию, а соотечественники Роберта, особенно когда тетрис стал настолько популярной игрой на Западе, начали копаться в подробностях. Вездесущие журналисты из телерадиокомпании CBS выходят на Алексея Пажитнова и берут у него интервью. Штайну ничего не оставалось, кроме как подписать несчастный контракт на условиях русских.

Казалось бы, «ура, наконец-то!», но не тут-то было. Николай Беликов, сотрудник ЭЛОРГ, которому предстояло отстаивать права на тетрис, вспоминал: «Когда я прочитал контракт с Andromeda Software, мне стало плохо. В этом соглашении было указано, что первый платеж должен был пройти в течение трех месяцев. Договор был подписан 10 мая 1988 года, а уже был октябрь. После этого я стал думать, что делать с этим договором и как заставить Andromeda Software платить деньги».

За рубежом британская Mirrorsoft, уже убедившаяся в перспективности тетриса, просит Штайна приобрести у русских права на консольную и аркадную версии, а сама тем временем продает (не имея на то разрешения) права на аркадную версию игры американской компании Atari, которая, в свою очередь, сразу же перепродала их японской SEGA, на тот момент одной из крупнейших игровых компаний в мире.

Andromeda Software в лице Штайна, присылавшая в ЭЛОРГ телексы с просьбой начать переговоры по новому лицензионному договору, получила однозначный ответ: сначала выполните условия первого договора, только после этого мы начнем переговоры по следующему контакту.

Таким Алексея Пажитнова впервые увидел Хэнк Роджерс.

Как раз в это время тетрис выпускается в Японии на РС и игровой приставке Famicom (NES) от Nintendo и расходится в итоге более чем двухмиллионным тиражом (!).

Приметил тетрис президент американского подразделения Nintendo Минору Аракава. На одной из выставок он случайно увидел тетрис и загорелся желанием приобрести права на его консольную версию. Выяснив, что права на данный момент принадлежат Atari (которая искренно была в этом уверена, так как полагала, что честно выкупила соответствующие права у Mirrorsoft), Аракава приходит в некоторое расстройство: Atari и Nintendo на тот момент были злейшими конкурентами, которые бесконечно судились друг с другом. Однако еще один счастливый случай сталкивает его с Хэнком Роджерсом, владельцем небольшой японской фирмы Bullet Proof Software, которой американская Spectrum Holobyte продала права на продажу PC-версии тетриса на японском рынке. Остальные права в то время были у Atari, но Роджерс сумел добиться прав и на консольную версию игры для японского рынка. В этот самый момент он и знакомится с Аракавой. Nintendo позарез нужны были права на консольную версию игры, и цена ее не интересовала, так как на носу был запуск портативной приставки Game Boy, успех которой в случае приобретения тетриса был бы еще более прогнозируемым.

Роджерс сначала ведет переговоры со Штайном, но, сообразив, что тут что-то не то, прослеживает всю цепочку и выходит прямиком на Москву, куда тут же и отправляется. Штайн тоже просто так не сидит и решает лично встретиться с представителями «Электроноргтехники» для разговора с глазу на глаз. В это же время в Москву отправляется и Кевин Максвелл, сын медиамагната Роберта Максвелла, которому принадлежали Mirrorsoft и Spectrum Holobyte.

21 февраля 1989 года Николаю Беликову позвонили из протокольного отдела и сообщили, что к нему прибыл иностранец из Японии, его зовут Хэнк Роджерс и это является нарушением существовавшего в те времена режима — о таких встречах нужно было договариваться заранее и давать информацию в протокольный отдел (что за человек, что будет обсуждаться, какова цель переговоров и т. д.). Хэнк вынужден был прийти снова на следующий день.

22 февраля Николаю предстоит встреча сразу с тремя людьми, которым нужно одно и то же — права на тетрис. Он не хотел, чтобы кто-то из них встретился друг с другом, поэтому тщательно продумал график встреч.

Первым в офис к Беликову пришел Хэнк Роджерс. Вот что Николай Беликов рассказывал об этой встрече: «Как только мы сели за стол с господином Роджерсом, он без промедления достал игровую приставку и заявил: «Господин Беликов, я продаю ваш товар очень успешно». Я ему в ответ: «ЭЛОРГ никому не давал права на выпуск тетриса на игровых приставках. Единственная компания, которой были переданы какие-либо права, это Andromeda Software, и распространяются они только на версию для персональных компьютеров. Вы незаконно продаете то, что вам не принадлежит». Роджерс, конечно, был в шоке. В конце концов, он сказал: «Я просто не знал… Вы меня извините, я хочу работать с вами, у меня очень хорошие связи с Nintendo, крупнейшей в мире игровой компанией. У нее 70% рынка». Я предложил только одно решение — чисто бюрократический ход: «Господин Роджерс, напишите, пожалуйста, все на бумаге». Хэнк сказал «хорошо», и я сразу его выпроводил — вот-вот должен был прийти Роберт Штайн, и я не хотел, чтобы они встречались».

Роберт и Кевин Максвеллы.

Дальше в ЭЛОРГ приходит Роберт Штайн. Беликов ему говорит: «Господин Штайн, скажите мне честно, как этот документ называется?» Тот отвечает: «Соглашение». Я говорю, что это не соглашение, а набор каких-то безответственных фраз, по которым одна сторона передала права, а другая сторона не выполняет их, не компенсирует право использования этих прав». В итоге эту встречу тоже перенесли на следующий день.

«К моменту прихода Кевина Максвелла я уже знал, что он сын Роберта Максвелла, очень могущественного человека, одного из самых богатых в мире, — продолжает Беликов, — поэтому, конечно, я был очень напряжен. Я спросил: «Господин Кевин Максвелл, откуда у Mirrorsoft права на продажу тетриса на игровых приставках?» И тут Максвелл неожиданно сказал: «Это пиратская версия. У нас нет никаких прав». Я спросил: «А вы заинтересованы в том, чтобы получить права на версию для игровых приставок?» Максвелл: «Да, конечно». Я говорю: «А когда вы можете дать предложение?» — «Мне нужно вернуться в Великобританию, и я очень быстро пришлю наше предложение».

Алексея Пажитнова больше всего заинтересовал Хэнк Роджерс и его предложение. Кевин Максвелл был слишком сложным человеком, смотрел на всех свысока, и это отталкивало. Таким образом, Штайну достались только права на аркадный вариант игры (да еще втридорога). Хэнк вскоре вернулся с представителями Nintendo, и 21 марта 1989 года им были переданы все права на версию тетриса для игровых приставок. Максвелл-младший, оставшись ни с чем, понял, что позиции Mirrorsoft и Atari под угрозой, и пожаловался своему отцу. Роберт Максвелл обвинил Беликова в том, что он своими действиями сорвал торговые отношения между Англией и СССР. Всем нужно было действовать быстро и решительно.

23 марта Беликов получает телекс (что-то вроде факса, которого у наших тогда как раз не было) от Кевина Максвелла, в котором тот сообщил, что Николай совершил ряд ошибок и что вопрос будет поднят во время визита в Англию президента Горбачева. Одним словом, в телексе шли сплошные угрозы, причем серьезные. Потом Беликову позвонил человек, который занимался подготовкой визита Горбачева, и настоятельно предложил ему немедленно лететь в Лондон, встать на колени перед Робертом Максвеллом и умолять его ни слова не говорить Горбачеву, потому что иначе, если он скажет хоть одно слово, то Беликова «просто не будет».

Николай Беликов.

Николаю, как и любому нормальному человеку, который оказался бы в такой ситуации, стало просто страшно. Но ему повезло: был 1989 год, время большой перестройки, так что все обошлось. Если бы это был 1988 год, то история была бы другая, в этом Николай уверен.

В июне 1989 года все эти споры вылились в суд между Atari и Nintendo. Беликову также предстояло принять в нем участие от «Электроноргтехники» на стороне Nintendo. Перед отъездом Николая пригласили в Государственный комитет по вычислительной технике и информатике, где сказали, что в случае проигрыша судебного процесса будет создана специальная комиссия, которая рассмотрит вопрос о том, сколько «миллионов американских долларов потеряло советское государство от ваших непродуманных действий».

Суд в итоге постановил, что Mirrorsoft никакими правами не владеет, а следовательно, и контракт с Atari недействителен — сотни тысяч картриджей отправились на склад. Когда Хэнк сказал Николаю, что они победили, и забрал его кататься по Сан-Франциско с включенным на всю громкость приемником, нарушая при этом все мыслимые правила дорожного движения, то лишь через некоторое время к нему стало возвращаться чувство реальности — теперь он мог спокойно возвращаться домой, ничего не опасаясь.

Тетрис же стал одной из самых популярных игр всех времен. Game Boy, в комплекте с которым продавалась игра, не в последнюю очередь благодаря тетрису за несколько первых лет разошелся более чем 30-миллионным тиражом, а самих картриджей с игрой было продано около 15 млн. Впоследствии Game Boy стал одной из самых успешных консолей за всю историю электронных развлечений. Непосредственно тетрис принес Nintendo порядка $2-3 млрд (если учитывать все порты, версии и лицензионные отчисления). За двадцать лет игра (включая официальную статистику, электронные устройства и нелегальные продажи) продалась фантастическим тиражом, который оценивают примерно в четверть миллиарда экземпляров. Подобной популярности, вероятно, не удалось достичь ни одному тайтлу в мире. И даже приблизиться. И не факт, что когда-нибудь удастся.

К сожалению, сам Алексей вплоть до 1996 года никаких денег от тетриса не получал — сначала права на игру были у государства в лице ЭЛОРГА, а после распада Союза в 1991-м их унаследовал сам ЭЛОРГ, который Беликов тогда реорганизовал в частную компанию. Даже заплатить хорошую премию Алексею не могли, потому что эти деньги надо было согласовывать с руководством Академии наук.

Изо всех версий тетриса колоритнее всего был оформлен Super Tetris 1991 года, которому даже придумали подзаголовок «Пионеры смотрят на слона».

Скачать и распечатать тетрис из бумаги: это и настольная соревновательная игра, и головоломка с древними корнями, и несложная развивашка для детей.

Тетрис из бумаги – увлекательный способ для детей и родителей играть в полезную и интересную игру без экранов.

В классической видеоигре Тетрис каждая из форм падает сверху вниз по экрану. Ее можно переворачивать и перемещать по горизонтали, чтобы выстаиваемые снизу блоки образовали целостную строку – тогда они исчезнет.

На бумаге, конечно, такие правила не работают.

Как играть в тетрис на бумаге?

Версия 1

Если играете в одиночку, достаточно распечатать один экземпляр бумажного тетриса, или два – для усложненной версии.

Версия 2

Интереснее играть в тетрис как в настольную игру с соперниками. Тогда распечатываете одну доску и 3 или более страниц фигурок.

Положите фигуры стопкой, и пусть игроки по очереди вытягивают фигуру из верхней части и пытаются заполнить страницу. Можно играть, пока страница не заполнится или слегка «стряхивать” фигурки вниз, как в видеоигре.

Версия 3

Можно раздать фигурки игрокам и собирать тетрис на общем поле – побеждает тот, кто первый избавится от своих фигурок.

Версия 4

Можно раздать каждому игроку по игровому полю для тетриса, а элементы (понадобится несколько комплектов) сделать общими и тянуть по очереди. Победит тот, кто первым заполнит свое поле.

Игра пентамино

Пентамино – это, как правило, плоские фигуры, каждая из которых состоит из пяти одинаковых квадратов, соединённых между собой сторонами. Как в игре «тетрис” используются фигурки разных форм, но все они составлены из 5 квадратов. Всего существует ровно 12 фигур пентамино.

Суть игры заключается в построении из плоских геометрических фигур различных силуэтов – предметов окружающего мира.

Игра развивает глазомер, воображение, восприятие формы, мышление, зрительный анализ/синтез, способность выделять фигуру из фона, способность к выделению основных признаков объекта, зрительно-моторную координацию, умение работать по правилам.

Тетрис в свое время был придуман на основе пентамино. А сейчас мы предлагаем его более простой и дешевый аналог (даже бесплатный).

Распечатать тетрис

Распечатывать удобно на обычных листах формата А4. Ввиду легкости и нетрудоемкости изготовления бумажного тетриса, его можно даже не укреплять скотчем, ведь распечатать и разрезать новый экземпляр игры – дело пары минут.

Кстати, совместная настольная игра с родителями – отличная мотивация для детей взять в руки ножницы, даже если они не любят резать.

Вариант 1

Вариант 2

Распечатать тетрис во втором варианте – это более полноценная версия игры, особенно при условии участия нескольких игроков, с соревновательными правилами.

Вы можете распечатать и вырезать все 7 основных элементов игры тетрис.

Неконкурентная игра тетрис

Здесь вы можете распечатать бумажный тетрис для одного игрока.

Просто скачайте, распечатайте и вырежьте фигуры.

Деревянный тетрис

Если вам понравилась идея тетриса на бумаге, возможно приглянется и более долговечный аналог – деревянный тетрис.

Деревянный тетрис с алиэкспресс

Все бесплатные развивающие материалы на сайте ищите по тэгу РАЗВИВАШКИ 👇👇👇

В предыдущих статьях этой серии мы уже успели написать сапёра, змейку и десктопный клон игры 2048. Попробуем теперь написать свой Тетрис.

Нам, как обычно, понадобятся:

  • 30 минут свободного времени;
  • Настроенная рабочая среда, т.е. JDK и IDE (например, Eclipse);
  • Библиотека LWJGL (версии 2.x.x) для работы с графикой (опционально). Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
  • Спрайты, т.е. картинки плиток всех возможных состояний (пустая, и со степенями двойки до 2048). Можно нарисовать самому, или скачать использовавшиеся при написании статьи.

Прежде чем задавать вопрос в комментариях, не забудьте заглянуть в предыдущие статьи, возможно там на него уже давался ответ. Исходный код готового проекта традиционно можно найти на GitHub.

С чего начать?

Начать стоит с главного управляющего класса, который в нашем проекте находится выше остальных по уровню абстракции. Вообще отличный совет – в начале работы всегда пишите код вида if (getKeyPressed()) doSomething(), так вы быстро определите фронт работ.

public static void main(String args) { initFields(); while(!endOfGame){ input(); logic(); graphicsModule.draw(gameField); graphicsModule.sync(FPS); } graphicsModule.destroy(); }

Это наш main(). Он ничем принципиально не отличается от тех, что мы писали в предыдущих статьях – мы всё так же инициализируем поля и, пока игра не закончится, осуществляем по очереди: ввод пользовательских данных (input()), основные игровые действия (logic()) и вызов метода отрисовки у графического модуля (graphicsModule.draw()), в который передаём текущее игровое поле (gameField). Из нового разве что метод sync – метод, который должен будет гарантировать нам определённую частоту выполнения итераций. С его помощью мы сможем задать скорость падения фигуры в клетках-в-секунду.

Вы могли заметить, что в коде использована константа FPS. Все константы удобно определять в классе с public static final полями. Полный список констант, который нам потребуется в ходе разработки, можно посмотреть в классе Constants на GitHub.

Оставим пока инициализацию полей на потом (мы же ещё не знаем, какие нам вообще понадобятся поля). Разберёмся сначала с input() и logic().

Получение данных от пользователя

Код, честно говоря, достаточно капитанский:

private static void input(){ /// Обновляем данные модуля ввода keyboardModule.update(); /// Считываем из модуля ввода направление для сдвига падающей фигурки shiftDirection = keyboardModule.getShiftDirection(); /// Считываем из модуля ввода, хочет ли пользователь повернуть фигурку isRotateRequested = keyboardModule.wasRotateRequested(); /// Считываем из модуля ввода, хочет ли пользователь «уронить» фигурку вниз isBoostRequested = keyboardModule.wasBoostRequested(); /// Если был нажат ESC или «крестик» окна, завершаем игру endOfGame = endOfGame || keyboardModule.wasEscPressed() || graphicsModule.isCloseRequested(); }

Все данные от ввода мы просто сохраняем в соответствующие поля, действия на основе них будет выполнять метод logic().

Теперь уже потихоньку становится понятно, что нам необходимо. Во-первых, нам нужны клавиатурный и графический модули. Во-вторых, нужно как-то хранить направление, которое игрок выбрал для сдвига. Вторая задача решается просто – создадим enum с тремя состояниями: AWAITING, LEFT, RIGHT. Зачем нужен AWAITING? Чтобы хранить информацию о том, что сдвиг не требуется (использования в программе null следует всеми силами избегать). Перейдём к интерфейсам.

Интерфейсы для клавиатурного и графического модулей

Так как многим не нравится, что я пишу эти модули на LWJGL, я решил в статье уделить время только интерфейсам этих классов. Каждый может написать их с помощью той GUI-библиотеки, которая ему нравится (или вообще сделать консольный вариант). Я же по старинке реализовал их на LWJGL, код можно посмотреть в папках graphics/lwjglmodule и keyboard/lwjglmodule.

Интерфейсы же, после добавления в них всех упомянутых выше методов, будут выглядеть следующим образом:

public interface GraphicsModule { /** * Отрисовывает переданное игровое поле * * @param field Игровое поле, которое необходимо отрисовать */ void draw(GameField field); /** * @return Возвращает true, если в окне нажат «крестик» */ boolean isCloseRequested(); /** * Заключительные действия, на случай, если модулю нужно подчистить за собой. */ void destroy(); /** * Заставляет программу немного поспать, если последний раз метод вызывался * менее чем 1/fps секунд назад */ void sync(int fps); } public interface KeyboardHandleModule { /** * Считывание последних данных из стека событий, если модулю это необходимо */ void update(); /** * @return Возвращает информацию о том, был ли нажат ESCAPE за последнюю итерацию */ boolean wasEscPressed(); /** * @return Возвращает направление, в котором пользователь хочет сдвинуть фигуру. * Если пользователь не пытался сдвинуть фигуру, возвращает ShiftDirection.AWAITING. */ ShiftDirection getShiftDirection(); /** * @return Возвращает true, если пользователь хочет повернуть фигуру. */ boolean wasRotateRequested(); /** * @return Возвращает true, если пользователь хочет ускорить падение фигуры. */ boolean wasBoostRequested(); }

Отлично, мы получили от пользователя данные. Что дальше?

А дальше мы должны эти данные обработать и что-то сделать с игровым полем. Если пользователь сказал сдвинуть фигуру куда-то, то передаём полю, что нужно сдвинуть фигуру в таком-то направлении. Если пользователь сказал, что нужно фигуру повернуть, поворачиваем, и так далее. Кроме этого нельзя забывать, что 1 раз в FRAMES_PER_MOVE (вы же открывали файл с константами?) итераций нам необходимо сдвигать падающую фигурку вниз.

private static void logic(){ if(shiftDirection != ShiftDirection.AWAITING){ // Если есть запрос на сдвиг фигуры /* Пробуем сдвинуть */ gameField.tryShiftFigure(shiftDirection); /* Ожидаем нового запроса */ shiftDirection = ShiftDirection.AWAITING; } if(isRotateRequested){ // Если есть запрос на поворот фигуры /* Пробуем повернуть */ gameField.tryRotateFigure(); /* Ожидаем нового запроса */ isRotateRequested = false; } /* Падение фигуры вниз происходит если loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE итераций. */ if( (loopNumber % (FRAMES_PER_MOVE / (isBoostRequested ? BOOST_MULTIPLIER : 1)) ) == 0) gameField.letFallDown(); /* Увеличение номера итерации (по модулю FPM)*/ loopNumber = (loopNumber+1)% (FRAMES_PER_MOVE);

Сюда же добавим проверку на переполнение поля (в Тетрисе игра завершается, когда фигурам некуда падать):

/* Если поле переполнено, игра закончена */ endOfGame = endOfGame || gameField.isOverfilled(); }

Так, а теперь мы напишем класс для того магического gameField, в который мы всё это передаём, да?

Не совсем. Сначала мы пропишем поля класса Main и метод initFields(), чтобы совсем с ним закончить. Вот все поля, которые мы использовали:

/** Флаг для завершения основного цикла программы */ private static boolean endOfGame; /** Графический модуль игры*/ private static GraphicsModule graphicsModule; /** «Клавиатурный» модуль игры, т.е. модуль для чтения запросов с клавиатуры*/ private static KeyboardHandleModule keyboardModule; /** Игровое поле. См. документацию GameField */ private static GameField gameField; /** Направление для сдвига, полученное за последнюю итерацию */ private static ShiftDirection shiftDirection; /** Был ли за последнюю итерацию запрошен поворот фигуры */ private static boolean isRotateRequested; /** Было ли за последнюю итерацию запрошено ускорение падения*/ private static boolean isBoostRequested; /** Номер игровой итерации по модулю FRAMES_PER_MOVE. * Падение фигуры вниз происходит если loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE итераций. */ private static int loopNumber;

А инициализировать мы их будем так:

private static void initFields() { loopNumber = 0; endOfGame = false; shiftDirection = ShiftDirection.AWAITING; isRotateRequested = false; graphicsModule = new LwjglGraphicsModule(); keyboardModule = new LwjglKeyboardHandleModule(); gameField = new GameField(); }

Если вы решили не использовать LWJGL и написали свои классы, реализующие GraphicsModule и KeyboardHandleModule, то здесь нужно указать их конструкторы вместо, соответственно new LwjglGraphicsModule() и new LwjglKeyboardHandleModule().

А вот теперь мы переходим к классу, который отвечает за хранение информации об игровом поле и её обновление.

Класс GameField

Этот класс должен, во-первых, хранить информацию о поле и о падающей фигуре, а во-вторых, содержать методы для их обновления, и получения о них информации – кроме тех, которые мы уже использовали, необходимо написать метод, возвращающий цвет ячейки по координатам, чтобы графический модуль мог отрисовать поле.

Начнём по порядку.

Хранить информацию о поле…

/** Цвета ячеек поля. Для пустых ячеек используется константа EMPTINESS_COLOR */ private TpReadableColor theField; /** Количество непустых ячеек строки. * Можно было бы получать динамически из theField, но это дольше. */ private int countFilledCellsInLine;

…и о падающей фигуре

/** Информация о падающей в данный момент фигуре */ private Figure figure;

TpReadableColor — простой enum, содержащий элементы с говорящими названиями (RED, ORANGE и т.п.) и метод, позволяющий получить случайным образом один из этих элементов. Ничего особенного в нём нет, код можно посмотреть .

Это все поля, которые нам понадобятся. Как известно, поля любят быть инициализированными.
Сделать это следует в конструкторе.

Конструктор и инициализация полей

public GameField(){ spawnNewFigure(); theField = new TpReadableColor; countFilledCellsInLine = new int;

«Что это за OFFSET_TOP?» – спросите вы. OFFSET_TOP это количество неотображаемых ячеек сверху, в которых создаются падающие фигуры. Если фигуре не сможет «выпасть» из этого пространства, и хоть одна ячеек theField выше уровня COUNT_CELLS_Y будет заполнена, это будет обозначать, что поле переполнено и пользователь проиграл, поэтому OFFSET_TOP должен быть строго больше нуля.

Далее в конструкторе стоит заполнить массив theField значениями константы EMPTINESS_COLOR , а countFilledCellsInLine – нулями (второе в Java не требуется, при инициализации массива все int‘ы равны 0). Или можно создать несколько слоёв уже заполненных ячейкам — на GitHub вы можете увидеть реализацию именно второго варианта.

А что это там за spawnNewFigure()? Почему инициализация фигуры вынесена в отдельный метод?

Вы правильно догадались, spawnNewFigure() действительно инициализирует поле figure. А в отдельный метод это вынесено, потому что нам придётся делать инициализацию каждый раз, когда будет создаваться новая фигура.

/** * Создаёт новую фигуру в невидимой зоне * X-координата для генерации не должна быть ближе к правому краю, * чем максимальная ширина фигуры (MAX_FIGURE_WIDTH), чтобы влезть в экран */ private void spawnNewFigure(){ int randomX = new Random().nextInt(COUNT_CELLS_X — MAX_FIGURE_WIDTH); this.figure = new Figure(new Coord(randomX, COUNT_CELLS_Y + OFFSET_TOP — 1)); }

На этом с хранением данных мы закончили. Переходим к методам, которые отдают информацию о поле другим классам.

Методы, передающие информацию об игровом поле

Таких метода всего два. Первый возвращает цвет ячейки (для графического модуля):

public TpReadableColor getColor(int x, int y) { return theField; }

А второй сообщает, переполнено ли поле (как это происходит, мы разобрали выше):

public boolean isOverfilled(){ for(int i = 0; i < OFFSET_TOP; i++){ if(countFilledCellsInLine != 0) return true; } return false; }

Методы, обновляющие фигуру и игровое поле

Начнём реализовывать методы, которые мы вызывали из Main.logic().

Сдвиг фигуры

За это отвечает метод tryShiftFigure(). В комментариях к его вызову из Main было сказано, что он «пробует сдвинуть фигуру». Почему пробует? Потому что если фигура находится вплотную к стене, а пользователь пытается её сдвинуть в направлении этой стены, никакого сдвига в реальности происходить не должно. Так же нельзя сдвинуть фигуру в статические ячейки на поле.

public void tryShiftFigure(ShiftDirection shiftDirection) { Coord shiftedCoords = figure.getShiftedCoords(shiftDirection); boolean canShift = true; for(Coord coord: shiftedCoords) { if((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) || ! isEmpty(coord.x, coord.y)){ canShift = false; } } if(canShift){ figure.shift(shiftDirection); } }

Что мы сделали в этом методе? Мы запросили у фигуры ячейки, которые бы она заняла в случае сдвига. А затем для каждой из этих ячеек мы проверяем, не выходит ли она за пределы поля, и не находится ли по её координатам в сетке статичный блок. Если хоть одна ячейка фигуры выходит за пределы или пытается встать на место блока – сдвига не происходит. Coord здесь – класс-оболочка с двумя публичными числовыми полями (x и y координаты).

Поворот фигуры

Логика аналогична сдвигу:

Coord rotatedCoords = figure.getRotatedCoords(); boolean canRotate = true; for(Coord coord: rotatedCoords) { if((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) ||! isEmpty(coord.x, coord.y)){ canRotate = false; } } if(canRotate){ figure.rotate(); }

Падение фигуры

Сначала код в точности повторяет предыдущие два метода:

public void letFallDown() { Coord fallenCoords = figure.getFallenCoords(); boolean canFall = true; for(Coord coord: fallenCoords) { if((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) ||! isEmpty(coord.x, coord.y)){ canFall = false; } } if(canFall) { figure.fall();

Однако теперь, в случае, если фигура дальше падать не может, нам необходимо перенести её ячейки («кубики») в theField, т.е. в разряд статичных блоков, после чего создать новую фигуру:

} else { Coord figureCoords = figure.getCoords(); /* Флаг, говорящий о том, что после будет необходимо сместить линии вниз * (т.е. какая-то линия была уничтожена) */ boolean haveToShiftLinesDown = false; for(Coord coord: figureCoords) { theField = figure.getColor(); /* Увеличиваем информацию о количестве статичных блоков в линии*/ countFilledCellsInLine++; /* Проверяем, полностью ли заполнена строка Y * Если заполнена полностью, устанавливаем haveToShiftLinesDown в true */ haveToShiftLinesDown = tryDestroyLine(coord.y) || haveToShiftLinesDown; } /* Если это необходимо, смещаем линии на образовавшееся пустое место */ if(haveToShiftLinesDown) shiftLinesDown(); /* Создаём новую фигуру взамен той, которую мы перенесли*/ spawnNewFigure(); }

Так как в результате переноса ячеек какая-то линия может заполниться полностью, после каждого добавления ячейки мы проверяем линию, в которую мы её добавили, на полноту:

private boolean tryDestroyLine(int y) { if(countFilledCellsInLine < COUNT_CELLS_X){ return false; } for(int x = 0; x < COUNT_CELLS_X; x++){ theField = EMPTINESS_COLOR; } /* Не забываем обновить мета-информацию! */ countFilledCellsInLine = 0; return true; }

Этот метод возвращает истину, если линию удалось уничтожить. После добавления всех кирпичиков фигуры в сетку (и удаления всех заполненных линий), мы, при необходимости, запускаем метод, который сдвигает на место пустых линий непустые:

private void shiftLinesDown() { /* Номер обнаруженной пустой линии (-1, если не обнаружена) */ int fallTo = -1; /* Проверяем линии снизу вверх*/ for(int y = 0; y < COUNT_CELLS_Y; y++){ if(fallTo == -1){ //Если пустот ещё не обнаружено if(countFilledCellsInLine == 0) fallTo = y; //…пытаемся обнаружить (._.) } else { //А если обнаружено if(countFilledCellsInLine != 0){ // И текущую линию есть смысл сдвигать… /* Сдвигаем… */ for(int x = 0; x < COUNT_CELLS_X; x++){ theField = theField; theField = EMPTINESS_COLOR; } /* Не забываем обновить мета-информацию*/ countFilledCellsInLine = countFilledCellsInLine; countFilledCellsInLine = 0; /* * В любом случае линия сверху от предыдущей пустоты пустая. * Если раньше она не была пустой, то сейчас мы её сместили вниз. * Если раньше она была пустой, то и сейчас пустая — мы её ещё не заполняли. */ fallTo++; } } } }

Теперь GameField реализован почти полностью — за исключением геттера для фигуры. Её ведь графическому модулю тоже придётся отрисовывать:

public Figure getFigure() { return figure; }

Теперь нам нужно написать алгоритмы, по которым фигура определяет свои координаты в разных состояниях. Да и вообще весь класс фигуры.

Класс фигуры

Реализовать это всё я предлагаю следующим образом – хранить для фигуры (1) «мнимую» координату, такую, что все реальные блоки находятся ниже и правее неё, (2) состояние поворота (их всего 4, после 4-х поворотов фигура всегда возвращается в начальное положение) и (3) маску, которая по первым двум параметрам будет определять положение реальных блоков:

/** * Мнимая координата фигуры. По этой координате * через маску генерируются координаты реальных * блоков фигуры. */ private Coord metaPointCoords; /** * Текущее состояние поворота фигуры. */ private RotationMode currentRotation; /** * Форма фигуры. */ private FigureForm form;

Rotation мод здесь будет выглядеть таким образом:

public enum RotationMode { /** Начальное положение */ NORMAL(0), /** Положение, соответствующее повороту против часовой стрелки*/ FLIP_CCW(1), /** Положение, соответствующее зеркальному отражению*/ INVERT(2), /** Положение, соответствующее повороту по часовой стрелке (или трём поворотам против)*/ FLIP_CW(3); /** Количество поворотов против часовой стрелки, необходимое для принятия положения*/ private int number; /** * Конструктор. * * @param number Количество поворотов против часовой стрелки, необходимое для принятия положения */ RotationMode(int number){ this.number = number; } /** Хранит объекты enum’а. Индекс в массиве соответствует полю number. * Для более удобной работы getNextRotationForm(). */ private static RotationMode rotationByNumber = {NORMAL, FLIP_CCW, INVERT, FLIP_CW}; /** * Возвращает положение, образованое в результате поворота по часовой стрелке * из положения perviousRotation * * @param perviousRotation Положение из которого был совершён поворот * @return Положение, образованное в результате поворота */ public static RotationMode getNextRotationFrom(RotationMode perviousRotation) { int newRotationIndex = (perviousRotation.number + 1) % rotationByNumber.length; return rotationByNumber; } }

Соответственно, от самого класса Figure нам нужен только конструктор, инициализирующий поля:

/** * Конструктор. * Состояние поворота по умолчанию: RotationMode.NORMAL * Форма задаётся случайная. * * @param metaPointCoords Мнимая координата фигуры. См. документацию одноимённого поля */ public Figure(Coord metaPointCoords){ this(metaPointCoords, RotationMode.NORMAL, FigureForm.getRandomForm()); } public Figure(Coord metaPointCoords, RotationMode rotation, FigureForm form){ this.metaPointCoords = metaPointCoords; this.currentRotation = rotation; this.form = form; } }

И методы, которыми мы пользовались в GameField следующего вида:

/** * @return Координаты реальных ячеек фигуры в текущем состоянии */ public Coord getCoords(){ return form.getMask().generateFigure(metaPointCoords, currentRotation); } /** * @return Координаты ячеек фигуры, как если бы * она была повёрнута проти часовой стрелки от текущего положения */ public Coord getRotatedCoords(){ return form.getMask().generateFigure(metaPointCoords, RotationMode.getNextRotationFrom(currentRotation)); } /** * Поворачивает фигуру против часовой стрелки */ public void rotate(){ this.currentRotation = RotationMode.getNextRotationFrom(currentRotation); } /** * @param direction Направление сдвига * @return Координаты ячеек фигуры, как если бы * она была сдвинута в указано направлении */ public Coord getShiftedCoords(ShiftDirection direction){ Coord newFirstCell = null; switch (direction){ case LEFT: newFirstCell = new Coord(metaPointCoords.x — 1, metaPointCoords.y); break; case RIGHT: newFirstCell = new Coord(metaPointCoords.x + 1, metaPointCoords.y); break; default: ErrorCatcher.wrongParameter(«direction (for getShiftedCoords)», «Figure»); } return form.getMask().generateFigure(newFirstCell, currentRotation); } /** * Меняет мнимую X-координату фигуры * для сдвига в указаном направлении * * @param direction Направление сдвига */ public void shift(ShiftDirection direction){ switch (direction){ case LEFT: metaPointCoords.x—; break; case RIGHT: metaPointCoords.x++; break; default: ErrorCatcher.wrongParameter(«direction (for shift)», «Figure»); } } /** * @return Координаты ячеек фигуры, как если бы * она была сдвинута вниз на одну ячейку */ public Coord getFallenCoords(){ Coord newFirstCell = new Coord(metaPointCoords.x, metaPointCoords.y — 1); return form.getMask().generateFigure(newFirstCell, currentRotation); } /** * Меняет мнимую Y-координаты фигуры * для сдвига на одну ячейку вниз */ public void fall(){ metaPointCoords.y—; }

Вдобавок, у фигуры должен быть цвет, чтобы графический модуль мог её отобразить. В тетрисе каждой фигуре соответствует свой цвет, поэтому цвет мы будем запрашивать у формы:

public TpReadableColor getColor() { return form.getColor(); }

Форма фигуры и маски координат

Чтобы не занимать лишнее место, здесь я приведу реализацию только для двух форм: I-образной и J-образной. Код для остальных фигур принципиально не отличается и выложен на GitHub.

Храним для каждой фигуры маску координат (которая определяет, насколько каждый реальный блок должен отстоять от «мнимой» координаты фигуры) и цвет:

public enum FigureForm { I_FORM (CoordMask.I_FORM, TpReadableColor.BLUE), J_FORM (CoordMask.J_FORM, TpReadableColor.ORANGE); /** Маска координат (задаёт геометрическую форму) */ private CoordMask mask; /** Цвет, характерный для этой формы */ private TpReadableColor color; FigureForm(CoordMask mask, TpReadableColor color){ this.mask = mask; this.color = color; }

Реализуем методы, которые использовали выше:

/** * Массив со всеми объектами этого enum’а (для удобной реализации getRandomForm() ) */ private static final FigureForm formByNumber = {I_FORM, J_FORM, L_FORM, O_FORM, S_FORM, Z_FORM, T_FORM,}; /** * @return Маска координат данной формы */ public CoordMask getMask(){ return this.mask; } /** * @return Цвет, специфичный для этой формы */ public TpReadableColor getColor(){ return this.color; } /** * @return Случайный объект этого enum’а, т.е. случайная форма */ public static FigureForm getRandomForm() { int formNumber = new Random().nextInt(formByNumber.length); return formByNumber; }

Ну а сами маски координат я предлагаю просто захардкодить следующим образом:

/** * Каждая маска — шаблон, который по мнимой координате фигуры и * состоянию её поворота возвращает 4 координаты реальных блоков * фигуры, которые должны отображаться. * Т.е. маска задаёт геометрическую форму фигуры. * * @author DoKel * @version 1.0 */ public enum CoordMask { I_FORM( new GenerationDelegate() { @Override public Coord generateFigure(Coord initialCoord, RotationMode rotation) { Coord ret = new Coord; switch (rotation){ case NORMAL: case INVERT: ret = initialCoord; ret = new Coord(initialCoord.x , initialCoord.y — 1); ret = new Coord(initialCoord.x, initialCoord.y — 2); ret = new Coord(initialCoord.x, initialCoord.y — 3); break; case FLIP_CCW: case FLIP_CW: ret = initialCoord; ret = new Coord(initialCoord.x + 1, initialCoord.y); ret = new Coord(initialCoord.x + 2, initialCoord.y); ret = new Coord(initialCoord.x + 3, initialCoord.y); break; } return ret; } } ), J_FORM( new GenerationDelegate() { @Override public Coord generateFigure(Coord initialCoord, RotationMode rotation) { Coord ret = new Coord; switch (rotation){ case NORMAL: ret = new Coord(initialCoord.x + 1 , initialCoord.y); ret = new Coord(initialCoord.x + 1, initialCoord.y — 1); ret = new Coord(initialCoord.x + 1, initialCoord.y — 2); ret = new Coord(initialCoord.x, initialCoord.y — 2); break; case INVERT: ret = new Coord(initialCoord.x + 1 , initialCoord.y); ret = initialCoord; ret = new Coord(initialCoord.x, initialCoord.y — 1); ret = new Coord(initialCoord.x, initialCoord.y — 2); break; case FLIP_CCW: ret = initialCoord; ret = new Coord(initialCoord.x + 1, initialCoord.y); ret = new Coord(initialCoord.x + 2, initialCoord.y); ret = new Coord(initialCoord.x + 2, initialCoord.y — 1); break; case FLIP_CW: ret = initialCoord; ret = new Coord(initialCoord.x, initialCoord.y — 1); ret = new Coord(initialCoord.x + 1, initialCoord.y — 1); ret = new Coord(initialCoord.x + 2, initialCoord.y — 1); break; } return ret; } } ); /** * Делегат, содержащий метод, * который должен определять алгоритм для generateFigure() */ private interface GenerationDelegate{ /** * По мнимой координате фигуры и состоянию её поворота * возвращает 4 координаты реальных блоков фигуры, которые должны отображаться * * @param initialCoord Мнимая координата * @param rotation Состояние поворота * @return 4 реальные координаты */ Coord generateFigure(Coord initialCoord, RotationMode rotation); } private GenerationDelegate forms; CoordMask(GenerationDelegate forms){ this.forms = forms; } /** * По мнимой координате фигуры и состоянию её поворота * возвращает 4 координаты реальных блоков фигуры, которые должны отображаться. * * Запрос передаётся делегату, спецефичному для каждого объекта enum’а. * * @param initialCoord Мнимая координата * @param rotation Состояние поворота * @return 4 реальные координаты */ public Coord generateFigure(Coord initialCoord, RotationMode rotation){ return this.forms.generateFigure(initialCoord, rotation); } }

Т.е. для каждого объекта enum‘а мы передаём с помощью импровизированных (других в Java нет) делегатов метод, в котором в зависимости от переданного состояния поворота возвращаем разные реальные координаты блоков. В общем-то, можно обойтись и без делегатов, если хранить в каждом элементе отсупы для каждого из режимов поворота.

Наслаждаемся результатом

Работающая программа

P.S. Ещё раз напомню, что исходники готового проекта доступны на GitHub.

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Honor Cup, бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

admin

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *