Дважды в год компания NextRoll организует мероприятие Hack Week, на котором сотрудники на неделю берутся за проект по своему выбору. Это превосходная возможность для экспериментов, изучения новых технологий и объединения с людьми из всех отделов компании. Узнать о Hack Week подробнее можно здесь.
Так как NextRoll всё активнее использует язык программирования Rust, на Hack Week инженеры обычно пытаются получить опыт работы с ним. Ещё одним популярным вариантом выбора является работа над видеоиграми, и, как вы уже могли догадаться, мы часто видим в проектах сочетание видеоигр и языка Rust.
В прошлом году группа сотрудников работала над развитием моей игры rpg-cli. На этот раз захотелось пойти дальше и взять проект, который показывает некоторые из сильных сторон Rust: низкоуровневым программированием, высоконагруженными вычислениями и операционной совместимостью данных с языком C. Поэтому мы решили портировать на Rust классическую игру Wolfenstein 3D.
Предыстория
id Software знаменита тем, что выжимает максимум из программирования игр для PC: сначала она реализовала сайдскроллеры в стиле NES на не предназначенном для них оборудовании, затем практически изобрела жанр трёхмерного шутера от первого лица и стала лидером в нём, а позже сделала реальностью сетевой и Интернет-мультиплеер. Параллельно эта компания популяризировала распространение ПО по методике shareware, вдохнула жизнь в любительский моддинг и выложила в открытый доступ исходный код всех своих хитовых игр. Об этой истории говорится в книге Masters of Doom Дэвида Кушнера; о технических подробностях рассказывается в серии Game Engine black books Фабьена Санглара.
Игра Wolfenstein 3D, менее известная, чем её потомки Doom и Quake, стала важным этапом в эволюции id Software и игр для PC в целом. Кроме того, из-за более примитивных технологий её исходный код лучше подходит для исследования и реализации. Игра не имеет реального 3D-движка, а симулирует 3D-мир из 2D-карты при помощи техники, называемой ray casting. Вся отрисовка выполняется непосредственным размещением пикселей на экране.
Прочитав несколько лет назад Wolfenstein black book, я попытался портировать игру на Python на основании другого современного порта wolf4sdl. Я стремился быть как можно ближе к исходникам оригинала, что оказалось очень сложно, поэтому со временем я забросил проект. Недавно Марио Ругиеро, тоже прочитавший книгу, предложил в качестве проекта на Hack Week сделать порт игры на Rust. К нему присоединилось множество людей, в том числе и я; однако по моему предыдущему опыту наша задача всё равно была пугающей: некоторые из нас были новичками в Rust, кто-то никогда не играл в Wolf, кто-то ещё не прочитал книгу, и никто из нас ранее не реализовывал ray casting. Мы начали, не надеясь получить что-то осязаемое к концу недели, но увидели в проекте огромные возможности для обучения, поэтому взялись за него.
Разработка
Мы приблизительно разбили игру на компоненты, с которыми можно было работать по отдельности, поэтому каждый участник команды выбрал свой и начал работу с ним:
- Распаковка и парсинг графических файлов.
- Распаковка, парсинг и интерпретация файлов карт.
- Манипуляции с графикой и рендеринг текстур при помощи SDL.
- Ray casting.
- Игровой цикл и управление вводом.
- Рендеринг мира.
В случаях, когда выходные данные компонента требовались в качестве входных данных для следующего компонента, мы использовали заранее обработанные или прописанные в коде данные, извлечённые из справочных реализаций wolf4py и wolf4sdl: распакованные двоичные дампы ресурсов, прописанные в коде карты и стены, и т. п. Это позволило нам работать параллельно.
Ресурсы
Первой задачей в портировании игры было считывание её данных. Wolfenstein имеет набор файлов с различными ресурсами: графикой (изображениями, текстурами и спрайтами), аудио (музыкой и звуковыми эффектами) и картами. Одна из сложностей заключалась в том, что в каждой версии игры файлы слегка различались, имели разные смещения, а в некоторых случаях и разные способы сжатия. Файлы .WL1 для Rustenstein мы взяли из shareware-версии [бесплатная неполная версия игры — прим. пер.] и добавили в репозиторий.
В каждом файле используется разная комбинация нескольких алгоритмов распаковки, и все эти алгоритмы нам нужно было портировать на Rust:
- Традиционное сжатие Хаффмана.
- Сжатие RLEW — алгоритм кодирования длин серий (run-length encoding), работающий на уровне слов.
- Сжатие «Кармака» — разработанный Джоном Кармаком вариант метода LZ (Лемпеля — Зива). Согласно Black Book, не имея доступа к литературе, Кармак «изобрёл» алгоритм, который, как выяснилось позже, был придуман другими.
Оригинальный движок Wolf имел компонент Memory Manager для управления распределением и сжатием памяти (вместо традиционного
malloc
языка C), а также Page Manager для перемещения ресурсов с диска в ОЗУ. На современном оборудовании оба компонента не нужны, так как мы можем считать, что все ресурсы поместятся в памяти, поэтому в наш порт эти компоненты не включены.Код парсинга и распаковки карт можно найти здесь, а для всех остальных ресурсов — здесь.
Карты
Карты Wolfenstein 3D описываются как сетка тайлов размером 64x64. Каждая карта содержит два слоя тайлов: один для стен и дверей, другой для размещения игрока, врагов и бонусов. Значения тайлов обозначают, какая текстура будет рендериться на стенах, какие замки требуются для дверей, куда смотрит игрок и т. д. Все стены имеют одинаковую высоту, и поскольку они представлены как блоки сетки тайлов, все они пересекаются под прямым углом; это сильно ограничивает дизайн уровней, зато значительно упрощает алгоритм рейкастинга для отрисовки 3D-мира.
Ниже показана первая карта первого эпизода такой, как она выглядит в редакторе карт Wolfenstein:
А вот та же карта в ASCII, выведенная нашим отладочным кодом:
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW WWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW WWWWWWWW W W WWWWWWWWWWWWWWWWWWWW
WWWWWW WWWWWWW | | W WWWWWWWWWWWWWWWWWWWW
WWWWWW WWWWWWW W W WWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWW WWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW WWW WWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW WWW WWWWWWWWWWWWW-WWWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW W WWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW | WWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW W WWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWW WWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W WW WWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W WWWWWW-WWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W WWWWW WWWWWWWWWWWWWWWW WW WWWWWWWWWWWWWWWWWWWWWWWWWW
WW-WWWWWW WWWWWWWWWWWWWWWW WWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W WWWWW WWWWWWWWWWWWWWWW WWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W W WWWWWWWWWWWWWWWW WWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W W WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W WWWWWW-WWWWWWWWWWWWWWWWWWWWWWW-WWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W WW WWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWW WW
W WW WWWWWWWWWWWW WWWWWWWWWWWW WW
W WW WWWWWWWWWWWW WWWWWWWWWWWW WW
W W WWWWWWWWWWWW W W WW
W | WWWWWWWWWWWW | | WW
W W WWWWWWWWWWWW W W WW
W WW WWWWWWWWWWWW WWWW WWWW WW
W WW WWWWWWWWWWWW WWWWW WWWWW WW
W WW WWWWWWWWWWWWWWWWW WWWWWWWWWW WWWWW WW
W WWWWWW-WWWWWWWWWWWWWWWWWWWWWWW-WWWWWWWWWWWW WWWWWWW WW WWWW
W WWWWW WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW WWWWWWWWWWWWWWW
W WWWWW WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW WWWWWWWWWWWWWWW
W W W WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW WWWWWWWWWWWWWWW
W W WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW WWW W W W WWWWW
W W W WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW W WWWW
W WWWWW WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW | WWWW
W WWWWW WWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWW W WWWW
W W WWWWWWWWW WWWWWWWWWWWWWWWW W W W WWWWW
W | W WWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W WWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWW W W WWWWWWWWWWWWWW-WWWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWW W W W WWWWWWWWW W W WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWW WWWWWWWWWWW | | WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW W W WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWW WWWWWWWWWWWWW W W WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWW WWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW W W WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW P | | WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW W W WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW WWWWWWWWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Отрисовка пикселей
Когда дело касается графических ресурсов, распаковка и загрузка данных в память — это только половина работы. Двоичные блоки, из которых состоит каждый графический элемент (изображение, спрайт или текстура) имеют структуру, предназначенную для быстрого рендеринга на VGA-дисплеях, для которых изначально была рассчитана игра. Это означает, что графика поворачивается для отрисовки в столбцах, а сами столбцы расположены в файле попеременно, поскольку стандарт VGA позволял параллельно записывать до четырёх банков видеопамяти.
Каждый байт в двоичных блоках графики является индексом 256-цветной палитры, используемой в Wolfenstein 3D. Справочная реализация wolf4sdl записывает эти блоки в на SDL-поверхность, которая в свою очередь перед копированием на экран преобразуется в цвета RGB. Подробнее см. в этом посте.
Так как привязки Rust для SDL используют другой набор абстракций (в частности, они не раскрывают функцию SDL_ConvertPixels), мы выбрали вариант преобразования индекса палитры в цвета RGB на лету, выполняя запись напрямую в RGB-текстуру, которая затем копируется на холст. Это означает, что процедуры рендеринга необходимо адаптировать для записи байтов красного, синего и зелёного вместо одного байта индекса палитры.
fn put_pixel(buffer: &mut [u8], pitch: usize, x: u32, y: u32, color: (u8, u8, u8)) {
let (r, g, b) = color;
let offset = y as usize * pitch + x as usize * 3;
buffer[offset] = r;
buffer[offset + 1] = g;
buffer[offset + 2] = b;
}
Две реализованные нами процедуры рендеринга графики были напрямую портированы из реализации wolf4py, которая, в свою очередь, почти построчно была портирована из справочного форка wolf4sdl. Первая процедура обрабатывает вывод полного изображения непосредственно на экран. Она используется для экрана заставки, а также для полосы состояния игрока в нижней части экрана в процессе игры:
fn draw_to_texture(texture: &mut Texture, pic: &Picture, color_map: ColorMap) {
texture.with_lock(None, |buffer: &mut [u8], pitch: usize| {
for y in 0..pic.height {
for x in 0..pic.width {
let source_index =
(y * (pic.width >> 2) + (x >> 2)) + (x & 3) * (pic.width >> 2) * pic.height;
let color = pic.data[source_index as usize];
put_pixel(buffer, pitch, x, y, color_map[color as usize]);
}
}
});
}
Вторая, гораздо более сложная процедура, выполняет отрисовку спрайтов и в настоящее время применяется для отображения оружия игрока. Осталось портировать похожую, но ещё более сложную функцию: отрисовывающую отмасштабированные изображения, например, текстуры стен и спрайты врагов.
В процессе разработки вместо оружия отобразился неожиданный спрайт
Желательно будет усовершенствовать эту реализацию так, чтобы основная часть обработки выполнялась один раз, как часть этапа загрузки ресурсов, а двоичные блоки хранились в памяти, готовые к записи на экран.
Соответствующий код находится здесь.
Ray casting
Фундаментом движка Wolfenstein 3D является алгоритм рейкастинга. Эта процедура позволяет нам проецировать 2D-мир (заданный в виде карты тайлов размером 64x64) в 3D-окно только на основании одних 2D-операций. Вкратце этот алгоритм можно описать так:
- Испускаем луч из текущей позиции игрока для каждого столбца пикселей ширины экрана. Например, классическое разрешение Wolfenstein 3D равно 320x200, так что для отрисовки кадра нужно испустить 320 лучей.
- Продлеваем луч в направлении, определяемом текущим горизонтальным пикселем, позицией игрока и его областью обзора, пока он не коснётся стены на карте. Так как стены прямоугольны, вычисления продлевания луча сильно упрощены, поскольку расстояние между тайлами одинаково.
- После пересечения луча со стеной вычисляем при помощи тригонометрии расстояние от игрока до этой стены.
- Задаём высоту стены, обратно пропорциональную вычисленному расстоянию. То есть, чем дальше от игрока находится стена, с которой столкнулся луч, тем меньше она кажется с точки зрения игрока (и тем меньше столбец пикселей, который нужно отрисовать на экране).
Ниже представлена упрощённая версия алгоритма на JavaScript, созданная на основе этого туториала:
function rayCasting(screen, map, player) {
let precision = 64;
let incrementAngle = player.fieldOfView / screen.width;
let wallHeights = [];
let rayAngle = player.angle - player.fieldOfView / 2;
for(let rayCount = 0; rayCount < screen.width; rayCount++) {
// испускаем луч из позиции игрока
let ray = {
x: player.x,
y: player.y
};
// луч движется с постоянными инкрементами
let rayCos = Math.cos(degreeToRadians(rayAngle)) / precision;
let raySin = Math.sin(degreeToRadians(rayAngle)) / precision;
// двигаем луч вперёд, пока он не найдёт стену (ненулевой тайл)
let wall = 0;
while(wall == 0) {
ray.x += rayCos;
ray.y += raySin;
wall = map[Math.floor(ray.y)][Math.floor(ray.x)];
}
// вычисляем расстояние от игрока до стены, которой коснулся луч
let distance = Math.sqrt(Math.pow(player.x - ray.x, 2) + Math.pow(player.y - ray.y, 2));
// вычисляем высоту в текущем x, обратно пропорциональную расстоянию
wallHeights.push(Math.floor(screen.halfHeight / distance));
// выполняем инкремент угла для следующего луча
rayAngle += incrementAngle;
}
return wallHeights;
}
Для изучения реализации рейкастинга, более близкой к алгоритму из Wolfenstein 3D, рекомендуется эта серия туториалов.
Эта процедура — самое сложное, что попалось в эту Hack Week. Но уже на ранних этапах мы заложили пару решений, которые помогли снизить сложность и успеть в срок хоть что-то. Во-первых, мы взялись за самую простую версию алгоритма, поддерживающую стены из сплошных цветов, а не из текстур. Во-вторых, Джош Берроуз разбирался с рейкастингом на основе туториалов, а не пытался создать построчный порт реализации Кармака (которая, по словам Санглара, является «полностью написанными вручную 740 строками крайне неординарного и сверхэффективного ассемблерного кода») или близнеца wolf4sdl (написанного на C, но всё равно активно использующего операторы
goto
и имеющего множество глобальных побочных эффектов наряду с вычислением высот стен).Вот как выглядела первая карта Wolf в виде сверху после её интеграции в процедуру рейкастинга:
Полную реализацию можно найти здесь.
Рендеринг мира
Отображение 3D-мира начинается с разделения экрана горизонтально на две части, верхняя половина раскрашивается в сплошной цвет потолка, а нижняя — в сплошной цвет пола. После этого нужно отрисовать столбец пикселей с высотой, полученной от алгоритма рейкастинга для каждой горизонтальной координаты. Пока продолжалась разработка алгоритма, мы тестировали код рендеринга при помощи прописанных в коде стен:
После того, как процедура рейкастинга была реализована и ей была передана реальная карта Wolfenstein, мы получили массив высот стен для каждого столбца пикселей на экране и начали видеть мир:
Хоть мы и не реализовали рендеринг текстур, есть пара трюков, улучшивших внешний вид сцены: использование разных цветов для горизонтальной и вертикальной граней стены и обратная пропорциональность компонентов r, g, b каждого пикселя к расстоянию до игрока (которое мы знали по высоте стены), создающая эффект темноты:
Код рендеринга мира выглядит следующим образом:
texture
.with_lock(None, |buffer: &mut [u8], pitch: usize| {
// рисуем цвета пола и потолка
let floor = color_map[VGA_FLOOR_COLOR];
let ceiling = color_map[VGA_CEILING_COLOR];
let vm = view_height / 6;
for x in 0..pix_width {
for y in 0..pix_height / 2 {
let ceilings = darken_color(ceiling, vm - y, pix_center);
put_pixel(buffer, pitch, x, y, ceilings);
}
for y in pix_height / 2..pix_height {
let floors = darken_color(floor, y - vm, pix_center);
put_pixel(buffer, pitch, x, y, floors);
}
}
for x in 0..pix_width {
// используем разные цвета для горизонтальных и вертикальных граней стен
let mut color = if ray_hits[x as usize].horizontal {
color_map[150]
} else {
color_map[155]
};
let current = min(ray_hits[x as usize].height, pix_center);
color = darken_color(color, current, pix_center);
for y in pix_center - current..pix_center + current {
put_pixel(buffer, pitch, x, y, color);
}
}
})
fn darken_color(color: (u8,u8,u8), lightness: u32, max: u32) -> (u8,u8,u8) {
let (r,g, b) = color;
let factor = lightness as f64 / max as f64 / DARKNESS;
let rs = (r as f64 * factor) as u8;
let gs = (g as f64 * factor) as u8;
let bs = (b as f64 * factor) as u8;
(rs, gs, bs)
}
Соединяем всё вместе
За день до демонстрации у нас имелись лишь части игры: не была готова загрузка ресурсов, мы нашли баги в парсинге карт и рендеринге спрайтов, из-за которых проект невозможно было показать, а движок рейкастинга работал с прописанной в коде 2D-картой, отдельной от остальной части проекта. За несколько потрясающих последних часов мы связали всё вместе: устранили баги, соединили разные компоненты, и благодаря нескольким хакам и куче уродливого кода нам удалось собрать впечатляющее видео как раз к моменту демонстраций на Hack Week. Нам даже хватило времени в последние минуты добавить анимацию лица персонажа! Этот процесс напомнил мне истории о видеоигровых компаниях, в спешке собирающих демо, чтобы успеть показать их на E3.
До работающей игры далеко, но мы не ожидали даже подобного результата всего за несколько дней работы. В течение этой недели мы довольно много узнали о Rust, и продвинулись дальше, чем если бы работали по одиночке. И в конечном итоге наш проект выиграл награду за техническое исполнение!
Теперь прототип выложен в open source, однако, как я сказал, код требует значительной чистки. Так как работать с проектом было очень интересно и за эту первую неделю мы смогли решить самые сложные задачи (загрузку ресурсов, рейкастинг, рендеринг спрайтов и стен), нам не терпится продолжить его. Вот некоторые из возможностей, которые мы хотели бы добавить следующими:
- Рендеринг текстур стен.
- Отображение и подбирание предметов.
- Добавление врагов на карту, реализация боя и ИИ врагов.
- Реализация дверей и ключей.
- Реализация толкаемых стен.
Комментарии (14)
Myxach
15.02.2022 15:01+2Rustenstein 3D: программируем, как будто сейчас 1992 год
Используем язык высокого уровня
А потом появляется люди, которые не понимают, как раньше так долго делали простенькие по нынешным меркам игры
H_I_R_U_R_G
15.02.2022 19:04Если вспомнить, какой интерес вызывал геймплей тех игр и нынешних, то не очень понятно, как можно так долго делать простенькие нынешние игры....
axe_chita
16.02.2022 04:25+4Ожидание: полноценная (почти полноценная) реализация Wold3D на RUST, с азартными играми и женщинами с пониженной социальной ответственностью.
Реальность: мы смогли реализовать raycasting на RUST и «благодаря нескольким хакам и куче уродливого кода нам удалось» хоть что-то показать людям.
Программист на QBASIC, скептически хмыкает, стряхивает пыль с тридцатилетней IDE и пишет свой raycasting полностью на QbasicSource code re-write from video by Bernhard Slawik 2020-10-01' Shown by Carl Mahnke, but now offline
' www.youtube.com/watch?v=3Qpht8dSps8
' rws.sebus.de/misc/raycast.rar
' Re-write by Bernhard Slawik 2020-10-01
' But there is stuill a bug in normaly% i think…
' See "***"
DEF SEG = 0
pl.x = 66: pl.xtmp = 66
pl.y = 220: pl.ytmp = 220
pl.ld = 5
angf% = 1440
angtol% = 20
cols% = 320
colstep% = 1 ' Set to >1 to decrease horizontal resolution
notexture% = 0 ' Set to 1 for fast no-texture mode
colsh% = cols% / 2
DIM map(15, 15) AS INTEGER
DIM costable#(angf% — 1)
DIM sintable#(angf% — 1)
pi# = 3.14159265#
DIM buffer%(63, 448)
angh% = angf% / 2
angq% = angf% / 4
angtq% = 3 * angf% / 4
' Pre-calc sine table
PRINT «Calculating sine table...»
FOR x = 0 TO angf% — 1
PRINT (x):
y# = x / (angf% — 1)
costable#(x) = COS(y# * pi# * 2)
sintable#(x) = SIN(y# * pi# * 2)
NEXT x
' Read map data
FOR y = 0 TO 15
FOR x = 0 TO 15
READ map(x, y)
NEXT x
NEXT y
' graphic stuff
SCREEN 13
PRINT «Loading textures...»
'GOTO fake ' Do not load texture, use pattern
' Read from BMP file
OPEN «MAH_TEX2.BMP» FOR BINARY AS #1
'SEEK #1, 1079
SEEK #1, 55 + (4 * 256) + (64 * 32)
FOR y% = 447 TO 0 STEP -1
FOR x% = 0 TO 63
c% = ASC(INPUT$(1, #1))
buffer%(x%, y%) = c%
NEXT x%
NEXT y%
SEEK #1, 55
FOR x% = 0 TO 255
b% = ASC(INPUT$(1, #1)) \ 4
g% = ASC(INPUT$(1, #1)) \ 4
r% = ASC(INPUT$(1, #1)) \ 4
q% = ASC(INPUT$(1, #1))
PALETTE x%, r% + g% * 256 + b% * 256 ^ 2
NEXT x%
CLOSE #1
GOTO main
fake: ' Create procedural textures
PRINT «Generating textures...»
FOR y% = 447 TO 0 STEP -1
FOR x% = 0 TO 63
c = (x% * y%) MOD 256 ' Random
buffer%(x%, y%) = c
NEXT x%
NEXT y%
main:
' Main loop
DO
lookdir% = pl.ld
FOR ray% = 0 TO cols% STEP colstep%
pl.ld = ((lookdir% + angf% — colsh%) + ray%) MOD angf%
' horizontal detection
dis1% = -1
IF pl.ld MOD angh% > angtol% AND pl.ld MOD angh% < (angh% — angtol%) THEN
dir% = (ABS(pl.ld — angh%) / (pl.ld — angh%)) * -1
pl.xb = pl.x \ 64
pl.yb = pl.y \ 64
rest% = ABS(((pl.y MOD 64) + (64 * (dir% * -1)) + ((1 + dir%) / 2)) MOD 64)
shortx% = rest% * costable#(pl.ld) / sintable#(pl.ld) * dir%
actdef% = shortx%
nblockx% = (shortx% + pl.x) \ 64
nblocky% = pl.yb + dir%
IF nblockx% > -1 AND nblockx% < 16 THEN
'dir1% = ABS(rest% / sintable#(pl.ld))
dis1% = ABS(rest% / sintable#(pl.ld))
fblock% = map(nblockx%, nblocky%)
IF fblock% = 0 THEN
count% = 1
dis1% = -1
normalx% = 64 * (costable#(pl.ld) / sintable#(pl.ld)) * dir%
'normalx% = 64 * costable#(pl.ld) / sintable#(pl.ld) * dir%
'normalx% = 64 / costable#(pl.ld) * sintable#(pl.ld) * dir%
DO
actdef% = shortx% + (count% * normalx%)
nblockx% = (actdef% + pl.x) \ 64
nblocky% = pl.yb + (dir% * (count% + 1))
'IF nblockx% < 16 AND nblockx% > -1 THEN fblock% = map(nblockx%, nblocky%)
IF nblockx% > -1 AND nblockx% < 16 AND nblocky% > -1 AND nblocky% < 16 THEN fblock% = map(nblockx%, nblocky%)
IF fblock% > 0 THEN dis1% = ABS(rest% / sintable#(pl.ld) + (64 / sintable#(pl.ld) * count%)): count% = 11
count% = count% + 1
LOOP UNTIL count% > 10
END IF
END IF
IF dis1% <> -1 THEN dis1% = dis1% * costable#(ABS(ray% — colsh%))
texoffh% = ABS((pl.x + actdef%) MOD 64)
tex1% = fblock%
END IF
' vertical detection
dis2% = -1
IF ((pl.ld + angq%) MOD angh% > angtol%) AND ((pl.ld + angq%) MOD angh% < (angh% — angtol%)) THEN
dir% = (ABS((pl.ld MOD angtq%) — angq%) / ((pl.ld MOD angtq%) — angq%)) * -1
pl.xb = pl.x \ 64
pl.yb = pl.y \ 64
rest% = ABS(((pl.x MOD 64) + (64 * (dir% * -1)) + ((1 + dir%) / 2)) MOD 64)
'shorty% = rest% * costable#(pl.ld) / sintable#(pl.ld) * dir%
shorty% = rest% / costable#(pl.ld) * sintable#(pl.ld) * dir%
actdef% = shorty%
nblocky% = (shorty% + pl.y) \ 64
nblockx% = pl.xb + dir%
IF nblocky% > -1 AND nblocky% < 16 THEN
dis2% = ABS(rest% / costable#(pl.ld))
's# = sintable#(pl.ld)
'' IF ABS(s#) < .01 THEN s# = .01 * SGN(s#)
'IF s# >= 0# AND s# < .0001 THEN s# = .0001
'ELSE IF s# < 0# AND s# > -.0001 THEN s# = -.0001
'dis2% = ABS(rest% / s#)
fblock% = map(nblockx%, nblocky%)
IF fblock% = 0 THEN
count% = 1
dis2% = -1
normaly% = 64 / costable#(pl.ld) * sintable#(pl.ld) * dir%
DO
actdef% = shorty% + (count% * normaly%)
nblocky% = (actdef% + pl.y) \ 64
nblockx% = pl.xb + (dir% * (count% + 1))
IF nblocky% < 16 AND nblocky% > -1 THEN fblock% = map(nblockx%, nblocky%)
IF fblock% > 0 THEN dis2% = ABS(rest% / costable#(pl.ld) + (64 / costable#(pl.ld) * count%)): count% = 11
count% = count% + 1
LOOP UNTIL count% > 10
END IF
END IF
IF dis2% <> -1 THEN dis2% = dis2% * costable#(ABS(ray% — colsh%))
texoffv% = ABS((pl.y + actdef%) MOD 64)
tex2% = fblock%
END IF
vertical = 0
IF dis2% <> -1 AND dis1% = -1 THEN dis1% = dis2%: vertical = 1
IF dis2% <> -1 AND dis1% <> -1 AND dis2% < dis1% THEN dis1% = dis2%: vertical = 1
IF dis1% = -1 THEN dis1% = 0
IF vertical = 1 THEN texoffh% = texoffv%: tex1% = tex2%
IF dis1% <> 0 THEN lineh% = 200 * (65 / dis1%)
scaler# = lineh% / 64
IF tex1% <> 0 THEN
linehh% = lineh% / 2
' Draw simple color
IF notexture% > 0 THEN LINE (ray%, 100 — linehh%)-(ray%, 100 + linehh%), tex1%: GOTO skiptexture
' Draw texture
FOR pixel% = 0 TO 63
col% = buffer%(texoffh%, pixel% + ((tex1% — 1) * 64))
IF scaler# > 1 THEN
LINE (ray%, 100 — linehh% + pixel% * scaler#)-(ray%, 100 — linehh% + (pixel% + 1) * scaler#), col%
ELSE 'IF scaler# <= 1 THEN
PSET (ray%, 100 — linehh% + pixel% * scaler#), col%
END IF
NEXT pixel%
skiptexture:
END IF
IF lineh% < 200 AND lineh% > 0 THEN
LINE (ray%, 100 — linehh%)-(ray%, 0), 1 ' Sky
LINE (ray%, 100 + linehh%)-(ray%, 200), 17 ' Floor
END IF
NEXT ray%
pl.ld = lookdir%
' Keyboard
'POKE &H41A, PEEK(&H41C)
'k% = INP(&H60)
k$ = INKEY$
'LOCATE 1, 1: PRINT k$
'IF k% = 30 THEN pl.ld = pl.ld — 20
'IF k% = 32 THEN pl.ld = pl.ld + 20
IF k$ = CHR$(0) + «M» THEN pl.ld = pl.ld + 20
IF k$ = CHR$(0) + «K» THEN pl.ld = pl.ld — 20
walkdir% = (pl.ld + angq%) MOD angf%
'speed# = speed# / 2
speed# = 0
'IF k% = 17 THEN speed# = 10
'IF k% = 31 THEN speed# = -10
IF k$ = CHR$(0) + «H» THEN speed# = 10
IF k$ = CHR$(0) + «P» THEN speed# = -10
pl.ytmp = (pl.ytmp — speed# * costable#(walkdir%))
pl.xtmp = (pl.xtmp + speed# * sintable#(walkdir%))
pl.y = INT(pl.ytmp)
pl.x = INT(pl.xtmp)
pl.ld = ABS((pl.ld + angf%) MOD angf%)
LOOP UNTIL k$ = CHR$(27) 'k% = 16
' Insert Map
DATA 7,2,1,2,1,1,1,2,1,1,1,1,1,1,1,1
DATA 7,0,0,0,2,2,2,2,2,6,0,0,0,0,0,3
DATA 7,0,0,0,0,5,0,0,3,6,0,0,0,0,0,3
DATA 7,0,0,0,0,0,0,0,3,6,0,3,0,3,0,3
DATA 7,0,0,0,0,5,0,0,3,6,0,0,0,0,0,3
DATA 7,2,2,2,2,3,0,0,3,6,0,0,0,0,0,3
DATA 1,4,1,1,1,1,7,0,7,6,0,0,0,0,7,3
DATA 3,4,7,0,0,0,7,0,7,6,0,0,0,0,0,3
DATA 3,0,3,5,0,5,7,0,7,7,0,0,0,0,0,3
DATA 3,0,0,5,0,0,0,0,0,0,0,0,0,0,0,3
DATA 3,0,0,5,5,5,5,1,5,7,0,0,0,0,0,3
DATA 5,0,5,5,2,2,2,2,2,6,0,0,0,0,0,3
DATA 5,0,0,0,7,0,0,0,0,0,0,3,0,3,0,3
DATA 5,0,0,0,0,0,0,0,0,6,0,0,0,0,0,3
DATA 5,0,0,0,7,0,0,0,0,6,0,0,0,0,0,3
DATA 5,5,5,5,2,2,2,2,2,1,1,1,1,1,1,6
avk013
16.02.2022 09:01download source textures упорно требует логин и пароль
axe_chita
16.02.2022 10:44Сайт ушел в даун, не оплата хостинга и т.д.
Но в комментах к видео, один добрый самаритянин Bernhard Slawik год назад, расшифровал листинг показанный в видео. Таким образом утерян только файл текстур (обычный BMP файл).
Bernhard Slawik 1 год назад
Since the file is offline, I re-constructed the code from the video here: pastebin.com/64MYbJjY
Ниже под катом исходник raycaster'аSource code re-write from video by Bernhard Slawik 2020-10-01' Shown by Carl Mahnke, but now offline
' www.youtube.com/watch?v=3Qpht8dSps8
' rws.sebus.de/misc/raycast.rar
' Re-write by Bernhard Slawik 2020-10-01
' But there is stuill a bug in normaly% i think…
' See "***"
DEF SEG = 0
pl.x = 66: pl.xtmp = 66
pl.y = 220: pl.ytmp = 220
pl.ld = 5
angf% = 1440
angtol% = 20
cols% = 320
colstep% = 1 ' Set to >1 to decrease horizontal resolution
notexture% = 0 ' Set to 1 for fast no-texture mode
colsh% = cols% / 2
DIM map(15, 15) AS INTEGER
DIM costable#(angf% — 1)
DIM sintable#(angf% — 1)
pi# = 3.14159265#
DIM buffer%(63, 448)
angh% = angf% / 2
angq% = angf% / 4
angtq% = 3 * angf% / 4
' Pre-calc sine table
PRINT «Calculating sine table...»
FOR x = 0 TO angf% — 1
PRINT (x):
y# = x / (angf% — 1)
costable#(x) = COS(y# * pi# * 2)
sintable#(x) = SIN(y# * pi# * 2)
NEXT x
' Read map data
FOR y = 0 TO 15
FOR x = 0 TO 15
READ map(x, y)
NEXT x
NEXT y
' graphic stuff
SCREEN 13
PRINT «Loading textures...»
'GOTO fake ' Do not load texture, use pattern
' Read from BMP file
OPEN «MAH_TEX2.BMP» FOR BINARY AS #1
'SEEK #1, 1079
SEEK #1, 55 + (4 * 256) + (64 * 32)
FOR y% = 447 TO 0 STEP -1
FOR x% = 0 TO 63
c% = ASC(INPUT$(1, #1))
buffer%(x%, y%) = c%
NEXT x%
NEXT y%
SEEK #1, 55
FOR x% = 0 TO 255
b% = ASC(INPUT$(1, #1)) \ 4
g% = ASC(INPUT$(1, #1)) \ 4
r% = ASC(INPUT$(1, #1)) \ 4
q% = ASC(INPUT$(1, #1))
PALETTE x%, r% + g% * 256 + b% * 256 ^ 2
NEXT x%
CLOSE #1
GOTO main
fake: ' Create procedural textures
PRINT «Generating textures...»
FOR y% = 447 TO 0 STEP -1
FOR x% = 0 TO 63
c = (x% * y%) MOD 256 ' Random
buffer%(x%, y%) = c
NEXT x%
NEXT y%
main:
' Main loop
DO
lookdir% = pl.ld
FOR ray% = 0 TO cols% STEP colstep%
pl.ld = ((lookdir% + angf% — colsh%) + ray%) MOD angf%
' horizontal detection
dis1% = -1
IF pl.ld MOD angh% > angtol% AND pl.ld MOD angh% < (angh% — angtol%) THEN
dir% = (ABS(pl.ld — angh%) / (pl.ld — angh%)) * -1
pl.xb = pl.x \ 64
pl.yb = pl.y \ 64
rest% = ABS(((pl.y MOD 64) + (64 * (dir% * -1)) + ((1 + dir%) / 2)) MOD 64)
shortx% = rest% * costable#(pl.ld) / sintable#(pl.ld) * dir%
actdef% = shortx%
nblockx% = (shortx% + pl.x) \ 64
nblocky% = pl.yb + dir%
IF nblockx% > -1 AND nblockx% < 16 THEN
'dir1% = ABS(rest% / sintable#(pl.ld))
dis1% = ABS(rest% / sintable#(pl.ld))
fblock% = map(nblockx%, nblocky%)
IF fblock% = 0 THEN
count% = 1
dis1% = -1
normalx% = 64 * (costable#(pl.ld) / sintable#(pl.ld)) * dir%
'normalx% = 64 * costable#(pl.ld) / sintable#(pl.ld) * dir%
'normalx% = 64 / costable#(pl.ld) * sintable#(pl.ld) * dir%
DO
actdef% = shortx% + (count% * normalx%)
nblockx% = (actdef% + pl.x) \ 64
nblocky% = pl.yb + (dir% * (count% + 1))
'IF nblockx% < 16 AND nblockx% > -1 THEN fblock% = map(nblockx%, nblocky%)
IF nblockx% > -1 AND nblockx% < 16 AND nblocky% > -1 AND nblocky% < 16 THEN fblock% = map(nblockx%, nblocky%)
IF fblock% > 0 THEN dis1% = ABS(rest% / sintable#(pl.ld) + (64 / sintable#(pl.ld) * count%)): count% = 11
count% = count% + 1
LOOP UNTIL count% > 10
END IF
END IF
IF dis1% <> -1 THEN dis1% = dis1% * costable#(ABS(ray% — colsh%))
texoffh% = ABS((pl.x + actdef%) MOD 64)
tex1% = fblock%
END IF
' vertical detection
dis2% = -1
IF ((pl.ld + angq%) MOD angh% > angtol%) AND ((pl.ld + angq%) MOD angh% < (angh% — angtol%)) THEN
dir% = (ABS((pl.ld MOD angtq%) — angq%) / ((pl.ld MOD angtq%) — angq%)) * -1
pl.xb = pl.x \ 64
pl.yb = pl.y \ 64
rest% = ABS(((pl.x MOD 64) + (64 * (dir% * -1)) + ((1 + dir%) / 2)) MOD 64)
'shorty% = rest% * costable#(pl.ld) / sintable#(pl.ld) * dir%
shorty% = rest% / costable#(pl.ld) * sintable#(pl.ld) * dir%
actdef% = shorty%
nblocky% = (shorty% + pl.y) \ 64
nblockx% = pl.xb + dir%
IF nblocky% > -1 AND nblocky% < 16 THEN
dis2% = ABS(rest% / costable#(pl.ld))
's# = sintable#(pl.ld)
'' IF ABS(s#) < .01 THEN s# = .01 * SGN(s#)
'IF s# >= 0# AND s# < .0001 THEN s# = .0001
'ELSE IF s# < 0# AND s# > -.0001 THEN s# = -.0001
'dis2% = ABS(rest% / s#)
fblock% = map(nblockx%, nblocky%)
IF fblock% = 0 THEN
count% = 1
dis2% = -1
normaly% = 64 / costable#(pl.ld) * sintable#(pl.ld) * dir%
DO
actdef% = shorty% + (count% * normaly%)
nblocky% = (actdef% + pl.y) \ 64
nblockx% = pl.xb + (dir% * (count% + 1))
IF nblocky% < 16 AND nblocky% > -1 THEN fblock% = map(nblockx%, nblocky%)
IF fblock% > 0 THEN dis2% = ABS(rest% / costable#(pl.ld) + (64 / costable#(pl.ld) * count%)): count% = 11
count% = count% + 1
LOOP UNTIL count% > 10
END IF
END IF
IF dis2% <> -1 THEN dis2% = dis2% * costable#(ABS(ray% — colsh%))
texoffv% = ABS((pl.y + actdef%) MOD 64)
tex2% = fblock%
END IF
vertical = 0
IF dis2% <> -1 AND dis1% = -1 THEN dis1% = dis2%: vertical = 1
IF dis2% <> -1 AND dis1% <> -1 AND dis2% < dis1% THEN dis1% = dis2%: vertical = 1
IF dis1% = -1 THEN dis1% = 0
IF vertical = 1 THEN texoffh% = texoffv%: tex1% = tex2%
IF dis1% <> 0 THEN lineh% = 200 * (65 / dis1%)
scaler# = lineh% / 64
IF tex1% <> 0 THEN
linehh% = lineh% / 2
' Draw simple color
IF notexture% > 0 THEN LINE (ray%, 100 — linehh%)-(ray%, 100 + linehh%), tex1%: GOTO skiptexture
' Draw texture
FOR pixel% = 0 TO 63
col% = buffer%(texoffh%, pixel% + ((tex1% — 1) * 64))
IF scaler# > 1 THEN
LINE (ray%, 100 — linehh% + pixel% * scaler#)-(ray%, 100 — linehh% + (pixel% + 1) * scaler#), col%
ELSE 'IF scaler# <= 1 THEN
PSET (ray%, 100 — linehh% + pixel% * scaler#), col%
END IF
NEXT pixel%
skiptexture:
END IF
IF lineh% < 200 AND lineh% > 0 THEN
LINE (ray%, 100 — linehh%)-(ray%, 0), 1 ' Sky
LINE (ray%, 100 + linehh%)-(ray%, 200), 17 ' Floor
END IF
NEXT ray%
pl.ld = lookdir%
' Keyboard
'POKE &H41A, PEEK(&H41C)
'k% = INP(&H60)
k$ = INKEY$
'LOCATE 1, 1: PRINT k$
'IF k% = 30 THEN pl.ld = pl.ld — 20
'IF k% = 32 THEN pl.ld = pl.ld + 20
IF k$ = CHR$(0) + «M» THEN pl.ld = pl.ld + 20
IF k$ = CHR$(0) + «K» THEN pl.ld = pl.ld — 20
walkdir% = (pl.ld + angq%) MOD angf%
'speed# = speed# / 2
speed# = 0
'IF k% = 17 THEN speed# = 10
'IF k% = 31 THEN speed# = -10
IF k$ = CHR$(0) + «H» THEN speed# = 10
IF k$ = CHR$(0) + «P» THEN speed# = -10
pl.ytmp = (pl.ytmp — speed# * costable#(walkdir%))
pl.xtmp = (pl.xtmp + speed# * sintable#(walkdir%))
pl.y = INT(pl.ytmp)
pl.x = INT(pl.xtmp)
pl.ld = ABS((pl.ld + angf%) MOD angf%)
LOOP UNTIL k$ = CHR$(27) 'k% = 16
' Insert Map
DATA 7,2,1,2,1,1,1,2,1,1,1,1,1,1,1,1
DATA 7,0,0,0,2,2,2,2,2,6,0,0,0,0,0,3
DATA 7,0,0,0,0,5,0,0,3,6,0,0,0,0,0,3
DATA 7,0,0,0,0,0,0,0,3,6,0,3,0,3,0,3
DATA 7,0,0,0,0,5,0,0,3,6,0,0,0,0,0,3
DATA 7,2,2,2,2,3,0,0,3,6,0,0,0,0,0,3
DATA 1,4,1,1,1,1,7,0,7,6,0,0,0,0,7,3
DATA 3,4,7,0,0,0,7,0,7,6,0,0,0,0,0,3
DATA 3,0,3,5,0,5,7,0,7,7,0,0,0,0,0,3
DATA 3,0,0,5,0,0,0,0,0,0,0,0,0,0,0,3
DATA 3,0,0,5,5,5,5,1,5,7,0,0,0,0,0,3
DATA 5,0,5,5,2,2,2,2,2,6,0,0,0,0,0,3
DATA 5,0,0,0,7,0,0,0,0,0,0,3,0,3,0,3
DATA 5,0,0,0,0,0,0,0,0,6,0,0,0,0,0,3
DATA 5,0,0,0,7,0,0,0,0,6,0,0,0,0,0,3
DATA 5,5,5,5,2,2,2,2,2,1,1,1,1,1,1,6
axe_chita
16.02.2022 15:48Но если вам этот raycaster все ещё нужен, то держите его:
raycast.rar
Чего только не найдешь в своём каталоге Downloads...
AlexeyK77
16.02.2022 10:30+1Если из текста статьи убрать слово RUST, то ничгео не изменится. А все же интересно, как именно Rust помог/улучши/ускорил/снизил кол-во циклов правки багов в разработке этого проекта.
Мне очнеь интеренсен Rust, т.к. дает на выходе относительно более безопасные сборки, но я нигде не могу найти инфу относительно того, а дает ли раст качественный скачек в процессе непосредсвенно самой разработки (скорость, кол-во багов и прочее). Страы ерпоекты для этого очень хороши, можно сравнить лоб в лоб.
Есть у меня опасение, что клоны паскаля могут оказаться по совокупности критериев более "производительными", но я не уверен.
punzik
Осталось запустить этот код на Intel 286 с полумегабайтом памяти.
forthuser
в Rust под DOS? ????
Xотя на Rust делают Forth (Форт) для запуска в рамках контроллеров для встроенного оборудования, а это где то уже близко DOS использованию.
P.S. Немногие современные инструменты программирования ещё поддерживают возможность создания программ для DOS.
Один из таких инструментов FPC (Free Pascal Compiller)
1992г уже «эра» 386-х, 486-х ПК.
pfemidi
Нету уж ничего такого уж невозможного как программировать на Rust для DOS.