Предисловие

В прошлой статье, с которой мы настоятельно рекомендуем ознакомиться перед прочтением этой, мы рассмотрели:

  1. Программирования в Mindustry

  2. Карту

  3. Луч и основные принципы псевдо-3D

  4. Рисование текстур.

Сегодня же мы углубимся в детали того, как работает эффект рыбьего глаза и немного перестроим псевдо-3D, а так же создадим физику для движка и спрайты.

Мелочи

В прошлой статье мы забыли упомянуть FOV (Field of view). По сути это угол обзора, в котором все лучи лежат равномерно с разницей, допустим, в ~1⁰.

К текущей статье мы успели заменить мелкую ячейку памяти настроек cell1, на банк настроек памяти bank12, bank2, bank1 (в зависимости от процессора).

Также мы решили убрать редактор карты, ведь он только запутывал людей, хотевших пользоваться нашим псевдо-3D движком, и теперь она создается автоматически.

Ещё изменился внешний вид нашего псевдо-3D движка, теперь он более аккуратный.

Эффект рыбьего глаза, что и почему

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

Эффект рыбьего глаза возникает из-за того, что длинна луча от игрока до стены не равномерна. На примере картинки вы поймёте про что я говорю:

L1, L2 - длины линий
L1, L2 - длины линий

Умножив на косинус мы сделали L1 и L2 равными:

l - длинна луча
l - длинна луча

Но вот незадача, псевдо-3D всё ещё выглядит странным:

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

А вот так оно должно выглядеть:

Один из способов максимально уменьшить влияние этого эффекта — уменьшить угол обзора FOV. Его оптимальное значение 70⁰.

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

Перестройка движка

Мы решили перестроить небольшую часть движка, отвечающую за распределение лучей, а она потащила за собой другие части. Но всё же переписывали мы не много.

Мы решили отказаться от FOV в пользу plane (о нем ниже), чтобы полностью убрать эффект рыбьего глаза. В данной версии будут важны 4 переменные: dirX, dirY (направление взгляда игрока) и plane_X, plane_Y (поверхность экрана по x и y).

Взгляните на картинку:

Так выглядела абстракция движка (то, как мы это представляли в голове): был FOV, лучи распределены в нём равномерно, и также был угол направления взгляда игрока player_angle.

Мы решили заменить это на другую абстракцию:

dirX = player_dir_x, dirY = player_dir_y;
dirX = player_dir_x, dirY = player_dir_y;

По сути ничего не изменилось, мы лишь подошли к тому же, но с другой стороны.

Переменная plane, отвечающая за длину экрана, не является вектором, потому что нам не нужны переменные, содержащие одни и те же значения. Ведь plane всегда перпендикулярна направлению взгляда игрока, так что для описания вектора экрана можно использовать направление взгляда игрока повёрнутое на 90⁰ и умноженное на длину экрана. Но ниже мы будем в некоторых местах для удобства вставлять переменные по типу plane_x, plane_y. Для удобства считайте, что plane является вектором, а эти переменные - его направление.

В данной версии луч вместо переменной угла направления имеет 2 координаты ray_dir_x и ray_dir_y, которые задают направление луча вектором. В прошлой версии движка не было равномерного распределения лучей по плоскому экрану из-за того, что все лучи отличались друг от друга на строго заданный угол, а с ним не получится полностью избежать эффекта рыбьего глаза (вспоминаем написанное выше). Поэтому вместо угла мы решили использовать вектор направления луча. Его нужно просчитать для каждого луча отдельно, а просчитывается он так:
ray_dir_x = player_dir_x + plane_x * (ray_number - number_of_rays/2)
ray_dir_y = player_dir_y + plane_y * (ray_number - number_of_rays/2)

Вектор направления игрока легко просчитывается от угла направления взгляда игрока:
player_dir_x = cos(player_angle)
player_dir_y = sin(player_angle)

Вектор экрана plane просчитывается чуть сложней, ведь он на 90⁰ отличается от вектора направления игрока:
plane_x = -sin(player_angle)
plane_y =  cos(player_angle)

Чтоб не просчитывать синус и косинус по несколько раз, мы упростили формулу до этого:
ray_dir_x = player_dir_x - player_dir_y * k
ray_dir_y = player_dir_y + player_dir_x * k

Вот часть псевдокода:

k = (number_of_ray - 88)/176
read dirX at 6 in bank12
read dirY at 7 in bank12
ray_dir_x = dirX - dirY*k
ray_dir_y = dirY + dirX*k

Алгоритм луча из за этого изменения не сильно поменялся, разве что тангенс теперь просчитывается так:
tan_of_ray_angle = ray_dir_y/ray_dir_x

Ведь тангенс это противолежащий катет делённый на прилежащий, в роли противолежащего выступает ray_dir_y, а в роли прилежащего ray_dir_x

Ещё изменилась проверка того, в какую плоскость пускается луч. Например, для проверки по вертикальным линиям процессор теперь смотрит больше ли нуля ray_dir_x, если да, то луч пускается вправо, если нет - луч пускается влево.

Также изменилось просчитывание косинуса между направлением взгляда и лучом:
cos = 1/sqrt(k² + 1²)

Косинус — это прилежащий катет делить на гипотенузу, гипотенуза просчитывается по теореме Пифагора sqrt(k² + 1²), а катет равен 1.

И вуаля:

Эффекта не видно.

Спрайты

Спрайты — это те самые противники в Woolfenstein 3d или в Doom, которых вы все видели. По сути они являются текстурками, постоянно смотрящими в нашу сторону, из-за чего они всегда прямоугольные, что упростит их отрисовку.

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

Как это выглядит:

А теперь сложная часть — как это устроено?

Спрайт — это лишь координаты спрайта с номером изображения. Изображение мы тут упустим, нас интересует то, как они рисуются.

Представьте карту, на которой нет ничего, кроме игрока и спрайта, игрок смотрит в 0⁰ (то есть вправо).

Нам дана задача: отрисовать на экране спрайт. Для этого надо найти координаты спрайта на экране:

screen_sprite_x — то, что нужно найти
screen_sprite_x — то, что нужно найти

Ищутся они так:

diff_x = sprite_x - player_x
diff_y = sprite_y - player_y
screen_sprite_x = half_display_weight - display_weight*diff_y/diff_x
screen_sprite_y = 88 //откуда взялось 88 мы писали в прошлой статье

diff_x, diff_y — расстояние от игрока до спрайта по x и y.

Тут display_weight*diff_y/diff_x минусуется от half_display_weight, а не наоборот из-за того, что координатная ось дисплея отзеркалена относительно display_weight*diff_y/diff_x.

Отлично, но что делать, если игрок не всегда смотрит ровно вправо (в 0⁰)? Нужно повернуть экран, но тогда спрайты не будет адекватно на нём показываться:

A — то, каким должен быть screen_sprite_x
A — то, каким должен быть screen_sprite_x

Поэтому надо повернуть спрайт относительно своего прошлого положения:

Для поворота воспользуемся матрицей поворота:

[cos(player_angle), -sin(player_angle)]
[sin(player_angle), cos(player_angle)]

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

[cos(player_angle),  sin(player_angle)]
[sin(player_angle), -cos(player_angle)]

По сути мы отзеркалили матрицу по y.

Упростим её до этого:

[dirX,  dirY]
[dirY, -dirX]

Ведь dirX равен cos(player\_angle), а dirY равен sin(player\_angle)
Вот как оно будет выглядеть в псевдокоде:

read dirX
read dirY
read player_x
read player_y

diff_x = sprite_x - player_x

diff_y = sprite_y - player_y

sx = diff_x*dirX + diff_y*dirY
sy = diff_y*dirX - diff_x*dirY

screen_sprite_x = half_display_weight - display_weight*sy/sx

Теперь рисуем желтый квадрат на координатах спрайта (его координаты на экране X:screen_sprite_x, Y:88), и вот что видим:

Поворачиваемся на 180⁰ и снова видим спрайт:

Так быть не должно. Это происходит из за того, что когда спрайт позади экрана, он не перестаёт рисоваться:

Он рисуется сзади игрока как будто на втором отзеркаленном вдоль экране. Решить это не трудно: нам нужно узнать, когда спрайт сзади игрока. Спрайт сзади игрока, если sx < 0, так что делаем эту проверку, и если sx < 0, то мы просто приравниваем screen_sprite_x к значению -500.

На карте возможен только 1 спрайт, но хочется же больше.

Чтобы сделать больше спрайтов, процессор сортирует спрайты по расстоянию до игрока, а потом поочередно рисует стену, спрайт1, спрайт2 и т.д.

Как это было реализовано в mindustry

Для начала расскажу о том, как у нас хранятся спрайты. Есть два банка памяти, обозначим их как bank9 и bank10, первый хранит ссылки на все спрайты (тут ссылка — это просто номер ячейки другого банка памяти, где хранится спрайт), а второй хранит спрайты. Второй банк памяти хранит каждый спрайт на трёх ячейках сразу:

  1. Расстояние до спрайта

  2. screen_sprite_x

  3. Номер текстурки

Спрайты сортируются быстрой сортировкой Хоара. Ну, как спрайты, сортируются ссылки на спрайты. Значение, по которому они сортируются — расстояние от игрока до спрайта.

Спрайты просчитываются в семи процессорах, каждый читает значение из мелкой ячейки памяти, где записаны координаты спрайтов и номер текстурки. Далее он вычисляет расстояние до спрайта и sprite_x, после записывает в bank10 то, что просчитал и номер текстурки.

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

Физика

Физика — это относительно важная часть псевдо-3D движка. Будет не приятно, если игрок сможет проходить сквозь стены, и именно для этого и нужна физика.

В нашем псевдо-3D движке мы решили физику способом с багами, но «И так сойдет!»

Способ такой: проверяем, если в следующем местоположении игрока по x и текущем по y есть стена, то мы не делаем этот шаг по x. Тоже самое и для y.

Вот часть псевдокода:

xperson1 = xperson
yperson1 = yperson

// Тут код передвижения. Тоесть координаты xperson и yperson изменятся в сторону движения. Мы решили его не показывать для удобства

if map[xperson][yperson1] == 0 {
    xperson = xperson1
}
if map[xperson1[yperson] == 0 {
    yperson = yperson1
}

Для удобства тут мы jump заменили на if.

Финал

Для красоты финала изменим текстурки, добавим ИИ спрайтам и готово:

В этот раз мы уже имеем что-то, чем можно похвастаться!

Доп. материалы
https://lodev.org/cgtutor/raycasting.html
https://github.com/xdettlaff/mindustry-3d-engine/blob/main/3d_v0.2.2

Комментарии (0)