Какую игру ни делай, а в итоге все равно получится ГТА. Каждый школьник мечтает создать свой клон ГТА. ГТА всему голова. Без труда не пройдешь и ГТА. Ой, что это я? Короче говоря, я делал игры и в какой-то момент осознал, что достиг дзена, и теперь настала пора и мне тоже написать свой вариант той самой исходной игры, игры-прародительницы всех игр, игры-протовселенной, канонической игры, а именно игры про езду на тачке в открытом мире. Каждый мужчина должен посадить дом, родить дерево и создать свой клон ГТА. Э-э... Ладно. Нет, конечно, GTA - это не только про тачку. Позже добавим и ходьбу, и копов, и плоские шуточки, хотя, последнее я, кажется - уже. Похоже, что сейчас моя игра, скорее, ближе к Need For Speed: в ней уже можно гонять по городу, но еще нельзя выходить из машины, да и пешеходов пока нет. Зато есть открытый мир. Ничего, скоро доведем этот NFS до состояния полного GTA. Тут мне подумалось, что все игры - это одна и та же игра, но с разными урезанными возможностями. Это как в случае со скульптором, который просто отсекает все лишнее... Короче, вы поняли, я философ.
Я расскажу вам о том, как я создал довольно большую локацию, содержащую более 20 000 объектов (это еще не предел), с физической моделью, при этом сохраняющую неплохую производительность в браузерах, в том числе мобильных. Будет интересно, не переключайтесь.
Почему я вообще начал делать эту игру? По той же причине, что и делал другие. Когда я запускаю что-нибудь не свое, то сразу подмечаю детали, которые мне не нравятся. И приходит естественное желание написать нечто подобное, но чтобы там уже всё было так, как хочется мне.
Мои претензии к автогонкам
Итак, что же мне не нравится в существующих играх в разрезе механики управления автомобилем. Ниже - сугубо мое личное мнение. И с ним, естественно, можно не соглашаться.
1. Вид не от первого лица
Я не понимаю, как можно играть в автогонки не от лица водителя. Вид от лица бога уместен в градостроительных симуляторах, в стратегиях и так далее. Но в гонке надо сидеть за рулем. Иначе получается игра в кого-то вроде наблюдателя за гонкой, игра в менеджера гоночной команды, игра в оператора, который снимает гонку для телевидения или, нет, скорее, игра в птицу, которая летит над машиной и управляет ей, контролируя разум водителя силой своей птичьей мысли. (Караул! Птицы захватили мир! Апокалипсис!) В общем, это уже не совсем игра в автогонщика.
И в пику NFS, где когда-то убрали вид от первого лица, я в своей игре решил убрать вид от третьего. Барабанная дробь. Запомните этот исторический момент. Пусть это будет первая в мире гонка без вида от третьего лица. Я лишь сделал переключение камеры между тремя вариантами вида от руля с немного разными углами обзора на дорогу. А если кому-то очень хочется полюбоваться экстерьером автомобиля, так пусть идет в гараж и любуется им там! Ой, гаража пока еще нет, но он будет, когда я реализую выбор машины перед началом игры. Будет ли это гараж с блэкджеком и пивом или просто обычный гараж, я еще подумаю. Но во время поездки надо смотреть на трассу!
Кстати, есть пара претензий и к тем играм, где вид от первого лица все же присутствует. Во-первых, это слишком низкая посадка за рулем, связи с чем трассу становится видно лишь на некотором отдалении, а не прямо перед машиной. А на горке перед спуском трассу в какой-то момент может быть не видно совсем. Как можно так играть, не понимаю? Более того, когда взгляд фокусируется на дороге слишком далеко, то скорость кажется более низкой. Во-вторых - затемненное стекло. Типа, давайте, покажем реалистичное лобовое - затемним его на 30%, будет круто. Ага, в реальных авто всегда тонируют лобовуху, чтобы было плохо видно дорогу. В результате при игре от первого лица теряется половина красок окружающего мира и играть с такого ракурса как-то не тянет: быстро надоедает. То есть, такой вид - это, скорее, перфоманс, ну или вещь для галочки, а не игровое пространство. Нет, стекло должно быть прозрачным. В своей игре я реализовал переключение между тремя видами от руля и только в одном из них слегка, почти незаметно, затемнил лобовое стекло. И мне это уже не нравится. Не знаю, может быть, уберу это. Или сделаю четвертый вид, такой же, но как вариант с затемнением.
2. Слишком широкие дорожные полосы
Как следствие наличия вида от третьего лица, в играх обычно делают слишком широкие полосы движения. А всё потому, что держаться в полосе с видом от третьего лица сложнее: банально меньше видимая ширина трассы перед машиной на экране. Зато сзади видна вся нереальная широта такой полосы: в нее теперь могут поместиться сразу два, а то и три автомобиля
В результате того, что дорожные полосы слишком широкие, теряется увлекательность обгона: можно просто втопить по разделительной или по обочине и ехать с максимальной скоростью, никак не мешая ни встречному, ни попутному трафику. Прощайте захватывающие «шашечки», на которых, кстати, был завязан весь геймплей первых автогонок на аркадных автоматах.
Обзор же на трассу с видом от первого лица шире, поэтому можно делать полосы поуже, более реалистичные. Ну а «шашечки» становятся еще более интересными, когда вид из твоего авто загораживает идущий впереди автомобиль.
3. Высокая скорость
Видимо, как следствие того, что у нас теперь нереально широкие полосы движения, будем увеличивать скорость машины до околосветовой, иначе ездить по таким полосам станет ну совсем уж не интересно - как по взлетной полосе. По-моему, из всей серии NFS только в первой лучше всего ощущалась скорость: просто она там была не слишком высокой. Да, увеличим скорость, чтобы ехать было сложнее, но, вот незадача - на такой скорости теперь даже в широкие полосы вписываться становится трудно... Сделаем их еще шире? Какой-то замкнутый круг. А можно было просто играть от первого лица. В общем, максимальная скорость в игре не должна быть слишком большой.
4. Утюг на льду
Одна из причин, по которой игрокам могут не нравиться гонки как таковые, в смысле, сам процесс вождения, это слишком сложная или, наоборот, слишком легкая, или аркадная физика. Со сложной все понятно: автосимулятор - игра для специфической публики, не все поймут. А в случае с «аркадкой» машина зачастую ведет себя как утюг на льду. Но это не страшно! Сделаем так, чтобы утюг сам ехал, сам поворачивал - нужно только давить на кнопку «вперед». Все это делает процесс вождения абсолютно неинтересным. А есть игры, где соблюден некий баланс между симулятором и аркадой и где я поначалу просто катался, даже не имея никакого желания двигаться по сюжету. Настолько мне понравился сам процесс.
5. Кольцевые трассы, линейные трассы
Я не любитель замкнутых трасс. Кататься по кольцу на гонках в симуляторе - это нормально. Но, если сеттинг представлен дорогой общего пользования с трафиком, то кольцевая трасса тут выглядит довольно странно. По дороге общего пользования я хочу ехать вперед с целью куда-нибудь приехать. К далекому желанному финишу, к призу, к райскому уголку или как-то так. Должен быть стимул добраться докуда-то. Это некий дополнительный интерес в игре, дополнительные эмоции. Есть идея в следующей игре реализовать трассу с разными климатическими условиями: из снежной бури приезжаешь к финишу на жарком пляже. Или можно - наоборот, по желанию.
6. Стрелочки
Стрелочки в воздухе, а также, на дорожном покрытии - это ужас. Вообще, когда на более-менее реалистичное окружение накладываются подсказки в форме эффекта «дополненной реальности», то в моих глазах это сразу превращает игру в несерьезную мобильную «кликалку» со «свистоперделками» и убивает реализм. Да, знаю, что это применяется во всех современных автогонках. А мне не нравится, поэтому я в них и не играю. Мы имеем дополненную реальность в и так нереальном мире (игровом, виртуальном)? Что? Ну и бред. Я допускаю стрелочки в форме дорожных знаков-указателей направления движения. Но только не в виде светящихся объектов, просто висящих в пространстве. Дизайнерам было лень даже подумать об иммерсивности? Ну а неоновые стрелочки на асфальте - это вообще ужас. Где вы такое видели в жизни? По-моему, в Южной Корее применяется подсветка, вмонтированная в дорожное покрытие на пешеходных переходах для тех, кто ожидают зеленого сигнала светофора, уткнувшись в экран мобильного телефона. Светофор на земле. А, в целом, конечно, дизайн трассы должен быть таким, чтобы было и так понятно, куда ехать. Нельзя ограничивать движение стрелочками, невидимыми стенами и прочим непотребством. Да и зачем тогда вообще делать открытый мир? Чтобы его закрывать?
7. Анимация рук водителя
Казалось бы, руки на руле - это хорошо, это добавляет реализма. Но, если взять, например, игру «Driver: San-Francisco», то там прослеживается такой эффект, при котором дерганье рук в стороны сбивает с толку относительно направления движения машины. Это раздражитель, который заставляет суетиться и вилять влево-вправо в попытках как бы компенсировать эти движения рук. Мне сложно это объяснить, но попробуйте просто закрыть ладонью нижнюю часть экрана так, чтобы не видеть руль и руки - и водить станет намного проще. Начинаешь легче вписываться в повороты. Поэтому руки на руле не нужны - достаточно только вращения самого рулевого колеса. Либо руки не должны быть такими контрастными и заметными. Пусть они сливаются с фоном. Короче, руки на руле в играх почти всегда реализованы отвратительно и портят игровую механику вождения.
8. Чрезмерная кастомизация автомобиля или тюнинг
Я понимаю, что есть большие любители данного процесса, которые могут 90% игрового времени проводить в гараже. Но ведь для этого существуют симуляторы автомеханика. Я считаю, что в игре про гонки кастомизация авто не нужна совсем. Достаточно только выбора самой машины. Я до сих пор помню эти синий Додж и черный Ламборгини из первой части NFS. Уже забыл, можно ли там было менять их цвета, но я даже и не пытался. Те машины были харизматичными. Если бы их можно было глубоко кастомизировать, то они, скорее всего, не отпечатались бы в моей памяти так сильно. Я в детстве не разбирался в машинах и садился играть в данную игру с мыслью типа: «Хочу погонять на той черной спортивной тачке». А оттого, что она была черная, казалось, что она и едет как-то особенно, как-то круто что ли. Машины должны быть с характером, их нельзя давать кастомизировать игроку, а особенно - менять цвет. Вот, какая есть в гараже, на той и езжай. И нечего перед каждой гонкой ее перекрашивать. В реальном мире же не будешь этим заниматься. Стучит где-то в двигателе - и пусть стучит. Зато это будет запоминающейся особенностью данного автомобиля. А еще машин должно быть мало, чтобы запомнилась каждая. А миллион автомобилей на выбор, которыми хвастаются разные крутые игры, мне напоминает гонку за количеством мегапикселей среди производителей цифровых камер - абсолютно бессмысленно.
9. Сюжет
Кому-то скучно просто ехать, и хочется еще и какого-то сюжета, может быть, с длинной предысторией и социальной составляющей, завязанной на взаимодействии между персонажами. Но, слушайте, так это гонка или РПГ? Зачем смешивать эти два жанра? Мне всегда было неинтересно в гонках даже проходить карьеру. А особенно бесило то, что я не мог выбрать любую трассу: я хочу прокатиться по зимней - а нет, она закрыта, сначала пройди осеннюю. Нет, я просто хотел погонять с другими соперниками, выбрав трассу по своему настроению... Что же касается финала, то максимум, что мне нужно, это таблица рекордов с указанием того, какое я занял место на данной трассе, и, может быть, еще время моего заезда. Всё. Я считаю, что сюжет в гонках не нужен. Просто садись в тачку - и дави на газ. Даже карьера не нужна. Незачем разбираться с этими нюансами попадания в какие-то полуфиналы и финалы. Фан игры в гонки - в том, чтобы просто весело гонять, а не читать длинные правила и таблицы. Это же гонки, а не симулятор менеджера гоночной команды.
Вся эта бюрократия с таблицами заездов, а также, бухгалтерия с покупками запчастей и машин в гонках меня всегда сильно бесила. Я сел поиграть в гонки, чтобы расслабиться. Если я захочу заняться финансами, то я просто задержусь на работе, ну или, там, установлю какой-нибудь симулятор бухгалтера. Я играю в машинки, чтобы, как раз, отдохнуть от всего этого!
Существуют люди, которые относятся к играм с точки зрения прохождения этапов прокачки. И им, вероятно, даже не так важно, что это за игра - гонки, шутер или рогалик. Удовольствие доставляет само продвижение вверх по некой игровой карьерной лестнице. По крайней мере, эту возможность они воспринимают как обязательную, и без нее играть не интересно. Можно сравнить с бизнесменом, которому не так важно, каким заниматься делом - торговать помидорами, нефтью или держать сеть салонов красоты. Главное - чтобы зарабатывались деньги. Не имею ничего против, но я не отношусь к такому типу людей. Для меня гонки должны быть про гонки, а не про карьеру. Впрочем, сложно сказать где проходит грань. Является ли карьера частью гонок? Если нет, то является ли победа на отдельной трассе частью гонок? А это, очевидно, да...
Думаю, что проблема с тем, что игрокам становится скучно просто гонять, проистекает из предыдущих пунктов: игрока заставляют смотреть на трассу от третьего лица и тем самым не дают ему погрузиться в процесс вождения. Вследствие такого угла обзора делают слишком широкие полосы, что приводит к легким и неинтересным обгонам. Увеличивают до небес максимальную скорость, что отключает восприятие скорости вообще. И так далее, и так далее. Поэтому убивается вся суть гонок, и внезапно начинает требоваться какой-то сторонний интересный сюжет а-ля «Санта-Барбара». Сюжет гонке нужен настолько же, насколько он нужен фильму для взрослых. Долой сюжет из гонок! (Кажется, Кармак то же говорил о шутерах.)
10. Последним пунктом выделю сразу несколько мелких неудобств
Слишком чувствительное или слишком тугое управление: иногда складывается такое впечатление, что авторы игры сами не играли в свое творение.
Повреждения. Повредил машину - и едешь медленно до конца гонки. Ну не знаю, по мне так какой смысл тащиться позади всех аж до самого финиша, заведомо проиграв. Не то, чтобы повреждения совсем не нужны, но пусть они будут чисто визуальными.
Слишком короткие расстояния между поворотами или светофорами (как в GTA V в черте города - я понимаю, что это не автосимулятор, но как пример плохой локации для гонок). Не успел разогнаться - и уже надо тормозить перед следующим перекрестком. Не интересно. В этом смысле в первой части NFS между кривыми располагались куда более длинные и динамичные скоростные участки, на которых можно было вдоволь наиграться в «шашечки».
Фокусное расстояние камеры. Если камера в игре настроена с большим «зумом», то движение ощущается как намного более медленное. Зато становится уже угол обзора на дорогу, что позволяет легче вписываться в полосы движения. То есть, полосы визуально становятся шире. По мне так лучше маленький зум и игра от первого лица, где угол будет и так достаточно узким, а видимая полоса движения широкая.
Так о чем игра-то?
Я так увлекся размышлениями об автогонках, что забыл, что делаю GTA, а эта игра, как известно, совсем не про гонки. Но это шутка, на самом деле. В действительности, у меня нет цели создать клон GTA по геймплею, по крайней мере. Моя игра, скорее, все-таки, про вождение. (Помним про скульптора, который просто отсекает... Ладно.) У меня есть открытый мир с возможностью менять машину. Будет также возможность перемещаться на небольшие расстояния пешком. Чисто внешне это напоминает GTA. По форме, но не по содержанию. В дальнейшем на этой базе я планирую написать коридорные гонки с длинными трассами без открытого мира, но с небольшими ветвлениями. Но это уже другая история. А данную игру я назвал «Капиталист». Здесь нужно ездить и собирать монеты, купюры и кредитные карты. Цель - набрать всего этого на миллион. За вами будет гоняться полиция и пытаться вас оштрафовать. Если вас настигают копы, то они забирают у вас все наличные. Поэтому деньги нужно периодически свозить в свой банк, расположенный в городке Булл Таун. Метафора: в большом городе можно легко стать миллионером, так как там заработки высокие, но, однако, они не совсем законные. На данном этапе разработки такая незамысловатая завязка позволит создать какую-то законченную игру. Позже я буду добавлять человечков, не слишком замысловатые миссии и так далее. Придам домам больше объема, а то сейчас они представляют собой просто коробки. Новая игра будет называться по-другому. А сейчас это такая мини-игра в локации некой будущей большой и более красивой игры. Демо-уровень, так сказать.
Успешный успех
Я отправил ссылку на свою игру в мессенджер соседям, и спустя пару дней ко мне завалился консилиум, состоящий из малышни - соседского сына и его друзей, с умным видом и восторгом объясняющих мне, что еще нужно добавить в игру: какие машины, дома, аэропорты и так далее. Важный разговор в дверях длился минут десять. Я понял, что это успех. И пообещал детям сделать многое из того требуемого и крайне необходимого.
Теперь перейдем к техническим моментам
Далее я опишу различные задачи оптимизации, которые мне предстояло решить для достижения максимальной производительности и минимального размера игры в мегабайтах, а также, сложности, с которыми я столкнулся при использовании Three.js для вывода трехмерной графики и Cannon.js в качестве физического движка.
Генератор города
Итак, начнем с малого. Во-первых, мне нужна локация. И да, я умею делать браузерные игры, поэтому вся эта красота будет работать в браузере. Я задался вопросом поиска генератора городов, так как делать все вручную долго, а, учитывая то, что я никакой 3D-моделер, и результат будет не сказать, что чем-то лучше - те же улицы, те же дома. Я решил сгенерировать основу. И довести до ума руками, если так можно выразиться. Я испробовал несколько вариантов процедурных генераторов города.
Greeble. Довольно простой инструмент, плагин к Blender или 3DS Max. Здесь все сводится к тому, чтобы рисовать улицы на плоскости вручную. А потом скрипт построит здания. Однако нужно очень хорошо знать 3D-редактор, чтобы сделать дороги изогнутыми и, при этом, остающимися равномерными по ширине, а также, придется вручную размечать тротуары и так далее. Мне этот плагин показался недостаточно автоматизированным.
CityEngine. Тяжелый софт, создающий тяжелый город. Я его попробовал и сразу снес. Уверен, это прекрасный инструмент, но я просто не смог с ним разобраться. На выбор предлагается почему-то всего пара-тройка вариантов города, причем, я не нашел, как задать произвольный размер карты. Сам город получается очень тяжелым и требует серьезного редактирования. Сразу после генерирования он в моей игре выдавал 3 кадра в секунду. Вероятно, там нужно вручную оптимизировать геометрию, объединять материалы и так далее. В общем, я от него отказался. Скорее всего, это инструмент создания города для какого-нибудь рендера, а не для игры.
GhostTown. Вот этот мне подошел. GT старых версий умеет строить изогнутые улицы, но не позволяет создавать простые дома-коробки: геометрия зачем-то разбивается по этажам. А это лишние полигоны, ведущие к сильной просадке производительности в игре. Если у вас 50-этажный небоскреб и каждый этаж - это отдельные полигоны... Полигоны приходилось объединять вручную. Зато GT последней версии позволяет строить требуемые здания. Однако не умеет делать изогнутые улицы. У него улицы почему-то всегда представляют собой прямые. Тогда я решил сгенерировать саму карту в старой версии, а дома построить поквартально в новой - благо там можно указывать отдельные участки (меши), на которых должны появляться здания. Задавая настройки этажности и размеры зданий для каждого квартала в отдельности, я потихоньку создал более-менее вменяемый город. И все равно ни одна из этих версий не умеет корректно накладывать текстуры ни на здания, ни на дороги. Поэтому я генерировал чисто геометрию и текстурировал ее потом вручную... Та еще канитель. Ну и стык дороги с тротуаром создается как-то криво, поэтому во многих местах его приходилось подтягивать, чтобы не было дыр. Стоковые текстуры имеют размер не кратный двойке и не подходят для игр. Автор явно не из сферы геймдева. Но я и не стал использовать стоковые, а сделал свои, а также, скачал бесплатных, ну и что-то по мелочи прикупил. Пришлось везде менять текстурную развертку.
В общем, с горем пополам я создал локацию. А вот старый скриншот результата, полученного еще в Greeble. Это был самый первый город, который я сгенерировал и дотянул руками. В принципе, не так уж плохо, но это - максимум, что мне удалось вытащить. Прямые улицы и ручная разметка всего и вся меня не устроила. И этот город умер, едва увидев свет.
А это уже город, полученный с помощью GhostTown последней версии. Неплохо. Но прямые улицы меня не устроили. К тому же, он лепит много полигонов на дороги и тротуары. Но зачем, если все они прямые? От этого падает производительность. Можно было просто замостить текстурой. В итоге этот город тоже отправился в утиль.
Ну а актуальный город вы можете видеть на всех остальных скриншотах из моей игры.
Коллайдеры
Ездить по городу - это хорошо, но проходить сквозь стены - не очень: это вторжение на частную собственность и в личную жизнь, что, как вы понимаете, чревато. Первое - справедливым судом, а второе - веселым мордобоем. Но, поскольку мы пишем не Mortal Kombat, то нам это не нужно. Может быть, как-нибудь в будущем: вломился в квартиру - и начинается файтинг с использованием подручных средств - сковородок, ножей... Гибрид NFS, GTA и MK, супер-игра обо всем сразу. Кстати, в моей игре нужно собирать разбросанные по городу деньги, убегая от полиции, поэтому она еще похожа и на Pac-Man... На данном этапе я, как великий творец, просто гениально отсекаю эту идею. Столкновения с препятствиями можно реализовать множеством различных способов. Я постарался выбрать такой, чтобы он оказывал максимально щадящее влияние на производительность. Для обработки столкновений со стенами я взял физический движок Cannon.js.
Сначала нужно подготовить коллайдеры. Я решил отказаться от использования самих стен домов в их качестве: ведь, стены могут состоять из большого количества полигонов, тем более, в будущем я, как раз, планирую это самое количество увеличить, чтобы немного разнообразить архитектуру зданий и затекстурировать их не одной повторяющейся текстурой, а двумя-тремя, преимущественно на первых этажах. Дело в том, что для одной текстуры достаточно одной плоскости из двух треугольников, а для разнообразных нужно бить стены на гораздо большее количество элементов. Поэтому в качестве колладйеров я взял простые низкополигонные коробки, повторяющие здания. Кроме того, я удалил с них полы и потолки, потому что мне нужны только сами стены. Так физический движок будет меньше напрягаться, рассчитывая столкновения. Я сохранил эти коробки в отдельном файле. Они будут загружаться в игру, но станут прозрачными, невидимыми.
И это еще только здания четырех из семи районов - около 3000 коллайдеров. А есть еще тысячи фонарных столбов и тысячи деревьев. О них - позже. Коллайдеры большого размера здесь - это не препятствия, а маяки для включения анимации воды в водоемах, которые где-то там, в городе, находятся в центрах этих коллайдеров: воду незачем анимировать, если ее не видно за домами.
Эти коллайдеры зданий загружаются в игру из файлов формата json (сохраненных из 3D-редактора), их всего 7 штук, то есть, по семи районам города. Каждый файл весит 1-2 Мб. Затем элементы их массива children (то есть, уже отдельные здания) помещаются в большой массив colliders в виде элементов:
colliders.push({
t: 'walls',
v: colliderVisual,
o: colliderBody,
added: false,
bounds: bounds,
p: {
x:colliderBody.position.x,
y:colliderBody.position.y,
z:colliderBody.position.z
}
});
colliderVisual - это, собственно, визуальная модель коллайдера одного здания (один из children всей загруженной 3D-модели), это будет работать в пространстве Three.js
colliderBody - это физическая модель в пространстве Cannon.js, создаваемая из загруженной визуальной модели Three.js. Как это делается:
var scale = 1;
var colliderGeometry = colliders[i];
var colliderGeomData = getVertsFaces(colliderGeometry, scale);
var colliderVerts = colliderGeomData.verts;
var colliderFaces = colliderGeomData.faces;
var colliderBody = new CANNON.Body({mass: 0});
colliderBody.addShape(new CANNON.Trimesh(colliderVerts, colliderFaces));
// процессы создания физической модели визуальных объектов из файлов форматов json и fbx немного различаются
getVertsFaces = function(geometry,sc) {
var res = {verts:[], faces:[]};
var terrainVerts = [];
var terrainFaces = [];
if (geometry.index === null) {
// если вы сохранили коллайдеры в формате fbx
var positionArray = geometry.attributes.position.array;
var indexArrayO=THREE.BufferGeometryUtils.mergeVertices(geometry);
var indexArray = indexArrayO.index.array;
var vertexCount = positionArray.length / 3;
var faceCount = indexArray.length / 3;
for (var i = 0; i < vertexCount; i++) {
var x = positionArray[i * 3];
var y = positionArray[i * 3 + 1];
var z = positionArray[i * 3 + 2];
terrainVerts.push(sc*x);
terrainVerts.push(sc*y);
terrainVerts.push(sc*z);
};
for (var i = 0; i < faceCount; i++) {
var a = indexArray[i * 3];
var b = indexArray[i * 3 + 1];
var c = indexArray[i * 3 + 2];
terrainFaces.push(a);
terrainFaces.push(b);
terrainFaces.push(c);
};
} else {
// если вы сохранили коллайдеры в форматах json, glb
for (var i = 0; i < geometry.vertices.length; i++) {
terrainVerts.push(sc*geometry.vertices[i].x);
terrainVerts.push(sc*geometry.vertices[i].y);
terrainVerts.push(sc*geometry.vertices[i].z);
};
for (var i = 0; i < geometry.faces.length; i++) {
terrainFaces.push(geometry.faces[i].a);
terrainFaces.push(geometry.faces[i].b);
terrainFaces.push(geometry.faces[i].c);
};
};
res.verts = terrainVerts;
res.faces = terrainFaces;
return res;
};
added - флаг того, добавлен ли данный коллайдер в данный момент времени в физический движок, будет использоваться уже во время игрового процесса
p - координаты колладйера, честно говоря, зря продублировал, можно их было просто взять из colliderBody.position, но так было удобнее
bounds - это границы коллайдера, ориентированные по осям координат, иначе говоря, boundingBox коллайдера (mesh), создаваемый средствами. Three.js:
var helper = new THREE.BoxHelper(mesh);
helper.geometry.computeBoundingBox();
var b = helper.geometry.boundingBox;
var d = 10;
b.min.x-=d; b.max.x+=d; b.min.z-=d; b.max.z+=d;
Самое важное! Bounds (границы) строятся вокруг каждого коллайдера в форме боксов, стороны которых ориентированы по осям координат. Причем я беру только данные, а сама модель - как визуальная, так и физическая - конкретно от этих боксов мне не нужна. В этом весь смысл: в процессе игры не нужно будет проверять рейкастингом весь огромный массив коллайдеров сцены. По данным границам путем простой проверки координат игрока на нахождение внутри (больше min и меньше max по всем осям) будет производиться включение коллайдера, то есть добавление colliderBody в физическую модель Cannon.js. Вот так можно представить это схематически:
Почему бы просто не делать рейкастинг со всеми моделями зданий по всему массиву? При десятках тысяч объектов это накладно: гораздо быстрее проверять на вхождение в границы тупо по координате, да и, к тому же, рейкастинг требует наличия всех проверяемых объектов всегда на сцене, а координаты - это просто массив данных. Далее. Зачем расширять границы на некое d? А чтобы коллайдер здания добавлялся в физическую модель на некотором расстоянии нахождения игрока возле здания, а не в момент касания. Иначе физический движок не успеет среагировать на столкновение; d подбирается экспериментально. Слишком малое значение приведет к тому, что движок не успеет отреагировать на столкновение, а слишком большое - к большому количеству активных коллайдеров в момент времени, что скажется на производительности. В итоге на сцене всегда включено всего 3-5 коллайдеров вокруг игрока. Ну, может быть, в какие-то моменты до 10. Но не десятки тысяч.
Да, вы можете заметить, что для рейкастинга можно задать определенный радиус, в котором будут искаться пересечения. Но дома в игре разные, и маленькие, и большие, поэтому в данном случае вероятна обработка стоящих друг за другом маленьких объектов, а нам это не нужно. По координатам же границ можно искать коллайдеры точнее, то есть, реально только ближайшие.
Фонарные столбы и деревья добавляются в массив коллайдеров аналогичным образом, только их позиции берутся не из файла, выгружаемого из 3D-редактора, а по заготовленному массиву координат. Эти модели незачем выгружать из редактора и подгружать в игру из какого-то большого файла, поскольку каждый такой объект, в отличии от зданий, не обладает уникальной геометрией, а сделан по принципу дублирования одной модели, поэтому достаточно только их координат. Об этом - чуть ниже.
Ну и, наконец, перебор массива координат границ (bounds) происходит отнюдь не при каждой отрисовке мира (60 раз в секунду), а с неким шагом (степом игрока): если игрок проходит от исходной точки по любой из осей некое минимальное расстояние, то тогда массив перебирается. Совершенно незачем перетасовывать коллайдеры в случае, если игрок существенно не продвинулся или вовсе стоит на месте.
Подытожив, можно сказать, что коллайдеры всех многочисленных объектов добавляются и удаляются из физического движка Cannon.js вокруг игрока по мере его движения. Проверка происходит по массиву данных границ (bounds) путем простого сравнения координат. А уже для добавленных коллайдеров, коих всегда оказывается не более 3-5 штук, работает рейкастинг и физическая модель столкновений. Таким образом на сцену можно добавить практически неограниченное количество объектов. Если же возникнет проблема со скоростью перебора массива, то его можно разбить, например, на некие подмассивы по регионам.
Жажда скорости
На низких скоростях движения автомобиля (примерно до 120 км/ч) коллайдеры работают отлично. Но не плестись же, как черепаха: жажду скорости никто не отменял. И вот на более высоких скоростях довольно часто случались пролеты сквозь препятствия. Если подумать, то есть способы, как этого избежать:
1. Уменьшить скорость.
Ну, сами понимаете... Нет. Здесь вспоминается мем с Денни Де Вито.
2. Увеличить число итераций обработки столкновений в физическом движке.
Тоже нет. Это требует больше вычислительных мощностей. Да и, как показала практика, все равно не страхует на 100% от прохождения сквозь стены: они нет-нет, да случаются.
3. Ничего не делать, но устранить последствия.
Можно попробовать бороться с последствиями пролета сквозь стену. Когда игрок хоть на миллиметр оказывается внутри здания, то можно телепортировать его обратно на ту позицию, где он еще был снаружи. Но каким образом это реализовать:
3.1. При помощи рейкастинга физического движка Cannon.js.
К сожалению, функция испускания луча в Cannon.js выдает только самое первое пересечение. А смысл рейкастинга состоит в том, чтобы испустив луч от позиции машины игрока в любую сторону, подсчитать количества его пересечений с каждым активным боксом-коллайдером. Если луч пересек только одну стенку коллайдера, значит, машина находится внутри. Если луч пересек две стены либо ни одной, значит, машина находится снаружи.
Я пробовал испускать два луча в противоположные стороны от машины в надежде таким образом зафиксировать возможные два пересечения. Однако я наблюдал такой эффект, когда, находясь внутри коллайдера, я получал пересечение только с дальней стенкой, а с ближней - лишь при нахождении снаружи. Кроме того, мои боксы-коллайдеры сами пересекаются между собой, то есть, иногда соседние дома стоят таким образом, что их стенки немного утопают друг в друге. Поэтому невозможно даже по ближайшим пересечениям лучей определить, внутри ты или снаружи. В общем, поигравшись немного, я решил отказаться от этого варианта.
3.2. При помощи рейкастинга визуального движка Three.js.
Про некоторой информации из Интернета, рейкастинг в Three.js работает быстрее, чем в Cannon.js. Я не проверял, но что точно, так это то, что он работает стабильнее и выдает все пересечения луча со всеми активными коллайдерами. Здесь проблем вообще не возникло, и я взял этот вариант.
Но рассмотрим и другие возможные способы.
4. Устранить причину проблемы: снизить скорость перед столкновением.
Коллизии обрабатываются отлично при скорости до 120 км/ч. А что, если перед стеной принудительно сбрасывать скорость? Логическое обоснования для игрока? Ну, типа, когда едешь очень близко к зданию, то цепляешься колесами за всякие неровности, газоны и т.д. Поэтому скорость падает. Логично? Как реализовать эту идею:
4.1. Поиск расстояния до стен.
Можно при движении вычислять расстояния до ближайших стен и тормозить машину, если это расстояние становится менее какого-то значения. Вычислений будет совсем не много, поскольку, как я уже упомянул ранее, активных коллайдеров в каждый момент времени бывает всего 3-5 штук, потому что обрабатываются из них только ближайшие к игроку.
Cannon.js действительно вычисляет расстояние до стен. Однако, как я уже упомянул, ввиду какой-то не очень понятной мне и нестабильной работы его рейкастинга, я отказался от этой идеи.
Three.js, кажется, тоже может вычислять расстояние до стенок коллайдера, но на данном этапе я решил, что это пока не приоритетная задача. А так, да, можно совмещать телепортацию игрока из метода 3.2 с данным вариантом снижения скорости перед препятствиями. Должно выглядеть реалистично.
Как еще можно снизить скорость перед столкновением?
4.2. Дополнительные коллайдеры большего размера вокруг препятствий
Тогда нужно было бы в 3D-редакторе создать второй комплект, или комплект «тормозных» коллайдеров чуть большего размера вокруг зданий, что ведет к большему расходу времени на сам процесс и к снижению производительности. Я отказался от этого метода.
4.3. Коллайдеры, которые ограничивают дорогу
Собственно, расставить коллайдеры не вокруг зданий, а вокруг дороги. Тогда их будет меньше, но в целом все недостатки предыдущего варианта остаются.
4.4. Дорога-коллайдер
Я имею в виду - взять дорогу в 3D-редакторе и выдавить по ее контуру форму вверх. То есть, создать на ней некий тоннель, который будет использоваться в качестве коллайдера. И здесь уже вести проверку не на отсутствие внутри него машины игрока, а, наоборот, на наличие. То есть, это будет не black-, а white-лист, так сказать. Но, во-первых, это тоже дополнительный коллайдер. А, во-вторых, не уверен, что это хорошая идея - принудительно сбрасывать скорость либо вообще делать полную остановку всегда, когда съезжаешь с дороги. Такое широко практиковалось в старых играх. Впрочем, вариант с небольшим снижением скорости вне дороги не так уж и плох... Но, опять же, тогда придется полагаться только на события столкновений в физическом движке, а они там срабатывают не в 100% случаев. Вероятно, ограничение скорости вне дороги можно было бы использовать как дополнительную фичу к событиям столкновения в физическом движке и рейкастингу (с телепортацией) в визуальном движке. Еще думаю над этим. В любом случае, красоту в деталях будем наводить позже.
Размер дистрибутива
Игра загружается в браузер. И для сокращения времени загрузки она должна «весить» как можно меньше. В итоге я уложился в 40 мегабайт. Что я для этого сделал.
Текстуры. Первый очевидный шаг - использовать повторяющиеся текстуры для стен зданий. Первые этажи можно замостить другой текстурой - с витринами. Я использовал максимально простую геометрию - буквально пара треугольников на каждую стену. И две текстуры на здание. Одной из них я мостил стены выше первого этажа, эта текстура тайлится (повторяется) во все стороны. А вторая текстура тайлится только по горизонтали и представляет собой текстурный атлас, в него вместились 4 варианта первых этажей. Зачем пихать в него 4 первых этажа? Ну, у 4-х разных зданий будет общая текстура первого этажа, а значит, один материал. А чем меньше материалов на сцене, тем лучше производительность. Кроме того, текстура должна быть квадратная, поэтому все равно придется ее чем-то заполнить. Текстуры же верхних этажей поместить в единый атлас не представляется возможным, поскольку они должны тайлиться во все 4 стороны.
Материалы. Их лучше объединять при загрузке однотипных объектов. Например, при загрузке одинаковых деревьев можно к каждой их единице применять один и тот же материал. В противном случае у деревьев будут уникальные материалы с разными айдишниками, а это сильно бьет по производительности.
var matPlants=null;
function loadPlantCallback(obj) {
if (matPlants===null) {
matPlants=obj.material;
} else {
obj.material=matPlants;
};
};
Одинаковые объекты. И да, я загружал по одному экземпляру каждого объекта - дерева, например, коих всего 6 видов. А потом просто клонировал их функцией .clone(). 6 деревьев с текстурой «весят» всего 686 Кб. И из этих шести клонируются тысячи и расставляются по карте с применением одного и того же материала. Тогда как все предварительно расставленные объекты (и это только деревья), то есть, весь лес, при выгрузке из редактора в одном файле весил бы 108 Мб. И загружать такой объем в игру было бы недопустимо.
Как я заготовил карту объектов (фонарных столбов и деревьев). Собственно, я расставил все объекты на территории в 3D редакторе, а потом выгрузил их в fbx-файл. Я написал простой скрипт, который считал этот файл, вытащил из него координаты, углы поворота и масштабы всех однотипных объектов и поместил эту информацию в массив данных. Это происходит один раз, это заготовка. А по данному массиву (например, для деревьев это 115 Кб данных) уже в игре и расставляются объекты. То есть, загружается по одному экземпляру объекта каждого вида, а затем происходит их клонирование по позициям и углам в соответствии с массивом данных, который уже предопределен в игре как константа. Таким образом, из 108 Мб деревьев получается менее 1 мегабайта: 686 Кб (шесть моделей с текстурой) плюс 115 Кб данных (с координатами, углами вращения и масштабами) в массиве. Ну а большой 108-мегабайтный файл больше не нужен. С фонарными столбами аналогично - выгруженные из редактора они «весят» 32 Мб, а затем превращаются в модель одного столба - 300 Кб + массив данных по их координатам и углам поворота 50 Кб.
Расстановка клонированных объектов. Из всего вышесказанного вытекает одна проблема - тысячи однотипных объектов начинают долго клонироваться и расставляться по игровому миру сразу после загрузки игры - примерно 3 минуты на моем компьютере. Я решил вместо экрана ожидания поставить внизу информационную панель с отображением хода загрузки мелких объектов. Да, это может выглядеть некрасиво, когда у тебя перед глазами появляется дерево или столб. Но я отсортировал массив данных по расстоянию удаления объектов от точки старта игрока. При желании, конечно, можно нагнать появляющиеся столбы и деревья, если начать быстро двигаться в каком-нибудь одном направлении. Что ж, это недостаток. Кстати, коллайдеры деревьев и столбов определяются чуть ранее - они загружаются из файлов коллайдеров вместе с локацией. Поэтому оказаться внутри какого-нибудь объекта все равно невозможно: можно врезаться в пустое пространство, где чуть позже появится объект. Я разбил объекты на важные и второстепенные. Сначала расставляются первые, а затем вторые. Последние - это деревья в глубине лесных массивов, куда так сразу не поедешь. Деревья же, стоящие вдоль дорог так же, как и, например, фонарные столбы, отнесены к важным и загружаются в первую очередь.
Формат моделей. Наконец, все 3D-модели я выгружал из Блендера в формате fbx. Он обеспечивает довольно компактный размер контента. А коллайдеры выгружал в формате json.
Производительность
Про объединение материалов и супер-легкую геометрию я уже рассказал - а это все влияет и на производительность тоже.
Еще один момент - это дальность прорисовки объектов. Имея на сцене более 10 000 моделей, конечно, ограничиваешь радиус их прорисовки. Здания ограничены настройкой общей дальностью видимости в игре, а мелкие объекты прорисовываются чуть ближе. Когда это происходит на фоне зданий, то это не так заметно.
Кстати, сами здания тоже неплохо бы отображать лишь на некоторой дистанции от игрока. Да, для этого существует настройка общей дальности обзора камеры. Однако, то, как работает эта функция, меня не устроило. Дело в том, что изображение остается видимым в пределах некоего усеченного конуса (frustum), что при его малом размере может некрасиво обрезать верхушки домов, если они очень высокие. То есть, если у вашего компьютера недостаточная производительность и вы уменьшили дальность обзора, то ближайшие небоскребы начинают усекаться вверху, а это выглядит некрасиво. Я задался вопросом, а можно ли ближайшие объекты оставлять полностью видимыми, а отдаленные по горизонтали - скрывать. Очевидно, тогда для этого нужно увеличить общую дальность обзора камеры, а отдаленные объекты просто скрывать со сцены. При загрузке игры скрипт проходит по всем объектам сцены (кроме однотипных мелких, о которых сказано выше) и создает массив, по которому при движении игрока отображаются ближние объекты и скрываются дальние. Эта мера, кстати, еще и существенно повысила производительность. Оба параметра - дальность обзора камеры и дальность объектов - я вынес в меню настроек.
На картинке слева уменьшена общая дальность обзора камеры, но оставлена высокая дальность объектов. Поэтому усекается здание, обведенное красным. А справа дальность камеры выкручена до большого значения, но дальность объектов уменьшена. Это позволяет ближайшему зданию отображаться полностью (обведено зеленым). Но отдаленное здание (обведено красным) уже не отображается.
А здесь видно, что при низкой дальности обзора камеры усекаются даже боковые стены зданий. Чего не наблюдается справа - но за счет уменьшения дистанции отображения объектов.
На самом деле, можно найти приемлемую комбинацию этих двух параметров, при которой у вас будут и ближайшие здания отображаться полностью, и исчезновение удаленных объектов не будет особо заметно. Но самое главное - что в результате всего этого большинство отдаленных объектов сцены скрывается (делается невидимыми или visible=false) и перестает обрабатываться визуальным движком Three.js. С этим и связано общее увеличение производительности.
Еще я рекомендую использовать браузер на основе Хромиум и включить максимальную производительность в настройках питания.
Прорисовка теней. Тени от всех объектов ограничены квадратом, который следует за игроков, находящимся всегда в центре этого квадрата. Иначе говоря, тени отбрасывают только ближайшие объекты. И, да, в Three.js тени, как всегда, какие-то странные. Я пытался их настроить, но не особо успешно. В общем, какие есть - такие есть.
Коллайдеры. Как я уже упомянул ранее, невидимые коллайдеры Three.js включаются вокруг объектов вблизи игрока, чтобы застраховать его от прохождения сквозь стены. Этим коллайдерам я отключил свойства castShadow и receiveShadow, чтобы они не участвовали в расчетах теней, а также, для их материалов отключил свойства fog, depthTest и depthWrite, то есть участие в расчете тумана и в сортировке отображения объектов на сцене по удалению от камеры - ведь они все равно не видимы.
Но самое интересное, что мне помогло существенно уменьшить «тормоза», это, как я уже упомянул, исключение из просчета мира невидимых в данный момент объектов. По умолчанию Three.js просчитывает матрицы всех, как видимых, так и невидимых, объектов (параметр visible=false) на случай, если разработчики используют последние на сцене в качестве коллайдеров. Я пошел на хитрость. Да, коллайдеры, конечно, должны быть невидимыми, но ведь можно добиться этого другим способом: оставить их параметр visible в true, но убрать свойство opacity материала в ноль - и они станут невидимыми, а, точнее, видимыми, но прозрачными. А по-настоящему невидимые можно не просчитывать совсем. Видимыми, но прозрачными (visible=true, opacity=0) можно делать только активные (ближайшие к игроку) коллайдеры. И тогда просчитываться движком могли бы только они, а не все скрытые объекты сцены вообще. Я задался этим вопросом, но сначала пошел с ним в Интернет, где нашел уже готовый скрипт, который как раз патчит движок нужным образом, и невидимые объекты перестают обрабатываться.
(function () {
let _updateMatrixWorld = THREE.Object3D.prototype.updateMatrixWorld
THREE.Object3D.prototype.updateMatrixWorld = function () {
if (!this.visible) {
return
}
_updateMatrixWorld.apply(this)
}
})();
Патч можно применить на этапе загрузки в своем js-коде, не влезая в код движка
Наверно без этих строк создание игры с такой огромной картой и таким большим количеством объектов было бы невозможно: потому что все начинало жутко лагать и тормозить.
Все эти оптимизации дали неплохой результат, что позволило использовать в игре довольно большую карту с двумя городами и тремя мелкими населенными пунктами.
Не прощаюсь
Собственно, игры как таковой пока еще нет: есть только свободное вождение по городу. Что-то вроде демо-версии города получается. В данный момент я как раз работаю над маршрутами трафика и полицейской погоней. Поэтому и расскажу обо всем об этом наверно уже в следующей статье. Кажется, эта статья и так получилась довольно длинной. И испугавшись этого, я решил даже не упоминать о смене времени суток и погоды и много о чем еще... Ждите дальнейших рассказов о ходе разработки моего убийцы GTA и NFS в одном флаконе.
Автор: Kempston