Kotlin, KorGE и корутины для организации игрового цикла.
Я вчера решил ради интереса посмотреть движок KorGE. Он довольно простой, для 2д игр, поддерживает сборку под разные платформы типа мобильных и веб.
Что меня заинтересовало — в нём активно используются корутины!
И тут я позволю себе лирическое отступление. Тот язык, на котором люди пишут что-то для UE, хоть и называется C++, очень далёк от идиоматического языка. Своя стандартная коллекция, свои классы движка, макросы для объектов движка и сборка мусора — получается страшный франкенштейн, когда поверх сложности С++ наслаивается сложность всех велосипедов, что навертели поверх него. Или например паттерн ECS — он прекрасен и хорошо подходит для геймдева, но в С++ как язык буквально зашито ООП и модель объекта с полями и нет никакой поддержки ECS в синтаксисе и компиляторе и стандартной библиотеке. Создатели языка не предполагали, что поля объекта будут не разложены внутри объекта, а будут раскиданы по разным местам.
В том случае, когда движок начинает сильно опираться на возможности языка, с одной стороны он становится к нёму привязанным, но с другой — он становится проще и удобнее. Именно это меня и зацепило в KorGE.
Обычно игровой цикл делается так — есть функция, которая вызывается на каждый кадр или на каждое обновление физики. Чтобы делать что-то, растянутое на несколько кадров (например, рисовать анимацию после победы и перекидывать игрока на новый уровень), придётся где-то явно хранить этот стейт, проверять его в начале кадра и с точки зрения кода это будет выглядеть довольно запутанно.
Итак, а что можно делать с корутинами? Всё просто, её снаружи могут вызывать раз за разом и она будет раз за разом как-то продвигаться по своей логике выполнения.
Гипотетический пример для шахмат (я сейчас не про реализацию в KorGE, а вообще про корутины):
Что мы видим? Это красивое, абстрактное описание алгоритма. Мы не думаем про обновление кадров, мы не думаем кто кого вызывает
А например showGameEnd() может выглядеть так:
Функция типа waitNextFrame() приостанавливает корутину и ждут, когда её снова продолжат.
Кто-то раз за разом в игровом цикле вызывает снаружи coroutine.next(), короутина доходит до следующей точки (например, ожидания хода игрока или следующего фрейма) и снова останавливается. Этот код простой и описывает саму суть происходящего.
Машина состояний и стейт с переменными типа isPlayerFirst, board и места исполнения тут всё равно будут, но ими занимается компилятор, а не программист.
Что ещё интереснее — корутины можно отменять. Например, если игрок захочет выйти из игры по alt+f4, мы просто отменим корутину. Если же мы захотим обработать выход, то мы можем поймать CoroutineCancelledException внутри корутины и обработать это — например, для сохранения состояния.
По моему, корутины это довольно красивое и простое решение для некоторых случаев. Что вы думаете об этом?
Что меня заинтересовало — в нём активно используются корутины!
И тут я позволю себе лирическое отступление. Тот язык, на котором люди пишут что-то для UE, хоть и называется C++, очень далёк от идиоматического языка. Своя стандартная коллекция, свои классы движка, макросы для объектов движка и сборка мусора — получается страшный франкенштейн, когда поверх сложности С++ наслаивается сложность всех велосипедов, что навертели поверх него. Или например паттерн ECS — он прекрасен и хорошо подходит для геймдева, но в С++ как язык буквально зашито ООП и модель объекта с полями и нет никакой поддержки ECS в синтаксисе и компиляторе и стандартной библиотеке. Создатели языка не предполагали, что поля объекта будут не разложены внутри объекта, а будут раскиданы по разным местам.
В том случае, когда движок начинает сильно опираться на возможности языка, с одной стороны он становится к нёму привязанным, но с другой — он становится проще и удобнее. Именно это меня и зацепило в KorGE.
Обычно игровой цикл делается так — есть функция, которая вызывается на каждый кадр или на каждое обновление физики. Чтобы делать что-то, растянутое на несколько кадров (например, рисовать анимацию после победы и перекидывать игрока на новый уровень), придётся где-то явно хранить этот стейт, проверять его в начале кадра и с точки зрения кода это будет выглядеть довольно запутанно.
Итак, а что можно делать с корутинами? Всё просто, её снаружи могут вызывать раз за разом и она будет раз за разом как-то продвигаться по своей логике выполнения.
Гипотетический пример для шахмат (я сейчас не про реализацию в KorGE, а вообще про корутины):
suspend fun doChessParty(isPlayerFirst: Boolean) {
val board = new Board()
while(true) {
val turn = if (isPlayerFirst) {
waitPlayerInput(board)
} else {
chooseAiTurn(board)
}
isPlayerFirst = !isPlayerFirst;
showTurnAnimation(turn, board)
if (board.isFinished) break;
}
showGameEnd(board)
}
Что мы видим? Это красивое, абстрактное описание алгоритма. Мы не думаем про обновление кадров, мы не думаем кто кого вызывает
А например showGameEnd() может выглядеть так:
suspend fun showGameEnd(board: Board) {
val message = makeMessage(board)
for(i in 1..10) {
drawAnimatedMessage(i, message)
waitNextFrame();
}
}
Функция типа waitNextFrame() приостанавливает корутину и ждут, когда её снова продолжат.
Кто-то раз за разом в игровом цикле вызывает снаружи coroutine.next(), короутина доходит до следующей точки (например, ожидания хода игрока или следующего фрейма) и снова останавливается. Этот код простой и описывает саму суть происходящего.
Машина состояний и стейт с переменными типа isPlayerFirst, board и места исполнения тут всё равно будут, но ими занимается компилятор, а не программист.
Что ещё интереснее — корутины можно отменять. Например, если игрок захочет выйти из игры по alt+f4, мы просто отменим корутину. Если же мы захотим обработать выход, то мы можем поймать CoroutineCancelledException внутри корутины и обработать это — например, для сохранения состояния.
По моему, корутины это довольно красивое и простое решение для некоторых случаев. Что вы думаете об этом?
Да, тож много думал про то как было бы прикольно их использовать. Например, что можно описать какой-нить игровой квест на await условий понятным линейным кодом.