В этом уроке мы создадим 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 = луч число). Вместо этого, поскольку мы можем разделить наши стены на сетку блоков, мы можем найти ближайшее горизонтальное столкновение и ближайшее вертикальное столкновение с помощью:
- Найдите расстояние между нашей точкой и следующей линией сетки
- Найдите, насколько мы увеличим наш другой компонент, если мы увеличим один на одну единицу размера блока (т.е. если я увеличу y на один размер блока, насколько я должен увеличить x?)
- Увеличьте каждый компонент на соответствующие дельты (изменения)
- Поскольку теперь мы находимся на одной линии с нашей сеткой, мы можем просто проверить, присутствует ли в нашем конкретном месте блок. Если это так, остановитесь и верните текущее расстояние. В противном случае продолжайте, пока не будет превышена глубина обзора.
Конечно, это означает, что мы должны проверять столкновения как вдоль горизонтальных линий сетки, так и вдоль вертикальных линий сетки, а затем выбирать наименьшее, но наша временная сложность теперь уменьшена до 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. То, как я написал это, немного отличается от того, как это изложено здесь, но основная концепция та же, поэтому, если вы застряли, не стесняйтесь копаться в моем коде или задавать мне вопросы в комментариях. Вот окончательный результат того, как это выглядит:
Это не должно быть сложно превратить в шутер от первого лица (помните, что вся логика двумерная), учитывая, что рендерер закончен.
Если вы обнаружите какие-либо ошибки или неточности в этой статье, сообщите мне, и я надеюсь, что вы получите удовольствие от своего пико или от того, для чего вы его используете!