Game Maker Language и так называемые Настоящие Языки Программирования

Game Maker Language и так называемые Настоящие Языки Программирования

или

Скриптинг против кодинга, разработчики игр против программистов, мёд против пчёл

а также

Среда разработки Game Maker против MS Visual Studio/SharpDevelop/CodeBlocks/Eclipse и, кажется, какиЕ-то ещё IDE, которыми я пользовался мало, и названия которых я забыл

Важное замечание: в этой статье я говорю о том GML, который был в Game Maker 8.1, потому что к GML в Game Maker Studio не относится добрая половина этого текста (причём та половина, которая описывает его уникальные возможности — уже, как понимаете, бывшие). Также я говорю об IDE той же версии 8.1, которая с 2011 года изменилась по большей части в сторону кросс-платформенных настроек, поддержки костной анимации и чего-то совсем уже отстранённого от её настоящих проблем. Таковые я в этом тексте упомяну в деталях, дабы попытаться развеять несуразное распространённое мнение о том, что в GM уже "всё за вас сделано".

Прежде всего, хочу заявить, что GML — весьма недооценённый язык программирования для создания игр, использования всей мощности которого я не видел ещё ни в чьём исполнении. Я не имею в виду, что он позволяет сделать любую игру за три строки кода, и также я не имею в виду, что его синтаксис позволяет программировать игру как-то особо быстро. Однако посмотрите, что есть в GML:

1. Интерпретатор кода на лету

Если вам заранее неизвестно, какой конкретно код вам нужно будет выполнить в какой-либо момент игры, вы можете собирать его динамически, прописав соответствующие инструкции, и выполнить его через execute_string. Вы также можете читать и выполнять код из внешних файлов, и даже вовсе не хранить никакого кода внутри проекта. С таким подходом любой файл с GML-кодом является по смыслу чем-то вроде DLL, только не надо возиться ни с заголовочными файлами (C/C++), ни со сборками (C#). DLL при играх по большей части используются для громоздких и сложных вычислений, или рендеринга, а GML для вычислений не приспособлен, и справляется с ними довольно медленно. Тем не менее, для своих внутренних задач такие фрагменты повторно используемого кода вполне могли бы быть полезны.
Но есть и ещё один важный аспект — игра, которая читает свой код из внешних файлов, легчайше поддаётся созданию модов к ней. То есть если игрок знает GML, он может легчайше создать своё дополнение к совершенно чужой игре, закинув пару спрайтов и звуков в соответствующую папку, и написав некоторое количество GML кода. Если же игрок сам делает игры на GML, он может вообще запустить любой объект своей любой игры в игру, в которую он играет. Естественно, его объекты игровой логики будут взаимодействовать только с его же собственными объектами и понятиями, но если обе игры одного жанра, то поменять имена в коде не составляет большого труда.
Вы слышите, что я говорю? Если бы люди сообща использовали GML таким образом, они могли бы совместить все существующие скролл-шутеры, платформеры, стратегии любых видов, аркады, и так далее (естественно, каждый жанр со своим жанром), создав некие супер-игры каждого из жанров, в которых можно играть за всех игроков и побеждать всех врагов и боссов, перемешивая между собой всё оружие, спецспособности и методы взаимодействия. Что до игр по сети, то поддержку сети пришлось бы написать всего лишь один раз, и все эти супер-игры к тому же были бы ещё и сетевыми. Поддержка джойстика? Та же история. Переназначение клавиш и устройств управления? Та же история. Редактор уровней? Настройки сложности? Система сохранения/загрузки? Ну вы поняли.

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

Единственный случай, когда я увидел использование внешних файлов с целью описания игровой логики, принадлежит перу того, кто ныне известен как SaintHeiser. Этот проект должен был переиграть механику Megaman Battle Network на новый лад, но так и не взлетел после прототипирования, а сама идея расшаривать код во внешние папки была отброшена навсегда.

2. Механизм добавления событий

С помощью встроенной функции object_event_add вы можете добавить любое событие в любой объект игры. На деле это значит, что ваши объекты по ходу игры могут получать такие возможности взаимодействия, которых изначально в них запрограммировано не было, а в теории — даже и не предсказанные вами изначально, например — полученные в результате симулированного генетического отбора. Кроме "обычного" поведения объекты GM могут иметь мета-поведение, регулирующее правила видоизменения их текущего поведения.
Естественно, этот концепт настолько экспериментален, что его так никто и не использовал, хотя это первое, что приходит в голову, когда узнаёшь про такую возможность GML. Соединяя это с упомянутыми выше возможностями интерпретации кода на лету, получаешь вообще немыслимый комбинационный взрыв вариантов взаимодействия не только внутри одной игры, но внутри серии игр, объединённых неким языком или стандартом взаимодействия. Можно запрограммировать игру, которая является неким хабом игр, и каким-то образом соединяется с другими играми, запущенными в этот момент, которые осуществляют конструктивные либо деструктивные действия по отношению друг к другу. Почему бы не заставить сами игры сражаться друг с другом? Слишком абстрактно? Эх, какие вы зануды.

Используя event_perform_object можно также заставить объект выполнять события совсем другого объекта как свои собственные, а функции variable_*_exists позволяют проверять, существуют ли указанные переменные в текущем контексте выполнения. То есть, если переменной нет, мы её можем всё равно объявить прямо на месте, и она появится.

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

Однако, на этом все аж два пункта неиспользованных возможностей заканчиваются, и начинается длинный ряд недостатков GML и его родной IDE, которые рано или поздно были с распростёртыми объятиями встречены всеми серьёзными разработчиками на GML, и которые ложатся серьёзным грузом на чашу весов, на другую чашу которых народной молвой поставлен, приклеен, прикручен и приварен аргумент "всё сделано за вас".

Почему GML уродлив?

Нет пользовательских структур данных

Уточняю — "пользовательских", потому что встроенные структуры данных есть, и включают в себя: стек, очередь, список, карту (словарь), очередь с приоритетами и некую сетку, смысла применения которой я так и не понял, и уверен, что никому она в GML не нужна.
Символ точки в коде GML используется (помимо очевидного разделения целой и дробной части вещественных констант) только для адресации переменных объекта. Методов у объекта GM нет, и к событиям объекта обращаться тоже нельзя через точку, но "вызывать" их можно через event_perform_object.
Можно имитировать структуры данных, создавая свои объекты, которые будут хранить ту или иную структуру, и действительно адресовать их составляющие через точку, однако всякий объект GM автоматически содержит в себе все встроенные переменные, касающиеся его отображения на экране, двухмерной физики перемещений и столкновений, использования путей и временных линий событий, и вроде чего-то ещё. Как следствие, такая имитация будет неслыханно тормозить, если этих объектов понадобится много.
Вообще-то огромное количество встроенных переменных и функций выглядит так, словно там вместо прочерков должны быть точки. Возьмём, например, переменные фонов:

background_color Background color for the room.background_showcolor Whether to clear the window in the background color.

background_visible[0..7] Whether the particular background image is visible.

background_foreground[0..7] Whether the background is actually a foreground.

background_index[0..7] Background image index for the background.

background_x[0..7] X position of the background image.

background_y[0...7] Y position of the background image.

background_htiled[0..7] Whether horizontally tiled.

background_vtiled[0..7] Whether vertically tiled.

background_xscale[0..7] Horizontal scaling factor for the background. (This must be positive; you cannot use a negative value to mirror the background.)

background_yscale[0..7] Vertical scaling factor for the background. (This must be positive; you cannot use a negative value to flip the background.)

background_hspeed[0..7] Horizontal scrolling speed of the background (pixels per step).

background_vspeed[0..7] Vertical scrolling speed of the background (pixels per step).

background_blend[0..7] Blending color to use when drawing the background. A value of c_white is the default. Only available in the Standard Edition!

background_alpha[0..7] Transparency (alpha) value to use when drawing the background. A value of 1 is the normal setting; a value of 0 is completely transparent.

То есть то, что в широко принятой практике было бы адресовано как Background[i].Visible, здесь выглядит несколько мутировавшим — у нас не массив заданных структур, а массив переменных структур без описания самих структур. И все аналогичные "структуры данных" тоже должны использоваться именно так, в виде слабо связанных наборов переменных. Впрочем, даже если бы в GM было иначе, следующая проблема (про точки) никуда бы не делась.
Встроенных типов данных в GML всего два: числа и строки. В связи с этим никогда не возникнет проблем с приведениями целого числа к вещественному, либо наоборот.
null отсутствует как таковой, но есть ключевое слово noone, обозначающее "никакой объект", а instance_exists позволит удостовериться в существовании объекта.
И нет, нельзя передавать в функцию вычисления расстояния объект точки в пространстве, нужно передавать отдельно каждую их координату. Впрочем, это невозможно ещё и потому что в GM нет указателей. При том что можно обращаться к любому объекту через его имя (строку) либо идентификатор экземпляра, указателей никаких не существует.

Нет подсказок после точки

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

Ограниченные массивы

Нельзя использовать индексы более 32000, индексов может быть не более двух, и весь массив не может содержать более 1000000 элементов. Вложенных или ступенчатых массивов — не существует здесь (впрочем, это — проблема отсутствия пользовательских структур данных, описанная выше). Это искусственное ограничение обычно не портит никому жизнь, но только до тех пор, пока не понадобится использовать третье измерение (причём опять же, если вам достаточно 30x30x30, это легко, но дальше вас ждёт алхимия), или хранить поток управления игрока без сжатия, длиной более чем на 10:40==640 секунд (32000/50FPS для примера). Отсюда вырастают странные костыли, начиная с практики использования массива длиной в 1000 в качестве трёхмерного, индексируя его через три десятичные цифры от 000 до 999, и до специфической DLL, позволяющей работать с массивами любых размерностей и объёмов. DLL для поддержки массивов, никаких шуток.

Нет механизма исключений

Всё, что вы можете сделать — это программировать всю игру так, чтобы она работала без ошибок. Вы можете отключить уведомления об ошибках, но если вы относитесь к геймдеву серьёзно, одно другого не лучше. Кстати, я был удивлён, когда коммерческий проект, Hotline Miami, швырнул на экран ошибку, выдающую его GM'овское происхождение.
Однако, и обнаружение ошибок в GM имеет свои недостатки. Если перед наступлением ошибки был изменён контекст вызова, отладчику до этого нет никакого дела, он помнит только начальную точку вызова, а дальше — разбирайтесь сами. Для чайников: если вы сделали execute_string, внутри которого ошибка, то отладчик вам укажет только на сам execute_string, начисто забыв про его аргументы, где-то в глубине которых (а возможно и в глубине функции, которая вызвана ещё и дальше оттуда) и находится причина ошибки.

Нет статических объектов

Конструкция вида static.variable (названия объекта и переменной могут быть на самом деле любыми, кроме зарезервированных для обычных названий) позволяет обратиться к первому найденному объекту static, который будет единственным, если его только один раз создать, и будет существовать в пределах всей игры, если ему выставить свойство постоянства (Persistent); так что разницы со статическими объектами C++/C#/Java в этом смысле — никакой. Технически это не проблема GM, но надобность создавать экземпляр объекта, который по сути является статическим и используется как статический — это в терминах ООП некорректно. Впрочем, вне зависимости от проекта существует постоянный контейнер global, в который можно записывать что угодно и который даже не является объектом, и поэтому не пытается куда-то перемещаться или рисоваться на экране, как болванки, имитирующие структуры данных, описанные выше; однако событий содержать не может, так как, повторюсь, не объект — но вообще-то для этого есть скрипты.

Нет именованных параметров у функций (в случае GML — скриптов)

Все параметры именуются через argument0, argument1, ... argument15 (в ранних версиях GM их было меньше). Конечно, можно переприсваивать значения переменным со своими названиями, делая что-то в духе x=argument0; y=argument1;, но это чревато снижением производительности при каждом вызове. Почему-то так часто в программировании: либо что-то работает быстро, либо что-то имеет понятный исходный код. И это логично в ассемблере, но не в GML — хватило бы одного стандарта, предписывающего, что скрипт GM должен начинаться со строки вида //name1 name2 name3 ... и IDE распознавала бы эти имена, а перед компиляцией просто-напросто заменяла все их на те самые argumentNN, и этой проблемы бы не было.

Нет множественного наследования

Обычное наследование с переопределением событий есть, но — никаких интерфейсов или утиной типизации; каждый объект выполняет только то, что сам содержит, либо наследует от одного конкретного объекта. Если вы хотите максимально разграничить способности в игре, то объект в GM не может просто так бегать, стрелять и плавать: он может унаследовать одно из этих действий, лично имплементировать второе, и всё. Но это только если программист пытается действовать по законам Настоящих Языков Программирования. А с другой стороны, как я уже говорил, объект GM может выполнить событие любого другого объекта того же проекта. Это даже не утиная типизация, это полная анархия, которую может грамотно использовать хорошо программирующий человек. Попытки использовать это кривыми руками, в отличие от традиционных механизмов множественных наследования в традиционных языках программирования (потому что для апофеоза в последних нужно иметь ещё и кривые мозги), заканчиваются довольно печально. Помножьте это на следующую проблему, и будет совсем жутко:

Нет пространств имён

Если вы хотите писать на GML вдвоём (Втроём? Вы в здравом уме???), вам придётся либо называть объекты и их переменные по заранее оговоренным правилам, например дописывать к их названиям приставки, определяющие автора, или какие-нибудь мнемоники их применения; либо просто надеяться, что ваш объект под именем obj_bonus будет единственным во всём исходнике. Более того, из-за глобальной видимости имён совпадать не должно ни одно имя ресурса ни с одной переменной внутри объектов (включая встроенные), и наоборот, иначе всё придётся переименовывать. Разумеется, вручную. К счастью, совпадения имён ресурсов можно проверить автоматически в IDE, однако с переменными такого нет (напомню: "со стороны IDE невозможно предугадать, по какому маршруту выполнения кода пойдёт игра"). Отсюда возникло правило хорошей практики на GML: добавлять к незваниям ресурсов приставки, которые дублируют название их категории. Поэтому спрайт будет называться spr_bonus, объект — obj_bonus, а звук — snd_bonus. Это, кстати, не недостаток языка, а именно недоработка IDE, в которой давно пора бы это автоматизировать.

Шутка:

— Почему профи C++ не любят GML?
— У них не получается написать заголовок цикла.

Пояснение:

В GML нет конструкции for (i=0; i<length; i++), так как нет оператора ++.

Следует писать i+=1.