6 интересных багов, с которыми я столкнулся, пока делал игру для ВКонтакте
Мне нравится делать игры, это мое хобби. В общей сложности около года в свободное время я делал игру на javascript для ВКонтакте. В настоящее время игра доделана, запущена, прошла модерацию ВКонтакте и доступна в каталоге игр. Это не статья вида «как я сделал игру», а рассказ о тех проблемах, с которыми я столкнулся, пока ее создавал. Надеюсь, кому-то данная информация поможет избежать сложных ситуаций в своих проектах. Знай я о некоторых багах заранее, было бы гораздо проще, и, возможно, я бы принимал другие решения относительно кода. Добро пожаловать под кат за подробностями.
Вступление
Чтобы суть багов была более понятна, расскажу коротко об игре. Игра основана на китайской головоломке «Танграм», смысл ее в том, что из нескольких простых фигур нужно собирать более сложные. В классическом танграме используют семь стандартных фигурок, я же в своей игре решил обобщить этот принцип на любое количество и вид фигур.
Изначально я использовал в качестве графического движка pixi.js; позднее, по ряду причин, я решил от него отказаться и просто рисовать все на canvas. Это спорное решение, и часть описанных багов появилась именно от этого. С другой стороны, с pixi я бы получил другие сюрпризы.
Еще несколько моментов, прежде чем перейду собственно к багам.
Во-первых, хоть я использовал в заголовке слово «баг», некоторые из описанных ниже ситуаций вовсе не баги. Это фичи, или даже правильное, но нелогичное или нестандартное поведение игры / браузера. Или просто сложности, с которыми я столкнулся.
Во-вторых, хотя проект и не особо велик размером, но, конечно, ошибок при создании было гораздо больше. Просто нет смысла описывать все. Я запоминал ситуации, которые отличаются редкостью, сложностью или интересностью. Также все эти баги объединяет то, что по ним довольно мало информации при беглом гуглении, а, возможно, и при вдумчивом. Таким образом, думаю, полезно будет их тут описать.
В-третьих, некоторые из багов я так и не исправил. Потому что я не знаю, как, и возможно ли это вообще. Думаю, написать о них все равно нужно, чтобы другие разработчики, столкнувшись с подобным, знали, что они не одиноки и получили хоть какой-то материал для размышлений. Если же кто-то знает, как исправить неисправимое, буду рад услышать ответ в комментариях.
На этом затянувшееся вступление заканчивается, переходим к сути.
№1. Танцы вокруг полноэкранного режима (fullscreen api).
И вот уже не один год прошел, как введена поддержка полноэкранного режима, а воз и ныне там. Есть много статей как в интернете, так и на Хабре, например вот. Но, по прежнему в каждом браузере делают все по своему.
Похоже сколько браузеров (ну или сколько движков), столько и вариантов названий у методов полноэкранного api.
Мало того, что сами по себе префиксы вносят неразбериху (moz, webkit). IE11 использует два префикса: ms и MS. Часть браузеров использует написание Fullscreen, часть FullScreen (привет firefox). Событие изменения полноэкранного режима пишется без заглавных букв: fullscreenchange (с префиксами, естественно). Но в IE11 оно пишется как MSFullscreenChange, в Edge — fullscreenChange.
Также, часть браузеров центрирует полноэкранный элемент на экране, часть оставляет его в той же позиции, в какой он был.
Казалось бы, тривиальная вещь, включить и выключить полноэкранный режим. В результате все это превращается в поиски названия нужного метода в нужном браузере и долгое тестирование.
Для интересующихся, вот результаты на CodePen. Проверено в chrome, yandex, opera, firefox, IE11. К сожалению, не имею возможности проверить в safari и edge, если кто-то может это сделать, отпишитесь в комментах.
№2. Исчезающая тень (context.shadowBlur).
При наведении мыши на фигуру, она подсвечивается с помощью добавления тени. То есть к обычной картинке я добавляю такую тень:
context.shadowBlur = 5
context.shadowColor = "#000”
У всех фигур это работает отлично, кроме обычного прямоугольника. У него тень не появляется.
Долгое время я откладывал разборку с этим багом на потом, считая его не сложным. Когда же пришло время взяться за него серьезно, оказалось, что все не так просто.
Долгие часы поисков, проб разных вариантов, и в результате я выяснил только вот что:
- тень на самом деле появляется, но её не видно из под картинки прямоугольника, поскольку отсутствует отрисовка Blur, то есть размытости
- если задать тени offset, то её будет видно как положено со смещением, но, опять же, без Blur
- такой эффект наблюдается только в прямоугольной фигуре, все остальные с тенью дружат хорошо
- если прямоугольник повернуть на любой угол, отличный от нуля, то тень отображается нормально
- непосредственно перед выводом на контекст изображения прямоугольника, значение shadowBlur равно пяти, как и должно быть, но отображения Blur все равно нет
- ни одна попытка повторить этот баг отдельно от игры, в отдельном файле, не увенчалась успехом
Все фигуры выводятся на экран одним и тем же кодом, значит дело не в коде. С другой стороны, никак не получается воспроизвести этот баг вне кода игры, значит дело в коде.
Скажу честно, что я очень устал бодаться с этим багом, и результат следующий: я не знаю, чем он вызван; я не знаю, как его исправить.
Но я знаю, как сделать костыль и пофиксить его. Что я сделал. Открыл картинку прямоугольника в графическом редакторе, выбрал один пиксель (в углу, ну тут не принципиально я думаю), и сделал ему прозрачность не 255, а 254. В итоге картинка внешне никак не отличима от оригинала, а тень, то есть Blur, волшебным образом отображается. И не спрашивайте меня, как я это придумал.
№3. Баг с декодированием звуков (decodeAudioData)
Для воспроизведения звуков в игре я использую AudioContext. Звуки загружаются через XMLHttpRequest, затем декодируются с помощью функции decodeAudioData. Все штатно.
Через некоторое время я взялся протестировать игру на стареньком ноуте, на котором еще стоит XP и, соответственно старая версия Chrome. Вот тут то я и получил ошибку следующего вида: Uncaught (in promise) DOMException: Unable to decode audio data. Та же ошибка возникает в yandex браузере.
Самое интересное в этом баге то, что мне не удалось его предсказать заранее. Можно проверить поддержку аудио-контекста просто проверив его существование: window.hasOwnProperty("AudioContext")
. Но как проверить можно ли декодировать звук, кроме как попытавшись его декодировать?
Также не помогает конструкция try — catch. Поскольку ошибка возникает не в js скрипте, а в коде браузера, то он просто выдает исключение и останавливает работу скрипта.
Оказалось, что ошибка возникает при попытке декодировать wav файл с одним каналом. Если конвертировать файл в двухканальный или записать в mp3, то баг пропадает.
В последних версиях chrome данный баг отсутствует.
№4. Beforeunload и сохранение данных.
При выходе из игры, в обработчике события beforeunload, я отсылаю запрос к серверу, чтобы сохранить последний открытый уровень.
Примерно в 50% случаев, а может и чаще, данные не сохранялись. Потратив некоторое время на раздумья о том, какое время дает браузер на обработку события beforeunload, и за какое время сервер обрабатывает запрос, я понял, что дело вообще не в этом. Ответ оказался простым, если его уже знаешь. Нужно использовать синхронный запрос к серверу, а не асинхронный.
Все таки удивительная вещь — стадный инстинкт, или сила привычки и убеждения. В любом руководстве и гайде написано: не используйте синхронные запросы! Используйте асинхронные запросы! И в большинстве случаев так оно и есть.
Ну, а вот то самое место, где можно (и нужно!) использовать синхронный запрос! При этом выполнение скрипта приостанавливается, beforeunload ожидает и не возвращает результат, таким образом данные успевают сохраниться.
P.S. В итоге всё же сделал сохранение после каждого уровня, так надёжнее.
№5. Canvas + Text + Firefox
Опять довольно старый баг движка gecko, о котором я заранее не знал, а зря. При выводе текста на canvas в firefox, текст отображается на несколько пикселей выше, чем в других браузерах (движках). Особенно это заметно, если использовать свойство context.textBaseline = "top"
. Визуально это выглядит примерно так:
Смещение зависит от размера шрифта, т.е. оно не одинаково. При использовании других значений context.textBaseline смещение меньше, но все равно заметно на глаз: пара пикселей при размере шрифта ~20.
Обсуждение этого бага можно почитать тут.
Что я пробовал:
- использование
context.textBaseline = "middle”
и смещение текста вниз на половину высоты; как уже сказал выше, все равно есть небольшое несоответствие. - определять высоту текста вручную попиксельно; т.е. берем какую-либо большую букву / цифру (например 0 или Т), сверху и снизу по центру начинаем проверять пиксели, пока не встретим закрашенный; таким образом определяется высота шрифта и можно вывести надпись точно в то место, куда хотим. Но, это довольно сложно, затратно по ресурсам, возникают трудности с выводом многострочного, подчеркнутого, зачеркнутого текста. Также метод может не подходить к разным шрифтам, у которых буквы наклонены, либо имеют нестандартное начертание.
- смещение текста на вычисляемое значение, процент от размера или типа того; не работает, т.к. нет прямой зависимости между размером шрифта и его смещением, во всяком случае я ее не знаю.
В итоге решал эту проблему «в лоб»: определял движок браузера и в случае необходимости смещал текст вниз на нужное расстояние вручную для каждой отдельной надписи. Как не странно, так оказалось проще всего.
№6. Внезапный mouseout
Думал писать ли про этот баг или нет, т.к. отловить его довольно сложно, я даже сомневался не показалось ли мне. Но в браузере vivaldi, которым я довольно активно пользуюсь, баг ловится стабильно. Также удавалось поймать его в chrome (но это не точно!). В firefox и opera добиться появления бага не удалось, но может плохо пытался.
Итак, началось все с того, что я заметил, что при нескольких последовательных нажатиях на кнопку в игре, если при этом не двигать мышью, курсор устанавливается в default, т.е. как-будто выходит с кнопки. После тестирования разных вариантов, оказалось, что баг возникает из-за того, что внезапно происходит событие mouseout.
Потестировать, есть ли у вас этот баг, можно тут.
Повторюсь, баг более-менее уверенно ловится в vivaldi (после 5 — 30 нажатия), неуверенно в chrome. В других браузерах поймать баг мне не удалось.
Суть тестирования: откройте pen, откройте консоль (F12), теперь наведите мышь на параграф с надписью и начинайте последовательно кликать мышью, стараясь при этом ею не шевелить. Следите за сообщениями консоли. После определенного количества кликов промелькнет надпись out, т.е. возникло событие mouseout. Удается добиться даже эффекта, что возникло событие mouseout, курсор мыши изменился на defаult, после этого достаточно малейшего шевеления мыши для того, чтобы снова возникло событие mouseover и курсор изменился на pointer.
Пробовал использовать события mouseenter / mouseleave; тестировать файл локально; отключать выделение по клику. Результаты плюс минус те же.
Я не знаю, что это и почему это. Могу только предполагать баг конкретного браузера. В игре я ничего не делал по этому поводу, так как в общем-то там нет таких кнопок на которые можно (или нужно) жать много раз подряд. Да и что с этим багом можно сделать, я честно говоря не придумал.
Ещё несколько
Хоть и указал в заголовке цифру 6, но вспомнил ещё пару забавных ошибок. О них кратко, много писать вроде и нечего.
Известно, что метод context.drawImage
можно вызывать с разным количеством аргументов. В том числе, есть возможность указать размер области, которую мы хотим скопировать из источника и нарисовать на контексте. Так вот, в chrome можно нарисовать область размером 0×0, а firefox при этом выдаст ошибку — будет ругать за неправильный размер.
Странный баг с кодом вида
bounds = {
left: 0
right: GAME_WIDTH
top: 0
bottom: GAME_HEIGHT
}
Вроде все хорошо, но почему то bounds.top далее в коде равняется не 0, а что-то вроде 1e-14, что конечно очень близко к нулю, но все же не 0. И если ниже в коде идет сравнение с нулем, то результат уже будет другой.
Странности тут две. Во-первых, этот баг возникает только после сборки кода в один файл с помощью require.js; пока код находится в разных модулях — файлах, такого поведения я не замечал. Во-вторых, почему то такая странность возникает только со значением top, в то время как bounds.left по прежнему строго ноль.
Спасибо!
На этом всё, больше ничего не вспомнил. Если кто-то захочет посмотреть результат работы, оценить и потестировать, проходите сюда.
Спасибо за внимание, надеюсь кому-то данная информация окажется полезной!