Весь код примера занимает менее 300 строк, щедро разбавленных переносами, разобраться в которых самостоятельно не составит особого труда. Однако, чтобы ещё больше облегчить Вашу участь, я напишу немного ниже пару слов о ключевых моментах.
Предыстория
Мы все слышали о людях, способных написать шутер за два дня, но можем ли мы сами стать в один ряд с легендами? Чтобы проверить свои силы, я обложился уроками по Three.js гуглом и начал ваять свой 2х-дневный шедевр. Однако через часика два мне надоело, я закоммитил что там было и пошёл подышать свежим воздухом почитать интернеты. Так повторялось каждый раз, когда я возвращался к этой затее. Проходили дни, потом недели. Но капля продолжала точить камень, и где-то через месяц я таки выточил свой шутер, в котором можно набегать и расстреливать караваны монстров из дробовика.
Теперь было самое время оглянуться на проделанный путь и подумать, что я сделал сносно, а где повернул не туда. Собственно, пример платформера, о котором речь в этой статье — одна из вещей, попавших, как мне кажется, в первую категорию.
Так шутер или платформер?
Возможно Вы спросите меня, почему я упорно называю по сути упрощённую версию своего шутера платформером. Мистер Дуб не только спросил, но и заставил меня переименовать пример обратно в шутер перед тем, как принять pull request. И тем не менее, я не считаю этот пример шутером. Как минимум потому, что в нём нельзя ни в кого стрелять. Зато можно бегать и прыгать по трёхмерной платформе. Код примера легко переделать под игру от третьего лица, добавив модель игрока и манипулируя ей вместо камеры, однако мне кажется это не принципиально.
Короче, Склифосовский!
Да, я малость отвлёкся от темы. Итак, чтобы сделать платформер, первым делом мы должны добавить в игровой мир хотя бы одну платформу. Дело это нехитрое, взял 3D модель, экспортнул в свой любимый формат (из числа babylon, ctm, dae, obj, ply, stl, vtk или wrl), загрузил в редактор Three.js, снова экспортнул, и загружай себе на здоровье. Тут есть два варианта:
- Сначала загрузить платформу, потом создать сцену и добавить туда платформу
- Создать сцену и добавить на неё платформу, а потом загрузить её в фоновом режиме
Первый вариант, ясное дело, идеологически более правильный, однако большинство примеров Three.js (включая этот) не заморачиваются и работают по второму сценарию. Надо отметить, что особой разницы в коде между 1 и 2 как бы и нет — просто в первом случае Вам следует перенести вызов инициализации сцены в обработчик загрузки, а во втором случае надо в основном цикле добавить костыль проверку на состояние платформы, чтобы не улететь далеко вниз, пока она не загрузилась. Я пошёл именно по этому пути, т к правильная реализация первого варианта в случае предзагрузки множества ресурсов всё равно потребует намного больше кода и/или сторонних библиотек.
function makePlatform( jsonUrl, textureUrl, textureQuality ) { var placeholder = new THREE.Object3D(); var texture = THREE.ImageUtils.loadTexture( textureUrl ); texture.anisotropy = textureQuality; var loader = new THREE.JSONLoader(); loader.load( jsonUrl, function( geometry ) { geometry.computeFaceNormals(); var platform = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial({ map : texture }) ); platform.name = "platform"; placeholder.add( platform ); }); return placeholder; };
Для ускорения загрузки я удалил нормали из json файла — поэтому Вы видите тут вызов computeFaceNormals — а platform.name устанавливается для упомянутой выше проверки наличия платформы. Без этого всего код мог бы выглядеть так:
loader.load( jsonUrl, function( geometry ) { placeholder.add( new THREE.Mesh( geometry, new THREE.MeshBasicMaterial({ map : texture }) ) ); });
Ладно, допустим Вы самостоятельно создали сцену, добавили в неё камеру и платформу. Далее, Вы должны заставить игрового персонажа как-то по ней двигаться, не пролетая и не проваливаясь сквозь неё. В этом деле Вам поможет класс Raycaster. Как несложно догадаться из названия, он рассчитывает пересечения заданного луча с выбранной геометрией, В данном случае мы просто направляем луч вниз, и находим ближайшее пересечение с платформой:
Просто, но есть нюансы. Например, нельзя использовать положение персонажа в качестве начала луча — в этом случае Вы не сможете найти пересечение с платформой, если персонаж по какой-либо причине провалится хотя бы на миллиметр, и отправите его в свободное падение вместо того, чтобы вытолкнуть обратно на платформу. Соответственно, начало луча должно находиться сверху, на высоте «птичьего полёта».
var raycaster = new THREE.Raycaster(); raycaster.ray.direction.set( 0, -1, 0 ); var birdsEye = 100; ... // далее, в цикле raycaster.ray.origin.copy( playerPosition ); raycaster.ray.origin.y += birdsEye; var hits = raycaster.intersectObject( platform );
В случае многоэтажной архитектуры уровня эта высота, очевидно, ограничена минимальным расстоянием между платформами по вертикали. Далее, следует тщательно продумать, когда принимать решение о выталкивании провалившегося персонажа наверх. Если не ограничить максимально допустимую глубину «провала», персонаж будет мгновенно телепортироваться на платформу, просто зайдя (или залетев) под неё; если же ограничить её слишком сильно, персонаж сможет легко проходить сквозь платформу при приземлениях после прыжков.
var kneeDeep = 0.4; ... // далее, в цикле // проверяем, сверху ли мы, или хотя бы не глубже чем по колено в платформе if( ( hits.length > 0 ) && ( hits[0].face.normal.y > 0 ) ) { var actualHeight = hits[0].distance - birdsEye; // если не слишком глубоко, вытаскиваем персонажа наверх if( ( playerVelocity.y <= 0 ) && ( Math.abs( actualHeight ) < kneeDeep ) ) { playerPosition.y -= actualHeight; playerVelocity.y = 0; } }
Внимательный читатель спросит, зачем тут проверка на playerVelocity.y <= 0? Ответ: для того, чтобы не создать проблем с отрывом от платформы при прыжке.
Теперь, собственно, надо заставить персонажа перемещаться в пространстве, подчиняясь базовым законам школьного курса физики. Положим, что в любой момент у персонажа известна скорость playerVelocity
и положение в пространстве playerPosition
; тогда рассчёт движения персонажа на первый взгляд мог бы выглядеть так (псевдокод):
if( в воздухе ) playerVelocity.y -= gravity; playerPosition += playerVelocity * time; if( на платформе ) playerVelocity *= damping;
Увы, и тут всё не так просто. Читателям с нешкольным образованием или ветеранам игростроя этот псевдокод известен под названием «метод Эйлера», а также известно что этот метод — просто отстой. И вот почему (картинка стырена с википедии):
Как видим, рассчётная траектория со временем всё сильнее расходится с ожидаемым результатом. Само по себе это обстоятельство не так страшно — страшным его делает одна скромная переменная — time
. Представим себе, как изменится эта картинка, если time
уменьшить на 10% (пересесть в более быстрый браузер, например):
Как видим, запустив игру в firefox, мы получим одну динамику, а запустив её в chrome — совершенно иную. Поведение персонажа будет «плавать» в зависимости от интенсивности фоновых задач и расположения звёзд. Что же делать?
Выход есть, и довольно простой. Необходимо заменить рассчёт с длинным переменным шагом time
на несколько рассчётов с коротким фиксированным шагом. Например, если два последовательных интервала между отрисовками составляют 19 и 21 мс, мы должны рассчитать 3 шага по 5 мс для первой отрисовки и, добавив оставшиеся 4 мс к 21, рассчитать 5 шагов по 5 мс для второй.
var timeStep = 5; var timeLeft = timeStep + 1; ... function( dt ) { // та самая проверочка ;) var platform = scene.getObjectByName( "platform", true ); if( platform ) { timeLeft += dt; // несколько шагов фиксированной длины dt = 5; while( timeLeft >= dt ) { // метод Эйлера ... timeLeft -= dt; } } }
На этом практически всё, Вам осталось лишь задать параметры движения персонажа (playerVelocity
например) в ответ на WASD или что-то подобное.
Ах да, совсем забыл. Полосатые выступы в примере отправляют персонажа в прыжок через всю платформу. Как? Всё очень просто — при приближении персонажа к выступу к playerVelocity
добавляется заранее подобранная вертикально-наклонная составляющая, которая гарантированно (благодаря вышеописанной схеме с фиксированным шагом) доставит его в заданную точку, подобно артиллерийскому снаряду. Никаких особых ухищрений не надо — всё уже и так работает.
Теперь точно всё. Читайте моё, пишите своё, критика приветстуется. До связи!
ссылка на оригинал статьи http://habrahabr.ru/post/231987/
Добавить комментарий