Русский
Русский
English
Статистика
Реклама

Игра

Вы просили подсказку? Мы ее вам дадим

13.08.2020 18:07:06 | Автор: admin
Вы далеко зашли и отгадали почти все загадки, но остановились на самом простом и одновременно самом сложном.



Все что вам надо было сделать, это проявить бОльшую слаженность, организоваться еще немного лучше и проверить действительно все ИНН в интернете.

Но вместо этого вы выбрали атаковать штурмом офис управляющей компании и просить подсказки. Кстати о подсказке вы только что ее получили.

Но ничего в мире не бывает бесплатно: как плату за эту подсказку мы ускоряем работу шредера теперь он будет поедать по купюре каждые 30 минут.

Хотите еще подсказок?

Начало игры здесь. Дискорд discord.gg/wgYxwb

Подробнее..

Как создавался бекенд хакерской игры про уничтожение сервера

20.08.2020 14:16:33 | Автор: admin

Продолжаем рассказывать, как был устроен наш лазерный квест с уничтожением сервера. Начало в предыдущей статье про разгадку квеста.

Всего у бекенда игры было 6 архитектурных единиц, которые мы и разберём в этой статье:

  1. Бекенд игровых сущностей, которые отвечали за игровые механизмы
  2. Шина обмена данных бекенда и площадки на VPS
  3. Транслятор из запросов бекенда (игровых элементов) на ардуино и железо на площадке
  4. Ардуино, которая занималась управлением релешками, получала команды с транслятора и делала фактическую работу
  5. Фактические устройства: вентилятор, гирлянды, торшеры и прочее
  6. Фронтенд сам сайт Сокола, с которого игроки управляли устройствами

Давайте пройдёмся по каждой из них.

Бекенд игровых сущностей


Бекенд был реализован, как spring boot-приложение: оно имело несколько rest-контроллеров, websocket endpoint и сервисы с игровой логикой.

Контроллеров было всего три:

  • Мегатрон. Через GET-запросы отдавалась актуальная страница Мегатрона: до и после включения питания. Через POST-запрос лазер стрелял.
  • Мапинг тильдовских страниц, чтобы они отдавались по имени страницы. У тильды на экспорт выдаются страницы не с оригинальными названиями, а внутренним ID и информацией по соответствию.
  • Контроллер для капчи, чтобы отдавать псевдо-высокозагружающую сервер капчу.

Websocket endpoint использовался для управления гаджетами: лампами, гирляндой и буквами. Его выбрали, чтобы синхронно отображать всем игрокам текущий статус устройства: включено оно или выключено, активно или нет, какой цвет буквы сейчас горит на стене. Для того чтобы чуть-чуть усложнить задачу включения лазера, мы накинули авторизацию на гирлянду и лазер с одинаковым логином и пароль admin/admin.

Игроки могли протестировать его на включении гирлянды и повторить то же самое на лазере.

Нами была выбрана настолько тривиальная пара логин-пароль для того, чтобы не мучить игроков лишним подбором.

Чтобы сделать задачу чуть поинтересней, в качестве идентификаторов устройств в комнате использовались object ID из mongodb.

ObjectId содержит в себе timestamp: два случайных значения, одно из которых берётся на основании идентификатора устройства, а второе на основании pid-процесса, который генерирует его и значение счётчика. Хотелось сделать идентификаторы сгенерированными через равные промежутки времени и с разными pid-процессов, но общим счётчиком, чтобы подбор идентификатора устройства лазера был интереснее. Однако в итоге все запустились с идентификаторами, различающимися только в значении счётчика. Возможно, это сделало этап слишком простым и не требующим анализа структуры идентификаторов objectId.

Транслятор из запросов бекенда


Питоновский скрипт, который занимался таймерами и переводил из абстракций игровых в физическую модель. Например включить торшер включить реле N2.

Скрипт подключался к очереди RabbitMQ и передавал запросы из очереди на Ардуино. Также на нём была реализована логика параллельного включения света: вместе с некоторыми устройствами на них включался свет, например, при первичной подаче питания на Мегатрон, он подсвечивался сценическим светом. Дизайн света для кинематографичности всей сцены отдельная история про большую работу нашего сопродюсера проекта и художника-постановщика Ильи Серова, и про неё мы расскажем в отдельном посте.

Транслятор также отвечал за логику запуска шредера по таймеру и передачу изображения на телевизор: таймер запуска шредера, кричащая капибара, рекламный ролик в конце игры.

Как была устроена логика генерации токена мегатрона


Тестовый выстрел


Каждые 25 секунд генерировался новый токен, его можно было использовать, чтобы включить лазер на 10 секунд на мощности 10/255. Ссылка на гитхаб с кодом Мегатрона.

Затем лазер охлаждался 1 минуту в это время он был недоступен и не принимал новые запросы на выстрел.

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

Для генерации токена использовался алгоритм хеширования MD5. И схема получалась MD5 от MD5 + счётчик + секрет для боевого токена и без секрета для тестового.

MD5 это отсылка к одному коммерческому проекту, который делал Павел, наш бекендер. Всего пару лет назад в этом проекте использовался MD5, и когда он сказал архитектору проекта, что это устаревший алгоритм шифрования, они начали использовать MD5 от MD5. Раз уж мы решили делать максимально нубский проект, то он вспомнил всё и решил сделать маленькую отсылку.

Боевой выстрел


Боевой режим Мегатрона это 100% мощность лазера в 3 Вт. Этого вполне достаточно на 2 минуты, чтобы пережечь верёвку, которая держала гирю, чтобы разбить аквариум и залить сервер водой.

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

Зная эти данные, можно было перебрать 2 последних символа соли и фактически выяснить, что для неё использовались числа из Lost, переведённые в 16-ичную систему.

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

Счётчик просто инкрементировался при каждом тестовом выстреле и каждые 25 секунд. Об этом мы нигде не писали, это должно было быть небольшой игровой неожиданностью.

Сервис взаимодействия с капчей


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



Сервис подсчитывал, что отображать в мониторинге как текущую нагрузку: температуру и CPU Fan. Метрики передавались в timebase database и отрисовывались графаной.

Если за последнее 5 секунд поступало более 50 запросов на отображение капчи, то нагрузка росла на фикс + рандомное количество шагов. Расчёт был на то, чтобы 100% нагрузку можно было получить за две минуты.

В самом деле логики в сервисе было больше, чем отобразилось в конечной игре: мы поставили монитор таким образом, что было видно только вращение CPU Fan.

Графану в начале квеста хотели оставить доступной с сайта Сокола. Но в ней были также springboot-метрики по отчёту бекенд-приложения, которые мы не успевали вычистить, поэтому решили закрыть доступ к ней. И правильно ещё в начале квеста некоторые игроки догадались, что приложение написано на фреймворке springboot и даже откопали название некоторых сервисов.

Хостинг и шина обмена данных


Инструмент передачи информации с бекенда на площадку, VPS-сервер на котором было запущено RabbitMQ.

Бекенд и шина данных держались на . Его мощность была сопоставима с компьютером, который вы видели на экране: 2-х ядерная VPS-ка с двумя гигабайтами оперативной памяти. Тариф брали за ресурсы, поскольку пиковая нагрузка планировалась всего несколько дней так и поступают наши клиенты, которые планируют нагружать VPS на короткий срок. Потом оказалось, что нагрузка оказалось выше, чем мы предполагали и фиксированный тариф оказался бы выгоднее. Будете делать квест выбирайте тарифы линейки .

Чтобы защитить сервер от DDoSa, мы использовали Cloudflare.

Стоит сказать, что VPS с честью выдержала всё.

Ардуино, которая занималась управлением релешками, получала команды с транслятора и делала фактическую работу


Это больше тема следующей статью про хардварную часть проекта: бекенд просто присылал запросы включить конкретное реле. Так вышло, что бекенд знал почти все сущности и запросы с него, выглядели как включи эту сущность. Мы делали это для раннего тестирования площадки (пока ещё не собрали все Ардуино и реле), в итоге так всё и оставили.

Фронтенд


Сайт мы быстро создали на тильде, это заняло один рабочий день и сэкономило нам тысяч 30 бюджета.

Изначально мы думали просто экспортнуть сайт и накинуть нехватающую нам логику, но нарвались на terms of use, которые нам это запрещали.

Мы не были готовы нарушать лицензию, поэтому было два варианта: сверстать всё самим или напрямую связаться с Тильдой, рассказать о проекте и попросить разрешения менять код.

Мы выбрали второй вариант и они не просто пошли нам навстречу, а даже подарили нам год бесплатного бизнес-аккаунта, за что мы им очень благодарны. Было очень неловко показывать им дизайн сайта Сокола.

В итоге мы прикрутили к фронтенду js-логику на отправку запросов на элементарные устройства, немного поменяли стили кнопок включения и выключения игровых элементов.

Дизайн сайта


История поисков, которая стоит отдельной главы.

Мы хотели создать не просто старомодный сайт, а абсолютно тошнотворный, нарушающий все базовые правила дизайна. При этом было важно сохранить правдоподобность: он должен был не ломать ЛОР истории, демонстрировать претенциозность автора и игроки должны были бы поверить в то, что такой сайт может существовать и даже приводить клиентов. И привёл! Пока шла игра, нам дважды обращались за созданием сайтов.

Сначала дизайн делала я сама, стараясь воткнуть побольше гифок и блестящих элементов. Но мой муж-дизайнер с 10-летним стажем, заглянув через плечо, забраковал его как слишком хороший. Чтобы нарушать правила дизайна, надо их знать.



Существует несколько комбинаций цветов, которые вызывают стойкое чувство омерзения: зелёный и красный одинаковой сочности, серый и розовый, синий плюс коричневый. В итоге мы остановились на сочетании красного и зелёного, как базовых цветов, добавили гифок с котиком и выбрали на фотостоке 3-4 фотографии самого Соколова. У меня было только несколько требований: мужчина средних лет, в плохо сидящем костюме на пару размеров больше и в позе профессиональная фотосессия в студии. Для теста показывали её друзьям и спрашивали ну как тебе?.

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

Фактические устройства


Вентиляторы и свет монтировали через твердотельные реле, чтобы они включались не сразу на полную мощность чтобы нарастание мощности происходило параллельно с мониторингом.

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

Stay tuned!

Остальные статьи про квест с уничтожением сервера


Подробнее..

Как взломать премию, пропустив деньги через шредер

30.04.2021 16:23:36 | Автор: admin

Отлично заканчивается апрель для RUVDS! Мы победили в ежегодном конкурсе среди профессионалов на рынке дата-центров и получили премию ЦОДы.РФ в номинации Креатив года. Помните тот эмоциональный хакерский квест, который мы устроили в августе 2020-го? Неплохо повеселились, правда? За него нам и дали премию. Кто пропустил посты про эту чумовую игру подробности внутри.

Немного про конкурс


Основатель и управляющий партнер Никита Цаплин

Его уже пятый год проводят организаторы маркетинговые компании Media Grus и AnyLine. Премия присуждается по нескольким , в этот раз их было 13. Победителей определяет сообщество крупных профессионалов российской ЦОД-отрасли методом открытого голосования.

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

Про квест, принёсший победу


12 августа 2020 года мы опубликовали с видео-роликом, в которых предлагали читателям Хабра сыграть в игру с денежным призом. Нужно было хакнуть реальную комнату, созданную для квеста, в которой якобы находился офис веб-студии Сокол, предлагающей услуги подкроватного хостинга. Комната была напичкана IoT-устройствами, управлять которыми нужно было с компании.

Завязка квеста была в том, что владелец компании Андрей Соколов, заработавший некоторую сумму услугами хостинга на самосборном сервере и создания стрёмных сайтов, улетел на Бали. Оставшиеся 200 000 рублей он спрятал в этом офисе, который снабдил умной защитой от грабителей с трансляцией системы слежения на том же сайте. Но спрятал он их не в сейф, а в шредер, который, при попытке взлома, начинает уничтожать купюры, чтобы деньги не достались грабителям.

По сценарию раз в час уничтожается 1000 рублей, что стимулирует участников думать и действовать быстро ведь победитель забирает сумму, оставшуюся на момент взлома. А взломом должно стать короткое замыкание, к которому приведёт управление IoT-устройствами через сайт и которое уничтожит сервер. Короткое замыкание надо было устроить так: найти спрятанный лазер, активировать его, подобрав код доступа и направить в подвешенную гирю, которая, падая, разбивала аквариум с водой, который заливал сервер. Были и . Прямая трансляция происходившего в комнате-офисе велась на YouTube двое суток: лампочки мигали, телефон звонил, интернет вещей свирепствовал.

Как проходил квест и кто в нём победил, забрав 134 000 рублей, можно почитать .

Как создавалась игра


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

Тестирование выстрела лазера.

Бэкенд


Состоял из шести архитектурных единиц:

  1. Бэкенд игровых сущностей, отвечающих за игровые механизмы. Реализация в виде spring boot-приложения, которое имело три rest-контроллера (Мегатрон для лазера, контроллер для капчи и контроллер для мапинга тильдовских страниц), websocket endpoint для управления гаджетами и сервисы с игровой логикой.
  2. Шина обмена данных бэкенда и площадки на наших . Сначала использовали тариф за ресурсы, но потом поняли, что лучше фиксированный тариф из-за постоянной высокой нагрузки. Для подобных квестов хорошо использовать тариф Turbo. На VPS-сервере был запущен RabbitMQ, от DDoSa применили Cloudflare.
  3. Транслятор из запросов бэкенда игровых сущностей на Ардуино и железо на площадке. Через скрипт на Питоне транслятор отвечал за таймер шредера, передачу изображений на экран и логику параллельного включения света на разные устройства.
  4. Ардуино, которая получала команды с транслятора и управляла разными реле.
  5. Игровые устройства: вентилятор, гирлянды на светящихся буквах, торшеры и так далее.
  6. Фронтенд вырвиглазный сайт компании Сокол на Тильде, с которого игроки управляли игровыми устройствами. Попросили у Тильды разрешения менять код, чтобы добавить недостающую js-логику на отправку запросов на игровые устройства.

Жуткий сайт Сокола.

Железо


Мы не использовали коробочные решения и устройства для умного дома и не использовали беспроводные подключения, поскольку все девайсы находились друг от друга на расстоянии максимум 3 метров. В кадре не было видно никаких реле и Ардуино. Мегатрон-3000 это лазерный модуль для резки с ручной фокусировкой LSMVR450-3000MF, 3000мВт, 450нм. Для резервирования электричества и интернета использовали привычную для ЦОДов схему N+1 и роутер на базе OpenWRT с пакетом mwan3 для переключения на резервный модем с Yota в случае обрыва. Больше железных подробностей и киношных нюансов .

Про награждение


Основатель и управляющий партнер и Сания Галимова на вручении премии /

Церемония была торжественная, как Оскар. Состязаться за премию в номинации нам выпало с профессионалами из ООО АФЕСПРО, C3 Solutions и GreenMDC. По задумке наш проект это не просто рекламная кампания, пусть даже такая креативная. Основной посыл заключался в том, что мы против распространения на рынке виртуальных /серверов таких компаний, которые оказывают услуги низкого качества, которые предлагают хостинг на непрофессиональном оборудовании и которые таким подходом дискредитируют всю сферу услуг виртуальных серверов в России. Спасибо устроителям и жюри премии, за то, что высоко оценили нашу акцию. И огромное спасибо хакерам Хабра, так увлечённо проходившим квест. Вы вдохновляете!

Подробнее..

Спаси котика из-под рояля

21.05.2021 14:10:59 | Автор: admin

Вашему другу нужна помощь. Точнее, не вашему, а гипотетическому. Или даже не другу, а котику хотя котик же не настоящий Чёрт, короче! Наверняка у многих из вас в жизни была такая бывшая, расставание с которой было как закрытие ипотеки: Господи, ну наконец-то!. И вот всё наконец-то закончилось, осталось только забрать свой ноутбук с очень важным NFT-токеном гифки с танцующим котиком. Этот котик дорог вам как память и как произведение цифрового искусства. Но ипотека теперь уже бывшая устроила в отместку целое шоу: протерла ноутбук от пыли, приготовила всё, чтобы залить его жидким азотом и подвесила над ним РОЯЛЬ.

Ну да, вот поэтому у вас вашего друга было такое счастье и чувство свободы от расставания с этой эксцентричной!

Короче, с 24 мая каждый день его бывшая будет перерезать один из тросов, на которых висит рояль.

А 28 мая в 12.00 (МСК) ноутбук будет заморожен и расплющен силой гравитации и роялем.

Ее манифест смотрите на видео:

Ваша задача:


  1. Получить удаленный доступ к ноутбуку.
  2. Запустить скрипт копирования NFT-токена на резервный сервер RuVDS в Швейцарии (подальше от этой сумасшедшей).
  3. В час Х посмотреть, как упадет рояль, и поздравить себя друга с тем, что эти абсурдные отношения наконец-то закончились.

Первые 3 игрока, которым удалось запустить скрипт копирования получат 1000$, 200$ и 100$.

Подробнее об операции спасения танцующего котика на сайте проекта.

Поможете?


Подробнее..

Котики в NFT революция в цифровом мире или хайповая пирамида?

24.05.2021 14:14:07 | Автор: admin
Коллаж из картинок художника beeple держит сегодня рекорд по стоимости среди всех проданных цифровых NFT-произведений.

NFT одна из самых горячих тем в мире блокчейна. Кто-то уже заработал на них миллионы долларов, кто-то потерял сотни и тысячи. Одни называют NFT революционным изобретением, которое изменит сферы разработки игр и наше восприятие цифровых (нематериальных) объектов. Другие недоумевают, как можно вестись на такой развод и платить мошенникам деньги. Но вне зависимости от того, кто прав, факт остаётся фактом: NFT сейчас на слуху, и их оборот достиг уже очень внушительных сумм.

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

Что за NFT такое?


NFT расшифровывается как non-fungible token, то есть невзаимозаменяемый токен. Идея в том, что есть взаимозаменяемые объекты, которые полностью идентичны друг другу например, один рубль равен другому рублю, а есть невзаимозаменяемые объекты, каждый из которых уникален и неповторим. Скажем, когда вы пришли в магазин и купили новые кроссовки, они в этот момент взаимозаменяемы: полностью идентичный любым кроссовкам того же размера. А когда вы их поносите, то эта пара станет уникальной за счёт невоспроизводимых потёртостей и прочих изменений, то есть кроссовки станут уникальными других таких же во всём мире нет, есть только похожие.

На принципе уникальности держится вся торговля предметами материального искусства. Например, даже если художник нарисует точную копию какой-либо картины, пусть и своей же, это всё равно будут две разные картины, хоть и очень похожие. А электронные объекты обычно взаимозаменяемы Ctrl+C, Ctrl+V, и вот у тебя уже идеальная копия какой-нибудь гифки или песни, попробуй найди два отличия.

В результате придумали NFT это блокчейн, который можно прикрепить к любому нематериальному объекту картинке, видеозаписи, музыкальному файлу, предмету в игре и тем самым заявить, что этот объект уникален и принадлежит только вам. Скопировать такой объект можно, но выдать за свой уже не получится ваш NFT не позволит. Эдакий сертификат уникальности. Прикрутить токен можно не только к цифровому, но и к материальному объекту. За это нужно благодарить смарт-контракты Etherium, именно эта крипта лежит в основе большинства NFT.

Сегодня за цифровые произведения искусства с NFT уже платят десятки миллионов долларов. Рекорд пока что у художника Майка Винкельмана, выступающего под псевдонимом Beeple его коллаж из 5 тысяч изображений на аукционе Christie продали аж за $69 млн. За цифровую картинку с токеном:


Другой известный пример продажа NFT мема с летающим котиком:

Этот мем продали за 580 тыс. долларов. Не спрашивайте, зачем. Мы сами недоумеваем.

Вот ещё: компания Injective Protocol подогрела NFTхайп, купив у Бэнкси картину за $95 тыс., прилюдно сожгла её и привязала к цифровой копии NFT. Теперь этот набор пикселей принадлежит компании, которая объявила его настоящим произведением криптоискусства.

То есть NFT успел стать важным фактором в торговле произведениями искусства разной степени ценности (или сомнительности). Существуют разные электронные площадки, на которых все желающие могут покупать и продавать NFT. И цены на них сильно варьируются. Однако есть важный момент: в NFT хранится не сам цифровой объект, а только его описание и свидетельство собственности. Это уже приводит к забавным ситуациям, когда кто-то покупал токен, причём за вполне себе ощутимые суммы, а сам объект неожиданно исчезал.


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

Большую популярность NFT снискали в мире игр. Токены прикрепляют к различным игровым объектам оружию, доспехам и прочему добру и продают как уникальные объекты, имеющие конкретного владельца внутри конкретной игры. Владельца не авторских прав, а игрового предмета. Некоторые поклонники NFT мечтают о светлом дне, когда токены станут универсальными и можно будет применять в играх предметы из других игр. Купил себе автомат в каком-нибудь шутере, а потом зажёг с ним в фэнтезийной РПГ.

Сегодня многие пророчат NFT светлое будущее гаранта авторских прав в условиях повального копирования изображений, видео, книг и музыки и прочих произведений. Но столь же активно технологию критикуют как очередной инструмент выманивания у простаков денег. Кто прав, покажет время.

Образец дегенеративного криптоискусства, за который кто-то заплатил пару десятков баксов.

А пока что вы можете совершенно бесплатно заполучить классную гифку с NFT, которая скоро может быть безвозвратно уничтожена совершенно варварским способом. Причём уничтожена вместе с хорошим дорогим ноутбуком и отличным роялем. Все подробности тут.


Подробнее..

Музыкальная криптография

26.05.2021 18:20:11 | Автор: admin
Несколько столетий назад когда именно, доподлинно вряд ли известно, возник любопытный метод шифрования информации: с помощью нот. Точнее, с помощью их буквенного обозначения. Как вы знаете, ноты могут обозначаться не только классическими закорючками:


Но и буквами латинского алфавита: A, B, C и так далее. И когда-то, кому-то пришло в голову, что можно вставлять в музыкальные произведения, короткие последовательности нот, которые в буквенном выражении обозначают чьи-то инициалы или даже слова. Этот метод шифрования информации называется музыкальная монограмма.

Есть несколько подходов к созданию музыкальных монограмм. Например, слоговый: гласным буквам в тексте присваивали фонетически схожие названия нот. Считается, что в этом произведении Жоскен Депре, живший в 15-16 веках, закодировал нотами фразу laisse faire moy не мешай мне.

А если ставить ноты в соответствие не слогам, а буквам, то возникает другая проблема: буквенных обозначений нот от A до G не набирается на весь латинский алфавит. Сегодня её можно решить с помощью немецкого или французского методов.

Немецкий метод применялся ещё в 18 веке. Во-первых, у немцев иной порядок букв для обозначения нот: до-ре-ми-фа-соль-ля-си у них обозначаются как C, D, E, F, G, A, H. А во-вторых, недостающие буквенные обозначения они получали за счёт фонетического сходства: допустим, нота E могла кодировать как букву s, так и слог es. А в начале 20 века появился французский метод: одна нота могла кодировать сразу несколько букв алфавита в соответствии с простейшей таблицей (в шапке обозначения нот):


То есть при желании можно закодировать в виде музыкальной последовательности любой текст из букв латинского алфавита, в том числе транслитерированный. Впрочем, творить такую дичь вряд ли кто-то станет, ведь получившаяся музыка уж точно не будет обладать мелодичностью, а искусственность звукоряда будет настолько очевидна, что ни о каком шифровании речи уже не будет. Большинство композиторов за прошедшие века ограничивались вставками в свои произведения закодированных инициалов, либо своих, либо тех, кому они посвящали свою музыку. Но даже такие коротенькие последовательности, обычно не длиннее 4-5 нот, редко звучат органично. По крайней мере, для уха дилетанта. Наверное, ценители классической музыки со мной не согласятся, но судите сами:

Здесь во вступлении нотами B-A-C-H закодирована фамилия Ивана Севастьяновича Баха:


Это не находка Листа, а дань уважения, Бах сам часто вставлял эту последовательность в свои произведения. К примеру, здесь:

Всего, на текущий момент, известно более 400 произведений с этой музыкальной монограммой.

Другой популярный пример: Шуман разбросал по всему своему Карнавалу нотную последовательность A-Es-C-H, которая обозначает его монограмму SCHA и название города ASCH, где он впервые влюбился.

Любопытный образец музыкального шифрования фраза берегись Лядова, записанная Николаем Мясковским в его Квартете для струнных No. 3: B, D, G, A, C, F = B, Re, Gis, La, Do, Fa. Лядов преподавал в Петербургской консерватории музыкальную композицию.

А в Сонате F-A-E, посвящённой скрипачу и композитору Йозефа Иоахиму, зашифрована фраза Frei aber einsam Свободен, но одинок:

Как видите, чтобы органично вписать монограмму в произведение, нужен талант, это не примитивное перекладывание букв на ноты. Тем сложнее и интереснее задача, именно шифрования всё более длинных фрагментов текста в звукоряде, чтобы он звучал как музыка, а не какофония.

P.S. Кстати, напоминаем про наш квест: https://royal.ruvds.com/.
Сегодня утром был перерезан второй провод и вероятность преждевременного падения рояля на ноутбук все выше. Сейчас его удерживают всего 3 троса, а тем временем победителя, который прошел бы квест и спас котика, все еще нет!

В эту пятницу 28 мая в 12.00 ноутбук будет залит жидким азотом и на него обрушится рояль массой 500 Кг.


Как подсказывают наши эксперты (главный инженер здания), пережить падение рояля ноутбуку будет очень непросто. Вернее, если дословно, то Петрович оценил ситуацию как ноутбуку ****ц.

Если вы находитесь в ступоре, то вот вам маленькая подсказка. Перечитайте пост выше и посмотрите на клавиши рояля на сайте




Подробнее..

Хроники котика брутфорс рояля, крыса-кун и деанон Оксаны

27.05.2021 18:04:31 | Автор: admin

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

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

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

К исходу вторых суток ребята исчерпали все идеи и сникли, как вялые корнишоны, чаще стали говорить о том, что шибко сложные конкурсы у тамады:









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



Потом пошли идеи позаковыристее:
Как вам варик задеанонить эту е*тую Оксану, подломить ей инсту, вытащить оттуда что-нибудь пикантное и шантажировать ее, что бы отдала нам ноут?
на форумах заказываем пробив сотового номера Оксаны, а к нему пробив координат ближайшей соты, приезжаем на адрес завтра до 12:00, ждём появления звуков болгарки для детекта помещения, устраиваем аварию на электросети и режем оптику, в суматохе проникаем в помещение бригадой в форменной одежде и втыкаем в ноут USB-троян (злоумышленную клавиатуру), но чот приз маловат, жаль. не разгуляешься на килобакс.
Есть вариант лучше! Берём в заложников детей сотрудников ruvds и требуем от них ноутбук
Вот так и выглядит отчаяние: интеллектуалы-айтишники уже готовы опуститься до популярнейших статей УК. \_()_/





Конечно :))

Пришлось быстренько написать пост-подсказку. Правда и это помогло не сильно:
Мы пока не поняли как эту подсказку использовать. Мы в принципе сам смысл поняли, а к чему именно нужно применить подсказку, не поняли.
К вечеру уже обвиняли друг друга:
Классический крыса-кун. У него PuTTy и ssss уже готовы к операции
да он по любому организатор под прикрытием, потом бабки залутает

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

На этом пока всё. Ребята наконец-то немного продвинулись вперёд и бьются над решением следующей задачи. Будем держать вас в курсе новостей.

P.S. Умилили такие цитаты:
Есть какой-то рассказ Neuromancer, фразы из него выдернуты в разном порядке и из них собрали эту новеллу
а ещё клавиши обозначены как do re mi
Похоже на французский
я кажется понял мысль, только поймать не могу


Подробнее..

Роман Шувалов Мне пришла в голову идея сделать трехмерный рендер карты OpenStreetMap

21.07.2020 00:08:15 | Автор: admin

Роман Шувалов разработчик инди-игр из Тольятти, который в начале этого года выпустил игру Generation Streets, основанную на данных OpenStreetMap. Не так давно он открыл часть кода своего проекта. Зачем он это сделал, как появилась игра и почему выбор пал на OSM обо всем этом Роман рассказал в интервью.

Как и когда вы узнали о проекте OpenStreetMap?

Это случилось около 5 лет назад в 2014-2015 годах, когда я искал решения, которые бы позволяли пользователям создавать карты для собственных нужд. На тот момент у меня уже был небольшой веб-проект карта дорог и тропинок тольяттинского леса. Он был выполнен на сервисе Яндекс.Карты, поверх стандартной подложки которого накладывался векторный слой с нужными мне объектами. Кстати, этот слой был сделан на основе GPS-треков, записанных мною и участниками нашего велоклуба. Это было нечто похожее на тепловую карту Strava. В какой-то момент я понял, что мне хочется чего-то большего и стал думать, как модернизировать свою карту.



В итоге узнал про OpenStreetMap. Когда впервые увидел его, очень удивился: меня поразила невероятная открытость и гибкость проекта. Я мог не только свободно использовать его исходные данные, но и принять участие в отрисовке карты, что для меня было достаточно важным.

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


Велосипедная карта Самарской области. Мелкие тропы и населенные пункты видны даже крупных масштабах.

Почему в итоге остались в проекте? Ведь могли отрисовать только нужные вам дорожки и всё.

Рисовать карту в OSM опасное занятие, так как оно невероятно затягивает, потом сложно остановиться. Как обычно это происходит? Отметил тропинку, но увидел, что рядом есть здание, а на карте его нет. Добавил. Затем понял, что тут еще должен быть тротуар, а за ним дорога и так далее. Тобою движет чувство прекрасного: хочется, чтобы всё было красиво, причем не только в том месте, где ты живешь, а всюду. Поэтому я и остался в проекте. Правда, сейчас уже не так много рисую как раньше, но все равно потихоньку продолжаю делать OSM точнее. В основном, исправляю мелкие неточности или актуализирую карту добавляю новые объекты, которые постепенно появляются в интересующей меня локации.

Чем вам интересна картография? Всё-таки достаточно необычное хобби.

Наверное, это исходит из внутреннего желания знать, что находится вокруг тебя, причем гораздо дальше, чем видят твои глаза. И это чувство у меня было всегда. Помню, что уже в старших классах школы я экспериментировал с ГИСами, например, написал на Delphi (тогда я умел программировать только на нем) приложение, которое строило трехмерную карту Самарской области. Причем тогда еще не было открытых наборов данных, или я о них просто ничего не знал, а потому данные о высотах я взял из обычного бумажного атласа. Отсканировал его и с помощью какого-то плагина построил 3D-модель. Так что интерес к картографии, а вернее к визуализации окружающего мира, у меня давно.

Давайте поговорим о вашей 3D-игре Generation Streets, в основе которой данные OSM. Как она появилась на свет?

Отмечу сразу, что разработкой игр я занимаюсь с 2010 года. Что же касается конкретно этой игры, несколько лет назад совершенно случайно мне пришла в голову идея сделать трехмерный рендер карты OSM. Причем всей планеты сразу. Я посмотрел уже имеющиеся аналогичные проекты (F4map, OSM Buildings), но, к сожалению, ни один из них меня не удовлетворил они слишком схематичные. Мне же хотелось более реалистичный рендер: с текстурами, визуальным мусором в виде деревьев, фонарей и пр. Одним словом с той ерундой, которую мы обычно не замечаем, но без которой мир не выглядит живым.



И с чего вы начали разработку?

С поиска метода массовой обработки данных, так как мне хотелось сделать всю планету разом. На это у меня ушло достаточно много времени. Поначалу я пытался это сделать через базу данных на основе osm2vectortiles. Но ничего не вышло, потому что у меня был слабый компьютер.

В итоге я остановился на двух маленьких программках: Osmconvert и Osmfilter. Для их работы не требуется поднятие БД. Они берут данные OSM в формате PBF и позволяют через параметры командной строки вырезать нужные кусочки. Таким образом я получил возможность без лишних проблем нарезать всю планету на тайлы.

Но тут же столкнулся со следующей: формат PBF не совсем удобен для генерации трехмерного рендера, а потому нужно было его преобразовать в подходящий для этого формат. Для этого мне пришлось написать несколько вспомогательных утилит, исходный код которых я уже выложил под свободной лицензией на GitHub. Отчасти они основаны на библиотеке ogr2ogr.

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



Почему решили придумать свой собственный формат? Предполагаю, что уже есть готовые решения.

Да, вы правы: готовые форматы уже есть, в том числе и векторные, например, у Mapbox, но всё дело в том, что они ориентированы на плоское двухмерное изображение. И у них есть одна неприятная особенность: если здание попадает на стык тайлов, то оно разрезается. Для меня это было абсолютно неприемлемо, так как для того, чтобы я потом смог сделать 3D-модель здания, оно должно было быть целым. Готового решения этой проблемы не было. Думаю, что просто ранее никто с этим не сталкивался. Потому я решил сделать что-то свое.



Поделитесь статистикой по игре? Какие отзывы?

С момента публикации игры на Steam (ранний доступ октябрь 2018 года, полноценный релиз февраль 2020 года) ее скачало около 3 тысяч человек. Для такого типа игры инди это достаточно неплохо. Важно еще учесть следующий момент: у игры слабый геймплей. И я сразу понимал, что в рамках этого проекта у меня никак не получится сделать его увлекательнее. Поэтому он такой, какой он есть.

Главный интерес в этой игре состоит в том, что у пользователя есть возможность скачать интересную для него территорию на планете Земля и полетать там. У большинства игр такой возможности нет. Например, те, кто живет в Нью-Йорке, регулярно радуются тому, что вышла очередная игра, в которой этот город воссоздан достаточно детально. А те, кто живет в Тольятти, вряд ли когда-нибудь дождутся игры созданной в локациях их города. Вот именно на это я и делал ставку.

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



Ваша игра использует данные OSM. Можно ли через неё править его?

Изначально я хотел встроить редактор OSM, но после небольшого эксперимента решил этого не делать. Какое-то время у пользователей была возможность вносить правки. Эти правки не шли напрямую в OSM, они оседали в локальной БД. Оказалось, что игроки далеки от мира OSM, а потому к карте они отнеслись весьма безответственно. Сейчас думаю о том, чтобы отключить даже и этот экспериментальный редактор, а всех желающих направлять в JOSM.

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

Я открыл не всю игру целиком, а только код, отвечающий за генерирование трехмерных моделей. Во-первых, я это сделал для того, чтобы поделиться своими наработками с сообществом, так как я ощущал некий долг перед ним, ведь я сам использовал открытый код, написанный другими программистами. Во-вторых, скорее всего, я уже более не буду вносить в этот продукт серьезных изменений, но при этом предполагаю, что он может быть кому-то интересен и даже кто-то возьмется его развивать. В-третьих, мне хочется, чтобы моим кодом пользовались и он помогал людям в решении их задач. Глядишь, так станет больше проектов, связанных с 3D-рендерингом, и может быть в них будет мой родной город Тольятти.



Что вам нравится в OSM?

Нравится открытая лицензия и возможность получить данные, а не только готовые тайлы. Я, например, сделал видеоигру. Ни один другой картосервис не позволил бы мне так просто это сделать. Как минимум, пришлось бы связываться с разработчиками и просить предоставить исходные данные. И я не уверен, что гиганты отрасли пошли бы мне навстречу. Тут же я взял и скачал всю планету и делал с ней всё то, что мне было нужно.
Еще мне нравится, что любой может принять участие в проекте и сделать карту точнее. Редактор JOSM не такой уж и сложный. И при этом не нужно ждать пока твои правки примут или одобрят как только ты их отправил, они тут же находятся в основной базе, а это значит, что ты можешь их немедленно использовать для своих нужд. Например, обновить карту в своем навигаторе.

Что не нравится?

Изобилие и несистемность тегов. Не всегда это хорошо. Помимо этого, пора уже что-то придумать со смещениями. Иногда открываешь город и понимаешь, что человек отрисовал его без учета этого нюанса. И ты не знаешь как поступить: переносить всё или продолжить рисовать в неправильном смещении? Конечно, я понимаю, что мир несовершенен, приходится переступать через какие-то проблемы. Если же говорить в целом, то у OSM больше плюсов, чем минусов.

Что бы сделали лучше?

Хотел бы увидеть хороший веб-редактор. Да, уже есть iD, но он ориентирован на новичков. Хочется что-то поудобнее. Также, мне кажется, что было бы намного лучше, если бы объекты внутри OSM были как-то связаны между собой. Сейчас если вносятся какие-то изменения в форму здания или меняется положение дороги, которая проходит рядом, то это изменение никак не влияет на соседние объекты. Даже если после изменения, например, дорога стала проходить поверх дома. Пока не знаю каким именно образом подобное взаимодействие можно реализовать, но вот хотелось бы, чтобы информация о зависимостях была. Тогда, например, не ломались бы маршруты общественного транспорта в случае редактирования дороги. Предполагаю, что это сделает данные OSM еще более удобными для редактирования и их дальнейшего использования.

Используете OSM в личной жизни? По работе? Кроме игры.

В повседневной жизни использую мобильные навигаторы OruxMaps и Maps.Me. Оба оффлайновые, то есть позволяют работать с картой без интернета, в основе их карт OSM. Чем мне нравится OruxMaps, так это тем, что он еще позволяет использовать собственные карты в векторном формате mapsforge.

Что посоветуете новичкам? Или тем, кто только думает: связываться с проектом или нет.

Если вы используете какой-либо мобильный навигатор, в основе которого OSM, подумайте о том, чтобы начать делать карту, которую, кстати, вы используете, еще точнее. Ведь многие просто не знают, что карту можно редактировать, причем прямо с телефона.
Тем же, кто уже знаком с OSM и даже немного картирует, первый и главный совет прежде чем редактировать карту, почитайте WikiOSM. Там почти всё есть. Это спасет вас от многих ошибок, а также позволит перенять культуру картирования и в большей степени осознать свою ответственность перед теми, кто после будет пользоваться картой, которую вы редактируете. Никогда не забывайте, что данные OSM используются в огромном количестве проектов, а потому всегда старайтесь поддерживать высокую планку качества вносимых данных.

Что скажете в завершении беседы?

Как бы это громко не звучало, но на данный момент OSM это единственный открытый и свободный картографический проект в мире. Это независимая альтернатива коммерческим картосервисам. Именно поэтому в нем нужно принимать участие и всячески его развивать. Ведь если OpenStreetMap перестанет существовать, мы все будем вынуждены пользоваться коммерческими картами на тех условиях, которые нам будут кем-то навязаны. А о свободном использовании геоданных можно вообще будет забыть.


Общение российских участников OpenStreetMap идёт в чатике Telegram и на форуме.
Также есть группы в социальных сетях ВКонтакте, Facebook, но в них, в основном, публикуются новости.

Присоединяйтесь к OSM!



Подробнее..

Как я разработал мобильную игру на Android с использованием React.js и выложил её в Google Play Store

30.12.2020 18:06:06 | Автор: admin

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

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

Скриншот готовой игрыСкриншот готовой игры

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

Предыстория

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

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

Построение и прорисовка мира

Сперва было принято решение создать изометрический мир, который мог бы поддерживать бесконечную прокрутку по горизонтали. После тщательного изучения статей на тему "Как создавать изометрические миры" и им подобных, а также просмотра библиотек для JavaScript и React, которые якобы должны помогать выполнять данную задачу, дело перешло к практике.

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

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

Сперва отрисуем ячейки мира построчно. Пускай они будут размером 64x64 пикселя. Далее развернём наш контейнер таким образом, чтобы он выглядел изометрично:

.rotate {  transform: rotateX(60deg) rotateZ(45deg);  transform-origin: left top;}

При имплементации данного подхода к отрисовке можно наблюдать, что необходимые нам "строки" мира на самом деле идут не прямо, а зигзагом, так как мы развернули нашу карту. Таким образом, каждую из ячеек необходимо позиционировать абсолютно с учетом текущего индекса строки, а также индекса колонки:

const cellOffsets = {};export function getCellOffset(n) {  if (n === 0) {    return 0;  }  if (cellOffsets[n]) {    return cellOffsets[n];  }  const result = 64 * (Math.floor(n / 2));  cellOffsets[n] = result;  return result;}

Использование:

import { getCellOffset } from 'libs/civilizations/helpers';// ...const offset = getCellOffset(columnIndex);// ...style={{  transform: `translateX(${(64 * rowIndex) + (64 * columnIndex) - offset}px) translateY(${(64 * rowIndex) - offset}px)`,}}

Для увеличения производительности нам необходимо перерисовывать только те ячейки карты, которые сейчас являются видимыми на экране. Для этого был использован компонент FixedSizeGrid из модифицированной версии библиотеки react-window с учетом нашего поворота и расположений ячеек, код которого здесь приводить не буду. Из того, что не получилось - это сделать бесконечную прокрутку мира. После изучений исходного различных библиотек для бесконечного скролла / слайдеров и тп. подходящего решения найдено не было. Что ж, значит наш мир будет с границами по всем бокам.

Графика

Для отображения поворотов наших юнитов, а также анимации атаки, используются png-спрайты. Поиск графических элементов занял очень большой промежуток времени, было очень сложно найти хотя бы какие-то картинки возможных юнитов для нашей игры. Изначально ещё до поиска игра выглядела вот так:

Игра до поиска графических элементовИгра до поиска графических элементов

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

Спрайт вертолётаСпрайт вертолёта

Локализация

Игра поддерживает 4 языка и, если честно, мне непонятно, зачем в несложных приложениях разработчики подключают массивные библиотеки типа react-i18next. Давайте напишем похожее кастомное решение, которое уместится в чуть более чем 100 строк с учетом красивой разметки кода, а также будет поддерживать определение языка девайса пользователя, переключение языков в реальном времени и сохранение последнего выбора пользователя. Здесь используется redux, однако данный код можно адаптировать и под другие реактивные хранилища. Да, здесь нет некоторых фишек больших библиотек типа поддержки переменных в строках, однако в таком проекте нам это и не нужно. И да, эту библиотеку можно использовать как легковесную замену react-i18next (или подобным) в уже существующем проекте.

import React, { Component } from 'react';import PropTypes from 'prop-types';import { connect } from 'react-redux';import get from 'lodash/get';import set from 'lodash/set';import size from 'lodash/size';import { emptyObj, EN, LANG, PROPS, langs } from 'defaults';import { getLang } from 'reducers/global/selectors';import en from './en';export function getDetectedLang() {  if (!global.navigator) {    return EN;  }  let detected;  if (size(navigator.languages)) {    detected = navigator.languages[0];  } else {    detected = navigator.language;  }  if (detected) {    detected = detected.substring(0, 2);    if (langs.indexOf(detected) !== -1) {      return detected;    }  }  return EN;}const options = {  lang: global.localStorage ?    (localStorage.getItem(LANG) || getDetectedLang()) :    getDetectedLang(),};const { lang: currentLang } = options;const translations = {  en,};if (!translations[currentLang]) {  try {    translations[currentLang] = require(`./${currentLang}`).default;  } catch (err) {} // eslint-disable-line}export function setLang(lang = EN) {  if (langs.indexOf(lang) === -1) {    return;  }  if (global.localStorage) {    localStorage.setItem(LANG, lang);  }  set(options, [LANG], lang);  if (!translations[lang]) {    try {      translations[lang] = require(`./${lang}`).default;    } catch (err) {} // eslint-disable-line  }}const mapStateToProps = (state) => {  return {    lang: getLang(state),  };};export function t(path) {  const { lang = get(options, [LANG], EN) } = get(this, [PROPS], emptyObj);  if (!translations[lang]) {    try {      translations[lang] = require(`./${lang}`).default;    } catch (err) {} // eslint-disable-line  }  return get(translations[lang], path) || get(translations[EN], path, path);}function i18n(Comp) {  class I18N extends Component {    static propTypes = {      lang: PropTypes.string,    }    static defaultProps = {      lang: EN,    }    constructor(props) {      super(props);      this.t = t.bind(this);    }    componentWillUnmount() {      this.unmounted = true;    }    render() {      return (        <Comp          {...this.props}          t={this.t}        />      );    }  }  return connect(mapStateToProps)(I18N);}export default i18n;

Использование:

import i18n from 'libs/i18n';// ...static propTypes = {  t: PropTypes.func,}// ...const { t } = this.props;// ...{t(['path', 'to', 'key'])}// ...или тоже самое, но слегка медленнее{t('path.to.key')}// ...export default i18n(Comp);

Мультиплеер

Игра поддерживает мультиплеер в реальном времени для устройств с Android 9 или выше (возможно, будет работать и на 8-м, однако данное предположение не проверялось) с рейтингом и таблицей лидеров.

Сам движок не поддерживает ходы в реальном времени, потому многопользовательский режим построен таким образом, что все события происходят в один и тот же ход, в это же время существуют кулдауны на определенные действия, которые реализованы через requestAnimationFrame. На Android 7 и ранее такой подход почему-то просто-напросто не работает.

Кастомный код библиотеки, которая регистрирует requestAnimationFrame, для интересующихся (не забывайте также, что после регистрации колбека его нужно и отрегистрировать, что обычно происходит по завершении колбека или на анмаут компонента):

import isFunction from 'lodash/isFunction';let lastTime = 0;const vendors = ['ms', 'moz', 'webkit', 'o'];for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {  window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];  window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] || window[`${vendors[x]}CancelRequestAnimationFrame`];}if (!window.requestAnimationFrame) {  window.requestAnimationFrame = (callback) => {    const currTime = new Date().getTime();    const timeToCall = Math.max(0, 16 - (currTime - lastTime));    const id = window.setTimeout(() => { callback(currTime + timeToCall); },      timeToCall);    lastTime = currTime + timeToCall;    return id;  };}if (!window.cancelAnimationFrame) {  window.cancelAnimationFrame = (id) => {    clearTimeout(id);  };}let lastFrame = null;let raf = null;const callbacks = [];const loop = (now) => {  raf = requestAnimationFrame(loop);  const deltaT = now - lastFrame;  // do not render frame when deltaT is too high  if (deltaT < 160) {    let callbacksLength = callbacks.length;    while (callbacksLength-- > 0) {      callbacks[callbacksLength](now);    }  }  lastFrame = now;};export function registerRafCallback(callback) {  if (!isFunction(callback)) {    return;  }  const index = callbacks.indexOf(callback);  // remove already existing the same callback  if (index !== -1) {    callbacks.splice(index, 1);  }  callbacks.push(callback);  if (!raf) {    raf = requestAnimationFrame(loop);  }}export function unregisterRafCallback(callback) {  const index = callbacks.indexOf(callback);  if (index !== -1) {    callbacks.splice(index, 1);  }  if (callbacks.length === 0 && raf) {    cancelAnimationFrame(raf);    raf = null;  }}

Использование:

import { registerRafCallback, unregisterRafCallback } from 'client/libs/raf';// ...registerRafCallback(this.cooldown);// ...componentWillUnmount() {  unregisterRafCallback(this.cooldown);}

Стандартная имплементация Lobby из библиотеки движка мне не подходила, так как она открывала ещё одно новое websocket-подключение на каждый инстанс игры, но мне также нужно было передавать данные пользователя и таблицу лидеров по своему уже существующему websocket-подключению, потому, чтобы не плодить подключения, здесь снова было использовано собственное решение на основе библиотеки primus. На стороне клиента подключение хендлится сбилдженной библиотекой от примуса, которое также выложил на npm с именем primus-client. Вы можете сами сбилдить себе подобную клиентскую библиотеку для определенной версии примуса через функцию save на стороне сервера.

Видео геймплея многопользовательского режима можно наблюдать ниже:

Звук и музыка

В игре присутствует несколько звуков - старта игры, атаки и победы. Для проигрывания звука - также кастомная библиотека (для музыки - схожий подход):

import { SOUND_VOLUME } from 'defaults';const Sound = {  audio: null,  volume: localStorage.getItem(SOUND_VOLUME) || 0.8,  play(path) {    const audio = new Audio(path);    audio.volume = Sound.volume;    if (Sound.audio) {      Sound.audio.pause();    }    audio.play();    Sound.audio = audio;  },};export function getVolume() {  return Sound.volume;}export function setVolume(volume) {  Sound.volume = volume;  localStorage.setItem(SOUND_VOLUME, volume);}export default Sound;

Использование:

import Sound from 'client/libs/sound';// ...Sound.play('/mp3/win.mp3');
Окно настроек игрыОкно настроек игры

Сборка проекта

Сборка web-части осуществляется вебпаком. Однако тут нужно учитывать особенности путей к файлам, ведь в процессе разработке на локалхосте или на сервере в продакшене они являются относительными корня домена, а для приложения в Cordova наши файлы будут размещены по протоколу file:// и потому после сборки нам необходимо провести некоторые преобразования, а именно:

const replace = require('replace-in-file');const path = require('path');const options = {  files: [    path.resolve(__dirname, './app/*.css'),    path.resolve(__dirname, './app/*.js'),    path.resolve(__dirname, './app/index.html'),  ],  from: [/url\(\/img/g, /href="\//g, /src="\//g, /"\/mp3/g],  to: ['url(./img', 'href="./', 'src="./', '"./mp3'],};replace(options)  .then((results) => {    console.log('Replacement results:', results);  })  .catch((error) => {    console.error('Error occurred:', error);  });

Итоги

Приложение разрабатывалось в течении года, находится в Google Play Store с середины сентября, а значит уже прошло три месяца. Общее количество установок - 46, из которых ещё непонятно, сколько там на самом деле настоящих людей. Если коротко, то это провал. Однако был приобретен первичный опыт как разработки игр, так и мобильных приложений.

Из того, что было задумано, но не получилось:

  1. Более сложный геймплей

  2. Бесконечная прокрутка карты по горизонтали

  3. Продвинутый ИИ компьютера

  4. Поддержка мультиплеера на всех устройствах

Дальнейшие планы

Сейчас понятно, что подобные игры мало кому интересны, так что в прогрессе изучение Unity, и возможно через некоторое время появится ещё одна игра в жанре tactical rts.

Можно потыкать?

Можно. Для интересующихся - ссылка на приложение на Google Play Store.

P.S. Отдельное спасибо музыканту Anton Zvarych за предоставленную фоновую музыку.

Подробнее..

Перевод Используем глубокое обучение, чтобы отгадывать страны по фотографиям в GeoGuessr

18.04.2021 14:12:21 | Автор: admin
Во время последнего локдауна в Великобритании мы с женой играли в GeoGuessr. Эта игра более размеренна, чем те, в которые мы обычно играем, но хорошо подходит для нашей семьи с 11-недельным младенцем, который становится активнее с каждым днём.

GeoGuessr это игра о географических исследованиях. Вас бросают на случайную точку в Google Street View, после чего ваша задача указать своё местоположение на карте. Можно осматривать окрестности, увеличивать изображение и двигаться по пути автомобиля на местных улицах.

image

Нас серьёзно заинтересовали ежедневные соревнования (Daily Challenge) на GeoGuessr. Мы начали заходить на сайт каждый день и пытаться поставить новый рекорд. В формате Daily Challenge на каждый раунд выделяется по три минуты, которые мы тратили или на бешеное кликанье по австралийскому бушу (при этом иногда путая его с Южной Африкой), или на обсуждение того, есть ли в шведском языке буква .

Теперь у меня накопился большой объём знаний типа увижу узнаю. Я могу опознать Гренландию с первого взгляда. Вернулись мои утерянные знания флагов стран, а также появились новые знания о флагах штатов США, о тех странах, где ездят по левой и правой полосам, где используют километры или мили. Я знаю почти все доменные имена стран (их часто можно встретить на рекламных билбордах вдоль дорог) мне ещё долго не забыть .yu.

Вы знали, что чёрно-белые дорожные ограждения распространены в России и Украине? Или что можно разобрать синюю полосу EU на автомобильных номерах, несмотря на размытие Google Street View? Подробнее об этом можно прочитать в этом руководстве из 80 тысяч слов Geoguessr the Top Tips, Tricks and Techniques.

Указывающая вниз полосатая красно-белая стрелка даёт понять, что вы находитесь в Японии, с большой вероятностью, на острове Хоккайдо или, возможно, на острове Хонсю рядом с горами.


image

Немного глубокого обучения


Однажды я прочитал, что машинное обучение уже умеет делать всё, что и человек, но меньше чем за одну секунду. Распознать лицо, выбрать текст из изображения, повернуть, чтобы не врезаться в другую машину. Это заставило меня задуматься, а размышления привели к статье под названием Geolocation Estimation of Photos using a Hierarchical Model and Scene Classification, написанной Эриком Мюллером-Будаком, Кадером Пусту-Иреном и Ральфом Эвертом. В этой статье геолокализация рассматривается как задача классификации, в которой Земля подразделена на географические ячейки.

Она прогнозирует GPS-координаты фотографий.

image

Даже по фотографиям, которые сделаны в помещении! (Daily Challenge игры GeoGuessr часто засовывает игрока внутрь музеев).

Недавно авторы статьи выпустили реализацию на PyTorch и указали веса для обученной модели base(M, f*) с внутренней архитектурой ResNet50.

Я предположил, что обученная модель не очень хорошо будет соответствовать тем частям фотосфер, которые я смогу получить от GeoGuessr. В качестве данных обучения авторы использовали подмножество набора данных из 100 миллионов фотографий Yahoo Flickr Creative Commons (YFCC100M). В него вошли примерно пять миллионов изображений Flickr с геометками и неопределённых фотографий, например, снимков внутри помещений, еды и людей, местоположение которых сложно спрогнозировать.

Любопытно было то, что в наборе данных Im2GPS люди определяли местоположение изображения с точностью на уровне страны (в пределах 750 км) в 13,9% случаев, а Individual Scene Networks справлялись с этой задачей в 66,7% случаев!

image

Итак, возник вопрос: кто лучше в GeoGuessr, моя жена (потрясающий игрок) или машина?

Автоматизируем GeoGuessr с помощью Selenium


Для скрейпинга скриншотов из текущего внутриигрового местоположения я создал программу на Selenium, четыре раза выполняющую следующие действия:

  • Сохраняем скриншот canvas
  • Делаем шаг вперёд
  • Поворачиваем обзор примерно на 90 градусов


Количество повторов этих действий можно настроить через NUMBER_OF_SCREENSHOTS в показанном ниже коде.

'''Given a GeoGuessr map URL (e.g. https://www.geoguessr.com/game/5sXkq4e32OvHU4rf)take a number of screenshots each one step further down the road and rotated ~90 degrees.Usage: "python file_name.py https://www.geoguessr.com/game/5sXkq4e32OvHU4rf"'''from selenium import webdriverimport timeimport sysNUMBER_OF_SCREENSHOTS = 4geo_guessr_map = sys.argv[1]driver = webdriver.Chrome()driver.get(geo_guessr_map)# let JS etc. loadtime.sleep(2)def screenshot_canvas():    '''    Take a screenshot of the streetview canvas.    '''    with open(f'canvas_{int(time.time())}.png', 'xb') as f:        canvas = driver.find_element_by_tag_name('canvas')        f.write(canvas.screenshot_as_png)def rotate_canvas():    '''    Drag and click the <main> elem a few times to rotate us ~90 degrees.    '''    main = driver.find_element_by_tag_name('main')    for _ in range(0, 5):        action = webdriver.common.action_chains.ActionChains(driver)        action.move_to_element(main) \            .click_and_hold(main) \            .move_by_offset(118, 0) \            .release(main) \            .perform()def move_to_next_point():    '''    Click one of the next point arrows, doesn't matter which one    as long as it's the same one for a session of Selenium.    '''    next_point = driver.find_element_by_css_selector('[fill="black"]')    action = webdriver.common.action_chains.ActionChains(driver)    action.click(next_point).perform()for _ in range(0, NUMBER_OF_SCREENSHOTS):    screenshot_canvas()    move_to_next_point()    rotate_canvas()driver.close()

Скриншоты содержат и интерфейс GeoGuessr, но я не стал заниматься его удалением.

Приблизительное определение геолокации


Я перешёл к ветке PyTorch branch, скачал обученную модель и установил зависимости с помощью conda. Мне понравился README репозитория. Раздел requirements был достаточно понятным и на новом Ubuntu 20.04 у меня не возникло никаких проблем.

Для выяснения отношений между человеком и машиной я выбрал в GeoGuessr карту World. Отправив URL своей программе Selenium, я прогнал её для четырёх скриншотов, сделанных в GeoGuessr.

Ниже представлены сокращённые результаты работы машины.

python -m classification.inference --image_dir ../images/                                lat        lngcanvas_1616446493 hierarchy     44.002556  -72.988518canvas_1616446507 hierarchy     46.259434  -119.307884canvas_1616446485 hierarchy     40.592514  -111.940224canvas_1616446500 hierarchy     40.981506  -72.332581

Я показал те же четыре скриншота своей жене. Она предположила, что точка находится в Техасе. На самом деле место находилось в Пенсильвании. Машина сделала для каждого из четырёх скриншотов четыре различные догадки. Все догадки машины находились в США. Две достаточно близко друг к другу и две подальше.

image

Если взять усреднённое местоположение, то машина в этом раунде побеждает!

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

После написания этого поста я узнал о потрясающей предыдущей работе со сравнением результатов человека и машины на поле боя GeoGuessr. В статье PlaNet Photo Geolocation with Convolutional Neural Networks Тобиас Вейанд, Илья Костиков и Джеймс Филбин пытались определить местоположение фотографии всего по нескольким пикселям.

Чтобы выяснить, насколько PlaNet сравнима с интуицией человека, мы позволили ей соревноваться с десятью много путешествовавшими людьми в игре Geoguessr (www.geoguessr.com).

В сумме люди и PlaNet сыграли в 50 раундов. PlaNet выиграла 28 из 50 раундов с медианной погрешностью локализации в 1131,7 км, в то время как медианная погрешность людей составляла 2320,75 км.

Веб-демо


Авторы статьи Geolocation Estimation of Photos using a Hierarchical Model and Scene Classification создали довольно милый веб-инструмент. Я проверил его на одном из скриншотов Selenium.

Графическая демонстрация, в которой вы сможете посоревноваться против описанной в статье системы с глубоким обучением находится здесь: https://tibhannover.github.io/GeoEstimation/. Также мы создали многофункциональный веб-инструмент, поддерживающий загрузку и анализ пользовательских изображений: https://labs.tib.eu/geoestimation

image

Обучаемость GeoGuessr


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

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

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

Если в street view посмотреть вниз то можно увидеть часть машины, снимавшей текущую фотосферу. Например, в Кении спереди у машины есть чёрная труба. Основная часть Вьетнама была снята с мотоцикла, и часто можно увидеть шлем водителя. Страны часто снимаются одной машиной с уникальным цветом или антенной.

image

В других местах в небе есть место, где сшитая фотосфера выглядит разорванной (в основном в Сенегале, Черногории и Албании). В Африке за автомобилем Street View иногда едут машины сопровождения. Есть разные поколения камер, с разным разрешением, типами гало, цветопередачей и размытием в нижней части сферы. В нижнем углу фотосферы есть сообщение об авторстве, обычно это Google и указание года, но иногда бывает и имя фотографа.

Если использовать эти подсказки, то я не удивлюсь, что машина когда-нибудь в соревнованиях на время победит даже лучших пользователей GeoGuessr. На самом деле, я считаю, что было бы достаточно одного исследовательского гранта, чтобы мы стали играть в GeoGuessr существенно хуже, чем машины.



На правах рекламы


Закажите сервер и сразу начинайте работать! Создание VDS любой конфигурации в течение минуты. Эпичненько :)

Подробнее..

Из песочницы Полноценная игра, сделанная мною в обычной windows консоли

25.08.2020 16:11:08 | Автор: admin

Привет!


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

Откуда идея?


Я вдохновился идеей сделать что-то простое на первый взгляд, но в тоже время интересное в плане разработки. Мне в голову пришла мысль, сделать игру в консоли, это интересно в плане разработки, и посмотреть на это будет интересно даже просто со стороны, как например на эту игру.

Игровой движок


Итак, начнем с того как игра устроена в корне, и какова ее идея работы.

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

Вот таким кодом:

for line_words in OUTPUT_IMAGE:       for word in line_words:           print(word, end="")       print("\n", end="")

Здесь мы рисуем все символы из списка, и переходим на новую строку, чтоб нарисовать следующий список символов.

Вот так выглядит переменная, которая хранит списки символов:

image

Тут cразу мы получаем решение как нам выводить по X и Y объекты, мы теперь можем указывать:

X символ в списке
Y список в котором содержится X
Тем самым нарисовать на поле какой-нибудь символ. Это мы будем использовать при рисовании игровых объектов.

Можем попробовать нарисовать на поле мяч, подставив на место X и Y букву O.

Для этого напишем такой код:

import osOUTPUT_IMAGE = [        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        ]OUTPUT_IMAGE[4][6] = "O"os.system("cls||clear")for line_words in OUTPUT_IMAGE:       for word in line_words:           print(word, end="")       print("\n", end="")

image

И вот, мы нарисовали объект на нашем игровом поле. Правда координаты X и Y получились не классическими. Во первых, мы указываем сначала Y, потом X, что не совсем по классике, во вторых, координата Y должна увеличиваться чтоб поднять объект, у нас же наоборот, она должна уменьшатся.

График X и Y в игре:

image

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

Теперь мы можем попробовать перемещать наш объект по игровому полю, т.е. создавать движение.

Нам понадобится очищать консоль, для того чтобы стирать старую картинку игрового поля.
Это мы сделаем командой:

os.system("cls||clear")

Также, нам понадобится переопределять переменную OUTPUT_IMAGE, для того чтобы очищать все ранее нарисованные в игровом поле объекты.

Также нам все это нужно будет поместить в while True.

Добавим в while True функцию time.sleep(1), для того чтобы ограничить FPS.

И вот, код нарисовался на глазах:

from time import sleepfrom os import systemOUTPUT_IMAGE = [        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],        ]x = 0y = 0while True:      sleep(1)      system("cls||clear")      OUTPUT_IMAGE[y][x] = "O"      for line_words in OUTPUT_IMAGE:             for word in line_words:                 print(word, end="")             print("\n", end="")      y += 1      x += 1      OUTPUT_IMAGE = [            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            ]

image

Теперь у нас есть возможность распределять объекты по полю.

Правда эти объекты слишком примитивы, и надо бы научится рисовать сложные объекты по типу игроков, домов, еды

Для того чтобы нарисовать сложный объект, нам нужно понять и придумать, как нарисовать объект указав лишь один раз его X и Y.

Для этого нам понадобится функция, которая принимает картинку (символы), X, Y;

Так и сделаем:

def SetImage(image: str, x: int, y: int):    pass

Теперь нам нужно ее реализовать. Для этого нужно решить, как нарисовать изображение, которое растягивается по оси X и Y, я придумал так:
рисовать объект разделяя его на символы, и как только встретится символ "\n", прибавить ось Y.

Ось Y как мы уже говорили неправильная, перевернутая наоборот, поэтому к ней мы прибавляем чтобы опустить объект.

Пример изображения который рисуется по моему принципу:

image = " O\n'|'\n |"#игрок

Теперь давайте это опишем в нашей функции:

def SetImage(x: int, y: int, image: str):  x_start = x  x = x  y = y  for word in image:      if word == "\n":          x = x_start          y += 1      else:          x += 1          try:            OUTPUT_IMAGE[y][x] = word          except IndexError:              break

Добавим try: except() для того чтобы небыло ошибок если объект имеет X и Y слишком мальенькие или слишком большие.

x_start Это X, с которого нужно начинать рисовать при увеличении Y (при символе "\n")

Теперь мы можем использовать нашу функцию, падать в нее X и Y, и картинку которую нужно рисовать:

код
from time import sleepfrom os import systemOUTPUT_IMAGE = [      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      ]def SetImage(x: int, y: int, image: str):  x_start = x  x = x  y = y  for word in image:      if word == "\n":          x = x_start          y += 1      else:          x += 1          try:            OUTPUT_IMAGE[y][x] = word          except IndexError:              breakwhile True:      sleep(1)      system("cls||clear")      SetImage(x=3,y=4,image=" O\n'|'\n |")      for line_words in OUTPUT_IMAGE:             for word in line_words:                 print(word, end="")             print("\n", end="")      OUTPUT_IMAGE = [            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            ]


И вот что у нас получилось:

image

абсолютно также как и шарик который мы рисовали, его можно двигать по оси X и Y.

код
from time import sleepfrom os import systemOUTPUT_IMAGE = [      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      ]px = 0py = 0def SetImage(x: int, y: int, image: str):  x_start = x  x = x  y = y  for word in image:      if word == "\n":          x = x_start          y += 1      else:          x += 1          try:            OUTPUT_IMAGE[y][x] = word          except IndexError:              breakwhile True:      sleep(1)      system("cls||clear")      SetImage(x=px,y=py,image=" O\n'|'\n |")      for line_words in OUTPUT_IMAGE:             for word in line_words:                 print(word, end="")             print("\n", end="")      px += 1      py += 1      OUTPUT_IMAGE = [            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            ]


image

И вот, у нас уже двигает игрок по карте.

Тут мы сделали уже многое, уже есть игрок, уже есть карта, и казалось бы, уже можно сделать игру, но нет. Нам нужна функция высчета столкновений объектов, ведь какая это игра без взаимодействий объектов. Поэтому приступим.

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

Итак, функцию я решил сделать по такой логике:

X хитбокс объекта по X ширине, это самое больше количество символов между знаками "\n" в картинке
Y хитбокс по Y это число символов "\n" в картинке

По этой логике не сложно сделать функцию, которая принимает картинку, считает у нее все символы между "\n", и выбирает из этого самое больше число символов получилась широта.
И если посчитать символы "\n", как я уже написал получится высота.

Функция получилась такой:

def GetSizeObject(img: str):    w = 0    weights = []    h = [word for word in img if word == "\n"]    for word in img:      if word == "\n":          weights.append(w)          w = 0      else:          w += 1      try:          return {"w": max(weights), "h":len(h)}      except ValueError:            return {"w": 0, "h":0}

Зачем здесь ValueError except?
Он здесь чтобы предотвратить ошибку при запуске игры.

Итак, давайте нарисуем нашего игрока, и вычислил его ширину и длину.

код с рисовкой и вычислением широты и высоты
from time import sleepfrom os import systemOUTPUT_IMAGE = [      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      ]px = 3py = 3def SetImage(x: int, y: int, image: str):    global OUTPUT_IMAGE          x_start = x    x = x    y = y    for word in image:        if word == "\n":            x = x_start            y += 1        else:            x += 1            try:              OUTPUT_IMAGE[y][x] = word            except IndexError:                breakdef GetSizeObject(img: str):    w = 0    weights = []    h = [word for word in img if word == "\n"]    h.append(1)    for word in img:        if word == "\n":            weights.append(w)            w = 0        else:            w += 1    try:        return {"w": max(weights), "h":len(h)}    except ValueError:        return {"w": 0, "h":0}player_image = " O\n'|'\n |"def draw():      global OUTPUT_IMAGE      sleep(1)      system("cls||clear")      for line_words in OUTPUT_IMAGE:             for word in line_words:                 print(word, end="")             print("\n", end="")      OUTPUT_IMAGE = [            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            ]while True:    SetImage(x=px,y=py,image=player_image)    print(GetSizeObject(img=player_image))    draw()


Ура! у нас есть функция вычисления широты и высоты, теперь нам предстоит сделать функцию вычисления хитбокса и столкновений объектов.

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

Для простоты понимания я нарисовал хитбоксы, Т.Е. квадраты:

image

Логика на словах
Для вычисления мы подаем

x X первого объекта
y Y первого объекта
h Высота первого объекта
w Широта первого объекта
x2 X второго объекта
y2 Y второго объекта
h2 Высота второго объекта
w2 Широта второго объекта

И смотрим:

если
y больше y2 - h2 + h и y - h меньше чем y2 + h2 - h
или же
y2 больше y - h + h2 и y2 - h2 меньше чем y + h - h2
Зачем проверять 2 раза?
Мы сделали проверку 2 раза, просто из-за того чтобы посмотреть на столкновение/не столкновение с разных объектов.

Объекты соприкасаются по оси Y

Дальше смотри соприкосновение по оси X, она такое же что и по оси Y, но вместо y x, а вместо h w.

если:

x больше x2 - w2 + w и x - w меньше чем x2 + w2 - w

или же

x2 больше x - w + w2 и x2 - w2 меньше чем x + w - w2

объекты соприкасаются по оси X

Логика в коде
Логика такая же как и на словах, только в функции:

def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int):    if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2):        if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2):            return True    return False

Функция возвращает True если объекты соприкасаются, и False если нет.

Я нарисовал дополнительно куб на нашем игровом поле, для того чтобы игроку было с кем сталкиваться.

И попробовал как работает функция высчета столкновения.

Вот игрок соприкасается и кубом:

image

А вот нет соприкасаются:

image

Код соприкосновения
Это полный код соприкосновения/не соприкосновения:

from time import sleepfrom os import systemOUTPUT_IMAGE = [      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],      ]def SetImage(x: int, y: int, image: str):    global OUTPUT_IMAGE          x_start = x    x = x    y = y    for word in image:        if word == "\n":            x = x_start            y += 1        else:            x += 1            try:              OUTPUT_IMAGE[y][x] = word            except IndexError:                breakdef GetSizeObject(img: str):    w = 0    weights = []    h = [word for word in img if word == "\n"]    h.append(1)    for word in img:        if word == "\n":            weights.append(w)            w = 0        else:            w += 1    try:        return {"w": max(weights), "h":len(h)}    except ValueError:        return {"w": 0, "h":0}def IsClash(x: int, y: int, h: int, w: int,x2: int, y2: int, h2: int, w2: int):    if (y >= y2 - h2 + h and y - h <= y2 + h2 - h) or (y2 >= y - h + h2 and y2 - h2 <= y + h - h2):        if (x >= x2 - w2 + w and x - w <= x2 + w2 - w) or (x2 >= x - w + w2 and x2 - w2 <= x + w - w2):            return True    return Falseplayer_image = " O\n'|'\n |"cube_image = "____\n|  |\n----"cx = 5#cy = 4  #Меняйте эти координаты для того чтобы менять позиции игрока и кубаpx = 10  #py = 3#def draw():      global OUTPUT_IMAGE      sleep(1)      system("cls||clear")      for line_words in OUTPUT_IMAGE:             for word in line_words:                 print(word, end="")             print("\n", end="")      OUTPUT_IMAGE = [            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".",],            ]while True:    SetImage(x=px,y=py,image=player_image)    SetImage(x=cx,y=cy,image=cube_image)    print("is clash: ",IsClash(      x=px,      x2=cx,      y=py,      y2=cy,      h=GetSizeObject(img=player_image)["h"],      h2=GetSizeObject(img=cube_image)["h"],      w=GetSizeObject(img=player_image)["w"],      w2=GetSizeObject(img=cube_image)["w"],      ))    draw()


Теперь у нас все стартовые функции для игры, собственно их основе я писал свою игру.

Игра


Идея игры в такая:

Есть игрок, вокруг появляется еда, которую он вынужден собрать чтоб не умереть. В игре также присутствуют функции: поднять еду, положить в инвентарь, съесть ее из инвентаря, положить на пол предмет из инвентаря

Я начал с того что сделал игровой цикл в 3 строчки, это просто While True:

from time import sleepwhile True:    sleep(0.1)

Дальше я посчитал нужным, создать класс, в котором будут хранится все функции будующий объектов. Поэтому создал файл main.py и папку lib, в которую поместил файл lib.py в котором был класс игры. Т.Е. файлы игры выглядели так:

+----game|    + -- |    | -- main.py|    \ --lib|         +--lib.py -> class Game()|         \|+---

В дальнейшем я работал в основном с классом Game(), в main.py просто вызывал его, создавал стартовые объекты, запускал игру.

В классе game сделал функцию run(), которая заупускает игровой цикл. Также сделал функцию draw_all(), она стирает все прошлые объекты, рисует новые, и печатает на игровое поле.

И так выглядел класс:

from time import sleepclass Game():    def __init__(self):        self.OUTPUT_IMAGE = []  # здесь игровое поле    def draw_all(self):        for line_words in self.OUTPUT_IMAGE:            for word in line_words:                print(word, end="")            print("\n", end="")    def run(self):        while True:            self.draw_all()            sleep(0.1)

Добавил все основные функции, по типу set_image(), size_object(), is_clash(), и все те которые являются игровым движком, и которые я описал выше.

Сделал новую функцию create_object() и переменную self.OBJECTS, функцию create_object()я использую для создания объектов, она принимает параметры img, name, x, y, up, rigid, data.

img картинка объекта
name имя объекта (дом, трава, житель, еда и.т.п.)
x X объекта
y Y объекта
up если этот параметр True, то объект рисуется над игроком, иначе игрок его перекрывает собой
rigid твердость, игрок не может пройти через этот объект (еще не реализовано)
data личные данные объекта, его личные характеристики

create_object()
Эта функцию которая сейчас у меня в игре:

def CreateObject(self,x: int, y: int, img: str, name: str = None, up: bool = False, rigid: bool = False, data: dict = {}):    size_object = self.GetSizeObject(img=img)    self.OBJECTS.append(        {"name": name,         "x": x,         "y": y,         "up": up,         "rigid": rigid,         "h":size_object["h"],         "w":size_object["w"],         "id":uuid4().hex,         "data":data,         "img": img}    )


На тот момент я уже добавил игрока, дом, траву, и жителя.

И решил использовать тот самый параметр в объекте up, использовать его в объекте Home, Т.Е. чтоб дом закрывал собой игрока. Для этого я сделал функцию CheckAll(), циклом for проходился по всем объектам, и рисовал их на исходящей картинке, Т.Е. использовать функцию SetImage(x: int, y: int, img:str), подавая в нее X и Y объекта, и картинку.

Тем самым рисовал объекты которые мог закрыть собой игрок. В этом же цикле я объявил списокup_of_payer_objects, и если у объекта стоял up=True, то я добавлял его в список, не рисуя его на поле. После рисовал самого игрока, и только этого я проходил циклом for по объектам в up_of_payer_objects, рисуя их, тем самым они были над игроком.

def CheckAll(self):    up_of_payer_objects = []    for object_now in range(len(self.OBJECTS)):        if object_now["up"]:            up_of_payer_objects.append(object_now)            continue        self.SetImage(x=object_now["x"],y=object_now["y"],image=object_now["img"])

Дальше я занялся движением игрока. Для этого я создал его как отдельный объект, который не находится в списке self.OBJECTS, но который хранится в переменной self.PLAYER.

Все его параметры, по типу X, Y, img, и.т.п. получить можно с помощью ключей, проще говоря это словарь (dict). С таким игроком и объектами уже можно было работать, двигать, вычислить столкновения. Я начал с движения.
Начал создавать движение с того что сделал функцию CheckKeysObjects(), которая отвечает за отслеживание нажатия клавиш, и которую я вызываю в функции CheckAll() в самом начале

def CheckAll(self):    self.CheckKeysObjects()    ....

Для отслеживания нажатий на клавиши я использовал библиотеку keyboard, и 4 переменные:

self.WALK_LEFT_PLAYER
self.WALK_RIGHT_PLAYER
self.WALK_UP_PLAYER
self.WALK_DOWN_PLAYER

И все оказалось просто, отслеживаем клавиши, и если нажата допустим d, то мы переменную self.WALK_RIGHT_PLAYER делаем True.

В самом начале функции объявляем все переменные в False, для того чтобы сбросить все прошлые результаты, а-то игрок не остановится.

CheckKeysObjects()
def CheckKeysObjects(self):    #делаю все переменные в False, чтоб сбросить прошлые результаты    self.WALK_LEFT_PLAYER = False    self.WALK_RIGHT_PLAYER = False    self.WALK_UP_PLAYER = False    self.WALK_DOWN_PLAYER = False    #а тут уже проверяю нажатия    if keyboard.is_pressed("a"):        self.WALK_LEFT_PLAYER = True    elif keyboard.is_pressed("d"):        self.WALK_RIGHT_PLAYER = True    if keyboard.is_pressed("w"):        self.WALK_UP_PLAYER = True    elif keyboard.is_pressed("s"):        self.WALK_DOWN_PLAYER = True


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

Если какая-то в True, узнаем какая, и двигаем предмет в противоположную сторону.

Получившийся код движения
def CheckAll(self):    self.CheckKeysObjects()  # check moves    up_of_payer_objects = []    for object_now in range(len(self.OBJECTS)):        self.PLAYER["img"] = self.PLAYER["image_normal"]        if self.WALK_LEFT_PLAYER:            self.OBJECTS[object_now]["x"] += 1        elif self.WALK_RIGHT_PLAYER:            self.OBJECTS[object_now]["x"] -= 1        if self.WALK_UP_PLAYER:            self.OBJECTS[object_now]["y"] += 1        elif self.WALK_DOWN_PLAYER:            self.OBJECTS[object_now]["y"] -= 1


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

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

Для отсчета времени спавна еды, я использовал простой time.sleep(), и библиотеку threading для того чтобы запустить 2 функции одновременно, спавн еды и основной игровой цикл. Функция спавна еды SpawnEat() это просто функция которая при запуске генерирует на случайных местах еду, вызывая для каждой единицы еды функцию CreateObject().

Также, как только я сделал функцию спавна еды, я сделал переменную у игрока self.PLAYER["hungry"], это его голод, в самом начале он равен 100 ед., его я буду уменьшать если игрок ходит и тратит энегрию (типа энергию, ее в игре нет) или увеличивать если игрок что-то съел.

Также я сделал функцию MinimizeHungry(), она вызывается каждые 5 секунд, и просто отнимает у игрока 2 единицы голода. Это я сделал для того чтобы игроку пришлось двигаться, а не стоять на месте.

И наконец в функции Eat(), эта функция которая вызывается в отдельном потоке от игрового цикла. Она проверяет не слишком ли много еды на карте, если еды больше 10 ед. то НЕ вызывает функцию SpawnEat(), если меньше 10 ед. то вызывает SpawnEat().

Вот какой она получилась:

Eat()
def Eat(self):    while True:        sleep(4)        if len([i for i in self.OBJECTS if i["name"] == "meat"]) < 10:            self.SpawnEat()        sleep(1)        self.MinimizeHungry()


Функция Start(), для запуска основного цикла:

Start()
def Start(self):    while True:          self.CheckAll()        self.DrawAll()        sleep(0.01)


И функция run(), которая запускает всю игру.

run()
def run(self):    proc1 = threading.Thread(target=self.Start)    proc1.start()    proc2 = threading.Thread(target=self.Eat)    proc2.start()


Сам процесс поедания, я реализовал просто в функции CheckAll() и CheckKeysObjects(). В CheckKeysObjects() я проверял не нажал ли игрок на кнопку E. Если нажал, то ставил переменную self.PRESS_E в True.

В цикле CheckAll(), проверял, не еда ли нынешний объект в цикле for, если еда то проверял не сталкивается ли с ним игрок, если сталкивается то проверял переменную self.PRESS_E, и если она в True то тогда просто удалял объект, и увеличивал голод, Т.Е. переменную self.PLAYER["hungry"].

Вот так это в коде
for object_now in range(len(self.OBJECTS)):    ....    if self.OBJECTS[object_now]["name"] == "meat":        items_objects.append(object_now)        is_clash = self.IsClash(            x=self.OBJECTS[object_now]["x"],            y=self.OBJECTS[object_now]["y"],            h=self.OBJECTS[object_now]["h"],            w=self.OBJECTS[object_now]["w"],            x2=self.PLAYER["x"],            y2=self.PLAYER["y"],            h2=self.PLAYER["h"],            w2=self.PLAYER["w"],        )        if is_clash:            if self.PRESS_E:                try:                    self.PLAYER["hungry"] += self.HUNGRUY_ADD                    del self.OBJECTS[object_now]                    break                except IndexError:                    pass


Скажу наперед, это все мне нужно будет переписовать, когда я буду делать инветнарь

Делаю инвентарь


Итак, настало сложное, нам нужно сделать инвентарь.

Сложность в том что все предметы нужно будет отображать, хранить историю, удалять, ставить на пол объекты.

Я начал с того что добавил игроку новый ключ, это был self.PLAYER["inventory"], там хранятся 4 яцчейки, вот в таком виде:

"inventory":{    "0":{"status":"space","name":"#0", "minimize_image":"#0"},    "1":{"status":"space","name":"#1", "minimize_image":"#1"},    "2":{"status":"space","name":"#2", "minimize_image":"#2"},    "3":{"status":"space","name":"#3", "minimize_image":"#3"},}

цифры просто номера ячеек.

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

name хранит в себе имя предмета, оно будет использовано когда игрок будет класть предмет.

minimize_image эта уменьшенная картинка предмета которая изображается в инвентаре игрока.

После, сделал новые проверки в нашем CheckKeysObjects(), при нажатии на X предмет будет бросаться на землю, и также при нажатии на кнопку E будет вызываться функция self.UseEat(), которую мы сейчас будем разбирать.

Итак, функция self.UseEat() представляет из себя проход по всем ячейкам инвентаря, в поисках еды, и если еда найдена, то она удаляется из инвентаря, и к голоду добавляется 10 единиц. Для удаление предмета из инвентаря я сделал функцию self.DestroyItem(), в которую подается индекс ячейки, и вся ячейкой просто становится по дефолту пустой и без ничего.

self.DestroyItem()
def DestroyItem(self,index_item: str):    item = self.PLAYER["inventory"][index_item]    self.PLAYER["inventory"][index_item] = self.PLAYER["default_inventory_item"](index_item)    self.PLAYER["inventory_must_update"] = True    return item


self.CheckKeysObjects()
def CheckKeysObjects(self):    self.WALK_LEFT_PLAYER = False    self.WALK_RIGHT_PLAYER = False    self.WALK_UP_PLAYER = False    self.WALK_DOWN_PLAYER = False    if key("a"):        self.WALK_LEFT_PLAYER = True    elif key("d"):        self.WALK_RIGHT_PLAYER = True    if key("w"):        self.WALK_UP_PLAYER = True    elif key("s"):        self.WALK_DOWN_PLAYER = True    if key("f"):        self.KEY_F = True    else:        self.KEY_F= False    if key("e"):        self.UseEat()


self.UseEat()
def UseEat(self):    for inventory_item in range(len(self.PLAYER["inventory"])):        if self.PLAYER["inventory"][str(inventory_item)]["name"] == "meat":            if self.PLAYER["hungry"] + self.ADD_HUNGRY_COUNT < 100.0:                self.PLAYER["hungry"] += self.ADD_HUNGRY_COUNT                self.DestroyItem(index_item=str(inventory_item))


Дальше функция бросания предмета на землю.

Там впрочем ничего сложного, при нажатии на X вызывается функция self.QuitItem(), в ней проходит цикл for по всем ячейкам инвентаря, и если ключ ["status"] не ровняется "space", то эту ячейку удаляем с помощью ранее рассмотренной функции self.DestroyItem(), и создаем объект на основе того что был в ячейке, X и Y ставит игрока, как бы он бросил его возле себя.

self.Quititem()
def QuitItem(self):    for inventory_item in range(len(self.PLAYER["inventory"])):        if self.PLAYER["inventory"][str(inventory_item)]["status"] != "space":            self.CreateObject(                img=self.PLAYER["inventory"][str(inventory_item)]["img"],                x=self.PLAYER["x"],                y=self.PLAYER["y"],                name=self.PLAYER["inventory"][str(inventory_item)]["name"],                data=self.PLAYER["inventory"][str(inventory_item)]["data"],            )            self.DestroyItem(index_item=str(inventory_item))            break


И впрочем все, многие вещи я не говорил как сделал, Т.К. они являлись не основной частью игры, хоть и интересной. Например сообщения о возможности поднятие предмета или нет (когда инвентарь полон), о том что я добавил анимацию ходьбы, о том что сделал отдельную библиотеку картинок, и тому прочих вещах.

На этом все?


Нет, я собираюсь в игру добавить нейросеть, используя библиотеку который я писал на Python,
Собираюсь сделать взаимодействие игрока с NPC оснащенными нейросетью,
небольшой, но какой-нибудь сюжет, и также какие-то припасы для игрока типа брони, еды. предметов, возможность строить блоками.

Попробовать игру


Ее можно свободно скачать с моего GitHub, для запуска потребуется лишь Python3, и библиотека keyboard. Запускать нужно файл main.py.

Игра
Подробнее..

Пишем игру Гонки на бумаге, C WPF

31.12.2020 18:20:33 | Автор: admin

Предистория

Дело было в начале 90-х, компьютера не было, но было желание поиграть в гонки ) Показал мне друг как можно на тетрадном листе бумаги в клеточку играть в гонки. А еще говорят, что есть настольная игра с такими правилами. И что чуть ли не все играли в эту игру в университете за парами.

Правила игры

Можно прочитать правила в Википедии, прочитав это и ничего не зная, я бы не понял ). Главное правило игры - правила перемещения машинки. Правило 2 - не можешь сделать ход попав в дорогу - ты проиграл.

Ход 1: машинка стоит на месте и может ехать в точки, которые расположены вокруг той точки в которую направлен вектор движения машинки. В начале игры вектор нулевой, никуда не направлен, значит мы можем пойти в точки вокруг машинки. Делаем ход в точку над машинкой.

Ход 2: Машинка передвигается на одну клетку вертикально и ноль клеток горизонтально. Это и есть текущая скорость машинки (1 по вертикали и 0 по горизонтали). Вектор скорости направлен на клетку сверху от машинки. Вокруг этой точки машинка может выбрать точки для выполнения хода. Сделаем ход в левую-верхнюю точку.

Ход 3: Машинка передвигается на две клетки вертикально и одну горизонтально. Вектор движения машинки соответственно изменяется.

Ход 4: Машинка передвигается на 3 клетки вертикально.

Теперь машинка движется со скоростью 3 клетки вверх и 0 клеток влево/вправо. Машинка имеет скорость и не может за один ход полностью остановиться или сделать резкий поворот в сторону. В данном случае машинка может:

  1. Ускориться если выбрать верхние (дальние от машинки) узлы. Если ход в верхнюю-левую точку, то скорость будет 4 вверх и 1 влево. Если ход в верхнюю-правую точку, то скорость будет 4 вверх и 1 вправо.

  2. Не изменять скорость если выбрать средний ряд. В этом случае появляется возможность поворота в сторону под более большим углом.

  3. Понизить скорость выбрав нижний (ближайший к машинке) ряд. Стоит выбирать при приближению к резкому повороту.

План разработки

  1. Генератор случайной трассы. Трасса должна уметь поворачивать в любом направлении на любое число градусов. Элементы трассы должны быть произвольной длины и ширины.

  2. Положение пользователя на игровой карте должно быть показано машинкой. Машинка должна уметь поворачиваться в сторону движения. Игровая карта может быть любого размера.

  3. На игровой карте нужно различать следующие типы полей:
    3.1 поле, куда машинка может сделать безопасный ход
    3.2 поле, куда машинка может сделать ход, но это будет выход за трассу
    3.3 поле, принадлежащее дороге
    3.4 поле, не принадлежащее дороге
    3.5 текущее поле с машинкой

  4. Показывать отрезки пройденного пути на игровой карте

  5. Возможность начать новую игру сначала

  6. Отображение текущей скорости

  7. Возможность изменения настроек программы через файл конфигурации. Список необходимых значений в файле конфигурации:
    7.1 Ширина фрагмента дороги
    7.2 Количество фрагментов дороги для генерации новой трассы
    7.3 Максимальный угол поворота дороги влево
    7.4 Максимальный угол поворота дороги направо
    7.5 Минимальная длина участка дороги
    7.6 Максимальная длина участка дороги
    7.7 Положение машинки на игровой карте

  8. Сделать генерацию пейзажа вокруг дороги для приятного юзабилити

  9. Показывать дорожные знаки предупреждающие об опасном повороте

Подготовка

Для разработки программы выбран язык программирования C#. Целевая рабочая среда .NET 5.0. Графическая среда WPF. Среда разработки Visual Studio 2019.

Генератор случайной трассы

Самы простой способ нарисовать трассу - воспользоваться фигурой Polyline. Но, встал вопрос, как рассчитывать нахождение принадлежности точки этой фигуре и от этого варианта пришлось отказаться. Самый надежный вариант - это представить трассу как массив прямоугольников. Для скругления поворотов трассы нужно дополнительно строить окружности в местах пересечения элементов трассы. Диаметр окружности - это ширина элемента дороги.

Для каждого сгенерированного элемента трассы нужно хранить:

  1. Точку начала (X и Y)

  2. Точку конца (X и Y)

  3. Ширину элемента трассы (в данной реализации ширина трассы одинаковая для всех элементов, задел на будущее)

  4. Длину элемента трассы

  5. Угол наклона элемента трассы к оси oX

Положение пользователя на игровой карте

Положение пользователя на игровой карте отображается в виде машинки. Чтобы игровая карта могла быть любого размера, нужно чтобы при выполнении хода пользователя двигалась карта относительно машинки, а не машинка относительно карты. Это значит, что машинка будет всегда в одной точке на игровой карте, а при выполнении хода нужно запоминать отклонение машинки от ее начальной точки и перерисовывать элементы трассы учитывая данное отклонение.

/// <summary>/// Текущий сдвиг карты по осям/// </summary>int _deltaX, _deltaY = 0;

Изображение машинки сделано в виде машины с четко выделенным задом и передом. Чтобы было понятно пользователю текущее направление движения машинки нужно поворачивать машинку в сторону направления движения.

Каждый выполненный ход сохраняем в списке _pathList - список отрезков пройденного пути.

/// <summary>/// Список отрезков пройденного пути/// </summary>List<PathElement> _pathList = new List<PathElement>();

Отрезок пройденного пути должен хранить следующие данные:

  1. Точка начала отрезка (X и Y)

  2. Точка конца отрезка (X и Y)

  3. Смещение машинки относительно карты в момент выполнения хода

Зная точку начала и конца отрезка можно вычислить угол наклона отрезка к оси oX.
AC - изменение координаты X от начала до конца отрезка
BC - изменение координаты Y от начала до конца отрезка

\tan{(\angle ABC)} = \frac{AC}{BC}\angle ABC = \arctan{(\frac{AC}{BC})} * \frac{180}{\pi}
/// <summary>/// Текущий угол наклона пути/// </summary>public double Angle{     get     {          if (ToY == FromY && ToX > FromX) return 90;          if (ToY == FromY && ToX < FromX) return -90;          if (ToX == FromX && ToY > FromY) return 180;          if (ToX == FromX && ToY < FromY) return 0;          if (ToY <= FromY && ToX >= FromX) return -Math.Atan((ToX - FromX) / (ToY - FromY)) * 180 / Math.PI;          if (ToY <= FromY && ToX <= FromX) return - Math.Atan((ToX - FromX) / (ToY - FromY)) * 180 / Math.PI;          if (ToY >= FromY && ToX <= FromX) return Math.Atan((ToY - FromY) / (ToX - FromX)) * 180 / Math.PI - 90;          if (ToY >= FromY && ToX >= FromX) return Math.Atan((ToY - FromY) / (ToX - FromX)) * 180 / Math.PI + 90;          return 0;     }}

Типы полей игровой карты

Для выполнения хода на игровой карте расположены узлы - кнопки (Button) на расстоянии 20 пикселей друг от друга, 39 рядов по 39 кнопок. Таким образом достигается функционал выполнения нового хода.

Было сказано, что при выполнении хода машинка остается на месте, а элементы карты меняют свое положение. Это значит, что при каждом выполнении хода необходимо проверять все узлы карты чтобы понять куда попадает в данный момент данный узел:

  1. поле, куда машинка может сделать безопасный ход

  2. поле, куда машинка может сделать ход, но это будет выход за трассу

  3. поле, принадлежащее дороге

  4. поле, не принадлежащее дороге

  5. текущее поле с машинкой


При проверке нужно найти поля в которые машинка может сделать ход. Для этого учитывается текущая скорость по оси X и по оси Y.

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

(X \text{-} X_0)^2 = (Y \text{-} Y_0)^2 <= R^2

Проверка принадлежности точки прямоугольнику немного сложнее:
Из точки C (это точка, которую проверяем на принадлежность прямоугольнику) опустим перпендикуляр на отрезок AB (точки A и B - точки начала и конца элемента дороги соответственно). Данный перпендикуляр - это высота треугольника. Поиск факта принадлежности точки прямоугольнику сводится к проверке:

h <= \frac{\text{Ширина дороги}}{2}


Существует 3 различные ситуации где может находиться точка по отношению к прямоугольнику:

  1. h > (ширина элемента дороги / 2) - точка не принадлежит прямоугольнику

  2. h <= (ширина элемента дороги / 2) - точка принадлежит прямоугольнику если ABC < 90 и CAB < 90

  3. высота h опускается не на отрезок AB, а на его продолжение. Хотя и длина высоты удовлетворяет нашей формуле, но если ABC > 90 или CAB > 90, то точка не принадлежит прямоугольнику.

Формула нахождения площади треугольника зная координаты его вершин:

S(ABC) = \frac{|(X_A \text{-} X_C)*(Y_B \text{-} Y_C) \text{-} (X_B \text{-} X_C)*(Y_A \text{-} Y_C)|}{2}

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

S(ABC) = \frac{h * AB}{2}

Данных двух формул достаточно для расчета высоты треугольника (а еще Вы могли не заморачиваться с площадью и рассчитать проще используя:

\sin( \angle ABC) = \frac{h}{BC}

но я это понял на момент написания статьи)


Формула нахождения длины отрезка по его координатам:

AB = \sqrt{(X_A \text{-} X_B)^2 + (Y_A \text{-} Y_B)^2}AC = \sqrt{(X_A \text{-} X_C)^2 + (Y_A \text{-} Y_C)^2}BC = \sqrt{(X_B \text{-} X_C)^2 + (Y_B \text{-} Y_C)^2}

Формула нахождения углов треугольника зная длины сторон треугольника:

\angle ABC = \arccos{(\frac{AB^2 + BC^2 - AC^2}{2*AB*BC})} * \frac{180}{\pi}\angle CAB = \arccos{(\frac{AB^2 + AC^2 - BC^2}{2*AB*AC})} * \frac{180}{\pi}

Отрезки пройденного пути

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

  1. Точка начала отрезка (X и Y)

  2. Точка конца отрезка (X и Y)

  3. Смещение машинки относительно карты в момент выполнения хода

  4. Угол наклона к оси oX рассчитывается автоматически

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

Возможность начать новую игру сначала

Для реализации данной фичи сделаем в главном меню программы элемент типа MenuItem, реализуем событие мыши Click, пропишем очиску всех элементов игровой карты, очистку сохраненных элементов списков пройденного пути, элементов дороги, обнуление переменных сдвига карты и текущей скорости машинки.

Отображение текущей скорости

Физика прототипа игры такова, что мы имеем две скорости: скорость по оси oX, скорость по оси oY.

Под текущей скоростью машинки подразумеваем большее значение между двух скоростей по осям.

Возможность изменения настроек программы через файл конфигурации

Чтобы изменять переменные в игре имеет смысл вынести значения данных переменных в конфигурационный файл:

  1. Ширина фрагмента дороги

  2. Количество фрагментов дороги для генерации новой трассы

  3. Максимальный угол поворота дороги влево

  4. Максимальный угол поворота дороги направо

  5. Минимальная длина участка дороги

  6. Максимальная длина участка дороги

  7. Положение машинки на игровой карте

{  "RoadWidth": "100",  "RoadElementsCount": "20",  "MinAngle": "-60",  "MaxAngle": "60",  "MinRoadLength": "100",  "MaxRoadLength": "200",  "UserPosition": {    "X": "400",    "Y": "400"  }}

В зависимости от данных настроек игровое поле выглядит по-разному:

Генерация пейзажа, улучшение GUI

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

  1. Заполнить игровое поле "Землей" - изображение, заполняющее все поле

  2. В момент выполнения хода мы каждый раз проверяем тип каждого узла игровой карты. В данный метод кода можно добавить генерацию "Елок" в случае если поле данного узла не принадлежит дороге. Чтобы елок не было слишком много, ограничим шанс появления елки в 20%.

    var random = new Random();var elkaChance = random.Next(1, 101);if (elkaChance < 20){// рисуем елку}
    
  3. Чтобы дорожное полотно было похоже на дорожное полотно можно нанести прерывистую разметку проходящую посередине элементов дорог, как раз соединяя точку начала и конца каждого элемента дороги. Чтобы данная разметка была более естесственной, воспользуемся фигурой Polyline передав фигуре коллекцию точек элементов дороги.

var polyLinePointCollection = new PointCollection();foreach (var roadElement in _roadElements){    if (polyLinePointCollection.Count == 0) polyLinePointCollection.Add(new Point(roadElement.StartPoint.X + 5 + _deltaX, roadElement.StartPoint.Y + _deltaY));    polyLinePointCollection.Add(new Point(roadElement.EndPoint.X + 5 + _deltaX, roadElement.EndPoint.Y + _deltaY));}var polyLine = new Polyline(){    Points = polyLinePointCollection,    Stroke = Brushes.White,    StrokeDashArray = new DoubleCollection() { 6, 4 },    StrokeThickness = 2};

Дорожные знаки опасного поворота

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

Angle_\text{curr} \text{-} Angle_\text{prev} > 70

Бывают случаи когда один угол +178, а второй -178. Реальная разница между данными углами всего 4. Для решения данной задачи нужно добавить условие, что угол будет опасным при разнице углов меньше 290. Формула изменится к данному виду:

70 \leq Angle_\text{curr} \text{-} Angle_\text{prev} \leq 290

Код программы

В моем GitHub

Подробнее..
Категории: C , Net , Разработка игр , Гонки , Игра

Math Invasion. Мой долгострой

23.11.2020 16:15:20 | Автор: admin
Привет, народ!

Расскажу я вам свою историю о том, как я разрабатывал игру. Идея о том, чтобы скрестить shoot em up с математикой, мне пришла ещё в студенческие годы (Где-то в 2008 году).

Собственно, уже тогда я ещё делал попытки воплотить идею в жизнь. Для реализации своих целей, тогда я использовал язык программирования Delphi и, только осваиваемую мною, библиотеку GLScene. Как результат, у меня получилась игра видео которой Вы можете наблюдать ниже. Кстати, саму игру Вы можете скачать по данной ссылке. Запускается она через файл Project1.exe который находится в папке TestFireCursorProject19

С чего начинался Math Invasion


Как Вы заметили из названия папки, игра была не доделанной. Это тормозило её публичную демонстрацию. Т.е. я стыдился показать её на людях. И отложил доработку до лучших времён.

Лучшие времена не пришли.

Но, спустя 10 лет, объявилось желание воскресить прежнюю задумку уже в новом формате. К тому же, миру были подарены мощные инструменты для разработки игр. Моё внимание пало на Unity3D. По слухам, удобный инструмент для разработки игр в 2D. Как раз, то что было мне нужно. В 2019 году я приступил к разработке. Для написания кода я выбрал C#, так как с магией JavaScript я был знаком и не хотел портить себе нервы. Но ввиду того что я не был знаком с C#, я тратил на разработку больше времени чем могло уйти. И вот, спустя 2 месяца, имея на руках MVP, по причине нехватки времени для работы которая меня кормит, я забросил разработку ;-D

Прошёл ещё один год.

Я вернулся к доработке. А точнее, к переделке. Потому что за год я успел показать недоделанную игру своим друзьям и знакомым (коим огромное спасибо) и собрать фидбек. Оказалось, что игру я создавал лично для себя, а не для пользователя. (Полную историю изменения игры вы сможете найти у меня в Telegram канале или на странице в Facebook).

Первая версия на Unity3D
image

Я адаптировал игру под мобильное приложение. Внёс изменения в интерфейс и механику игры. Что бы игра не выглядела совсем сухо, я добавил ей дух соперничества, т.е. врага который хочет, что бы вы проиграли. Он то и шлёт на вас эти математические задачи и радуется каждой вашей ошибке. Отсюда и название Math Invasion (Математическое наступление). Мои знакомые сказали, что враг в игре это лишнее.

Мне подсказали другую механику, при которой игра будет более интересна для пользователя. Я не стал переделывать игру ещё раз, но решил в дальнейшем внести новую механику как дополнительный способ прохождения. Назову я этот способ CHALLENGE!!!.

Релиз


Сейчас игра выложена в Play Market и любой желающий может опробовать её. В ней есть недостатки. В неё нужно добавлять дополнительные уровни. Добавить узбекский язык. Она сейчас на уровне чуть выше MVP. Я уже получаю фидбек и вношу изменения на его основе. Я добился того, что игра увидела свет.

Какой я вынес урок для себя?

До начала разработки нужно составить дорожную карту и зафиксировать примерные сроки. Иначе, Ваше творение может застрять в параллельном мире под названием Пока ещё не готово.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru