Создание простого бесконечного 3D раннера с использованием Three.js
() translation by (you can also view the original English article)



В последнее время веб-платформа значительно выросла благодаря использованию HTML5, WebGL и увеличению мощности устройств нынешнего поколения. Теперь мобильные устройства и браузеры способны доставлять высокопроизводительный контент как в 2D, так и в 3D. Популярность JavaScript (JS) как языка сценариев также стало определяющим фактором после исчезновения веб-платформы Flash.
Большинство веб-разработчиков уже знают, насколько сложна экосистема JS с различными доступными фреймворками и стандартами, которые могут сбить с толку новых разработчиков. Но когда дело доходит до 3D, благодаря разработчику Mr.Doob, выбор однозначен. Его библиотека Three.js в настоящее время является лучшим вариантом для создания высокопроизводительного 3D WebGL-контента. Другая мощная альтернатива - фреймворк Babylon.js, который также может быть использован для создания 3D-игр.
В этом уроке вы научитесь создавать простой бесконечный 3D раннер с использованием мощного фреймворка Three.js. Вы будете использовать клавиши со стрелками для управления снежным комом, который катится по склону горы, для того чтобы уклоняться от деревьев на его пути. Арт не задействован, и все визуальные эффекты создаются в коде.
1. Базовая 3D-сцена
На Envato Tuts+ уже есть несколько руководств, которые помогут вам начать работу с Three.js. Вот некоторые из них, для старта.
Давайте сначала создадим базовую трехмерную сцену, как показано ниже, где мы видим вращающийся куб. Вы можете использовать перетаскивание мышью, чтобы вращаться вокруг куба.
Любая графика, отображаемая на двухмерном экране, по своей природе практически 2D графика, с несколькими важными элементами, которые обеспечивают иллюзию 3D графики: освещение, затемнение, тени, и магия проецирования 3D в 2D она происходит с помощью камеры. В приведенной выше сцене, используя этот программный код, мы задействуем эффектное освещение.
1 |
camera = new THREE.PerspectiveCamera( 60, sceneWidth / sceneHeight, 0.1, 1000 );//perspective camera |
2 |
renderer = new THREE.WebGLRenderer({alpha:true});//renderer with transparent backdrop |
3 |
renderer.shadowMap.enabled = true;//enable shadow |
4 |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
5 |
|
6 |
//...
|
7 |
hero = new THREE.Mesh( heroGeometry, heroMaterial ); |
8 |
hero.castShadow=true; |
9 |
hero.receiveShadow=false; |
10 |
//...
|
11 |
ground.receiveShadow = true; |
12 |
ground.castShadow=false; |
13 |
//..
|
14 |
sun = new THREE.DirectionalLight( 0xffffff, 0.8); |
15 |
sun.position.set( 0,4,1 ); |
16 |
sun.castShadow = true; |
17 |
scene.add(sun); |
18 |
//Set up shadow properties for the sun light
|
19 |
sun.shadow.mapSize.width = 256; |
20 |
sun.shadow.mapSize.height = 256; |
21 |
sun.shadow.camera.near = 0.5; |
22 |
sun.shadow.camera.far = 50 ; |
Для рендеринга renderer
необходимо активировать свойство shadowMap
, а для сцены необходимо включить освещение активировав свойство castShadow
, а всем 3D-объектам необходимо соответствующим образом установить свойства castShadow
и receiveShadow
. Для правильного затенения мы должны также использоватьMeshStandardMaterial
или более многофункциональный материал для наших 3D-объектов. Камера управляется с помощью изящного скрипта OrbitControls. Прежде чем приступить к обучению я бы порекомендовал поиграться с основной трехмерной сценой, добавить больше примитивных фигур или поиграться с освещением и т. д.
2. Концепция бесконечного раннера
Существует много видов игр в жанре бесконечных раннеров, а наша - это "бесконечный роллер". Мы создадим игру, в которой снежный ком катится по бесконечному склону горы, где мы будем использовать клавиши со стрелками, чтобы избежать столкновения с деревьями. Один интересный факт заключается в том, что эта простая игра не будет задействовать какой-либо арт вообще, поскольку все компоненты будут созданы используя программный код. Здесь вся игра полностью, в которую вы можете попробовать поиграть.
3. Компоненты игры
Основными компонентами или элементами игры являются:
- катящийся снежный ком
- случайным образом сгенерированные деревья
- прокрутка земли
- появляющийся туман
- эффект столкновения
Мы подробно рассмотрим каждый из этих элементов в следующем разделе.
Туман
Туман fog
является свойством 3D сцены в библиотеке Three.js. Это всегда удобный трюк, чтобы имитировать глубину или показать горизонт. Цвет тумана важен для правильной работы иллюзии и зависит от цвета сцены и освещения. Как видно из приведенного ниже кода, мы также установили значение clearColor
средства визуализации renderer
, близкое к цвету fog
тумана.
1 |
scene = new THREE.Scene(); |
2 |
scene.fog = new THREE.FogExp2( 0xf0fff0, 0.14 ); |
3 |
camera = new THREE.PerspectiveCamera( 60, sceneWidth / sceneHeight, 0.1, 1000 );//perspective camera |
4 |
renderer = new THREE.WebGLRenderer({alpha:true});//renderer with transparent backdrop |
5 |
renderer.setClearColor(0xfffafa, 1); |
Чтобы соответствовать окружающей среде, мы также используем значения цвета, аналогичные источникам света, используемым в сцене. Каждый цвет окружающей среды имеет свой оттенок белого, который вместе создает необходимый эффект.
1 |
var hemisphereLight = new THREE.HemisphereLight(0xfffafa,0x000000, .9) |
2 |
scene.add(hemisphereLight); |
3 |
sun = new THREE.DirectionalLight( 0xcdc1c5, 0.9); |
4 |
sun.position.set( 12,6,-7 ); |
5 |
sun.castShadow = true; |
6 |
scene.add(sun); |
Снежный ком
Как показано ниже, наш снежный ком представляет собой три примитивных формы DodecahedronGeometry
.
1 |
var sphereGeometry = new THREE.DodecahedronGeometry( heroRadius, 1); |
2 |
var sphereMaterial = new THREE.MeshStandardMaterial( { color: 0xe5f2f2 ,shading:THREE.FlatShading} ) |
3 |
heroSphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); |
Для всех трехмерных элементов в этой игре мы используем THREE.FlatShading
, чтобы получить желаемый низкополигональный (low-poly) вид.
Вращающаяся гора
Вращающаяся земля с именем переменной rollingGroundSphere
- это большой примитив SphereGeometry
, и мы вращаем его по оси x
, чтобы создать иллюзию движущейся земли. Снежный ком на самом деле ничего не переворачивает; мы просто создаем иллюзию, поддерживая вращение земной сферы, оставляя снежный ком неподвижным.
Обычная сфера будет выглядеть очень гладкой и, следовательно, не обеспечит необходимую устойчивость, необходимую для склона горы. Поэтому мы делаем некоторые манипуляции с вершинами, чтобы превратить гладкую поверхность сферы в неровную, рельефную местность. Здесь приведен соответствующий код с последующим объяснением.
1 |
var sides=40; |
2 |
var tiers=40; |
3 |
var sphereGeometry = new THREE.SphereGeometry( worldRadius, sides,tiers); |
4 |
var sphereMaterial = new THREE.MeshStandardMaterial( { color: 0xfffafa ,shading:THREE.FlatShading} ) |
5 |
var vertexIndex; |
6 |
var vertexVector= new THREE.Vector3(); |
7 |
var nextVertexVector= new THREE.Vector3(); |
8 |
var firstVertexVector= new THREE.Vector3(); |
9 |
var offset= new THREE.Vector3(); |
10 |
var currentTier=1; |
11 |
var lerpValue=0.5; |
12 |
var heightValue; |
13 |
var maxHeight=0.07; |
14 |
for(var j=1;j<tiers-2;j++){ |
15 |
currentTier=j; |
16 |
for(var i=0;i<sides;i++){ |
17 |
vertexIndex=(currentTier*sides)+1; |
18 |
vertexVector=sphereGeometry.vertices[i+vertexIndex].clone(); |
19 |
if(j%2!==0){ |
20 |
if(i===0){ |
21 |
firstVertexVector=vertexVector.clone(); |
22 |
}
|
23 |
nextVertexVector=sphereGeometry.vertices[i+vertexIndex+1].clone(); |
24 |
if(i==sides-1){ |
25 |
nextVertexVector=firstVertexVector; |
26 |
}
|
27 |
lerpValue=(Math.random()*(0.75-0.25))+0.25; |
28 |
vertexVector.lerp(nextVertexVector,lerpValue); |
29 |
}
|
30 |
heightValue=(Math.random()*maxHeight)-(maxHeight/2); |
31 |
offset=vertexVector.clone().normalize().multiplyScalar(heightValue); |
32 |
sphereGeometry.vertices[i+vertexIndex]=(vertexVector.add(offset)); |
33 |
}
|
34 |
}
|
35 |
rollingGroundSphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); |
Мы создаем примитивную сферу с 40 горизонтальными сегментами (sides
) и 40 вертикальными сегментами (tiers
). Вы можете получить доступ к элементам массива вершин через свойство массива vertices
. Мы перебираем все ряды в цикле между крайними верхними и крайними нижними вершинами, чтобы выполнять наши манипуляции с вершинами. Каждый уровень геометрии сферы содержит ровно столько же sides
сторон, сколько вершин, которые образуют замкнутый круг вокруг сферы.
Первым шагом является вращение каждой нечетной окружности вершин, чтобы нарушить равномерность контуров поверхности. Мы перемещаем каждую вершину в круге сгенерированным значением между 0,25 и 0,75 от расстояния до следующей вершины. В результате вертикальные вершины сферы больше не выровнены по прямой линии, и мы получаем хороший зигзагообразный контур.
В качестве второго шага мы предоставляем каждой вершине произвольную регулировку высоты, выровненную по нормали в вершине, независимо от ряда, на котором она находится. Это приводит к неровной и шероховатой поверхности. Я надеюсь, что используемая здесь векторная математика проста, если учесть, что центр сферы считается от (0,0)
.
Деревья
Деревья появляются за пределами дороги по которой катится шар, чтобы добавить глубину игровому миру, а в пределах дороги как препятствия. Создавать дерево немного сложнее, чем рельефную поверхность, но используется та же логика. Мы используем примитив ConeGeometry
для создания верхней зеленой части дерева и CylinderGeometry
для создания нижней части ствола.
Для верхней части мы перебираем каждый ряд вершин и расширяем окружность вершин с последующим сокращением следующей окружности. В следующем коде показан метод blowUpTree
, который используется для расширения окружности вершин, а также метод tightenTree
, используемый для уменьшения следующей окружности вершин.
1 |
function createTree(){ |
2 |
var sides=8; |
3 |
var tiers=6; |
4 |
var scalarMultiplier=(Math.random()*(0.25-0.1))+0.05; |
5 |
var midPointVector= new THREE.Vector3(); |
6 |
var vertexVector= new THREE.Vector3(); |
7 |
var treeGeometry = new THREE.ConeGeometry( 0.5, 1, sides, tiers); |
8 |
var treeMaterial = new THREE.MeshStandardMaterial( { color: 0x33ff33,shading:THREE.FlatShading } ); |
9 |
var offset; |
10 |
midPointVector=treeGeometry.vertices[0].clone(); |
11 |
var currentTier=0; |
12 |
var vertexIndex; |
13 |
blowUpTree(treeGeometry.vertices,sides,0,scalarMultiplier); |
14 |
tightenTree(treeGeometry.vertices,sides,1); |
15 |
blowUpTree(treeGeometry.vertices,sides,2,scalarMultiplier*1.1,true); |
16 |
tightenTree(treeGeometry.vertices,sides,3); |
17 |
blowUpTree(treeGeometry.vertices,sides,4,scalarMultiplier*1.2); |
18 |
tightenTree(treeGeometry.vertices,sides,5); |
19 |
var treeTop = new THREE.Mesh( treeGeometry, treeMaterial ); |
20 |
treeTop.castShadow=true; |
21 |
treeTop.receiveShadow=false; |
22 |
treeTop.position.y=0.9; |
23 |
treeTop.rotation.y=(Math.random()*(Math.PI)); |
24 |
var treeTrunkGeometry = new THREE.CylinderGeometry( 0.1, 0.1,0.5); |
25 |
var trunkMaterial = new THREE.MeshStandardMaterial( { color: 0x886633,shading:THREE.FlatShading } ); |
26 |
var treeTrunk = new THREE.Mesh( treeTrunkGeometry, trunkMaterial ); |
27 |
treeTrunk.position.y=0.25; |
28 |
var tree =new THREE.Object3D(); |
29 |
tree.add(treeTrunk); |
30 |
tree.add(treeTop); |
31 |
return tree; |
32 |
}
|
33 |
function blowUpTree(vertices,sides,currentTier,scalarMultiplier,odd){ |
34 |
var vertexIndex; |
35 |
var vertexVector= new THREE.Vector3(); |
36 |
var midPointVector=vertices[0].clone(); |
37 |
var offset; |
38 |
for(var i=0;i<sides;i++){ |
39 |
vertexIndex=(currentTier*sides)+1; |
40 |
vertexVector=vertices[i+vertexIndex].clone(); |
41 |
midPointVector.y=vertexVector.y; |
42 |
offset=vertexVector.sub(midPointVector); |
43 |
if(odd){ |
44 |
if(i%2===0){ |
45 |
offset.normalize().multiplyScalar(scalarMultiplier/6); |
46 |
vertices[i+vertexIndex].add(offset); |
47 |
}else{ |
48 |
offset.normalize().multiplyScalar(scalarMultiplier); |
49 |
vertices[i+vertexIndex].add(offset); |
50 |
vertices[i+vertexIndex].y=vertices[i+vertexIndex+sides].y+0.05; |
51 |
}
|
52 |
}else{ |
53 |
if(i%2!==0){ |
54 |
offset.normalize().multiplyScalar(scalarMultiplier/6); |
55 |
vertices[i+vertexIndex].add(offset); |
56 |
}else{ |
57 |
offset.normalize().multiplyScalar(scalarMultiplier); |
58 |
vertices[i+vertexIndex].add(offset); |
59 |
vertices[i+vertexIndex].y=vertices[i+vertexIndex+sides].y+0.05; |
60 |
}
|
61 |
}
|
62 |
}
|
63 |
}
|
64 |
function tightenTree(vertices,sides,currentTier){ |
65 |
var vertexIndex; |
66 |
var vertexVector= new THREE.Vector3(); |
67 |
var midPointVector=vertices[0].clone(); |
68 |
var offset; |
69 |
for(var i=0;i<sides;i++){ |
70 |
vertexIndex=(currentTier*sides)+1; |
71 |
vertexVector=vertices[i+vertexIndex].clone(); |
72 |
midPointVector.y=vertexVector.y; |
73 |
offset=vertexVector.sub(midPointVector); |
74 |
offset.normalize().multiplyScalar(0.06); |
75 |
vertices[i+vertexIndex].sub(offset); |
76 |
}
|
77 |
}
|
Метод blowUpTree
выталкивает каждую альтернативную вершину в кольцо вершин, в то же время оставляя другие вершины в кольце на более низкой высоте. Это создает заостренные ветви на дереве. Если мы используем нечетные вершины на одном ряду, то мы используем четные вершины на следующем ряду, так что равномерность нарушается. После того, как дерево сформировано полностью, мы генерируем случайное значение для вращения по оси y, чтобы это выглядело немного по-другому.
Эффект взрыва
Эффект пиксельного взрыва не самый элегантный, который можно использовать, но он, безусловно, хорошо работает. Этот конкретный эффект частиц на самом деле является трехмерной геометрией, которую используют с помощью класса THREE.Points
, чтобы это выглядело как спецэффект.
1 |
function addExplosion(){ |
2 |
particleGeometry = new THREE.Geometry(); |
3 |
for (var i = 0; i < particleCount; i ++ ) { |
4 |
var vertex = new THREE.Vector3(); |
5 |
particleGeometry.vertices.push( vertex ); |
6 |
}
|
7 |
var pMaterial = new THREE.ParticleBasicMaterial({ |
8 |
color: 0xfffafa, |
9 |
size: 0.2 |
10 |
});
|
11 |
particles = new THREE.Points( particleGeometry, pMaterial ); |
12 |
scene.add( particles ); |
13 |
particles.visible=false; |
14 |
}
|
15 |
function explode(){ |
16 |
particles.position.y=2; |
17 |
particles.position.z=4.8; |
18 |
particles.position.x=heroSphere.position.x; |
19 |
for (var i = 0; i < particleCount; i ++ ) { |
20 |
var vertex = new THREE.Vector3(); |
21 |
vertex.x = -0.2+Math.random() * 0.4; |
22 |
vertex.y = -0.2+Math.random() * 0.4 ; |
23 |
vertex.z = -0.2+Math.random() * 0.4; |
24 |
particleGeometry.vertices[i]=vertex; |
25 |
}
|
26 |
explosionPower=1.07; |
27 |
particles.visible=true; |
28 |
}
|
29 |
function doExplosionLogic(){//called in update |
30 |
if(!particles.visible)return; |
31 |
for (var i = 0; i < particleCount; i ++ ) { |
32 |
particleGeometry.vertices[i].multiplyScalar(explosionPower); |
33 |
}
|
34 |
if(explosionPower>1.005){ |
35 |
explosionPower-=0.001; |
36 |
}else{ |
37 |
particles.visible=false; |
38 |
}
|
39 |
particleGeometry.verticesNeedUpdate = true; |
40 |
}
|
Метод addExplosion
добавляет 20 вершин к матрице вершин vertices
в переменной particleGeometry
. Метод explode
вызывается, когда нам нужно запустить эффект, который случайным образом позиционирует каждую вершину геометрии. Если объект частицы видимый,DoExplosionLogic
вызывается в методе обновления update
, где мы перемещаем каждую вершину за пределы. Каждая вершина в объекте points
рендерится как квадратный блок.
4. Геймплей
Теперь, когда мы знаем, как создавать необходимые для игры элементы, давайте перейдем к игровому процессу. Основными элементами игрового процесса являются:
- игровой цикл
- размещение деревьев
- взаимодействие с пользователем
- обнаружение столкновений
Давайте их подробно проанализируем.
Игровой цикл
Вся основная игровая механика происходит в игровом цикле, который в нашем случае является методом update
. Мы вызываем его впервые из функции init
, которая вызывается при загрузке окна. После этого он подключается к циклу рендеринга документа, используя функцию requestAnimationFrame
, так чтобы он вызывался повторно.
1 |
function update(){ |
2 |
rollingGroundSphere.rotation.x += rollingSpeed; |
3 |
heroSphere.rotation.x -= heroRollingSpeed; |
4 |
if(heroSphere.position.y<=heroBaseY){ |
5 |
jumping=false; |
6 |
bounceValue=(Math.random()*0.04)+0.005; |
7 |
}
|
8 |
heroSphere.position.y+=bounceValue; |
9 |
heroSphere.position.x=THREE.Math.lerp(heroSphere.position.x,currentLane, 2*clock.getDelta());//clock.getElapsedTime()); |
10 |
bounceValue-=gravity; |
11 |
if(clock.getElapsedTime()>treeReleaseInterval){ |
12 |
clock.start(); |
13 |
addPathTree(); |
14 |
if(!hasCollided){ |
15 |
score+=2*treeReleaseInterval; |
16 |
scoreText.innerHTML=score.toString(); |
17 |
}
|
18 |
}
|
19 |
doTreeLogic(); |
20 |
doExplosionLogic(); |
21 |
render(); |
22 |
requestAnimationFrame(update);//request next update |
23 |
}
|
24 |
function render(){ |
25 |
renderer.render(scene, camera);//draw |
26 |
}
|
В функции update
мы вызываем метод render
, который использует средства визуализации renderer
для рисования сцены. Мы вызываем метод doTreeLogic
, который проверяет наличие столкновений, а также удаляет деревья после их исчезновения из виду.
Снежный ком и земные сферы вращаются, в то время как мы добавляем логику случайного подпрыгивания для снежного кома. Новые деревья размещаются на его пути, вызывая метод addPathTree
по истечении заданного времени. Время отслеживается с помощью объектаTHREE.Clock
. Мы также обновляем счет score
, если не произошло столкновение.
Размещение деревьев
Один набор деревьев помещается за пределы дороги, чтобы создать их в игровом мире используется метод addWorldTrees
. Все деревья добавляются в качестве дочерних элементов для RollingGroundSphere
, поэтому они будут перемещаться, когда сфера вращается.
1 |
function addWorldTrees(){ |
2 |
var numTrees=36; |
3 |
var gap=6.28/36; |
4 |
for(var i=0;i<numTrees;i++){ |
5 |
addTree(false,i*gap, true); |
6 |
addTree(false,i*gap, false); |
7 |
}
|
8 |
}
|
9 |
function addTree(inPath, row, isLeft){ |
10 |
var newTree; |
11 |
if(inPath){ |
12 |
if(treesPool.length===0)return; |
13 |
newTree=treesPool.pop(); |
14 |
newTree.visible=true; |
15 |
//console.log("add tree");
|
16 |
treesInPath.push(newTree); |
17 |
sphericalHelper.set( worldRadius-0.3, pathAngleValues[row], -rollingGroundSphere.rotation.x+4 ); |
18 |
}else{ |
19 |
newTree=createTree(); |
20 |
var forestAreaAngle=0;//[1.52,1.57,1.62]; |
21 |
if(isLeft){ |
22 |
forestAreaAngle=1.68+Math.random()*0.1; |
23 |
}else{ |
24 |
forestAreaAngle=1.46-Math.random()*0.1; |
25 |
}
|
26 |
sphericalHelper.set( worldRadius-0.3, forestAreaAngle, row ); |
27 |
}
|
28 |
newTree.position.setFromSpherical( sphericalHelper ); |
29 |
var rollingGroundVector=rollingGroundSphere.position.clone().normalize(); |
30 |
var treeVector=newTree.position.clone().normalize(); |
31 |
newTree.quaternion.setFromUnitVectors(treeVector,rollingGroundVector); |
32 |
newTree.rotation.x+=(Math.random()*(2*Math.PI/10))+-Math.PI/10; |
33 |
|
34 |
rollingGroundSphere.add(newTree); |
35 |
}
|
Чтобы разместить деревья в игровом мире, мы вызываем метод addTree
, передавая значения по окружности нашей земной сферы. Утилита sphericalHelper
помогает найти положение на поверхности сферы.
Чтобы посадить деревья на дороге, мы будем использовать группу деревьев, которая создается при запуске с помощью метода createTreesPool
. У нас также есть предопределенные значения углов для каждого пути на сфере, хранящиеся в массиве pathAngleValues
.
1 |
pathAngleValues=[1.52,1.57,1.62]; |
2 |
//..
|
3 |
function createTreesPool(){ |
4 |
var maxTreesInPool=10; |
5 |
var newTree; |
6 |
for(var i=0; i<maxTreesInPool;i++){ |
7 |
newTree=createTree(); |
8 |
treesPool.push(newTree); |
9 |
}
|
10 |
}
|
11 |
function addPathTree(){ |
12 |
var options=[0,1,2]; |
13 |
var lane= Math.floor(Math.random()*3); |
14 |
addTree(true,lane); |
15 |
options.splice(lane,1); |
16 |
if(Math.random()>0.5){ |
17 |
lane= Math.floor(Math.random()*2); |
18 |
addTree(true,options[lane]); |
19 |
}
|
20 |
}
|
Функция addPathTree
вызывается из функции обновления, когда после посадки последнего дерева прошло достаточно времени. Она, в свою очередь, вызывает метод addTree
, показанный ранее, с другим набором параметров, где дерево помещается в выбранный путь. Метод doTreeLogic
вернет дерево в пул деревьев, как только оно выйдет из поля зрения.
Взаимодействие с пользователем
Мы добавляем слушатель событий в документ для поиска соответствующих событий нажатия клавиатуры. Метод handleKeyDown
устанавливает значение currentLane
, если нажаты клавиши со стрелками вправо или влево, или задает значение bounceValue
, если нажата стрелка вверх.
1 |
document.onkeydown = handleKeyDown; |
2 |
//..
|
3 |
function handleKeyDown(keyEvent){ |
4 |
if(jumping)return; |
5 |
var validMove=true; |
6 |
if ( keyEvent.keyCode === 37) {//left |
7 |
if(currentLane==middleLane){ |
8 |
currentLane=leftLane; |
9 |
}else if(currentLane==rightLane){ |
10 |
currentLane=middleLane; |
11 |
}else{ |
12 |
validMove=false; |
13 |
}
|
14 |
} else if ( keyEvent.keyCode === 39) {//right |
15 |
if(currentLane==middleLane){ |
16 |
currentLane=rightLane; |
17 |
}else if(currentLane==leftLane){ |
18 |
currentLane=middleLane; |
19 |
}else{ |
20 |
validMove=false; |
21 |
}
|
22 |
}else{ |
23 |
if ( keyEvent.keyCode === 38){//up, jump |
24 |
bounceValue=0.1; |
25 |
jumping=true; |
26 |
}
|
27 |
validMove=false; |
28 |
}
|
29 |
if(validMove){ |
30 |
jumping=true; |
31 |
bounceValue=0.06; |
32 |
}
|
33 |
}
|
В функции update
, позиция нашего снежного кома по оси x
медленно увеличивается, чтобы достигнуть текущей позиции currentLane
при смене полосы движения.
Обнаружение столкновения
В этой конкретной игре нет никакой реальной физики, хотя мы могли бы использовать различные физические фреймворки для обнаружения столкновений. Но вам прекрасно известно, что физический движок увеличивает нагрузку на игру, и мы всегда должны пытаться понять, сможем ли мы этого избежать.
В нашем случае мы вычисляем расстояние между нашим снежным комом и каждым деревом, если эти объекты сближаются мы инициируем столкновение. Это происходит в функции doTreeLogic
, которая вызывается из функции update
.
1 |
function doTreeLogic(){ |
2 |
var oneTree; |
3 |
var treePos = new THREE.Vector3(); |
4 |
treesInPath.forEach( function ( element, index ) { |
5 |
oneTree=treesInPath[ index ]; |
6 |
treePos.setFromMatrixPosition( oneTree.matrixWorld ); |
7 |
if(treePos.distanceTo(heroSphere.position)<=0.6){ |
8 |
console.log("hit"); |
9 |
hasCollided=true; |
10 |
explode(); |
11 |
}
|
12 |
});
|
13 |
//..
|
14 |
}
|
Как вы могли заметить, все деревья, присутствующие на нашем пути, хранятся в массиве treesInPath
. Функция doTreeLogic
также удаляет деревья с экрана и из пула, как только они исчезают из вида, используя код, показанный ниже.
1 |
var treesToRemove=[]; |
2 |
treesInPath.forEach( function ( element, index ) { |
3 |
oneTree=treesInPath[ index ]; |
4 |
treePos.setFromMatrixPosition( oneTree.matrixWorld ); |
5 |
if(treePos.z>6 &&oneTree.visible){//gone out of our view zone |
6 |
treesToRemove.push(oneTree); |
7 |
}
|
8 |
});
|
9 |
var fromWhere; |
10 |
treesToRemove.forEach( function ( element, index ) { |
11 |
oneTree=treesToRemove[ index ]; |
12 |
fromWhere=treesInPath.indexOf(oneTree); |
13 |
treesInPath.splice(fromWhere,1); |
14 |
treesPool.push(oneTree); |
15 |
oneTree.visible=false; |
16 |
console.log("remove tree"); |
17 |
});
|
Заключение
Создание 3D-игры - сложный процесс, если вы не используете визуальный инструмент, вроде Unity. Это может вас напугать и покажется непреодолимым, но позвольте заверить вас, как только вы освоите Three.js, вы почувствуете себя более сильным и творческим разработчиком. Я бы хотел, чтобы вы продолжили исследования, используя различные физические фреймворки или системы частиц, или примеры кода.