Недавно участвовал в конкурсе IGDC, писал игру на DEmbro. Это был достаточно интересный опыт использования на практике своего языка.
Сразу оговорюсь, что в DEmbro полно недоделок, он глючный, и я плохо на нём программирую :)
Средства разработки
DEmbro и текстовый редактор Vim. Выглядит это как-то так:
Из средств отладки — печать значений на консоль, комментирование подозриетльных фрагментов кода, и возможность без запуска всей игры загрузить какой-то отдельный модуль в REPL режиме и быстро посмотреть как себя ведут команды.
Коротко о языке
Я использовал (за исключением нескольких незначительных мест) только три типа: целое число, указатель и строка. Хотя типов как таковых в DEmbro нет. Все операции производятся на стеке, для чисел и указателей один стек, для строк — отдельный. Круглые скобки используются для многострочных комментариев, а для однострочных — общепринятое «//».
Несколько примеров:
1 // положить на стек число 1 2 // положить на стек число 2 3 4 5 // положить на стек числа 3, 4 и 5. Теперь стек выглядит так: 1 2 3 4 5 + // сложить два верхних числа. Теперь стек выглядит так: 1 2 3 9 * // умножить два верхних числа. Теперь стек выглядит так: 1 2 27 swap // поменять два верхних числа местами, 1 27 2 div // нацело поделить два верхних числа, 1 13 max // из двух верхних элементов оставить максимальный, 13 1+ // увеличить верхнее число на 1, получится 14 drop // скинуть верхний элемент со стека, теперь стек вернулся в начальное состояние |
Можно писать свои команды при помощи конструкции
: name ..... ; |
где name — имя команды.
(Именем может быть любая последовательность символов без пробелов. Никаких ограничений на имя нет (можно переопределять стандартные слова).)
Например, в программе всё измеряется в миллесекундах. Но иногда приходится задавать время в минутах. Чтобы задать пять минут, нужно 5 умножить на 60*1000, и получится соответствующее число миллисекунд. Чтобы делать это вычисление автоматически, можно написать команду
: mins 60000 * ; |
Теперь можно просто записать
5 mins |
и получить время в миллесекундах для пяти минут.
Команды можно выполнять косвенно. При помощи символа апостроф, за которым идёт название команды, можно положить указатель на эту команду. А при помощи команды execute выполнить указатель с вершины стека. Т.е.
5 ' mins execute |
эквивалентно коду «5 mins».
Можно создавать анонимные команды при помощи команды «:noname»:
:noname ." Hello world" cr ; |
После выполнения этого кода на стеке будет лежать указатель на команду, который можно скормить команде execute на выполнение. Так, например:
:noname ." Hello world" cr ; execute |
Пара примеров на работу с указателями и переменными:
variable x // создаёт область памяти под целое число или указатель // теперь вызов команды x кладёт на стек указатель на эту область памяти 5 x ! // записать число 5 в переменную x x @ // положить в стек значение переменной x . // распечатать на консоль полученное число x off // записать в переменную x ноль |
Ещё есть условный оператор и циклы.
: printbool if ." TRUE" else ." FALSE" then cr ; // создаём команду printbool // она снимает со стека верхний элемент, и если он ноль, то печатает FALSE // иначе TRUE false printbool // напечатает FALSE true printbool // напечатает TRUE 100 10 > printbool // напечатает TRUE 100 10 < printbool // напечатает FALSE 100 0<> printbool // напечатает TRUE // печатает числа от 0 до 10 :noname 0 begin dup 10 <= while dup . 1+ repeat drop ; execute |
Условные операторы и циклы являются компилирующими командами, и потому их можно использовать только внутри описания команд.
Сказанного должно быть достаточно для понимания того, что будет ниже. Чтобы не затягивать вступление до бесконечности, дам напоследок несколько ссылок
- Пишущееся вики
- Файл с реализацией условного оператора и простых циклов
- Файл с реализацией оператора switch
- Файл с реализацией работы с пространствами имён
Кратко об игре
Лучше один раз увидеть, чем сто раз услышать, так что отсылаю читателя самому сыграть в игру (в конкурсном архиве запускать надо Doj\mazejourney\release\run.bat). Я написал на DEmbro создание окна и инициализацию OpenGL достаточно давно. Т.к. числа с плавающей точкой в DEmbro немного сыроваты (в частности, нет нормального способа передать double в виде параметра в dll функцию), я решил для упрощения жизни их не использовать вообще. Кроме того, я не писал ни поддержки текстур, ни каких-то хитрых примитивов. Единственное, что я использовал при рендере — цветные квадраты. Игру я писал с девизом «каждый уровень имеет свой уникальный геймплей». Поэтому нужно было иметь удобные конструкции для описания уровней.
Как описывается уровень
Чтобы не томить любителей ковыряться самостоятельно, рекомендую посмотреть файл «game\levels\big.de» — он является очень простым примером того, как описывается уровень. Кстати, по-поводу этого уровня, в нём можно усмотреть букву O — это чит, позволяющий пройти в определённом месте сквозь стену и оказаться у выхода. Итак, первым делом я написал команду lab, которая позволяет в наглядном текстовом виде описывать уровни. Например, так:
lab ########## #S # # # # # # # # # # F# ########## \lab |
:noname pchar" data\sounds\newlevel.wav" sound ;init |
:noname drop drop ;enter_ # |
:noname passed ;enter_ O |
: draw_# COLOR 2dup PURPLES GEN[]2 ^ set-color draw-cell-rect ; :noname draw_# ;draw_ # :noname draw_# ;draw_ O |
:noname player-y! player-x! ;oninit_ S |
Уровень в темноте (dark.de)
На этом уровне игрок видит только соседние с собой клетки. Нужно просто при рисовании стены посчитать расстояние игрока до стены, и если оно больше одного, то не рисовать стену:
: 2dup player-y @ - abs swap player-x @ - abs max 1 > if drop drop exit then COLOR 2dup PURPLES GEN[]2 ^ set-color draw-cell-rect ;draw_ # |
Уровень-цикл (loop.de)
Тоже уровень в темноте, в котором если ходить по часовой стрелке, то будешь бегать по кругу, а если пойти против часовой стрелки, то прийдёшь к выходу. Реализация простая: описываются при помощи команды «lab» три лабиринта — тот, который изначально, тот, который с циклом, и тот, который с выходом. Изначальный нужен, чтобы закрутить игрока идти по часовой стрелки, его мы устанавливаем в уровне командой «maze!». Остальные два сохраняем в переменные. Вот, например, цикличный лабиринт:
lab ####### #1 # ###### ### # # # # # # ###### # # # # # # # # # # # ######## # # 2 # ############ \lab last-lab-x value 1maze-x last-lab-y value 1maze-y last-lab-maze value 1maze-ptr |
:noname passed 1maze-x maze-x ! 1maze-y maze-y ! 1maze-ptr maze-ptr ! ;enter_ 1 :noname passed 2maze-x maze-x ! 2maze-y maze-y ! 2maze-ptr maze-ptr ! ;enter_ 2 |
Уровень с убегающим выходом (runningexit.de)
Алгоритм убегания выхода простой. Если игрок приблизился на близкое расстояние по обеим координатам, то выход смещается. Выбирается координата, по которой расстояние до игрока больше, после чего выход смещается от игрока по этому направлению. Если это не удаётся (стена), пытаемся сместить выход в перпендикулярном направлении. Поэтому, например, если игрок идёт на выход по узкому коридору, то выход будет убегать по этому коридору от игрока, и свернёт в бок, как только упрётся в стену. Там используется событие after-update для всех этих расчётов и рендера выхода (рендер сделан отдельный, потому что выход при смещении с ячейки на ячейку должен смещаться плавно). Когда выход смещается, в лабиринте перезаписывается его расположение, поэтому событие попадания в выход обрабатывается корректно обычным образом.
Уровень с двигающимися стенами (movingwalls.de)
Идея аналогична уровню-циклу. Я создал два лабиринта: главный прямоугольник с выходом, и лабиринт со штуковиной. И сохранил его в переменные (moving-x, moving-y, moving-ptr). Далее, я переопределил события рендера и входа для пробела:
: draw#brown COLOR 2dup BROWNS GEN[]2 ^ set-color draw-cell-rect ; :noname 2dup moving-x * + player-y @ + cells moving-ptr + @ 35 = if draw#brown else 2drop then ;draw_ :noname 2dup moving-x * + player-y @ + cells moving-ptr + @ 35 = if 2drop else passed then ;enter_ |
Заключение
В целом, разрабатывать игру мне очень понравилось.
Разработка игры длилась суммарно около 10ти дней, большинство из которых я ещё и ходил на работу. При этом первая половина этого времени была потрачена на написание основного функционала, а вторая — на придумывание и реализацию уровней.
1 комментарий:
Ты маньяк! Но я это уважаю :)
Отправить комментарий