SDL2 — отличная библиотека, но туториалов по ней не очень много.
Common Lisp — отличный язык, но статей по нему катастрофически мало.
Думаю этого вполне достаточно, для того чтобы написать этот цикл статей.

Почему я выбрал общелисп? Что ж, на вкус и цвет как говорится.
Впрочем, причины есть:



Фактически эта серия — адаптация туториалов от Lazy Foo:
lazyfoo.net/tutorials/SDL/index.php

В этой статье я не буду детально объяснять как ставить все необходимое ПО, но
если это кому-то действительно интересно, пожалуйста, напишите об этом
в комментариях.

Для установки SDL2, воспользуйтесь советами Фу: lazyfoo.net/tutorials/SDL/01_hello_SDL/index.php.

Что касается лиспа, я пользуюсь реализацией www.sbcl.org и www.quicklisp.org.
На данный момент работоспособность проверялась только в ней.

Дальше я использую библиотеку cl-sdl2: github.com/lispgames/cl-sdl2.
Так уж вышло, что поддержка некоторого функционала из SDL2, например
surfaces, в ней реализована не полностью. Но это не беда, я дописываю
нужный функционал по мере продвижения по туториалам. Поэтому не
ставьте версию из quicklisp’а, а сразу клонируйте мастер ветку в ~/quicklisp/local-projects.

Предполагается, что вы минимально понимаете синтаксис лиспа и не пугаетесь от обилия скобок.
В качестве основного средства взаимодействия с лисповым окружениям я буду использовать slime.
Вы еще не пробовали spacemacs? Тогда мы идем к вам!

Тем кому не нужна лирика, а нужен код: github.com/TatriX/cl-sdl2-tutorial

Белый экран жизни


Начнем с того, что просто заставим SDL2 показать нам окошко, залитое белым цветом.

Очевидно, нам понадобится сам библиотека cl-sdl2. Подключим её:
CL-USER> (ql:quickload :sdl2)

Не будем усложнять себе задачу и создадим окошко фиксированного размера.
Для этого объявим глобальные (и более того специальные) переменные для ширины и высоты нашего окошка, ведь магические числа — зло.
(defparameter *screen-width* 640)
(defparameter *screen-height* 480)

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

Незатейливо обзовем нашу функцию main и добавим ей опциональный
именованный параметр, чтобы мы могли регулировать время через которое
окно будет закрываться. По умолчанию двух секунд нам за глаза.
(defun main (&key (delay 2000))

Затем нам нужно инициализировать sdl2, сказав библиотеки какие
подсистемы мы хотим использовать. Для этого cl-sdl2 предоставляем нам удобный макрос:
  (sdl2:with-init (:video)

Мы хотим использовать только подсистему вывода графики, поэтому
передадим символ :video. Вы спросите: «как я должен был догадаться,
что передать нужно именно :video?» Отвечаем: cl-sdl2 преобразовывает
SDL_ константы в соответствующие символы. Например,
мы можем открыть документацию по методу SDL_Init и посмотреть
доступные флаги: wiki.libsdl.org/SDL_Init#Remarks
Чтобы получить необходимый символ отрежем от имени константы SDL_INIT_ и
преобразуем флаг в нижний регистр.

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

Отлично, дальше создадим окно:
 (sdl2:with-window (window :title "SDL2 Window" :w *screen-width* :h *screen-height*)

Это очередной with макрос. В этот раз первым элементом списка
аргументов макроса будет символ window, через который в теле
макроса мы и будем обращаться к созданному окну. Именованные
параметры :title, :w, :h думаю вполне очевидны и не нуждаются в
объяснения.

С окном все понятно, но теперь мы хотим залить получившееся окно белым
цветом. Одним из вариантов реализации задуманного будет использование
«поверхностей», они же surfaces. По сути, поверхность это структура
содержащая пиксели некоторой области, используемая при программном
рендеринге. Например, мы можем получить поверхность нашего окна:
      (let ((screen-surface (sdl2:get-window-surface window)))

и залить его белым цветом:
        (sdl2:fill-rect screen-surface
                        nil
                        (sdl2:map-rgb (sdl2:surface-format screen-surface) 255 255 255))

Первый аргумент — прямоугольник, который мы хотим залить. Если его не
передать, будем заливать всю область. Зачем несчастный #fff
записывать таким сложным образом? Все дело в разнообразии форматов
пикселей, экранов и тому подобного. А так как SDL2 библиотека
кросcплатформенная, для применения всех необходимых преобразований
используются различные функции, как например map-rgb в данном случае.

Залить окно залили, но этого недостаточно. Теперь нам нужно вежливо попросить библиотеку обновить наше окошко:
        (sdl2:update-window window)

Учитывая что весь так долго описанный процесс пройдет за доли секунды,
а мы все-таки хотим насладится полученным результатом, попросим sdl подождать немного:
(sdl2:delay delay)
Ну и самое главное:
)))


Вот собственно и все. Осталось запустить наше творение, и надеятся, что мы не вылетим в отладчик:
CL-USER> (main)




На всякий случай,
исходник полностью
(defparameter *screen-width* 640)
(defparameter *screen-height* 480)

(defun main (&key (delay 2000))
  (sdl2:with-init (:video)
    (sdl2:with-window (window :title "SDL2 Window" :w *screen-width* :h *screen-height*)
      (let ((screen-surface (sdl2:get-window-surface window)))
        (sdl2:fill-rect screen-surface
                        nil
                        (sdl2:map-rgb (sdl2:surface-format screen-surface) 255 255 255))
        (sdl2:update-window window)
        (sdl2:delay delay)))))



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

Для нетерпеливых, еще раз ссылка на код туториалов (на момент написания статьи 16 штук):
github.com/TatriX/cl-sdl2-tutorial

Надеюсь это было интересно и познавательно.
Нужно ли больше лиспа на хабре?

Проголосовало 158 человек. Воздержалось 22 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (31)


  1. semenyakinVS
    22.01.2016 14:48

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

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


    1. TatriX
      22.01.2016 15:08

      Common Lisp обладает довольно мощной объектной системой. Вообще, его сложно назвать функциональным. Он поддерживают несколько парадигм, и функциональная далеко не главная.

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


      1. semenyakinVS
        22.01.2016 15:30

        Хм… Но ведь, как я понимаю, оригинальный lisp создавался как функциональный язык, с соответствующими для этого подхода целями. Объектное программирование и в прологе есть… Но зачем? Lisp, насколько я слышал, хорошо подходит для разбора текста, для обработки иерархических данных, для программирования искусственного интеллекта (как иерархической структуры). В эту сторону он развивается, в этом его задача. И использовать его для других целей — всё равно что шуруп молотком забивать.

        В данный момент мне кажется, что создавать на нём игры — это всё равно, что, например, на С++ сайты писать или даже всё равно, что на visual basic для excel тетрис делать. Если я ошибаюсь — взглянул бы на примеры сколь-нибудь серьёзных проектов на lisp, интересно как сложную игровую механику возможно в скобочки упаковать.


        1. TatriX
          22.01.2016 15:40

          Лисп лиспу рознь. Лисп это не какой-то конкретный язык, а целое семейство довольно разных языков объединенных некоторыми общими свойствами.
          Скажем тот-же Scheme гораздо более функциональный чем Common Lisp.

          Вообще насколько я знаю сейчас самый популярный диалект лиспа это Clojure.
          И чего только на нем не пишут: github.com/search?utf8=%E2%9C%93&q=stars%3A%3E1+language%3AClojure&type=Repositories&ref=advsearch&l=Clojure&l=


        1. EvilBlueBeaver
          22.01.2016 16:05

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


          1. TatriX
            22.01.2016 16:10
            +1

            Сообщество очень маленькое, популяризаторов нет, вот и не пишут. (http://lispgames.org/)

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

            Про игры на лиспе есть целая книжка, кстати: landoflisp.com


            1. roman_kashitsyn
              22.01.2016 17:57

              А как, кстати, обстоят дела с установкой игр, написанных на CL, на пользовательские машины?
              Насколько я знаю, раньше тот же SBCL тянул в бинарный файл весь образ лисп-машины, от чего бинарники, выводящие «Hello, World», получались размером с мегабайт 40. Конечно, в современных компьютерных играх 40мб — это мало, но всё же…


              1. TatriX
                22.01.2016 18:32

                Сейчас все обстоит так же. Разве что теперь есть roswell который упрощает процедуру.

                Если размер бинарника важен, можно воспользоваться другой реализацией. Кажется, CLISP умеет делать маленькие бинарники.

                Впрочем, лично мне кажется что статическая линковка вполне себе хорошая идея для дистрибуции.


                1. Warlock_29A
                  22.01.2016 19:50

                  Можно так же обратить внимание на ECL.


          1. PaGrom
            22.01.2016 16:25

            Почему никто не использует для меня самого загадка, видимо думают, что сложно.

            Как это никто? www.naughtydog.com/docs/Naughty-Dog-GDC08-Adventures-In-Data-Compilation.pdf


          1. Rezzet
            29.01.2016 03:19

            Можно и не на лиспе запилить DSL для игровой механики, беда в том что и этого никто не делает, или мне не попадалось, если есть хорошие примеры с радостью бы на них поглядел, потому что мы уже пол года пилим свой DSL для таких целей и хотелось бы посмотреть как люди решают вопросы которые у нас возникают, если надо могу рассказать свой опыт. Пример того что на данный момент достигли. Решение фаториала. Пусть вас не пугает слово quest изначально планировалось на нем квесты писать, хорошо бы заменить на superblock или state
            block Step(current, value)
            {
            exit mul
            {
            condition: current!=0
            action: add(current, -1) as newCurrent, mul(value, current ) as newValue
            return: newCurrent, newValue
            }

            exit finish
            {
            condition: current == 0
            return: value
            }
            }

            block ShowResult(result)
            {
            exit show
            {
            action: print(«result:»), print(result)
            }
            }


            quest Begin()
            {
            start: Step(5, 1 )

            exit finish

            Step
            {
            mul -> Step(::newCurrent, ::newValue)
            finish -> ShowResult(::value)
            }

            ShowResult
            {
            show->finish
            }
            }


    1. fshp
      22.01.2016 16:17
      +2

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


      1. semenyakinVS
        22.01.2016 17:59

        Да. Но игры итеративны как правило, существуют в рамках потока событий, обновляясь от кадра до кадра… Впрочем, возможно, я совсем не понимаю в функциональном программировании. Исходя из универских лекций (да, я только оттуда знаю и из общения с однокурсником), мне всегда казалось что оно не очень-то дружит с циклами и с хранением каких-либо состояний в течение обработки. С передачей дальше на обработку — да. А вот с интеративной обработкой и событиями — не очень.


        1. dezconnect
          22.01.2016 19:47

          Зато хорошо дружит с рекурсиями ;)


          1. semenyakinVS
            22.01.2016 20:04

            Вы имеете в виду имитацию цикла с помощью рекурсии? Если да — то ведь это и есть эмуляция одного подхода с помощью другого подхода непонятно зачем.


            1. Warlock_29A
              22.01.2016 20:29

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

              Вам выше уже указали, что лисп (конкретно я говорю про Common Lisp) не является чистым функциональным языком программирования, как например Хаскелл. На CL вы можете писать полностью в императивном стиле, со всеми вытекающими.


              1. semenyakinVS
                22.01.2016 21:02

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

                По поводу того, что lisp уже не является чисто функциональным я понял, комментарий читал. Но, тем не менее, мне непонятно зачем вводить разработку игр в перечень задач для языка (ладно, пусть языка, расширенного поддержкой других парадигм), который на это не было ориентирован изначально и у которого, как мне кажется, исторически сложился другой перечень задач… Нет, как, догадка есть: видимо потому, что есть программисты, которые знают lisp и хотят писать игры. Но:
                — во-первых, нужно смотреть сколько таких программистов.
                — во-вторых, нужно понять почему этим программистам трудно будет выучить какой-то из языков, который исторически использовался для разработки игр.
                — в-третьих, нужно оценить сколько кода придётся портировать (байндить, скорее всего) на lisp с других языков.
                — ну и, наконец, последнее: мне кажется что программисту на lisp, который привык в основном использовать функциональную парадигму (иначе не ясно зачем он использовал именно lisp) всё равно придётся переходить на другие парадигмы в рамках того же lisp — что, как мне кажется, по усилиям не намного сложнее изучения какого-нибудь несложного языка из используемых для разработки игр раньше.

                Исходя из этих размышлений, использование lisp как языка для программирования игр, как на меня, не самая лучшая идея. Эти усилия было бы эффективные потратить на какие-то улучшения lisp для задач, на которые он ориентирован.
                А если про геймдев — тот же C# выучить можно за неделю — и сразу делать игры на Unity. Это точно намного легче, чем специально делать Unity на lisp.


                1. TatriX
                  22.01.2016 21:08

                  Common Lisp как и C#, как и C++ языки общего назначения. А потому говорить о «сложившемся перечне задач» некорректно.
                  Все равно что говорить: «На си нужно писать только ядра операционных систем».


                  1. semenyakinVS
                    22.01.2016 22:01

                    Я не говорил «только». Я перечислил четыре пункта, исходя из которых сделал для себя вывод, что lisp не особо походит для написания игр.


                1. Warlock_29A
                  22.01.2016 21:28

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

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

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

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


                  1. semenyakinVS
                    22.01.2016 21:59

                    Куда лучше тратить свои усилия тоже думаю каждый сможет определить для себя сам.


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

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


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

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


                    Это да. Если статья писалась с такой целью — конечно. Я, например, чуть больше узнал благодаря этой статье и чтению вики в процессе дискуссии.


    1. lair
      23.01.2016 14:11

      Вот если серьёзно, зачем использовать функциональный язык для программирования игр?

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

      (что характерно, это не мешает декомпозиции игры на сущности с поведением)


    1. ForNeVeR
      25.01.2016 08:16

      А почему вы называете Common Lisp функциональным языком? Мне вот это не кажется верным — скорее уж Scheme с более функциональным уклоном, а CL как раз с более императивным.

      Помимо прочего, «стандартная» ООП-система без множественного наследования не очень хорошо позволяет описывать сущности во многих играх, и поэтому в частности OpenRA использует систему из паттерна «поведение» и композиции этих «поведений» в игровых объектах. На функциональном языке это также прекрасно можно смоделировать без какого-либо онтологического кризиса.


      1. semenyakinVS
        25.01.2016 11:47

        А можете привести пример, как бы мог выглядеть код с использованием (имитацией) прототипного программирования для описания игровых классов в lisp? Интересуют такие вещи, как реализация полиморфизма (так как в играх объекты очень разнотипные бывают, и при этом все существуют на сцене), инкапсуляция (так как объекты бывают огромные и хочется чтобы сам язык разделял данные на слои в смысле прав на работу с ними)… ну, наследование должно быть очевидно, если заявляется объектная парадигма.

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


        1. ForNeVeR
          25.01.2016 12:18

          Если под «прототипным программированием» вы имеете в виду модель ООП наподобие той, которая используется в JS — то нет, CLOS такое не поддерживает напрямую (конечно же, можно что-то эдакое кастомное изобрести, но мне не видится практического смысла в подобных экзерсисах).

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

          Да, объектно-ориентированный код на CL не отличается радикально (на мой взгляд) от других языков, поддерживаемых множественное наследование — например, от Clojure или Scala. Но никаких сложностей с реализацией, например, двойной диспетчеризации вы не испытаете — а в то же время в мейнстримных компилируемых языках это целая проблема.


      1. TatriX
        25.01.2016 12:40
        +1

        Туповатый пример на коленке
        ;;; http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html
        
        (defpackage #:ultra-game
          (:use :common-lisp)
          (:export :say-my-name :ship))
        
        (in-package :ultra-game)
        
        (defclass point ()
          ((x
            :initform 0
            :accessor point-x)
           (y
            :initform 0
            :accessor point-y)))
        
        (defclass entity (point)
          ((name
            :initarg :name
            :initform (error "Must supply a name")
            :accessor entity-name)))
        
        (defgeneric say-my-name (entity)
          (:documentation "Print entity name to the *standard-output*"))
        
        (defmethod say-my-name ((entity entity))
          (format *standard-output* "My name is ~a~%" (entity-name entity)))
        
        
        (defclass engine ()
          ((angle
            :initform 0
            :accessor engine-angle)
           (acceleration
            :initform 9000
            :accessor engine-acceleration)))
        
        (defclass ship (entity engine)
          ((cost
           :initform 0)))
        
        (defmethod say-my-name ((ship ship))
          (with-slots (name acceleration) ship
            (format *standard-output* "Colonial spaceship ~a; Acceleration: ~a~%" name acceleration)))
        
        ;;; usage example
        
        (in-package :cl-user)
        
        (let ((ship (make-instance 'ultra-game:ship :name "Foo")))
          (ultra-game:say-my-name ship))
        
        ;; output:
        ;; Colonial spaceship Foo; Acceleration: 9000
        ;; My name is Alarm!
        
        
        ;; note '::'
        (let ((entity (make-instance 'ultra-game::entity :name "Alarm!")))
          (ultra-game:say-my-name entity))
        
        ;; output:
        ;; My name is Alarm!
        
        


        1. semenyakinVS
          25.01.2016 14:02

          Спасибо. Здесь можно уже предметно говорить. По порядку:

          1. В примере вообще не понятно как в lisp задавать агрегацию (у меня этот вопрос не звучал — но всё равно непонятно). Из-за этого point является базовым классом для entity, или это просто кривой пример? Как сделать так, чтобы entity мог содержать несколько point, задающих векторные характеристики entity — ускорение, скорость, и.т.д.
          Такое же странное место вот тут — defclass ship (entity engine). Почему корабль наследует двигатель и сущность? Сущность-то правильно — но двигатель? Должна быть агрегация, а не наследование.

          2. В приведённом примере не ясно как будут выглядеть сколь угодно сложные алгоритмы в реализациях методов — с проверками и ветвлением алгоритмов. Как я понял, в скобках при defclass за именем класса указывают базовые классы (defclass ship (entity engine)), а метод объявляются вне классов. Вот пример метода (отсюда), с одним ветвлением:

          Код на common lisp
          (defmethod initialize-instance :after ((account bank-account) &key)
          (let ((balance (slot-value account 'balance)))
          (setf (slot-value account 'account-type)
          (cond
          ((>= balance 100000) :gold)
          ((>= balance 50000) :silver)
          (t :bronze)))))


          1. TatriX
            25.01.2016 14:08
            +1

            Нравится писать в императивном стиле — пишите.


          1. ForNeVeR
            25.01.2016 14:20

            И снова вы пытаетесь высказывать какие-то, простите, маргинальные идеи: «функционального прошлого», «мучится со скобками». Риторический вопрос: а зачем мучиться с C-подобными фигурными скобками и стилем оформления, унаследованным от императивного прошлого?

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

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

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


            1. semenyakinVS
              25.01.2016 15:57
              +1

              Вы как-то эмоционально восприняли то, что я написал… Под функциональным прошлым я не подразумевал что функциональный подход это нечто из прошлого, а имел в виду что сам язык common lisp образовался из функционального языка lisp (так же как, например, С++ образовался от С) и потому унаследовал часть его особенностей организации кода. Про «мучения» я говорил в контексте людей, которые переходят на common lisp с других языков с целью, например, написания игр — людей вроде меня. Для меня было бы мучительно (возможно, только в начале — но тем не менее) парсить глазами концы выражений вроде: (t :bronze))))) с шестью скобками в конце при наличии логики двух проверок в рамках этих выражений.

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

              Думаю, эту дискуссию стоит закончить… В общем и целом, я не особо имел право её начинать. У меня не было серьёзного работы с common lisp (кроме того, что я успел почитать в течение дискуссии и лабораторных в ВУЗе). Ко всему, я, кажется, единственный, кто имеет подобную точку зрения среди комментаторов статьи — поэтому трудно оценить объективность моих взглядов. Ещё раз просите если что не так написал.