В этом уроке мы создадим raycaster для запуска на пико, используя шляпу пиморони. Он был написан на микропитоне для простоты, однако его можно легко портировать на другие языки.

Шаг 0: Сбор оборудования

Тебе понадобится:

  • малиновый пи пико
  • Демонстрационный пакет pimoroni pico

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

Шаг 1. Настройка среды разработки

(Игнорируйте, если не используете дисплейный пакет pimoroni и микропитон)

Вам нужно будет загрузить файл .uf2 с github (нажмите на самую последнюю версию, затем найдите ту, которая говорит pimoroni-pico-x.x.x-micropython.uf2, явно заменяя x.x.x соответствующей версией).

Затем подключите свой pico к компьютеру, удерживая нажатой кнопку загрузки, и найдите его в поисковике/проводнике, он появится как RPI-PICO. Наконец, скопируйте файл .uf2. По сути, это установит пиратский микропитон pimoroni, чтобы мы могли использовать экран.

Наконец, установите thonny с thonny.org, а вверху на панели инструментов нажмите Инструменты Параметры…, теперь выберите Интерпретаторы и нажмите Micropython (Raspberry Pi Pico) в качестве интерпретатора.

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

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

Шаг 2: Понимание математики

Чтобы сделать наш raycaster, мы возьмем 2D-игру сверху вниз, как показано ниже:

и превращаясь в 3D-игру, отбрасывая лучи от игрока, используя тригонометрию для создания перспективы и теней:

Это тот же метод, который использовался id software в 90-х для создания знаменитой игры wolfenstein 3d, и сам алгоритм действительно быстрый. Чтобы преобразовать 2D-лучи в 3D-перспективу, нам нужен только угол луча и расстояние, на котором они сталкиваются.

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

Как видите, проблему можно разбить на три ключевые функции:

  • get_ray(player_x, player_y, ray_angle, block_size) — возвращает лучевое расстояние, если произошло столкновение, иначе -1.
  • lerp(x,min,max,new_min,new_max) — линейно интерполирует число x, которое имеет границы min и max, чтобы пройти между new_min и new_max
  • draw_bar(x,thickness,barspace) — рисует полосу в позиции x с указанием толщины и штрихового пространства (штриховое пространство — это количество пространства над и под полосой)

2.1 Лепинг

Начнем с определения функции lerp:

  • Найдите число x в процентах от того, насколько далеко оно находится между своим минимумом и максимумом: px = (x-min)/(max-min)
  • Умножьте это значение на ширину new_max и new_min, чтобы найти смещение в нашем новом диапазоне: off = px * (new_max-new_min)
  • Добавьте это к new_min, чтобы найти фактическое значение: new_x = new_min + off

Затем мы можем упаковать это как однострочную функцию в python (если вы используете C/C++, вы можете использовать для этого макрос):

def lerp(x,mn,mx,nmn,nmx):
    return ((x-mn)/(mx-mn)) * (nmx-nmn) + nmn

2.2 Чертежные стержни

Затем давайте определим функцию draw_bar. Учитывая, что у нас уже есть созданная функция draw_rectangle (это не должно быть очень сложно и оставлено читателю в качестве упражнения), наша позиция по оси y (помните, что мы рисуем из верхнего левого угла, и позиция нашего прямоугольника также измеряется таким образом) просто равна к нашему barspace.

Наш x — это просто данное нам x. Наше width равно thickness (или кратно ему), а наше height будет равно screen_height — barspace*2, поскольку мы должны учитывать barspace вверху и внизу.

Итак, наша функция draw_bar может выглядеть примерно так:

def draw_bar(x, barspace, thickness):
    display.rectangle(int(barspace),x,
    HEIGHT — int(barspace)*2,int(thickness))

2.3 Рейкастинг через тригонометрию

Наконец, давайте решим самую большую проблему — получение данных луча. Несмотря на то, что грубая форсировка размера нашего луча каждый раз и проверка того, действительно ли мы попали в какой-либо блок, крайне неэффективна и составляет O (n² * l * r) по временной сложности (n = количество блоков, l = длина луча, r = луч число). Вместо этого, поскольку мы можем разделить наши стены на сетку блоков, мы можем найти ближайшее горизонтальное столкновение и ближайшее вертикальное столкновение с помощью:

  1. Найдите расстояние между нашей точкой и следующей линией сетки
  2. Найдите, насколько мы увеличим наш другой компонент, если мы увеличим один на одну единицу размера блока (т.е. если я увеличу y на один размер блока, насколько я должен увеличить x?)
  3. Увеличьте каждый компонент на соответствующие дельты (изменения)
  4. Поскольку теперь мы находимся на одной линии с нашей сеткой, мы можем просто проверить, присутствует ли в нашем конкретном месте блок. Если это так, остановитесь и верните текущее расстояние. В противном случае продолжайте, пока не будет превышена глубина обзора.

Конечно, это означает, что мы должны проверять столкновения как вдоль горизонтальных линий сетки, так и вдоль вертикальных линий сетки, а затем выбирать наименьшее, но наша временная сложность теперь уменьшена до O(d * r), d — глубина обзора, в худшем случае. Учитывая, что d является постоянным, при достаточно большом r оно сокращается до O(r), превращая наше цепное квадратичное (то есть n⁴ = (n²)²) экспоненциальное изменение линейным.

Эта диаграмма полностью объясняет, как работает математика (примечание: только для вертикальных линий. Горизонтальные линии используют точно такой же принцип и оставлены читателю в качестве упражнения)

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

Шаг 3: Собираем все вместе

Вы можете сохранить уровень как массив из 1 и 0, чтобы указать, есть ли у блока в определенной позиции стена или нет. Сохраните положение игрока, угол и положение дельты. Каждый раз, когда вы перемещаете игрока, просто добавляйте дельта-позицию к позиции.

Каждый раз, когда вы поворачиваете его, он одновременно добавляет изменение угла и пересчитывает дельта-позицию, выполняя:

dx = r * cos(angle)
dy = r * sin(angle)

Это потому, что мы можем рассматривать наш вектор [dx,dy] как гипотенузу треугольника. Это означает, что мы можем затем использовать SOHCAHTOA, чтобы найти компоненты x и y (r — это множитель гипотенузы и теоретически то, как далеко игрок будет двигаться. Это может быть реализовано во вращении или в движении, поскольку умножение коммутативно).

Для каждого кадра:

  • очистить экран
  • Проверяйте ввод данных пользователем и соответствующим образом перемещайте/вращайте проигрыватель.
  • Перебрать количество лучей (например, 30), установив переменную на отрицательный относительный угол и перейдя на положительный (например, от -15 до 15). Для каждой итерации:
  • —-› Преобразование относительного угла в радианы
  • — -› Вызов get_rays(px,py,pa+ra,BLOCK_SIZE), px,py,pa -› игрок x, y, угол. ra -> относительный/угол луча
  • — -› Расстояние Lerp как до штрихового пространства (таким образом, чем дальше стена, тем больше штрихового пространства она занимает, поэтому выглядит меньше), так и цветового диапазона (что делает ее темнее, чем дальше она находится для имитации теней).
  • — — —› Примечание: может потребоваться заранее умножить dist на cos(ra), так как нам нужно компенсировать тот факт, что лучи под углом всегда будут длиннее, даже если они попадают на одну и ту же стену
  • —-› Draw_bar на экране, привязывая текущий ray_angle к ширине экрана и используя рассчитанное пространство штрихов (толщина должна быть равна ширине экрана / количеству лучей)

И это все! Я надеюсь, что вы нашли учебник полезным и не слишком откровенным. Как и обещал, вы можете найти мой написанный код на github. То, как я написал это, немного отличается от того, как это изложено здесь, но основная концепция та же, поэтому, если вы застряли, не стесняйтесь копаться в моем коде или задавать мне вопросы в комментариях. Вот окончательный результат того, как это выглядит:

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

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