[Программирование, Разработка игр, Изучение языков] Обзор GameLisp: нового языка для написания игр на Rust
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Программист, подписывающийся псевдонимом Fleabit, уже полгода разрабатывает свой язык программирования. Сразу же возникает вопрос: ещё один язык? Зачем?
Вот его аргументы:
- Разработка движка игры и разработка игры на этом движке – две очень разные задачи, и для них удобно использовать разные языки, при условии, что код на них хорошо стыкуется друг с другом. Например, код на языке с garbage collection и на языке с явным управлением памятью было бы сложно объединить в одном проекте.
- Rust идеально подходит для разработки движка игры: из языков, ориентированных на производительность скомпилированного кода, в нём максимум выразительных средств – enum-ы с полями; pattern matching с деструктуризацией; макросы, генерирующие произвольный код во время компиляции; и т.п. С другой стороны, для описания игровой механики Rust подходит плохо: задержки на перекомпиляцию усложняет подход «подправить и тут же проверить, что получилось»; строгое управление памятью усложняет использование одних данных одновременно несколькими объектами; а генераторы/сопрограммы, позволяющие удобно реализовать кооперативную многозадачность между внутриигровыми сущностями, ещё не реализованы.
- Для игровой механики идеально подходил бы скриптовый язык наподобие JavaScript, Lua, Python или Ruby; но интеграция кода на них в проект на Rust – нетривиальная задача, отчасти из-за того, что эти полновесные языки программирования устроены запредельно сложно. Вдобавок, внутри игры напрашивается очень простой garbage collector, отрабатывающий после генерации каждого кадра, чтобы частота кадров оставалась постоянной – без внезапных подвисаний раз в десять минут, когда GC решил пройтись по всем объектам, созданным за эти десять минут. Другое важное преимущество GameLisp перед популярными скриптовыми языками – гомоиконичность, упрощающая обработку и генерацию кода макросами.
- Фишка GameLisp, которой не было бы места в универсальном скриптовом языке – встроенная поддержка конечных автоматов, позволяющая сгруппировать члены класса в функциональные блоки, включаемые и отключаемые как одно целое. Это в некотором роде расширение идеи enum-ов с полями из Rust, к которой добавлен внутриклассовый полиморфизм, когда одно и то же имя метода связывается с разной реализацией в зависимости от состояния объекта. Моделируемые автоматы "недетерминированные" в том смысле, что одновременно может быть активно произвольное число состояний.
От Lisp в GameLisp взяты прежде всего простота синтаксиса и простота интерпретатора: реализация GameLisp вместе со «стандартной библиотекой» сейчас занимает 36 KLOC, по сравнению, например, с 455 KLOC в СPython. С другой стороны, по сравнению с обычным Lisp, в GameLisp нет списков и намного меньше ориентации на функциональное программирование и immutable-данные; вместо этого, как и большинство скриптовых языков, GameLisp ориентирован на императивное, объектно-ориентированное программирование.
Синтаксис на основе Lisp с непривычки может вызвать оторопь, но быстро привыкаешь вместо console.print(2 + 2) писать (.print console (+ 2 2)) и т.д. Этот синтаксис намного проще и гибче, чем в привычных скриптовых языках: запятая считается пробельным символом, и может использоваться для улучшения читаемости в любых местах кода; вместо двух видов скобок {}() используются только круглые; большинство знаков ASCII можно использовать в составе символов, так что I~<3~Lisp!~^_^ – допустимое имя для функции или переменной; не нужны; для разделения операций, и т.д. Могу сказать, что безо всякого прошлого опыта с Lisp я всего за пару вечеров сумел переписать классический NIBBLES.BAS на GameLisp: http://atari.ruvds.com/nibbles.html
Всё, что есть в «стандартной библиотеке» GameLisp из средств ввода-вывода – это функция prn для печати на stdout; нет работы ни с клавиатурой/мышью, ни с файлами, ни с графикой, ни со звуком. Предполагается, что пользователь GameLisp сам реализует на Rust все те интерфейсные средства, которые актуальны конкретно в его проекте. В качестве примера такой обвязки на https://gamelisp.rs/playground/ выложен минималистичный движок для браузерных игр, при помощи wasm-bindgen предоставляющий коду на GameLisp функции play:down?, play:pressed?, play:released?, play:mouse-x, play:mouse-y, play:fill и play:draw. В моём порте Nibbles используется тот же самый движок – я лишь добавил к нему функцию для воспроизведения звука. Интересно сравнить размеры: оригинальный NIBBLES.BAS занимал 24 КБ; мой порт на GameLisp занимает 9 КБ; файл на WebAssembly со скомпилированными воедино рантаймом Rust, интерпретатором GameLisp, и кодом игры – занимает 2.5 МБ, и к нему ещё прилагается обвязка на JavaScript в 11 КБ, сгенерированная wasm-bindgen.
Вместе с минималистичным движком на https://gamelisp.rs/playground/ выложены реализации на GameLisp трёх классических игр: pong, тетрис и сапёр. Тетрис и сапёр больше и сложнее, чем мой порт Nibbles, и в их коде есть чему поучиться.
Для демонстрации возможностей GameLisp я выбрал два примера; первый касается макросов. В NIBBLES.BAS уровни заданы стастрочным блоком SELECT CASE со вложенными циклами:
SELECT CASE curLevel
CASE 1
sammy(1).row = 25: sammy(2).row = 25
sammy(1).col = 50: sammy(2).col = 30
sammy(1).direction = 4: sammy(2).direction = 3
CASE 2
FOR i = 20 TO 60
Set 25, i, colorTable(3)
NEXT i
sammy(1).row = 7: sammy(2).row = 43
sammy(1).col = 60: sammy(2).col = 20
sammy(1).direction = 3: sammy(2).direction = 4
CASE 3
FOR i = 10 TO 40
Set i, 20, colorTable(3)
Set i, 60, colorTable(3)
NEXT i
sammy(1).row = 25: sammy(2).row = 25
sammy(1).col = 50: sammy(2).col = 30
sammy(1).direction = 1: sammy(2).direction = 2
...
Все эти циклы имеют похожую структуру, которую можно вынести в макрос:
(let-macro set-walls (range ..walls)
`(do ~..(map (fn1
`(forni (i ~..range) (set-wall ~.._))) walls)))
С этим макросом описание всех уровней сокращается вчетверо, и становится максимально близким к декларативному JSON-подобному описанию:
(match @level
(1 (set-locations '(25 50 right) '(25 30 left)))
(2 (set-walls (20 60) (25 i))
(set-locations '(7 60 left) '(43 20 right)))
(3 (set-walls (10 40) (i 20) (i 60))
(set-locations '(25 50 up) '(25 30 down)))
...
В языке без макросов – например, в JavaScript – аналогичная реализация затуманила бы всё описание уровней лямбдами:
switch (level) {
case 1: setLocations([25, 50, "right"], [25, 30, "left"]); break;
case 2: setWalls([20, 60], i => [25, i]);
setLocations([7, 60, "left"], [43, 20, "right"]); break;
case 3: setWalls([10, 40], i => [i, 20], i => [i, 60]);
setLocations([25, 50, "up"], [25, 30, "down"]); break;
...
На этом примере хорошо видно, насколько код на JavaScript перегружен разнообразной пунктуацией и служебными словами, без которых можно обойтись.
Второй мой пример касается конечных автоматов. Реализация игры у меня имеет следующую структуру:
(defclass Game
...
(fsm
(state Playing
(field blink-rate (Rate 0.2))
(field blink-on)
(field move-rate (Rate 0.3))
(field target)
(field prize 1)
(state Paused
(init-state ()
(@center "*** PAUSED ***" 0))
(wrap Playing:update (dt)
(when (play:released? 'p)
(@center " LEVEL {@level} " 0)
(@disab! 'Paused))))
(met update (dt)
...
(when (play:released? 'p)
(@enab! 'Paused) (return))
...
; Move the snakes
(.at @move-rate dt (fn0
(for snake in @snakes (when (> [snake 'lives] 0)
(let position (clone [[snake 'body] 0]))
...
; If player runs into any point, he dies
(when (@occupied? position)
(play:sound 'die)
(dec! [snake 'lives])
(dec! [snake 'score] 10)
(if (all? (fn1 (== 0 [_ 'lives])) @snakes)
(@enab! 'Game-Over)
(@enab! 'Erase-Snake snake))
(return))
...
(state Game-Over
(init-state ()
(play:fill ..(@screen-coords 10 (-> @grid-width (/ 2) (- 16))) ..(@screen-coords 7 32) 255 255 255)
(play:fill ..(@screen-coords 11 (-> @grid-width (/ 2) (- 15))) ..(@screen-coords 5 30) ..@background)
(@center "G A M E O V E R" 13))
(met update (dt)))))
На каждом кадре (по вызову из window.requestAnimationFrame) игровой движок вызывает метод Game.update. Внутри класса Game определён автомат из состояний Init-Level, Playing, Erase-Snake, Game-Over, каждое из которых определяет метод update по-своему. В состоянии Playing определены пять приватных полей, к которым невозможно обратиться из других состояний. Кроме того, в состоянии Playing есть вложенное состояние Paused, т.е. игра может находиться как в состоянии Playing, так и в состоянии Playing:Paused. Конструктор состояния Paused печатает на экране соответствующую строку каждый раз при переходе к этому состоянию; метод update в этом состоянии проверяет, нажата ли клавиша P повторно, и если нажата и отпущена, то выходит из состояния Paused, возвращаясь к «простому» состоянию Playing. Метод update состояния Playing обрабатывает нажатия клавиш, рассчитывает новое положение игроков, и если кто-то из них врезался в стену, то переходит либо в состояние Game-Over, либо в состояние Erase-Snake. Конструктор состояния Erase-Snake интересен тем, что он принимает параметром ссылку на змейку, которую нужно красиво стереть перед перезапуском уровня. Наконец, у состояния Game-Over конструктор выводит на экран соответствующее сообщение, а метод update пустой – это значит, что независимо от нажимаемых клавиш, ничего нового на экране рисоваться не будет, и выйти из этого состояния невозможно.
Аналогично можно было бы реализовать игру и на классическом скриптовом языке: у класса Game были бы вложенные классы InitLevel, Playing, EraseSnake, GameOver, было бы поле currentState, и метод Game.update делегировал бы вызов currentState.update. Внутри класса Playing был бы вложенный класс Paused, и метод Playing.update в свою очередь делегировал бы вызов подобъекту. Макросы стандартной библиотеки позволяют спрятать автоматическую генерацию полей currentState и делегирующих методов, чтобы разработчик игры видел содержательную реализацию состояний, а не их шаблонное обрамление.
Вместо конечного автомата можно было бы реализовать Nibbles и в виде цикла:
while (lives>0) {
InitLevel;
while (prize<10) {
Playing;
if (dies) {
EraseSnake;
break;
}
}
}
GameOver;
Так и была реализована оригинальная игра на QBasic. Для браузерного движка такой цикл был бы заключён в генератор с yield после отрисовки каждого кадра, а Game.update состоял бы из вызова iter-next!.. Я предпочёл реализацию в виде автомата по двум причинам: во-первых, именно так устроена реализация тетриса, которую автор GameLisp приводит как пример; и во-вторых, в генераторах в GameLisp нет ничего необычного по сравнению с другими скриптовыми языками. Основное предназначение для автоматов – реализация состояний игровых персонажей (ждёт, атакует, убегает и т.д.), невозможная посредством цикла внутри генератора. Дополнительный аргумент в пользу автоматов – изоляция данных, относящихся к каждому из состояний, друг от друга.
оригинал
===========
Источник:
habr.com
===========
Похожие новости:
- Выпуск языка программирования Rust 1.49
- [Разработка веб-сайтов, Программирование, Java] Пять причин, по которым следует использовать Apache Wicket (перевод)
- [Программирование, Реверс-инжиниринг, Читальный зал, История IT, Софт] Причуды обратной совместимости
- [Python, Программирование, Data Mining, Алгоритмы, Машинное обучение] ИИ итоги уходящего 2020-го года в мире машинного обучения
- [Программирование, Системное программирование, Промышленное программирование, Rust] Так ли токсичен синтаксис Rust?
- [Промышленное программирование, Программирование микроконтроллеров] OPC UA для CPU S7-1200 (FW4.4). Настройка сервера
- [Python, Программирование, Машинное обучение] Обеспечить январь настроением
- [Open source, Отладка, Angular, Визуализация данных, Rust] Легкие обновления
- [Программирование, Робототехника, Игры и игровые приставки, Логические игры, Электроника для начинающих] 5 игрушек, чтобы ребёнок почувствовал программирование
- [Разработка игр, Читальный зал, Дизайн игр, Игры и игровые приставки, Логические игры] Головоломка от будущих создателей GTA. История Lemmings (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_igr (Разработка игр), #_izuchenie_jazykov (Изучение языков), #_gamelisp, #_rust, #_ruvds_stati (ruvds_статьи), #_razrabotka_igr (разработка игр), #_obzor_jazyka (обзор языка), #_blog_kompanii_ruvds.com (
Блог компании RUVDS.com
), #_programmirovanie (
Программирование
), #_razrabotka_igr (
Разработка игр
), #_izuchenie_jazykov (
Изучение языков
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 05:12
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Программист, подписывающийся псевдонимом Fleabit, уже полгода разрабатывает свой язык программирования. Сразу же возникает вопрос: ещё один язык? Зачем? Вот его аргументы:
От Lisp в GameLisp взяты прежде всего простота синтаксиса и простота интерпретатора: реализация GameLisp вместе со «стандартной библиотекой» сейчас занимает 36 KLOC, по сравнению, например, с 455 KLOC в СPython. С другой стороны, по сравнению с обычным Lisp, в GameLisp нет списков и намного меньше ориентации на функциональное программирование и immutable-данные; вместо этого, как и большинство скриптовых языков, GameLisp ориентирован на императивное, объектно-ориентированное программирование. Синтаксис на основе Lisp с непривычки может вызвать оторопь, но быстро привыкаешь вместо console.print(2 + 2) писать (.print console (+ 2 2)) и т.д. Этот синтаксис намного проще и гибче, чем в привычных скриптовых языках: запятая считается пробельным символом, и может использоваться для улучшения читаемости в любых местах кода; вместо двух видов скобок {}() используются только круглые; большинство знаков ASCII можно использовать в составе символов, так что I~<3~Lisp!~^_^ – допустимое имя для функции или переменной; не нужны; для разделения операций, и т.д. Могу сказать, что безо всякого прошлого опыта с Lisp я всего за пару вечеров сумел переписать классический NIBBLES.BAS на GameLisp: http://atari.ruvds.com/nibbles.html Всё, что есть в «стандартной библиотеке» GameLisp из средств ввода-вывода – это функция prn для печати на stdout; нет работы ни с клавиатурой/мышью, ни с файлами, ни с графикой, ни со звуком. Предполагается, что пользователь GameLisp сам реализует на Rust все те интерфейсные средства, которые актуальны конкретно в его проекте. В качестве примера такой обвязки на https://gamelisp.rs/playground/ выложен минималистичный движок для браузерных игр, при помощи wasm-bindgen предоставляющий коду на GameLisp функции play:down?, play:pressed?, play:released?, play:mouse-x, play:mouse-y, play:fill и play:draw. В моём порте Nibbles используется тот же самый движок – я лишь добавил к нему функцию для воспроизведения звука. Интересно сравнить размеры: оригинальный NIBBLES.BAS занимал 24 КБ; мой порт на GameLisp занимает 9 КБ; файл на WebAssembly со скомпилированными воедино рантаймом Rust, интерпретатором GameLisp, и кодом игры – занимает 2.5 МБ, и к нему ещё прилагается обвязка на JavaScript в 11 КБ, сгенерированная wasm-bindgen. Вместе с минималистичным движком на https://gamelisp.rs/playground/ выложены реализации на GameLisp трёх классических игр: pong, тетрис и сапёр. Тетрис и сапёр больше и сложнее, чем мой порт Nibbles, и в их коде есть чему поучиться. Для демонстрации возможностей GameLisp я выбрал два примера; первый касается макросов. В NIBBLES.BAS уровни заданы стастрочным блоком SELECT CASE со вложенными циклами: SELECT CASE curLevel
CASE 1 sammy(1).row = 25: sammy(2).row = 25 sammy(1).col = 50: sammy(2).col = 30 sammy(1).direction = 4: sammy(2).direction = 3 CASE 2 FOR i = 20 TO 60 Set 25, i, colorTable(3) NEXT i sammy(1).row = 7: sammy(2).row = 43 sammy(1).col = 60: sammy(2).col = 20 sammy(1).direction = 3: sammy(2).direction = 4 CASE 3 FOR i = 10 TO 40 Set i, 20, colorTable(3) Set i, 60, colorTable(3) NEXT i sammy(1).row = 25: sammy(2).row = 25 sammy(1).col = 50: sammy(2).col = 30 sammy(1).direction = 1: sammy(2).direction = 2 ... Все эти циклы имеют похожую структуру, которую можно вынести в макрос: (let-macro set-walls (range ..walls)
`(do ~..(map (fn1 `(forni (i ~..range) (set-wall ~.._))) walls))) С этим макросом описание всех уровней сокращается вчетверо, и становится максимально близким к декларативному JSON-подобному описанию: (match @level
(1 (set-locations '(25 50 right) '(25 30 left))) (2 (set-walls (20 60) (25 i)) (set-locations '(7 60 left) '(43 20 right))) (3 (set-walls (10 40) (i 20) (i 60)) (set-locations '(25 50 up) '(25 30 down))) ... В языке без макросов – например, в JavaScript – аналогичная реализация затуманила бы всё описание уровней лямбдами: switch (level) {
case 1: setLocations([25, 50, "right"], [25, 30, "left"]); break; case 2: setWalls([20, 60], i => [25, i]); setLocations([7, 60, "left"], [43, 20, "right"]); break; case 3: setWalls([10, 40], i => [i, 20], i => [i, 60]); setLocations([25, 50, "up"], [25, 30, "down"]); break; ... На этом примере хорошо видно, насколько код на JavaScript перегружен разнообразной пунктуацией и служебными словами, без которых можно обойтись. Второй мой пример касается конечных автоматов. Реализация игры у меня имеет следующую структуру: (defclass Game
... (fsm (state Playing (field blink-rate (Rate 0.2)) (field blink-on) (field move-rate (Rate 0.3)) (field target) (field prize 1) (state Paused (init-state () (@center "*** PAUSED ***" 0)) (wrap Playing:update (dt) (when (play:released? 'p) (@center " LEVEL {@level} " 0) (@disab! 'Paused)))) (met update (dt) ... (when (play:released? 'p) (@enab! 'Paused) (return)) ... ; Move the snakes (.at @move-rate dt (fn0 (for snake in @snakes (when (> [snake 'lives] 0) (let position (clone [[snake 'body] 0])) ... ; If player runs into any point, he dies (when (@occupied? position) (play:sound 'die) (dec! [snake 'lives]) (dec! [snake 'score] 10) (if (all? (fn1 (== 0 [_ 'lives])) @snakes) (@enab! 'Game-Over) (@enab! 'Erase-Snake snake)) (return)) ... (state Game-Over (init-state () (play:fill ..(@screen-coords 10 (-> @grid-width (/ 2) (- 16))) ..(@screen-coords 7 32) 255 255 255) (play:fill ..(@screen-coords 11 (-> @grid-width (/ 2) (- 15))) ..(@screen-coords 5 30) ..@background) (@center "G A M E O V E R" 13)) (met update (dt))))) На каждом кадре (по вызову из window.requestAnimationFrame) игровой движок вызывает метод Game.update. Внутри класса Game определён автомат из состояний Init-Level, Playing, Erase-Snake, Game-Over, каждое из которых определяет метод update по-своему. В состоянии Playing определены пять приватных полей, к которым невозможно обратиться из других состояний. Кроме того, в состоянии Playing есть вложенное состояние Paused, т.е. игра может находиться как в состоянии Playing, так и в состоянии Playing:Paused. Конструктор состояния Paused печатает на экране соответствующую строку каждый раз при переходе к этому состоянию; метод update в этом состоянии проверяет, нажата ли клавиша P повторно, и если нажата и отпущена, то выходит из состояния Paused, возвращаясь к «простому» состоянию Playing. Метод update состояния Playing обрабатывает нажатия клавиш, рассчитывает новое положение игроков, и если кто-то из них врезался в стену, то переходит либо в состояние Game-Over, либо в состояние Erase-Snake. Конструктор состояния Erase-Snake интересен тем, что он принимает параметром ссылку на змейку, которую нужно красиво стереть перед перезапуском уровня. Наконец, у состояния Game-Over конструктор выводит на экран соответствующее сообщение, а метод update пустой – это значит, что независимо от нажимаемых клавиш, ничего нового на экране рисоваться не будет, и выйти из этого состояния невозможно. Аналогично можно было бы реализовать игру и на классическом скриптовом языке: у класса Game были бы вложенные классы InitLevel, Playing, EraseSnake, GameOver, было бы поле currentState, и метод Game.update делегировал бы вызов currentState.update. Внутри класса Playing был бы вложенный класс Paused, и метод Playing.update в свою очередь делегировал бы вызов подобъекту. Макросы стандартной библиотеки позволяют спрятать автоматическую генерацию полей currentState и делегирующих методов, чтобы разработчик игры видел содержательную реализацию состояний, а не их шаблонное обрамление. Вместо конечного автомата можно было бы реализовать Nibbles и в виде цикла: while (lives>0) {
InitLevel; while (prize<10) { Playing; if (dies) { EraseSnake; break; } } } GameOver; Так и была реализована оригинальная игра на QBasic. Для браузерного движка такой цикл был бы заключён в генератор с yield после отрисовки каждого кадра, а Game.update состоял бы из вызова iter-next!.. Я предпочёл реализацию в виде автомата по двум причинам: во-первых, именно так устроена реализация тетриса, которую автор GameLisp приводит как пример; и во-вторых, в генераторах в GameLisp нет ничего необычного по сравнению с другими скриптовыми языками. Основное предназначение для автоматов – реализация состояний игровых персонажей (ждёт, атакует, убегает и т.д.), невозможная посредством цикла внутри генератора. Дополнительный аргумент в пользу автоматов – изоляция данных, относящихся к каждому из состояний, друг от друга. оригинал =========== Источник: habr.com =========== Похожие новости:
Блог компании RUVDS.com ), #_programmirovanie ( Программирование ), #_razrabotka_igr ( Разработка игр ), #_izuchenie_jazykov ( Изучение языков ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 05:12
Часовой пояс: UTC + 5