Начнем с очевидного: зачем в принципе может понадобиться делать игру именно на kotlin?
Специалисты по браузерным играм пользуются продвинутыми движками: Unity, Godot, возможно Phaser и другими. Им нужен полноценный редактор, сцены, инспектор, анимационный редактор, тайлмапы, отладчик и большой игровой pipeline.
Но что если ваша работа бесконечно далека от игр, а бизнес резко захотел развлекуху-замануху для пользователей? Вот Timeweb недавно сильно облажался с охлаждением, и буквально за пару дней склипал маленькую игру-оправдашку:
Вот это и есть главная сфера применения таких решений. Нужны скорость разработки и сохранение стэка (в нашем случае Kotlin).
-
сделать админку? — взяли Kilua
-
простую игру? — KorGE
Timeweb-дисклеймер
Приведенный выше пример не имеет отношения к данному обзору. Мне искренне нравится Timeweb Apps Platform для frontend приложений за демократичную цену в 1 рубль. Поэтому демо развернуто именно там. Но, пользуясь случаем, кину камень в ваш огород: почините кнопку «Стать автором»! Тотальный игнор заявок не делает вас лучше.
Чтобы посмотреть на возможности KorGE — разработаем на скорость небольшую, но полноценную canvas/web-игру: спрайты, музыка, управление с клавиатуры и телефона, игровой цикл, проигрыш, победа, вот это все. А чем завалена вся лента в последнее время? Правильно, вот этим товарищем:
Поэтому и игра будет Viking Football. По духу это как динозаврик из Chrome, только с другими персонажами. Викинг бежит по футбольному полю, враги едут навстречу: летящие мячи надо ловить, игроков пинать, еду есть. Победа наступает, если продержаться до конца песни. Каждые 20 секунд уровень растет, скорость повышается.
Посмотрим возможности движка KorGE в условно-реальном проекте:
-
игра развернута здесь: viking-football.karandashov.com
-
репозиторий: GitLab/VladKarandashov/viking-football
Настройка проекта
KorGE живет в Kotlin/Gradle-мире, умеет собираться под разные таргеты и при этом не требует переходить в отдельный визуальный редактор или писать логику на другом языке.
Первым делом применяется gradle-плагин:
plugins { alias(libs.plugins.korge)}korge { id = "com.karandashov.viking.football" name = "Viking Football" targetJs()}
targetJs() включает сборку под браузер. В документации KorGE по Web target описаны задачи jsRun и jsBrowserDistribution, а также то, что web-таргет работает через WebGL и WebAudio. Под капотом это все равно Kotlin/JS, поэтому появляется JavaScript-инфраструктура. Gradle-плагин скачивает npm-зависимости и собирает bundle через webpack.
Сцена фиксированного размера
Входная точка задает основные параметры сцены:
suspend fun main() = Korge( windowSize = Size(1280.0, 720.0), virtualSize = Size(1280.0, 720.0), displayMode = KorgeDisplayMode.CENTER, backgroundColor = SkyColor, title = "Viking Football",) { VikingFootballGame(this).start()}
Я выбрал фиксированную виртуальную сцену 1280×720. Для пиксельных ассетов это нормальное решение: можно руками расставлять персонажей, кнопки и HUD, не пересчитывая весь layout под каждый телефон. На реальных мобильных устройствах это сильно упростило жизнь. Но конечно пришлось пожертвовать адаптивным UI, в перспективе можно сделать лучше.
Игровой цикл
В KorGE можно анимировать через tween, coroutine-loop или updater (по времени). Последний вариант как раз подходит для текущей игры: каждый delta time немного сдвигаем состояние и персонажей.
Упрощенно цикл игры выглядит так:
stage.addUpdater { dt -> val seconds = dt.seconds hero.update(seconds) if (state != GameState.RUNNING || portraitBlocked) return@addUpdater elapsed += seconds val level = floor(elapsed / 20.0).toInt() + 1 val speed = 150.0 + (level - 1) * 28.0 background.update(seconds, speed) obstacles.forEach { it.update(seconds, speed) } // Проигрыш наступает только если активное препятствие ушло за левый край. if (obstacles.any { it.canLose && it.leftEdge <= -18.0 }) { finish(GameState.LOST) }}
В KorGE нет физического движка, но здесь он и не нужен. Объекты движутся справа налево, герой стоит примерно в одной зоне, действия игрока проверяются простыми условиями: есть ли рядом робот, есть ли активный игрок, приблизился ли летящий мяч.
Доступно управление через кнопки и клавиши, есть событийный API. Для сенсорных экранов сделаны экранные кнопки через обычные KorGE views и onDown. Для клавиатуры хватило justDown, чтобы действие срабатывало один раз за нажатие:
stage.keys { justDown(Key.SPACE) { perform(Action.KICK) } justDown(Key.UP) { perform(Action.JUMP) } justDown(Key.RIGHT) { perform(Action.EAT) }}
Спрайты и ассеты
KorGE хорошо работает с frame-based анимациями. Нужна последовательность картинок, которую Sprite может проигрывать. Художник из меня плохой, поэтому ассеты сгенерировал нейронкой:
-
викинг бежит, прыгает, пинает мяч, ест робота;
-
футболист бежит и падает;
-
мяч крутится.
Вот так происходит чтение:
private suspend fun animation( path: String, frames: Int, frameMs: Int,): SpriteAnimation = SpriteAnimation( (1..frames).map { index -> // Файлы лежат в src/commonMain/resources/assets/sprites resourcesVfs[ "assets/sprites/${path}_${index.toString().padStart(2, '0')}.png" ].readBitmapSlice() }, frameMs.milliseconds, )
Поддерживаются PNG/JPEG картинки, а для звука есть MP3 и WAV.
Взаимодействие объектов
Самая приятная часть KorGE для такой игры в том, что все остается обычным Kotlin-кодом. Не надо раскладывать логику по визуальным нодам редактора. Нажали кнопку, нашли подходящую цель, изменили состояние объекта, запустили анимацию.
Например, удар по игроку:
private fun perform(action: Action) { if (state != GameState.RUNNING || portraitBlocked) return hero.playAction(action) if (action == Action.KICK) { val target = obstacles .filter { it.kind == ObstacleKind.PLAYER && it.isActive } .minByOrNull { it.x } if (target != null) { target.resolve() hero.kickBallTo(target.x, target.catchY) } else { // Ранний удар разрешен: мяч просто улетает вперед и возвращается. hero.kickBallTo(StageWidth - 58.0, GroundY - 96.0) } }}
Состояние препятствия тоже простое: активное едет к герою, resolved проигрывает свою финальную анимацию. Футболист падает, робот “съедается”, пойманный мяч падает за нижний край поля.
Что всплыло в браузере и на телефонах
KorGE закрывает игровой слой, но правила браузера остаются правилами браузера.
Первая проблема: fullscreen. По MDN requestFullscreen, для перехода в полноэкранный режим требуется transient user activation. То есть вызов надо делать из клика или тапа, а не когда игре захотелось.
Вторая проблема: orientation lock. ScreenOrientation.lock на MDN помечен как limited availability. Там же сказано, что блокировка ориентации обычно доступна на мобильных устройствах и часто в fullscreen-контексте. Поэтому надежная стратегия такая: просим landscape, но держим in-game overlay для портретного режима.
Третья проблема: звук. В KorGE Audio docs отдельно указано, что JavaScript использует WebAudio API, а браузеры почти всегда ограничивают его до первого взаимодействия пользователя. MDN в Autoplay guide говорит то же самое: аудио с программным стартом обычно блокируется, пока пользователь не кликнул, не тапнул или не нажал клавишу.
Поэтому старт игры завязан на кнопку. В обработчике я пытаюсь разблокировать аудио, запросить fullscreen/landscape и только потом запускаю музыку:
private fun resetGame() { unlockMobileAudio() requestLandscapeOrientation() // Подгрузка best effort. Игру не блокируем, если браузер отказал. runCatching { preloadMusicForMobile("music/compressed.mp3") } beginRun()}
Сами браузерные вызовы лежат в jsMain за expect/actual-оберткой:
internal actual fun requestLandscapeOrientation() { js( """ (function () { try { var orientation = screen.orientation || screen.mozOrientation || screen.msOrientation; if (!orientation || !orientation.lock) return; var result = orientation.lock('landscape'); if (result && result.catch) result.catch(function () {}); } catch (e) {} })() """ )}
Эта часть проекта выглядит грубее остального Kotlin-кода. Но это неизбежно, хотите жить в браузере — учите js, хотя бы минимально.
Отладка
Отладка игр сильно отличается от привычного backend. В backend очень просто можно написать воспроизводимый тест: пришел запрос, ушел ответ, состояние базы проверили. В игре много субъективных вещей: удобно ли нажимать, читается ли препятствие на скорости, не слишком ли рано начинается проигрыш, нормально ли выглядит падение мяча.
KorGE помогает базовыми вещами. Есть debug режим через runJvm и включение рамок views по F7. Но когда проблема живет в mobile Safari, WebAudio или orientation lock, приходится идти в браузер и устройство.
Фиксированная сцена 1280×720 сильно снизила количество мобильных сюрпризов. На реальных телефонах почти не было расхождений именно из-за того, что игра не пыталась быть адаптивной страницей.
Размер сборки
Рядовая сборка маленькой игры после clean заняла порядка 28 секунд. Не катастрофа, но могло быть лучше.
Финальная директория build/dist/js/productionExecutable занимает около 11 МБ. Внутри:
viking-football.js 1.3M viking-football.js.map 7.0Mmusic/compressed.mp3 1.4Massets/sprites/*.png 1.2Mindex.html 4.0K
Js bundle после gzip получился около 439 КБ. Webpack при production-сборке предупредил, что viking-football.js больше рекомендованного лимита 244 KiB. Для маленькой игры это неприятно, но ожидаемо: Kotlin runtime, KorGE/Korlibs и игровой код попадают в bundle. Но мы сделали это за вечер!
Выводы
KorGE хорошо подходит для мини-игр, прототипов, учебных проектов и Kotlin-first pet projects. Особенно если команда уже живет в Kotlin и хочет быстро получить браузерную игру без погружения в чужую экосистему.
Сильные стороны:
-
пишешь почти весь код на Kotlin;
-
Gradle-проект выглядит привычно;
-
общий код можно держать в
commonMain, браузерные детали уносить вjsMain; -
спрайты, ресурсы, input, audio и игровой цикл закрываются средствами движка;
-
для маленькой 2D-игры получается достаточно компактно по коду.
Слабые стороны:
-
сборка ощутимо тяжелее, чем ожидаешь от небольшой браузерной игры;
-
Kotlin/JS тянет npm/Yarn/webpack-инфраструктуру;
-
размер JS bundle не микроскопический;
-
документация KorGE местами выглядит неровно и иногда сама помечена как не полностью проверенная;
-
браузерные ограничения по fullscreen, orientation lock и audio никуда не исчезают.
Главное не ждать от KorGE, что он спрячет всю сложность внутри. Он хорошо дает игровой слой на Kotlin. А дальше все, как обычно: браузер попросит пользовательский gesture, телефон не всегда даст заблокировать ориентацию, сборка принесет webpack, а удобство прыжка придется проверять руками.
Прикольных вам pet-проектов!
ссылка на оригинал статьи https://habr.com/ru/articles/1057966/