Как построить кривую в редакторе GameMaker? Как с помощью этой кривой и текстуры дороги создать трассу? Нужно ли для этого знать тригонометрию? Как для этого использовать знания о векторах?
Всем привет! С вами программист Александр Кондырев. В этой статье я отвечу на вопросы, которые указаны выше.
Когда GameMaker был приобретён компанией YoYo Games, он получил новую IDE и стал называться GameMaker Studio. На днях коллега мне сообщил, что GameMaker Studio 2 теперь снова называется просто GameMaker. Я уже привык называть его GMS2, поэтому не заметил этого изменения. Исправляюсь и теперь снова называю его GameMaker или GM.
Когда я начинал программировать игры, я не понимал, как пользоваться кривыми.
Объекты, построенные по кривым, казались волшебством. Я тогда не осознавал одну важную концепцию: человек не может различить кривую линию от линии, разбитой на множество маленьких прямых отрезков. Эту концепцию я буду использовать в этом уроке. Мы построим кривую линию и разобьём её на прямые отрезки, чтобы на их основе создать трассу.
Для начала на бумаге я нарисовал кривую, по которой хочу построить дорогу.
В квадрате выделен фрагмент дороги, который я подробнее разберу на рисунке ниже.
Это увеличенный фрагмент кривой из квадрата. На рисунке видно, что я разбил этот фрагмент на десять кусочков одинаковой длины. Над и под каждым кусочком нарисованы квадраты. Каждый квадрат разделён пунктирной линией, показывая, что они состоят из треугольников. Эти фигуры хоть и близки к квадратам, но на самом деле это трапеции.
Они сужаются там, где кривая вогнута, и расширяются там, где она выпукла.
На следующей схеме я рассмотрю более детально один фрагмент.
AB и BC — это два фрагмента кривой. Обращаю внимание, что на этой схеме я сократил количество треугольников на фрагмент кривой с четырёх до двух. Мой фрагмент дороги, построенный по кривой, будет состоять из треугольников A2A1B1 и B1B2A2. Чтобы их построить, мне нужно найти координаты точек A1, A2, B1, B2.
Теперь перейду в GM, чтобы построить там тестовую кривую.
Для этого в новом проекте добавляю кривую Path1 — в GameMaker они называются path или пути.
Открываю Room1 и создаю в этой комнате слой Path_1.
В этом слое делаю активной созданную кривую Path1.
Кривая может быть с открытым началом и концом, а может быть закрытой, когда конечная точка соединена с начальной. Для круговой трассы мне нужна закрытая кривая, поэтому в чекбоксе Closed ставлю галочку. Пути могут состоять из прямых линий или из кривых, касающихся этих прямых. Для плавных поворотов на трассе я выбираю второй вариант и нажимаю кнопку Smooth Curve. Далее расставляю точки и передвигаю их внутри комнаты, чтобы построить желаемую кривую. На скриншоте показываю получившуюся кривую.
Добавляю в проект Object1 и создаю его экземпляр в комнате. Дальше код буду писать в этом объекте.
Для отрисовки дороги я буду использовать буфер вершин. Для этого в событии Create я создаю формат вершин для этого буфера и сам буфер.
С третьей по седьмую строку я создаю формат вершин. В нём будут координаты вершины, текстурные координаты и цвет. Для этого примера цвет мне не нужен, да и координаты у вершины нужны только две — X и Y. Однако в этом руководстве я буду использовать стандартный шейдер GameMaker, поэтому оставляю такой формат вершины.
С девятой по тринадцатую строку я создаю вершинный буфер и переношу его в видеопамять с помощью функции vertex_freeze. Все эти строки уже были описаны в статье О трёхмерной графике. Добавлять рассчитанные вершины в буфер я буду между строками десять и двенадцать.
Теперь нужно рассчитать координаты точек A1, A2, B1, B2. Для этого введу несколько переменных.
Во второй строке я создаю массив lines_array для хранения координат рассчитанных вершин. Называю его lines , потому что вершины в него будут сохраняться попарно, и таким образом они будут составлять линии. В третьей строке сохраняю значение 64 в переменную width , которая обозначает ширину трассы. А в четвёртой строке нахожу половину этой длины и сохраняю её в переменную width_half . В пятой строке я создаю переменную pth и присваиваю ей значение созданного пути Path1 . В шестой строке присваиваю значение 8 переменной part_size . Я хочу, чтобы примерно столько пикселей было в каждом фрагменте пути. Чем больше пикселей будет в каждом фрагменте, тем более угловатой будет дорога. Чем меньше пикселей в каждом фрагменте, тем плавнее будет дорога, но для её отрисовки потребуется больше треугольников. Я говорю "примерно" о длине фрагмента, потому что не знаю длины всей кривой, и необязательно эта длина будет кратной восьми.
В седьмой строке с помощью функции path_get_length я получаю и сохраняю длину кривой в переменной path_length . В восьмой строке я целочисленно делю длину кривой на длину фрагмента, чтобы получить количество фрагментов в кривой. В девятой строке делю длину кривой на количество фрагментов, чтобы скорректировать длину каждого фрагмента. В десятой строке я делю размер фрагмента на ширину трассы, чтобы найти этот размер в текстурных координатах, и сохраняю его в переменную tex_frac .
Далее я буду использовать цикл, чтобы пройтись по всем фрагментам кривой, рассчитать координаты A1, A2, B1, B2 и сохранить их в массив.
Вот ещё раз приведу схему трапеции с искомыми вершинами.
В приведённом ниже коде я вычисляю вектор AB. С его помощью нахожу вектор, находящийся слева от него AA1, и вектор, находящийся справа от него AA2. Используя эти векторы, я вычисляю координаты искомых точек A1 и A2, после чего сохраняю их в массив. В последующих шагах цикла я повторяю эту процедуру для всех остальных фрагментов кривой.
Детальнее разберём приведённый код. С четырнадцатой по девятнадцатую строку я вычисляю координаты точек A и B. Это делается с помощью функций path_get_x и path_get_y . Этим функциям нужно передать кривую и значение от нуля до единицы. Ноль обозначает начало кривой, единица — её конец. Чтобы найти нужное значение, требуется разделить индекс на количество частей. В цикле я прохожу по всем рассчитанным точкам, поэтому индекс каждой точки соответствует итератору i . В рамках каждого шага цикла для расчёта вектора нужно найти координаты двух точек, соответственно индекс второй точки будет i+1 .
С двадцатой по двадцать четвёртую строку я вычисляю вектор, нахожу его длину и нормализую его. Длина нормализованного вектора всегда равна единице. Чтобы найти вектор, нужно из координат точки B вычесть координаты точки A. С помощью функции point_distance вычисляется длина вектора. Затем координаты вектора делятся на его
длину для нахождения единичного вектора.
С двадцать пятой по тридцать вторую строку происходит вычисление левого и правого векторов. Угол между единичным вектором и искомыми векторами всегда равен 90 градусам, поэтому здесь нет необходимости использовать тригонометрические формулы. Я меняю местами координаты единичного вектора и к одной из координат добавляю знак минуса. Таким образом находятся левый и правый единичные векторы. Я умножаю их на половину ширины трассы, чтобы получить необходимую длину.
С тридцать третьей по тридцать седьмую строку я прибавляю найденные векторы к координатам точки A, чтобы получить координаты искомых точек A1 и A2. Полученные значения сохраняю в массив lines_array.
Для отображения трассы я нарисовал текстуру размером 16x16 пикселей. В настройках нужно обязательно поставить галочку в чекбоксе Separate Texture Page, иначе GameMaker на этапе компиляции добавит эту текстуру в общую текстурную страницу. Если это произойдёт, то расчёты, приведённые в этом руководстве, использовать не получится, и придётся действовать другим методом.
Дальше нужно добавить вершины в буфер. Кроме пространственных координат мне понадобятся текстурные координаты. Нарисую ещё одну схему:
На этой схеме изображён квадрат, который представляет текстуру. Квадрат разбит пунктирными линиями на 8 одинаковых отрезков. У верхнего левого угла текстуры координаты (0,0), у правого нижнего — (1,1). В центре текстуры координаты (x=0,5, y=0,5). Ранее я рассчитал переменную tex_frac . Это значение представляет собой расстояние по горизонтали между двумя пунктирными линиями.
В следующем фрагменте я заканчиваю построение буфера вершин.
Здесь я снова прохожу циклом по всем точкам и формирую из них на каждом шаге по два треугольника, составляющие трапецию. С пятьдесят первой по шестьдесят первую строку я извлекаю из массива две соседние линии. Для удобства сохраняю координаты линий в отдельные переменные ax1, ay1, ax2, ay2, bx1, by1, bx2, by2 . По текущему индексу i я получаю первую линию, а по индексу i+1 — следующую. Обратите внимание на конструкцию в строке пятьдесят два: % — это оператор «modulo» или «mod», который возвращает остаток от деления. Почему эта конструкция используется? В последней итерации цикла i+1 выйдет за пределы длины массива, что вызвало бы ошибку. Но оператор modulo возвращает 0 в этом случае, и ошибки не происходит. Это даёт первую линию из массива, которая как раз нужна для последнего значения. Раньше я не использовал эту конструкцию и прибегал к дополнительным условиям или другим решениям, что усложняло код и его чтение.
С шестьдесят второй по семидесятую строку я добавляю в буфер первый треугольник
A1B1A2. C семьдесят второй по восьмидесятую строку с буфер добавляется второй треугольник B1B2A2.
Добавление вершин в буфер было подробно описано в статье «О трёхмерной графике».
Обычно для текстурных координат вместо X и Y принято использовать U и V соответственно. Координата V для точек A1 и B1 всегда будет равна нулю, а для точек
A2 и B2 — единице. Это можно увидеть на схеме выше. Координата U рассчитывается умножением текущего индекса i или j на длину фрагмента текстуры tex_frac. Можно заметить, что значения U будут превышать единицу в какой‑то момент, но благодаря включению функции gpu_set_texrepeat(true) такие значения допустимы, потому что текстура будет повторяться во все стороны. На скриншоте буфер road_buff выделен синим цветом. Это означает, что я убрал слово var при объявлении этой переменной, чтобы она была доступна не только в событии Create, но и в Draw.
Осталось нарисовать созданный буфер на экране. Ниже представлен код из события Draw.
Во второй строке я сохраняю указатель на текстуру в переменную tex. Его возвращает функция sprite_get_texture, которой передаётся название спрайта и кадр, для которого нужно получить указатель на текстуру. У меня один кадр, поэтому значение кадра равно нулю.
В третьей строке я включаю повтор текстуры, как упоминалось выше. В четвёртой строке отключаю фильтрацию. Это не обязательно, но я использую пиксельную текстуру и не хочу, чтобы её цвета интерполировались. В шестой строке с помощью функции.
vertex_submit я рисую буфер вершин на экране. Этой функции передаётся буфер вершин road_buff , pr_trianglelist означает, что в переданном буфере вершины описывают треугольники, а tex — указатель на текстуру.
Запускаем компиляцию и видим отрисованную трассу на экране.
В этой статье я построил трассу по кривой в GameMaker и отобразил её на экране. В процессе я выяснил, что для построения нужны базовые знания о векторах и совсем не требуются знания тригонометрии. Статья получилась значительно длиннее предыдущих. Напишите в комментариях, показалась ли она вам слишком длинной или, наоборот, в самый раз.
Спасибо, что дочитали статью до конца. Надеюсь, она вам понравилась, и вы узнали что‑то новое. Если это так, проголосуйте за — возможно, тогда о ней узнает больше людей.
Сейчас я стараюсь выпускать по одной статье в неделю. Подписывайтесь на меня, чтобы не пропускать новые публикации.
Если вам понравилась эта статья, также рекомендую ознакомиться с другими материалами о программировании в GameMaker:
Спасибо за ваше внимание!
Комментарии (2)
Graphist
23.09.2024 08:25Хитрости начинаются, когда дорога широкая, а поворот крутой: тут внутренняя линия начинает пересекаться сама с собой. Ну хорошо, подолбались, нашли самопересечения, каждое заменили на одну точку. Но тут исказилась текстура дороги: если трапецию превратить в треугольник, то получается некрасиво; так что теперь надо это точку снова заменить на маленький участок кривой (а треугольники снова на трапеции). Так, вроде, справились; но теперь дорога начинает изгибаться ещё и по высоте -- и у нас отломалось обнаружение пересечений...
Как-то чуть ли не год провозился с этой задачкой :)
Zenitchik
А я, когда мне понадобилось решить подобную задачу для рисования узлов, просто ушёл от линий Безье к сопряжённым окружностям. Для прямых и окружностей эквидистантные линии находятся тривиально, а точки пересечения - через дельтоид.