⚠️ Обращайте внимание на даты.
Этот блог больше не ведётся с 17 января 2023, и на тот момент с написания этой страницы (11.06.2013) прошло 9 лет.
На настоящий момент мы знаем, что такое переменные, умеем присваивать в них данные, умеем вызывать функции. После этого логично рассказать о языковых конструкциях, вроде if
, while
, with
, for
и switch
- но мы споткнёмся об отсутствие данных, которые нам придётся обрабатывать.
Посему, этот урок посвящён некоторым мелочам, которые я упустил в предыдущих уроках, но которые понадобятся нам в следующих. Разговор пойдёт о том, что числами можно выразить не только количество, а также о том, как планировать код, чтобы в нём не запутаться.
Мелочей вышло… много. Каждая языковая конструкция требует некоторого понятия, которое я ещё не трогал. И я пока не хочу швыряться фразами вроде “воспринимайте это, как заклинание”, потому что это сбивает с толку учащихся - от “заклинания” порой тяжело отвыкнуть.
Рекомендации к названиям
Откройте справку и просмотрите указатель. Заметьте структуру, согласно которой выстроены почти все названия функций и переменных. Написаны маленькими буквами, латиницей, между словами ставится _
, и слова названия перечисляются, как правило, в порядке раздел_подраздел_действие. Пусть в этом списке не всегда есть подраздел, а иногда встречаются названия из более чем трёх слов - названиями таком формате удобно пользоваться. Чаще всего, роль функции можно узнать, просто переведя её название.
Отметьте также, что в названиях не встречаются цифры. Только рядом с ними. Позже узнаете, как так вышло. Сейчас же отмечу только то, чт любое название не должно начинаться с цифры, иначе всё сломается.
Подтипы
Фактически, в любой переменной может быть лишь число или фрагмент текста. Но и то, и другое - ерунда, если мы не придаём этим числам и тексту значения. До настоящего момента числа служили нам “мерилом расстояния” в пикселях, с помощью которого мы двигали объекты по комнате, а в прошлом задании нужно было хранить в таких числах углы наклона. Возможно, вы даже успели попользоваться переменными health
, lives
и score
- ведь для них даже создан набор действий-значков, чтобы с ними было легко обращаться. Там числа используются примерно с той же целью - для игрока они означают количество чего-то.
Но технически, числа и слова - всего лишь данные. И ими можно обозначать далеко не только количества и измеримые величины. И, следуя нуждам GML, мы сейчас изучим ряд способов применять числа чуть иначе.
Это не свойство языка, это свойство сознания разработчика придавать числам иной смысл. Различные виды смысла я здесь буду называть подтипами. У них есть одна неприятная особенность - нет никакого способа однозначно определить, к какому подтипу относится число. Единственный способ знать это наверняка - заранее договориться с самим собой о хранении в отдельных переменных значений известного подтипа. Это не так сложно, как кажется. Ведь нет никакой проблемы хранить в одной корзине только красные яблоки, а в другой только зелёные? Нет..? Ладно, сейчас посмотрим.
Логический подтип
С этим всё довольно просто. Выражение логического подтипа является числом, либо 0
, либо 1
. Причём из-за широкого использования этого подтипа в GML, для него выделены две специальные константы (как переменные, но неизменяемы): true
(истина, при вычислении 1
) и false
(ложь, при вычислении 0
). Константу можно считать “псевдонимом” для конкретного значения.
На практике критерии обычно слабее - выражение считается ложным лишь в том случае, если оно вычисляется в 0
, а иначе истиной. Ходят слухи, что в GameMaker “порог лжи” находится не на нуле, а в районе 0.5
, но это не должно вас интересовать. Использование такой неоднозначной природы чисел в программе ещё ни к чему хорошему новичков не приводило.
Для чего это надо? Это нам понадобится, когда мы будем рассматривать языковые конструкции if
, while
и for
. В них мы, среди прочего, задаём выполняющейся программе вопросы, на которые она должна ответить “да” (true
) или “нет” (false
). Вопрос — выражение, результатом которого является 0 или 1, и “по-научному” это называется “логическое выражение”. В серьёзных языках программирования они вычисляются из специальных переменных, которые могут быть только true
или false
, третьего не дано.
Для логического подтипа существует особый набор операций — “логические операторы”. Их можно понять интуитивно, но я опишу формально, чтобы избежать каких-либо вопросов. В описаниях я предполагаю, что a
и b
- любые выражения.
Сначала - как получить выражение логического подтипа из чего-то ещё? Во-первых, такое выражение может быть возвращено функцией (указывается в справке фразами вроде “returns whether…” и “returns true if…”).
Но из мира операций - сюда попадают все операции сравнения: ==
(равно), <
(меньше), >
(больше), >=
(больше или равно), <=
(меньше или равно), !=
(не равно).
=
тоже сработает в качестве оператора равенства. Но лучше не путаться, где вы его используете для сравнения, а где для присваивания. Используйте ==
.
Если у вас это ну никак не укладывается в голове, можно поступить и наоборот - пользоваться присваиванием в стиле Pascal (a := 2
), и использовать =
для сравнения. Поведение и правила никак от этого не изменяются.
-
Оператор
&&
(применяется, какa && b
) означает “и”. Он вычисляется вtrue
лишь в том случае, еслиa
иb
- неfalse
. Если хоть одно из выраженийa
иb
-false
, то иa && b
-false
. -
Аналогично работает оператор
||
, но он означает “или”, и вычисляется в истину, если любое из его выражений истинно (или не ложно, что неважно, но лучше правило “только 0 и 1” не нарушать). -
Операторы
==
и!=
прекрасно работают и на логическом подтипе. Это всего лишь числа, в конце концов! -
Оператор
!
(употребляется с одним выражением, как!a
) - слово “не” среди операторов. Выворачивает истинность выражения: если оно было истинным, вернёт ложь, иначе истину.
Есть смысл сделать “таблицу истинности” для них. Первые два столбца - исходные данные значения переменных a
и b
, а далее выражения с их участием. Каждая строчка — ситуация, когда a
и b
приняли указанные значения, а результаты выражений из первой строчки будут такими:
a | b | a && b | a || b | !a | !b | a == b | a != b |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 |
В качестве примера “зачем это всё надо”, я приведу парочку выражений, и соберу из них большую, практически полезную колбасу.
Предположим, у нас есть аркада-скроллер, где мы долго летаем космическим корабликом. И, скажем, нам необходимо понять, пора ли выпускать на него новые виды противников. Берём переменную, и с самого старта нашего полёта будем засекать, сколько шагов этот будущий кусок металлолома уже летит. Параллельно с этим будем начислять ему очки за каждого сбитого противника.
Пусть у нас в комнате есть некоторый “генератор противников”, которому надо знать, пора ли ему выпускать некий вид противника. А выпускать мы его будем по следующему критерию: игрок должен либо уже долго выживать, либо проявлять большие успехи на поле боя. То есть, у него должен быть некий минимум очков, или же он должен лететь довольно продолжительное время. Скажем, количество шагов в полёте будем записывать в переменную time
, изначально она 0
, каждый шаг прибавляем 1
.
Запишем условия. Скажем, если за каждого противника начисляется по 10 очков, то если он сбил таких уже полсотни - явно они ему наскучили. С другой стороны, если он не особо активничает, чаще уворачивается, скажем, минуту - пора добавить ему проблем (может, хоть стрелять начнёт?).
Итого. У нас есть условия на score и time.
- Не менее, чем по 50 противников по 10 очков:
score >= (10 * 50)
- Не менее, чем минута времени:
time >= (room_speed * 60)
(гдеroom_speed
- это “шагов в секунду”, системная переменная)
Теперь их надо склеить, чтобы любое из условий срабатывало. Оператор ||
! Для полной уверенности, что вычисление будет в правильном порядке, обернём выражения в круглые скобки и для наглядности добавим if
.
if (score >= (10 * 50)) || (time >= (room_speed * 60))
Страшновато выглядит? Можно воспользоваться свободой GML в расстановке переводов строк и пробелов, и записать это дело так:
Если вас не пугает компактная запись - используйте её. Главное - чтобы вы не запутались в собственном коде. Если вы будете работать в команде с другими программистами, будет уже совсем другая история — нужно будет договориться о том, какой стиль соблюдать при написании: как придумывать названия, как расставлять пробелы и переносы.
Индексный подтип
Почти все ресурсы игры представлены в GML при помощи индексов. Индекс - это некое целое неотрицательное число (0
, 1
, 2
…), по которому GM может найти ресурс.
Это такой подтип чисел, значение которого можно исключительно копировать. Изменять значение этого типа как-либо - очень плохая идея, потому что изменив число, вы не сможете по нему найти ресурс, на который оно ссылалось. Для этого числа важен сам факт его существования.
Не без исключений, конечно. Вы можете делать с ним разные обратимые вычисления. Если прибавить к индексу 1, а потом вычесть 1 - индекс станет прежним, и по нему вновь можно будет найти указанный ресурс. Но нужно быть достаточно осторожным, чтобы не трогать изменённый индекс, поскольку вы рискуете попасть на совсем другой ресурс (это не так страшно) или вовсе несуществующий (это уже фатально).
Также очень плохая идея - пихать индекс одного ресурса в функцию для другого. Это может привести к очень необычным эффектам, поскольку индексы некоторых ресурсов разных видов вполне могут совпадать. К примеру, у sprite0
, object0
и room0
, которые создаются в пустом проекте, индексы наверняка будут одни и те же, но означать каждый из них будет что-то своё, и применять это значение нужно будет соответствующе. Здесь есть где ошибиться. Я даже приведу пример рассуждений, которые видел у кого-то.
Хочется повернуть спрайт у object1
, которому присвоен спрайт sprite0
, что ж, спрайт и повернём: sprite0.image_angle = 70;
Ожидание: поворот sprite0
, присвоенный object1
.
Реальность: поворачивается спрайт у object0
, который мы вообще не трогали. Как так вышло? Очень просто. image_angle
- это переменная объекта, у спрайтов их просто нет. Операция .
применима только к объектам. И этой строчкой был изменён image_angle
объекта с индексом 0
(равному индексу sprite0
). Оказалось, что это object0
, никак, казалось бы, к sprite0 не относящийся. Будьте осторожны с такими моментами.
С этим же связана необходимость никогда не называть различные ресурсы одинаковыми именами. Потому что все названия ресурсов, что вы вводите, можно применять и в коде, и в ваших интересах сделать так, чтобы это было возможно.
Значения переменных индексных подтипов не берутся из ниоткуда. Они есть всего двух видов: в константах и в переменных.
Скорее всего, констант у вас в любом проекте тонна, потому что каждое название спрайта, объекта, комнаты, фона, звука и всего остального - это константа, содержащая соответствующий индекс. Выражение, если угодно. К примеру, у sprite0
(если вы его не переименовывали) значение 0
.
Для примера разберём такую строчку: draw_sprite(sprite0, -1, x, y);
. Согласно справке, этой функции требуются, по очереди: индекс спрайта, индекс кадра в спрайте (или -1, чтобы использовать обычную анимацию) и две координаты положения спрайта в комнате. sprite0 - константа, содержащая индекс спрайта (число индексного подтипа). Это не текст "sprite0"
, и попытка подставить именно в таком виде приведёт к проблемам: система не поймёт, какой спрайт ей использовать. А остальные аргументы - самые обычные числа, обозначающие количества или величины. С ними вы уже должны быть в состоянии разобраться сами.
В переменных такие числа могут оказаться двумя способами. Либо вы переписали их из констант, либо создали новые ресурсы при помощи функций - а возвращают такие функции индекс созданного ресурса. К примеру, sprite_add
, если вы хотите загрузить спрайт извне. Конструкция получится вроде: ваша_переменная = sprite_add(...)
.
Различных видов ресурсов - огромное количество, и в дереве ресурсов слева пишутся далеко не все. Сюрфейсы, структуры данных, системы частиц, типы частиц, излучатели… это всё ресурсы, которые могут быть представлены числами-индексами.
Но немножко особняком среди них стоят объекты. Потому что к индексам объектов применима особая операция: .
. Она позволяет обращаться из кода одного объекта к данным другого. Есть ещё одна особенность: на один и тот же объект ссылается несколько индексов сразу. Каждый объект отзывается на свой id
(о них далее), на индекс своего объекта (object_index
), индексы объектов своих родителей (об этом позже) и ключевое слово all
(на которое отзываются вообще все). Как отзываются — расскажу в уроке по языковым конструкциям.
Чаще всего .
применяют к индексам объектов-ресурсов, и очень часто при этом ошибаются. Скажем, если у вас в проекте есть obj_player
, и вы всегда знаете, что он один, то вы можете спокойно обращаться к его переменным, к примеру, вот так: obj_player.x
.
Но будьте аккуратны. Если таких объектов несколько, то чьи именно данные вы получите, точно сказать нельзя. Вы их получите только у одного из объектов этой разновидности. Несколько объектов нужно как-то различать между собой. Способы есть!
Когда нужных вам объектов существует несколько, стоит использовать другой вариант индексного подтипа - индекс экземпляра (instance index). Экземплярами здесь я называю не разновидности объектов, которые вы создаёте в дереве ресурсов, а отдельные “изделия”, которые вы расставляете в комнате или создаёте уже в процессе игры. В каждом объекте он хранится в константе id
. Для чего это может понадобиться, мы узнаем, когда будем рассматривать with
. Нас больше интересует другое: функция instance_create возвращает индекс экземпляра, который она создала.
Вопрос прежний: “нафига нужно”. Предположим, что у нас есть obj_tank
и obj_turret
- танк и его башня (турель, чтобы был понятен перевод). И логично, что у каждого танка должна быть своя башня, и они не должны их путать между собой. Выход прост - размещать будем только сами танки, без башен - а башни будем создавать тогда, когда создаётся танк. При создании танка создадим на нём же его башню, и запишем её к себе в переменную-поле (поле у каждого экземпляра будет своё, помните?): my_turret = instance_create(x, y, obj_turret);
. Тогда в коде нашего танка можно легко менять направление только его собственной турели: my_turret.image_angle = 60;
. Другие турели этот код не тронет. Вообще этот приём чрезвычайно полезен, и о нём я расскажу в одном из последних уроков, где мы будем говорить о чуть более абстрактных вещах, чем написание скриптов - о планировании игры и взаимодействии между объектами. Той самой “чёрной магии”, о которой я говорил в самом первом посте.
Есть и другие способы достать индексы отдельных экземпляров. Можно это делать функциями instance_nearest
и instance_furthest
- которые особенно полезны для выбора цели обстрела. Как можно предположить из названия, эти функции просматривают все указанные им объекты, и возвращают тот, который ближе всего (или дальше, если furthest) к исполняющему скрипт.
Символьный подтип
Об этом много не расскажешь, всё его использование крутится вокруг функций chr
и ord
. Делают они взаимно обратные вещи: ord
делает из символа его код, а chr
обратно — выдаёт код символа.
Зачем это нужно практически - вопрос неоднозначный. Как правило, код символа удобно применять с различными свойствами кодировки. К примеру, цифры обычно перечисляются подряд: 0123456789
. Вот это выражение всегда истинно: ord("7") == (ord("0") + 7)
. А латиница организована в два больших блока - 26 подряд идущих строчных букв, и 26 подряд идущих заглавных. И не только она - все национальные алфавиты стараются организовывать так же. Русский - не исключение (единственное, что там проблемы с буквой Ё).
В GML пользы от подобных приёмов мало, поскольку под самые частые задачи, где они нужны, в GML уже есть функции. К примеру, этот приём позволяет достаточно быстро собирать из известного числа строчку (что в GML делает функция string
).
Некоторые используют chr
, чтобы определять прямо в коде символы, не имеющие толкового визуального представления. К примеру, перевод строки или “нуль-символ” (подробнее тут, откроется в новой вкладке).
Можно выделить и больше подтипов, но ничего принципиально нового вырвать из них не удастся.
Массивы
Та самая причина, по которой я рекомендую избегать цифр в названиях ваших переменных. Потому что если вам понадобилось их нумеровать - вам нужны массивы, а не отдельные переменные.
Можно подумать, что массивы - это просто группа переменных. Так и есть. В этом случае вы должны придумать название только группы, а каждый элемент группы можно достать по названию и номеру (которых в массивах называют индексом): my_array[2]
- третий элемент массива my_array
(здесь нет опечатки, он правда третий: после 0
и 1
). У номера есть ограничения: он должен быть числом, целым и неотрицательным. Говоря проще: любым из 0
, 1
, 2
и так далее.
Забавно то, что название группы массива также можно считать и названием переменной. my_array
всегда равно my_array[0]
, просто потому что это одно и то же. Это к нам пришло из тех традиций, где массив - просто адрес в памяти и известный размер элемента. По этим данным можно вычислить местонахождение любого элемента массива, а если взять просто адрес, без изменений, получим самый первый его элемент с индексом 0. Это по сей день живо и активно используется в “быстрых” языках, вроде С и С++.
Для чего это надо - для хранения некоторого ряда однотипных данных. Раз уж мы говорим о языке для создания игр, пример будет из игр. Инвентарь. Вспомним также об индексном подтипе, и будем считать, что в массиве хранятся индексы спрайтов тех предметов, что там лежат. В самых простых инвентарях это рационально.
Массивы делают код удобнее. Мы можем взять и нарисовать спрайт предмета, просто зная его номер. Скажем, если это 3, выйдет что-то такое: draw_sprite(inventory[3], -1, mouse_x, mouse_y);
. Но вместо 3 может быть и переменная: draw_sprite(inventory[selected_slot], -1, mouse_x, mouse_y);
, и переменную можно изменять. Уже сейчас вы можете сделать переключатель слотов инвентаря. А после урока с языковыми конструкциями сможете его ещё быстро и целиком рисовать.
Эксперимент III
Сделайте звезду, которая при создании делает 3 планеты, крутящихся по кругу вокруг неё. Причём индекс каждой планеты должен быть записан в некотором массиве - на 0 самая близкая к звезде, на 2 самая дальняя.
Сделайте кружок (draw_circle
), который обводит одну из планет, и клавишу, с помощью которой можно переключать обведённую планету на ту, что дальше (а если самая дальняя - то снова на самую близкую). Здесь воспользуйтесь приёмом: изменяйте индекс выбранной планеты вот так: sel_planet = (sel_planet + 1) mod 3;
. Этот хитрый кусочек кода никогда не даст sel_planet
уйти из множества значений 0, 1 и 2. mod
- это взятие остатка от деления.
К примеру, 49/5
- это, строго говоря, 9.8
, а вот 49 div 5
- это 9
, 5*9 = 49 - 4, и значит, остаток от деления 49 на 5 - это 4. В коде - 49 mod 5 == 4
.
К сожалению, с текущими навыками вам может быть нужно подублировать код, а не обрабатывать его циклами. Но если умеете - делайте циклами. Кто я такой, чтобы вам что-то запрещать?
Я считаю, что вы чему-то учитесь в процессе прохождения этих уроков, поэтому формулировки будут становиться более свободными. Под конец практики вовсе не будет, поскольку новым навыкам вы сможете найти применение почти в любой своей игре. Я не буду вас отвлекать от ваших идей своими странными упражнениями - вы вполне сможете придумывать их сами.
Следующий урок - очень важная ступень, отделяющая вас от просторов мира скриптов. Поэтому на написание следующего урока я потрачу побольше времени. Но я надеюсь, что я оставил достаточно пищи для размышления, чтобы вы не скучали. Можете попридумывать собственные подтипы, например. Поставить на них строгие ограничения и придумать способы применения. Если надумали что-то забавное - буду рад увидеть идеи в комментариях. Может, что-то мне настолько понравится, что я внесу это в основной текст :)