Как я написал онлайн шутер на C++ за неделю?

Как я написал онлайн шутер на C++ за неделю?

Привет, Вектозавры! Сегодня я расскажу о том, как за 15 дней я написал свой онлайн шутер от первого лица. В этом ролике я с самого начала продемонстрирую, как создать свою псевдо 3D игру в стиле DOOM или Wolfenstein 3D.

Мы начнем с установки необходимой библиотеки, рисования объектов и управления камерой с клавиатуры.
После этого мы научимся строить 3D изображение, добавим освещение и управление мышью.
Далее мы реализуем текстурирование и сделаем нашу игру светлой и красивой. В такую игру уже захочется поиграть.
Мы добавим объекты разной высоты, скины, оружия и врагов, а также зеркала, в которых будет видно отражение объектов. А потом посмотрим, что будет, если поставить два зеркала напротив друг друга.
Ну и в конце концов, мы добавим онлайн в игру, чтобы можно было играть с другом. В общем, вас ждет большая и очень интересная статья, приятного чтения, вектозавры!

Первая часть статьи.





В предыдущем ролике я показал, как можно с помощью алгоритма ray-cast и консольной графики сделать простую бродилку. Много кому ролик понравился, некоторые даже сами пытались реализовать свою версию и, как по мне, получилось очень здорово!




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




В качестве основы я выбрал библиотеку SFML, которая позволяет рисовать линии, многоугольники и окружности. Также она дает возможность удобного использования клавиатуры, добавления звуков и загрузки изображений. В общем все, что нужно для того, чтобы писать игру и не заморачиваться над не существенными деталями на низком уровне.

Подключение SFML


Первое, что нужно сделать — это подключить эту библиотеку. Это не самая простая задача - пришлось хорошенько покопаться в мануалах и исходниках.
Естественно, подключилась SFML далеко не с первого раза, но меня это не испугало и в конце концов я смог разобраться.




Для проверки корректности работы попробуем нарисовать зелёную окружность.




Ура! Как видно, всё отлично работает, а значит можно приступать к проектированию каркаса проекта и реализации первых и самых важных интерфейсов.

Формирование каркаса игры


Основой будет является класс "Мир", в котором будут храниться все объекты на карте. Важно отметить тот факт, что все объекты представляют из себя набор из точек в двумерном пространстве. То есть реально никакого 3D не будет, но далее я создам иллюзию трёхмерного изображения и игроку будет казаться, что он бегает по трёхмерной карте.




Половину дня я потратил на проверку и отладку кода, но в итоге всё-таки смог нарисовать первый объект на карте.




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




По сути класс "Мир" готов и теперь можно создать небольшую 2D сцену из нескольких объектов. Хорошо бы также добавить на сцену камеру и прописать управление с клавиатуры.

2D карта, камера и управление


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




Клавишами вперёд и назад камера будет передвигаться вдоль направления взгляда, а клавиши «влево» и «вправо» будут поворачивать игрока.
После нескольких неудачных попыток я заставил камеру двигаться по карте. Как видно, управление отлично работает: камера перемещается и вращается. Погнали дальше.




Ray-cast и получение 3D изображения


Теперь самое главное – движок игры. Нужно сделать так, чтобы камера могла рисовать изображение на экран, и чтобы это выглядело, как настоящее 3D. Для этого, мы будем использовать алгоритм ray-cast.
Его суть заключается в следующем: Игрок пускает луч до стены. Если мы найдем точку пересечения луча и препятствия, то сможем определить расстояние от камеры до стенки. Так вот если стенка близко, то мы в этом направлении нарисуем большую полоску, а если стенка далеко, то и полоска должна быть маленькой.




Так мы делаем для всех направлений в пределах угла обзора. Пускаем лучи во все стороны и мерим расстояния. Вот и всё.




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




Встаёт проблема определения расстояния до двумерное объекта. На Хабре я нашел мощный и простой алгоритм обнаружения пересечения отрезков. Это, по сути, главный алгоритм во всей игре. Им я буду пользоваться буквально постоянно.




Задача состояла только в его адаптации под мои структуры. После того, как реализован алгоритм, остаётся только пустить лучики по всей зоне видимости и определить расстояния.
Как оказалось, это не самая простая задача: поначалу всё очень тормозило, а камера неправильно обнаруживала расстояния. В итоге получалось, что я как будто вижу невидимую стену прямо перед собой.




Естественно, это было связано с тем, что я ошибся при реализации алгоритма. Итерационно, исправляя ошибки и отлаживая результат, я пришел к тому, что всё работает как мне нужно.




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




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




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




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




Видно, что результат почти правильный, но что-то явно не так. Оказалось, проблема не в математике, а в том, что я неправильно использовал библиотеку. Только минут через 30 понял, что неправильно рисую полоску.
Исправил и всё сразу заработало.




Отлично – выглядит очень круто. Единственное, можно более далёкие полоски делать более темными чтобы создать иллюзию освещения и придать объем изображению. Поигравшись с углом обзора, я получил довольно-таки красивый результат.




Всё плавно и красиво – можно идти дальше!

Текстурирование


Хорошо бы теперь сделать так, чтобы стенки могли иметь какую-нибудь текстуру. Я полез в поисковик и нашел подходящую для кирпичной стены.




Теперь немного отвлечёмся на то, как именно я придумал реализовать текстурирование. Я не видел этого метода в интернете и придумал это сам, поэтому вкратце расскажу, как это происходит.
Допустим мы имеем текстуру стены. Теперь мы кромсаем её на вертикальные полоски и каждую из этих полосок накладываем на соответствующую полоску стены.




В принципе, это и есть весь алгоритм. Очень простой и понятный.
Теперь каждый объект имеет свою текстуру в качестве свойства и каждая точка на поверхности объекта теперь соответствует конкретному сдвигу в текстуре.




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




Теперь игра стала выглядеть светлой и смотрится намного лучше. И весь этот результат я добился всего за 5 дней. Но нужно понимать, что я программировал практически в режиме «нон-стоп» по 6-8 часов каждый день.
Мне и вправду стало очень интересно, что может получится и я решил продолжить, хотя изначально я задумывал остановиться на этом.

Оружие


Какой шутер может обойтись без оружия? Нужно скорее добавлять! Сначала я написал класс “Weapon”, содержащий всю необходимую логику, вроде стрельбы, подсчета количества патронов и скорости перезарядки. Класс «Camera» (то есть игрок) будет содержать в себе массив оружий и индекс выбранного оружия. Да, сначала оружие будет только одно, но я делаю задел на будущее.
Каждое оружие имеет свою текстуру. Я полез в поисковик и нашел вот такой вот дробовик.




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




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




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

Коллизия камеры со стенками


Раз уж делать полноценную игру, так делать по совести – нужно заняться коллизией камеры со стенами, чтобы человек не мог ходить сквозь препятствия.
Теперь вместо того, чтобы кидать лучи только в направлении взгляда я кидаю их вообще во все стороны. Зачем это нужно? Если я обнаруживаю объекты, которые потенциально могут испытать коллизию с игроком, то я добавляю их в массив «collision» потом при смещении я прохожусь по всем потенциальным коллизиям.




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




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




То есть объекты дают возможность выходить из текстуры, но входить внутрь запрещается.

Меню игры


Решил пока передохнуть и потратить время на создание меню игры. Сначала нарисовал основные кнопки, вроде «играть», «настройки» и т.д. Кнопки просто вывожу на экран, как обычную картинку, а при наведении мыши на неё меняю изображение. Если игрок кликнул мышью в пределах кнопки, то считаем это нажатием.




Тут я увидел, что есть проблема дребезга кнопки, но пока забил и оставил так. Хотя бы потому, что такими древними технологиями меню уже никто не делает и для достижения красивого результата придется всё переделывать.
Но цель была сделать «дружественный» пользовательский интерфейс и с ней я в каком-то виде справился. Можно двигаться дальше.

Зеркала и стены разной высоты


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




Единственный нюанс, который нужно понять – это как правильно отразить луч от данной поверхности с известным вектором нормали. Это чисто математическая задача, которую я быстро решил.
Для того, чтобы добавить стенки разной высоты, нужно производить ray-cast не только до ближайшего предмета, но и для всех предметов в данном направлении и сохранять все коллизии в вектор. Мне пришлось значительно переработать движок для того, чтобы сделать достаточный уровень абстракции для реализации рекурсивных зеркал с учетом стен и зеркал разной высоты.
Теперь я храню все расстояние до всех объектов, которые попали в радиус видимости.




Обрисовывать препятствия можно с помощью алгоритма художника, начиная с самых дальних и заканчивая самыми ближними. Алгоритм художника заключается в том, что объекты, которые должны находится ближе и перекрывать собой дальние, рисуются самыми последними на экране.
После долгих часов отладки и доработки движка я все-таки смог реализовать зеркала. Теперь можно увидеть самого себя. Для персонажа я выбрал первую попавшуюся мне текстуру.




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




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




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

Противник и обработка выстрелов


Чтобы добавить противника достаточно просто добавить еще одну камеру на карту.




И вот, противник готов.
Но как сделать обработку выстрелов? Я сделал так: после выстрела я пускаю луч в направлении взгляда. Если этот луч пересекает какую-нибудь камеру, то мы уменьшаем у неё количество здоровья.




Естественно, я использую тот же самый алгоритм бросания лучей, что и при измерении расстояний. Фактически ничего нового я вообще не писал, а просто использовал уже готовую функцию.
Но как мы помним, при встрече на пути зеркала луч рекурсивно отражается. Так как я использовал ту же функцию, то и пули так-же будут отражаться от зеркал.




Это вводит новую интересную механику в игру, ведь теперь если умеешь пользоваться зеркалами, можно убивать врагов, стреляя прямо в зазеркалье. Урон от пуль зависит от расстояния до цели. То есть если стоять вплотную, то враг сразу погибает, а если стрелять издалека или, например, через зеркало, то урон будет куда меньше.

Multiplayer


По сути, сейчас уже можно играть, но проблема в том, что не с кем. Что же, придётся решать эту проблему.
Давайте добавим возможность играть нескольким игрокам на одной карте. В SFML есть встроенные сокеты, позволяющие обмениваться сообщениями между клиентами по протоколу TCP или UDP. Я решил выбрать UDP, так как моя домашняя сеть достаточно надёжная и ошибки доставки встречаются редко, а если и встречаются, то это не особо скажется на геймплее.
Сначала задача была написать простое клиент-серверное приложение. На стороне сервера перемещается окружность, а клиент должен показывать, где сейчас эта окружность находится.




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




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




Проектирование карты для сражений


Теперь нужно сделать интересную карту для deathmatch’a и можно будет звать друзей играть.
Сначала я набросал будущую карту на листе, а потом расставил колонны по углам, чтобы потом соединить их кирпичной стенкой.




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





Результаты


Можно наконец начать тест игры. Урон по противнику наносится, а значит, всё работает правильно.




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




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

В общем, пока как-то так. Этот проект я не забрасываю и буду развивать его дальше. Подпишись на канал, если хочешь первым узнать о выходе новой части. Делать такие большие и информационно объемные ролики очень сложно, особенно учитывая, что я работаю один, так что ваша подписка и будет мотивировать меня делать видео дальше.
Огромное спасибо моим пятерым спонсорам на patreon. Сумма выходит не большая, но тот факт, что вы поддерживаете меня и следите за каналом намного важнее любых денег.

GitHub & Как запустить игру?


А теперь для тех, кто хочет попробовать запустить эту игру или покопаться в коде. На своём GitHub’е я создал репозиторий с проектом, откуда его можно скачать. Тут же будет список того, над чем я работаю в данный момент и что уже сделано.




Для того, чтобы всё заработало, нужно поставить библиотеку OpenAL. Заходим в папку “Release” и запускаем .exe файл. Если вы увидели эту менюшку, то поздравляю, всё заработало.




Планы на будущее


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



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







Если у вас остались вопросы или пожелания, то вы можете оставить комментарий (регистрироваться не нужно)

Ivan Ilin:

Как же много времени я потратил на создание этого ролика и статьи.
Такой формат не очень популярен на русском YouTube, да и красивых и интересных статей тоже не очень много.
Надеюсь, что мне удастся изменить ситуацию :)


Дата: 15-03-2020 в 12:47

Анонимно:

молодец хороший контент делаешь


Дата: 24-03-2020 в 13:10

Dat boi:

Круто! Отличная работа. Надеюсь, это перерастет во что-то большее


Дата: 26-03-2020 в 08:39

Анонимно:

Предложение! Можно добавить физику, если это возможно! Сделать карту по красивее и объемнее (аналог того же De_DUst) ! Попробовать это все перевести на API Vulkan и DX12, если это возможно! Конечно я понимаю, что не все сразу! Можно еще и с шейдерами поиграться


Дата: 26-03-2020 в 09:36

Cat-code:

Я бы хотел увидеть игру не на райкастинге а игру где мир описан обьектами с точками и текстурами .Я пытался что то сделать но фпс падал из за того что я поворачивал всю карту вокруг игрока а не взгляд -я делал массив точек относительно игрока потом умнажал на матрицу поворота затем рисовал текструры по растоянию .(формула перспективы и перевода в 2д по X "Z это кордината дальности"-(точкаX+игрокX)*(fov/(точкаZ+игрокZ))


Дата: 30-03-2020 в 02:13

Анонимно:

Предложение! Можно сделать игру на одного игрока или мультиплеер, где надо справляться с "волнами" врагов" К примеру они могут выходить из какой-нибуть двери, А после убийства определенного количества врагов может выходить бос(он будет с большим количеством HP и моделька к примеру будет по больше)


Дата: 05-04-2020 в 10:26

Назар Ус:

гле можно скачять игру


Дата: 09-04-2020 в 09:54

Анонимно:

Не могли бы вы пояснить начинающему программисту, что делать после скачивания с репозитория, чтобы можно было самому менять что-то в игре, ибо visual studio отказывается что-либо билдить?


Дата: 12-04-2020 в 17:39

Анонимно:

Используй gpu и твоя игра похожа на Team Fortres 2


Дата: 13-04-2020 в 10:32

Кашпировский:

Хотел денюшку перевести, но карту заблокировало


Дата: 09-07-2020 в 19:53


Мои курсовые | 30.11.2019: Выложил мои курсовые в открытый доступ. Теперь они отображаются в колонке слева под новостями.

Для будущих авторов | 12.10.18: Если вы хотите стать автором статей на сайте и получить подтвержденный аккаунт, то обращайтесь на почту! support@ilinblog.ru

Обновления | 21.08.18: Добавлена возможность комментировать статьи. Сайт адаптирован под мобильные устройства.

Обновления | 19.01.18: Добавлена возможность добавления математических формул в статьи посредством языка latex. Пример использования тут. Также добавлена возможность редактирования статей.

Информация о пользователях | 28.10.17: Расширена функциональность страницы пользователей, теперь можно добавить статус и личную информацию.

Мои статьи и исследования:

Измерение спектра квантовой эффективности полупроводникового фотокатода на основе арсенида галлия (курсовая)
Исследование индукционного метания цилиндрических проводников импульсным магнитным полем (курсовая)
Броуновское движение
Температура и методы её измерения
Исследование механики движения мячей