Предыстория

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

Прототип увидел мой друг, и мы совместно сделали вторую версию:

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

Что мы хотим

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

Сейчас компьютеры быстрые, но в том же mindustry процессоры (игровые блоки, в которых можно программировать) медленные, поэтому написание на них быстрого псевдо-3D было интересной задачей.

Как работает программирование в mindustry

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

Также процессор может влиять на подключённые к нему блоки, такие как турель, кнопка, сортировщик и так далее.

Помимо влияния на них, он может считывать информацию с этих блоков, например, вкл/выкл отдельного блока или уровень его здоровья. Ещё стоит отметить, что в mindusrty есть несколько представлений кода:

  1. Визуальное

  2. Текстовое

Также у процессора есть 2 буфера:

  1. Текстовый

  2. Графический

Для начала рассмотрим некоторые возможности процессора:

1. Команда set задаёт значение переменной (все переменные есть и их не надо объявлять):

2. Команда operation выполняет вычисление и записывает результат в переменную:

Переменная i стала равна 188
Переменная i стала равна 188

Теперь составим небольшой код, который рассчитает периметр окружности:

Команда print записывает текст или значение переменной в буфер данного процессора (автоматически переведя его в строку):

Теперь значение текстового буфера равно строке: «периметр окружности: 94.2»
Теперь значение текстового буфера равно строке: «периметр окружности: 94.2»

4. Команда printFlush выводит текст из буфера, обнуляя его значение, в блок сообщения, который мы укажем:

Но сперва его надо подключить:

Он подключился под именем message1. Процессор сразу же вывел содержимое буфера (и будет постоянно выводить результат заново, ведь код в процессоре находится в цикле):

Теперь подключим к нашему процессору ячейку памяти:

Данный блок ячейки содержит, 64 ячейки (от 0 до 63-й включительно) для значений переменных типа number (double в mindustry).

Теперь запишем значение переменной diameter с помощью команды write в нулевую ячейку блока cell1:

В нулевой ячейке содержится значение диаметра нашей окружности
В нулевой ячейке содержится значение диаметра нашей окружности

6. Далее построим второй процессор и подключим его к логическому дисплею (88 на 88 пикселей) и ячейке памяти:

Прочитаем значение в нулевой ячейке и запишем его в переменную diameter с помощью команды read, после поделим значение диаметра на 2 и запишем его в переменную radius. Далее в статье подобные действия мы будем записывать как radius = diameter / 2 или op div radius diameter 2:

7. Теперь используем команду draw как draw clear 0 0 0, чтобы заполнить весь графический буфер процессора пикселями с чёрным цветом:

После зададим зелёный непрозрачный цвет для последующих фигур: draw color 0 255 0 255

И нарисуем правильный 24-х угольник радиусом radius в координатах X: 44 Y:44 (середина экрана 88 на 88) под углом 0 градусов:

  1. Далее выведем всё из графического буфера с помощью команды drawFlush в дисплей display1

    И увидим:

  2. Теперь подключим наш второй процессор к кнопке:

    И прочитаем её состояние (вкл/выкл) с помощью команды sensor @enabled и запишем статус кнопки в переменную status. Данную команду расположим в 3-й строчке:

  3. Ниже поставим команду jump с условием status == 0 и настроим её на прыжок в 7-ю строку.

Как вы уже могли догадаться, данная команда заставляет процессор как бы «прыгнуть» на 7-ю строку, при условии, что переменная status равна нулю (то есть кнопка выключена), в противном случае мы просто продолжим читать код со следующей строки, то есть продолжим отрисовку 24-х угольника.

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

diameter = 30
Pi = 3.14
perimeter = Pi * diameter
print "периметр окружности: "
print perimeter
printflush message1
write diameter to cell1 at 0

read diameter = cell1 at 0
radius = diameter / 2
draw clear 0 0 0
status = @enabled in switch1
jump 7, if status == 0
draw color 0 255 0 255
draw linePoly 40 40 24 radius 0
drawflush display1

11. И последняя команда: pack color

Данная команда соеденяеят 4 цвета RGBA в одно число типа number В этой команде все цвета записаны числом от 0 до 1

Карта

Самый базовый элемент нашего движка - карта. На карте отображаются игрок и все блоки. Карта имеет свой размер - widthmap, этот размер означает одновременно и ширину, и длину карты (как несложно догадаться, она квадратная). Карта состоит из блоков (как майнкрафт, только в 2D), есть блоки стен, воздуха и т.д. Карту описывает двумерный массив чисел, где 0 - это воздух, а другое число - тип блока/стены.

Все блоки имеют размер - cell_size. Для примера представим, что cell_size = 1. Также блоки могут находиться только в целых координатах (опять же, как и в майнкрафте).

На карте присутствует игрок. Игрок - это вымышленная точка, описывающаяся двумя на сей раз нецелочисленными координатами, т.к. игрок может находиться и между клеток. В данной статье мы будем их называть player_x, player_y (или же start_x, start_y).

Вот пример карты:

[0] [1] [1] [1] [1] [1] 
[0] [1] [0] [0] [1] [1] 
[0] [1] [0] [0] [1] [1] 
[1] [1] [0] [0] [1] [0] 
[0] [0] [0] [1] [1] [0] 
[1] [1] [1] [1] [1] [0]

поле 6 на 6

Луч

Что нужно для псевдо-3D? Пускать от игрока лучи и считать точку столкновения луча с ближайшей стеной.

Далее по формуле line_height = wall_height/length_of_ray рисовать полоску на экране (далее название line_height будет заменено на wall_height).

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

В данной статье координаты луча будут записаны в переменных ray_x и ray_y (координаты луча) и ray_angle (угол направления луча). Также надо понимать, что луч будет как бы «шагать», то есть его координаты будут меняться.

В этом псевдо-3D движке мы использовали очень красивый и быстрый алгоритм луча. Работает он так: представьте квадратное поле n на n клеток (карту), каждая клетка может быть либо стеной, либо воздухом.

Ещё у нас есть игрок, который пускает луч. Начальные координаты луча — это координаты игрока. В данном алгоритме луч будет проходить дважды для проверки пересечения с какой-либо стеной. Для удобства представьте, что игрок находится на пересечении четырех клеток.

Первый проход: мы будем двигаться на длину клетки по координате Y y_step = cell_size. Шаг по X тут будет не трудно просчитать, зная, что тангенс — это противолежащий катет на прилежащий.Представьте любую точку на карте прямоугольным треугольником, где координаты X и Y — катеты.

Несложно догадаться, что тангенс угла луча — это y/x или же y_step/x_step. Отсюда нам нужно вывести x_step (шаг луча по координате X): y_step/x_step = tan(ray_angle) => x_step = y_step/tan(ray_angle).

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

Во втором заходе делается всё то же самое, что и в первом, но для X, то есть x_step = cell_size, y_step/x_step = tan(ray_angle) => y_step = x_step * tan(ray_angle)

А если игрок находится не на пересечении четырех клеток? Тут тоже нет проблем. В первом цикле для проверки координат по Y нужно сделать Y целым числом, чтобы луч был на верхнем или нижнем бортике клетки.

Рассмотрим только случай с верхним бортиком: Y будет на верхнем бортике, если мы округлим его в большую сторону, new_y = ceil(ray_y). Теперь нам нужно найти координату X. Для этого нам сначала нужно узнать, насколько изменился Y: new_y - ray_y. Отсюда, с помощью нашего любимого тангенса, можно узнать, насколько изменился X: (new_y - ray_y) / tan(ray_angle). Далее нам нужно найти новые координаты луча по X (ведь по Y мы уже знаем). Это можно сделать, прибавив изначальную координату X к смещению.

Смещение мы знаем, изначальную координату тоже: new_x = ray_x + (new_y - ray_y) / tan(ray_angle). Для второго цикла аналогично, вот формулы для одного из случаев: new_x = ceil(ray_x), new_y = ray_y + (new_x - ray_x) / tan(ray_angle).

По словам, сказанным в начале объяснения, рисуем полоску и... пока что ничего не понятно, ведь на экране лишь одна полоска.

Для псевдо-3D таких полосок понадобится около ста, где каждая полоска по номеру просчитывается с таким же по номеру лучом.

Угол всех лучей отличается друг от друга на ~1 градус, но это значение можно изменять как угодно. И вуаля! - это похоже на 3D. Только есть странное ощущение, что что-то не так.

А не так тут то, что появился эффект рыбьего глаза. Частично это решить не трудно: нужно length_of_ray = length_of_ray * cos(center_ray_angle - ray_angle), где center_ray_angle — это угол направления взгляда игрока, а ray_angle — это угол луча.

Код: https://github.com/xdettlaff/mindustry-3d-engine/tree/main/parts/clean_ray.mlog

Создание движка

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

Вот его псевдокод:

widthmap = 13  // задаем ширину карты в 13 блоков (позже мы можем менять это значение)
FOV = 100  // угол обзора
Sensitivity = 100  // чувствительность курсора
Stepsize = 0.333333333 // длина шага
wall_height = 0.67 // высота стены
AbsError = 0.001 // рудимент
cell_size = 1 // размер клетки(он всегда будет равен единице
texture_size = 7 // ширина(и длина) текстуры в пикселях
shadow_k = 40 // коэффициент затемнения shadow_distance = 2 // растояние тени
max_shadow = 200 // максимально возможная тень
write widthmap to cell1 at 0 // ниже просто записываем все 
write texture_size to cell1 at 1
write Stepsize to cell1 at 10
write FOV to cell1 at 11
write Sensitivity to cell1 at 12
write cell_size to cell1 at 14
write wall_height to cell1 at 17
write shadow_k to cell1 at 19
write shadow_distance to cell1 at 20
write max_shadow to cell1 at 21

Код: https://github.com/xdettlaff/mindustry-3d-engine/tree/main/parts/settings.mlog

Теперь поставим блок памяти (он вмещает 512 значений), гиперпроцессор и логический дисплей.

Блок памяти хранит в себе карту в виде двухмерного массива. Для того, чтобы получить индекс этого массива, зная X и Y блока и ширину карты, нужно использовать эту формулу i = y * widthmap + x:

i = y * widthmap
i = i + x
 

Или

op mul i y widthmap
op add i i x

Также можем использовать формулу нахождения X и Y по индексу i:

x = i % widthmap
y = i // widthmap

Или

op mod x i widthmap
op idiv y i widthmap

Теперь начнем писать код для мини-карты, которая отображается на дисплее.

Коротко о коде:

1. Считываем необходимые данные и записываем их в соответствующие переменные: размер карты, положение персонажа, вектор взгляда персонажа (косинус и синус), положение курсора и т.д.
2. Задаём толщину рисуемой линии и стираем прошлый кадр.
3. Цикл отрисовки блоков из банка памяти (тут будет небольшой костыль)
4. Отрисовка персонажа.

Важное примечание: когда процессор выполняет команду draw, на экране ничего не рисуется и лишь записывается в графический буфер, а вот уже команда drawflash за раз отрисовывает всё из этого буфера. Однако есть одна помеха: в буфере не может находиться много фигур (максимум примерно 170) и если он забьётся, то при отрисовке потеряется часть фигур, поэтому нужно вовремя всё отрисовывать. Для проверки на количество фигур в буфере будем использовать переменную dr:

read widthmap = cell1 at 0 // считываем данные из блока памяти и записываем их в соответствующие переменные (тут читаем размер карты)
// тут что - то ненужное было
// тут что - то ненужное было
read xperson = cell1 at 4 // координаты игрока
read yperson = cell1 at 5
read cos = cell1 at 6 // косинус направления взгляда
read sin = cell1 at 7 // синус направления взгляда
read xrect = cell18 at 8 // координаты курсора редактора карты
read yrect = cell1 at 9
maxnum = widthmap ^ 2 // находим количество блоков на всей карте (т.к она квадратная, просто возводим в квадрат ее размер)
mul = 88 / widthmap //размер одного блока в пикселях на мини-карте
draw stroke 1 // задаем толщину линии в 1 пиксель
draw clear 0 0 0 // очищаем экран Чёрным цветом
i = 0 // ставим счетчик цикла отрисовки
dr = 0 // количество фигур в буфере обмена
read status = bank1 at i // читаем состояние блока из банка карты (воздух или стена)
x = i % widthmap // тут формула нахождения координат блока из его индекса в блоке памяти
y = i // widthmap 
x = x * mul // нахождение координат блока на дисплее
y = y * mul
draw color 0 255 0 128 // задать зеленый цвет и немного прозрачности следующим рисующимся фигурам фигурам
jump 24, if status == 0 // если это блок воздуха, то мы перепрыгиваем часть с его отрисовкой
draw rect x y mul mul // рисуем блок
dr = dr + 2 // увеличиваем счетчик фигур на 2(т.к мы 2 раза использовали команду draw)
jump 27, if dr < 170 // если мы не нарисовали более 170 фигур, то перепрыгиваем полную отрисовку на дисплее
drawflush display1 // отрисовываем все фигуры
dr = 0 // обнуляем счетчик фигур
i = i + 1 // добовляем 1 к счётчику цикла
jump 15, if i < maxnum // прыгаем в начало цикла, если не отрисовали все блоки
x = xperson * mul // находим координаты игрока на дисплее
y = yperson * mul
cosm = cos * 255 // вектор длиной в 255 пикселей по направлению взгляда игрока
sinm = sin * 255
xm = x + cosm // координаты конца линии взгляда
ym = y + sinm
draw color 0 255 0 255 // задаем зеленый полностью непрозрачный цвет
draw poly x y 24 5 0 // рисуем кружочек(игрока) в координатах игрока
draw line x y xm ym // рисуем длинную линию(длиннее самого экрана) вдоль направления взгляда игрока
x = xcross * mul
y = ycross * mul
draw color 255 0 0 255
draw poly x y 24 5 0
xrect = xrect * mul находим координаты курсора редактора карты на дисплее
yrect = yrect * mul
draw stroke 3 // толщина линии - 3
draw color 0 255 0 255 // задаем зеленый непрозрачный цвет
draw lineRect xrect yrect mul mul  рисуем незакрашенный прямоугольник в координатах курсора редактора карты
drawflush display1 // отрисовываем всё

Код: https://github.com/xdettlaff/mindustry-3d-engine/tree/main/parts/drawmap.mlog

Однако на экране почти ничего нет, так как наша карта состоит из блоков воздуха, а игрок стоит в нулевых координатах и смотрит под углом 0°.

Теперь поставим ещё процессор, турель и пару кнопок:

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

Примечание: чтобы человеку, играющему на телефоне, было удобно поворачивать камеру (менять угол взгляда персонажа от 1-го лица), нужно, чтобы угол менялся только при стрельбе турели, в которой сидит игрок, то есть когда игрок зажал палец на экране и начал им вести, в противном случае при любом нажатии на экран угол взгляда будет постоянно меняться рывками, даже если мы просто нажали на кнопку, сидя в турели. Также необходимо запоминать координаты, где игрок начал и закончил стрельбу в прошлый раз, чтобы при зажатии экрана телефона направление взгляда персонажа резко не изменилось. Для записи координат конечного и начинающегося выстрела мы будем использовать конструкцию под названием флаг.

В роли флага у нас переменная. Когда флаг поднят, то она = 1, если опущен, то она = 0. Таким образом, нам нужно 2 флага: флаг начала стрельбы (чтобы записать координаты начального выстрела) и флаг завершающего выстрела (чтобы записать координаты последнего выстрела).

Данные координаты нам пригодятся для того, чтобы человеку было удобно поворачивать камеру.

Вот псевдокод процессора:

read widthmap in cell1 at 0
read step in cell1 at 10  // длина шага (переменная step)
read sensitivity = cell1 at 11  // чувствительность курсора
mul = 176 / widthmap
mul = 176 / widthmap
shoot = @shooting in arc1  // записываем статус выстрела турели (стреляет не стреляет) (1 или 0) в переменную shoot
jump 24, if shoot == false  // пропускаем блок кода ниже до 24 строки, если турель не стреляет (завершающий выстрел)
shootX = @shootX in arc1  // координата Х выстрела турели в блоках на карте МД
displayX = @x in arc1  // координата Х в блоках на карте МД
xcursor = shootX - displayX  // находим координату выстрела в блоках относительно дисплея
op mul xcursor = xcursor * 29.33333  // находим координату выстрела в пикселях на дисплее
xcursor = xcursor + 88  // ищемм координаты выстрела в пикселях относительно центра дисплея
xcursor = xcursor / -176  // делим на -178
op mul xcursor = xcursor * sensitivity  // умножаем координату на чувствительность
jump 17, if startshoot != 0  // если флаг startshoot (мы  только что начали стрелять) поднят, то пропускаем 2 строки ниже
xstartcursor = xcursor  // задаем начальную точку стрельбы
startshoot = 1 // поднимаем флаг
xcursor = xcursor - xstartcursor  // находим координаты выстрела относительно начальной точки стрельбы
xcursor = xcursor + xcursorlast  // добавляем координаты относительно точки начала стрельбы к координатам прошлого завершающего выстрела
cos = cos(xcursor)  // находим синус и косинус угла, учитывая, что угол - это координата xcursor
sin = sin(xcursor)
angle = xcursor
stopshoot = 0  // опускаем флаг завершающего выстрела(ведь данный участок кода выполняется, когда стрельба идет)
jump 28  // перепрыгиваем участок в флагом завершающего выстрела
jump 28, if stopshoot != 0  // если флаг завершающего выстрела опущен(мы только что закончили стрельбу), то выполняем код ниже, а если поднят - пропускаем
xcursorlast = xcursor  // задаем координаты завершающего выстрела
stopshoot = 1  // поднимаем флаг завершающего выстрела
startshoot = 0  // опускаем флаг начала стрельбы
print "buttons"  // рудимент
xstep = cos * step  // считаем вектор одного шага вперёд
ystep = sin * step
read xperson = cell1 at 4  // читаем координаты 
read yperson = cell1 at 5
up = @enabled in switch1  // читаем состояние в кнопке "вверх"
jump 38, if up == false  // пропускаем действие, если кнопка не нажата
@enabled in switch1 = 0  // выключаем кнопку
xperson = xperson + xstep  // добавляем к координатам персонажа вектор ходьбы персонажа
yperson = yperson + ystep
left = @enabled in switch2  // далее такая же конструкция как и с кнопкой "вверх"
jump 43, if left == false
xperson = xperson - ystep  //тут мы немного меняем знаки координат вектора, чтобы персонаж двигался не вперёд, а 
yperson = yperson + xstep
@enabled in switch2 = 0
right = @enabled in switch3  // конструкция с кнопкой ""
jump 48 equal right false
xperson = xperson + ystep
yperson = yperson - xstep
@enabled in switch3 = 0
down = @enabled in switch4  // конструкция с кнопкой ""
jump 53 equal down false
xperson = xperson - xstep
yperson = yperson - ystep
@enabled in switch4 = 0
START = @enabled in switch5  // кнопка, отвечающая за запуск (работу схемы) (если нажата- работает, если нет - то нет, если сделать выкл. вкл., то мы перезапустим движок)
jump 56, if down == 1  // запишем 0 в ячейку синхронизации, если кнопка "старт" выключена
write 0 to cell1 at 15
yperson = min(yperson, widthmap)  // ограничиваем координаты
xperson = min(xperson, widthmap)
yperson = max(yperson, 0)
xperson = max(xperson, 0)
write xperson to cell1 at 4  // записываем необходимые  в ячейку
write yperson to cell1 at 5
write cos to cell1 at 6
write sin to cell1 at 7
write START ro cell1 at 16
angle = angle % 360
write angle to cell1 at 13

Код: https://github.com/xdettlaff/mindustry-3d-engine/tree/main/parts/input.mlog

Наконец-то мы можем ходить по карте и поворачивать камеру:

Теперь желательно создать редактор карты, дабы легко её изменять. Для начала поставим 5 кнопок и гиперпроцессор.

Подключим его как на картинке.

И напишем несложный псевдокод, который позволит перемещать курсор по мини-карте и заполнять/стирать блоки.

read widthmap = cell1 at 0
read xcell = cell1 at 8 // читаем координаты курсора (xcell и ycell)
read ycell = cell1 at 9
up = @enabled in switch1 // далее знакомая конструкция из 4-х кнопок по перемещению 
jump 7, if up == false
@enabled in switch1 = 0
ycell = ycell + 1
left = @enabled in switch2
jump 11, if left == false
xcell = xcell - 1
@enabled in switch2 = 0
right = @enabled in switch3
jump 15, if right == false
xcell = xcell + 1
@enabled in switch3 = 0
down = @enabled in switch4
jump 19, if down == false
ycell = ycell - 1
@enabled in switch4 = 0
status = @enabled in switch5 // эта кнопка либо заполняет клетку воздухом, либо стеной
jump 22, if xcell >= 0 // если курсор заходит за пределы поля, то мы его переносим в 
xcell = widthmap - 1
jump 24, if ycell >= 0
ycell = widthmap - 1
x = xcell % widthmap
y = ycell % widthmap
i = y * widthmap // тут формула нахождения индекса в банке карты
i = i + x
write status to bank1 at i // запишем состояние 5й кнопки в выбранный курсором блок
write x to cell1 at 8 // запишем координаты курсора
write y to cell1 at 9

Теперь мы можем редактировать карту:

Код: https://github.com/xdettlaff/mindustry-3d-engine/tree/main/parts/input_to_map.mlog

Рисуем текстуры

В предыдущих пунктах мы разобрали, как найти координаты пересечения луча со стеной (ray_x и ray_y). Также мы разобрали формулу нахождения высоты отрисовываемой полоски пикселей на экране (wall_height в пикселях). Однако нам необходимо отрисовывать не просто полоску одного цвета, а часть картинки, которая нарисована на стене.

Для отрисовки текстуры нам необходима сама картинка - текстура стены. Данная картинка хранится в блоках памяти. У неё есть ширина и высота в пикселях, и мы будем хранить ее в одной переменной - texture_size. Так как это квадрат, то и ширина, и высота описываются одной переменной. Сами пиксели (точнее их цвета) расположены в блоках памяти так: в одной ячейке блока памяти хранится RGBa цвет одного пикселя, запакованный командой pack color в одно число.

Далее мы будем называть блоки памяти для текстур – массивом текстур, а номер ячейки — индексом массива текстур.

Как мы знаем, пиксель на картинке имеет две координаты — X и Y, но массив текстур — одномерный (ведь блок памяти - это одномерный массив). Как же найти индекс пикселя в массиве текстур? Всё просто: X-координата — это номер столбца пикселей, а Y — это номер пикселя в столбце.

Высота и ширина одной картинки всегда равны переменной texture_size, и, допустим, она равна 4. Пускай в первых четырёх индексах хранятся пиксели одного столбца снизу вверх, а в следующих четырёх — следующий столбец, ведь texture_size = 4.

Отсюда можно вывести формулу нахождения индекса i по координатам пикселя: i = Y * texture_size + X

А как рисуется-то текстура?

Рисуется полоска некой высоты, а далее мы находим, какой части текстуры соответствует данная полоска. Теперь осталось определить, какой столбец отрисовывать в виде отдельной полоски. Определить это довольно легко: если луч пересекается по вертикали со стеной (то есть ray_x равен целому числу или ray_x % cell_ = 0), то координатой X на картинке будет ceil(ray_y % cell_size) × texture_size (где ceil() — округление, % - остаток от деления).

Следующим шагом будет отрисовка столбца пиксель за пикселем. Как уже известно, высота стены в пикселях на дисплее — это wall_height, значит, высота одного пикселя картинки в пикселях на экране: pixel_height = wall_height / texture_size

Теперь найдём на какой координате дисплея будет начинаться стена по формуле: start_y = 88 − (wall_height / 2) (где 88 - это 176/2, а это кол-во пикселей в высоту у маленького дисплея)

Высота пикселя картинки:

pixel_height = wall_height / texture_size

Финал

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

Это, конечно, не лучший псевдо-3D, который можно было сделать, ведь туда можно добавить «спрайты» или же убрать рыбий глаз на 100%, а не частично. Но даже так получилось неплохо.

Проект: https://github.com/xdettlaff/mindustry-3d-engine

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


  1. vesper-bot
    29.05.2024 17:19
    +1

    Мда. Я едва-едва с основами разобрался, ещё с мобами воюю, а тут внезапное тридэ, ещё немного и дум портируете :)