Хабрахабр

Введение в программирование: простой 3Д шутер с нуля за выходные, часть 2

Продолжаем разговор про 3Д шутер за выходные. Если что, то напоминаю, что это вторая половина:
Как я и говорил, я всеми силами поддерживаю желание в студентах делать что-то своими руками. В частности, когда я читаю курс лекций по введению в программирование, то в качестве практических занятий я оставляю им практически полную свободу. Ограничений только два: язык программирования (С++) и тема проекта, это должна быть видеоигра. Вот пример одной из сотен игр, которые сделали мои студенты-первокурсники:

К сожалению, большинство студентов выбирает простые игры типа 2Д платформеров. Я пишу эту статью для того, чтобы показать, что создание иллюзии трёхмерного мира ничуть не сложнее клонирования марио броз.
Напоминаю, что мы остановились на этапе, который позволяет текстурировать стены:

Что такое монстр в нашей игре? Это его координаты и номер текстуры:

struct Sprite { float x, y; size_t tex_id;
}; [..] std::vector<Sprite> sprites, {5.323, 5.365, 1}, {4.123, 10.265, 1} };

Определив несколько монстров, для начала просто отрисуем их на карте:

Внесённые изменения можно посмотреть тут.
Open in Gitpod

Теперь будем рисовать спрайты в 3Д окошке. Для этого нам нужно определить две вещи: положение спрайта на экране и его размер. Вот так выглядит функция, рисующая чёрный квадратик на месте каждого спрайта:

void draw_sprite(Sprite &sprite, FrameBuffer &fb, Player &player, Texture &tex_sprites) { // absolute direction from the player to the sprite (in radians) float sprite_dir = atan2(sprite.y - player.y, sprite.x - player.x); // remove unnecessary periods from the relative direction while (sprite_dir - player.a > M_PI) sprite_dir -= 2*M_PI; while (sprite_dir - player.a < -M_PI) sprite_dir += 2*M_PI; // distance from the player to the sprite float sprite_dist = std::sqrt(pow(player.x - sprite.x, 2) + pow(player.y - sprite.y, 2)); size_t sprite_screen_size = std::min(2000, static_cast<int>(fb.h/sprite_dist)); // do not forget the 3D view takes only a half of the framebuffer, thus fb.w/2 for the screen width int h_offset = (sprite_dir - player.a)*(fb.w/2)/(player.fov) + (fb.w/2)/2 - sprite_screen_size/2; int v_offset = fb.h/2 - sprite_screen_size/2; for (size_t i=0; i<sprite_screen_size; i++) { if (h_offset+int(i)<0 || h_offset+i>=fb.w/2) continue; for (size_t j=0; j<sprite_screen_size; j++) { if (v_offset+int(j)<0 || v_offset+j>=fb.h) continue; fb.set_pixel(fb.w/2 + h_offset+i, v_offset+j, pack_color(0,0,0)); } }
}

Давайте разбираться, как она работает. Вот схема:

Относительный угол между спрайтом и направлением взгляда очевидно получается простым вычитанием двух абсолютных углов: sprite_dir — player.a. В первой строчке мы считаем абсолютный угол sprite_dir (угол между направлением от игрока к спрайту и осью абсцисс). Ну, на всякий случай я обрезал двумя тысячами сверху, чтобы не получить гигантские квадраты (кстати, этот код запросто может поделить на ноль). Расстояние от игрока до спрайта посчитать тривиально, а размер спрайта — простое деление размера экрана на расстояние. Проверьте с ручкой и бумажкой правильность вычисления h_offset и v_offset, в моём коммите (некритичная) ошибка, верить коду в статье 🙂 Ну, и более свежий код в репозитории тоже уже исправлен. h_offset и v_offset дают координаты верхнего левого угла спрайта на экране; затем простой двойной цикл заливает наш квадратик чёрным цветом.

Внесённые изменения можно посмотреть тут.

Open in Gitpod

Чудо хороши наши квадратики, да только одна проблема: дальний монстр выглядывает из-за угла, а квадратик нарисован целиком. Как быть? Очень просто. Мы отрисовываем спрайты уже после того, как были нарисованы стены. Поэтому для каждого столбца нашего экрана нам известно расстояние до ближайшей стены. Сохраним эти расстояния в массив 512 значений, и передадим массив функции отрисовки спрайта. Спрайты же тоже рисуются столбец за столбцом, вот и будем для каждого столбца спрайта сравнивать расстояние до него со значением из нашего массива глубины.


Внесённые изменения можно посмотреть тут.

Open in Gitpod

Отличные получились монстры, не правда ли? А вот на этом этапе я не буду добавлять никакой функциональности, наоборот всё сломаю, добавив ещё одного монстра:


Внесённые изменения можно посмотреть тут.

Open in Gitpod

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

Скрытый текст

А вот как? Правильный ответ: «можно». Пишите в комментариях.

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


Внесённые изменения можно посмотреть тут.

Open in Gitpod

Настало время SDL. Кросплатформенных оконных библиотек очень много разных, и я в них совершенно не разбираюсь. Лично мне нравится imgui, но мои студенты почему-то предпочитают SDL, поэтому я линкуюсь с ним. Задача на этот этап очень простая: создать окно и вывести на него изображение из предыдущего этапа:

Ссылку на гитпод больше не даю, т.к. Внесённые изменения можно посмотреть тут. SDL в браузере пока не научился запускаться 🙁

Добавить реакцию на нажатия клавиш это даже не смешно, описывать не буду. При добавлении SDL я удалил зависимость от stb_image.h. Он прекрасен, но уж больно долго компилируется.

Ну а вот так выглядит типичное исполнение: Для тех, кто не понял, исходники девятнадцатого этапа лежат тут.

Мой код на данный момент содержит только 486 строк, и при этом я на них совершенно не экономил:

haqreu@daffodil:~/tinyraycaster$ cat *.cpp *.h | wc -l
486

Я не вылизывал свой код, намеренно вывалив грязное бельё. Да, я так пишу (и не я один). Одним субботним утром я просто сел и написал вот это 🙂

Пишите ваш собственный код, он наверняка будет лучше моего. Я не стал делать законченную игру, моя задача — лишь придать начальный импульс для полёта вашей фантазии. Делитесь вашим кодом, делитесь вашими идеями, шлите пулл реквесты.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть