Русский
Русский
English
Статистика
Реклама

Создание браузерных 3d-игр с нуля на чистом html, css и js. Часть 22

В данной статье мы продолжим создавать трехмерную браузерную игру лабиринт на чистом html, css и javascript. В предыдущей части мы сделали простой 3-мерный мир, реализовали движение, управление, столкновения игрока со статическими объектами. В этой части мы будем добавлять гравитацию, статическое солнечное освещение (без теней), загружать звуки и делать меню. Увы, как и в первой части, демок здесь не будет.


Вспомним код, который мы сделали в предыдущей части. У нас имеются 3 файла:

index.html
<!DOCTYPE HTML><HTML><HEAD><TITLE>Игра</TITLE><LINK rel="stylesheet" href="style.css"><meta charset="utf-8"></HEAD><BODY><div id="container"><div id="world"></div><div id="pawn"></div></div></BODY></HTML><script src="script.js"></script>


style.css
#container{position:absolute;width:1200px;height:800px;border:2px solid #000000;perspective:600px;overflow:hidden;}#world{position:absolute;width:inherit;height:inherit;transform-style:preserve-3d;}.square{position:absolute;}#pawn{display:none;position:absolute;top:400px;left:600px;transform:translate(-50%,-50%);width:100px;height:100px;}


script.js
// Мировые константыvar pi = 3.141592;var deg = pi/180;// Конструктор playerfunction player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;}// Массив прямоугольниковvar map = [   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,100,0,90,0,0,2000,2000,"#666666"]];// Нажата ли клавиша и двигается ли мышь?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;var MouseX = 0;var MouseY = 0;// Введен ли захват мыши?var lock = false;// На земле ли игрок?var onGround = true;// Привяжем новую переменную к containervar container = document.getElementById("container");// Обработчик изменения состояния захвата курсораdocument.addEventListener("pointerlockchange", (event)=>{lock = !lock;});// Обработчик захвата курсора мышиcontainer.onclick = function(){if (!lock) container.requestPointerLock();};// Обработчик нажатия клавишdocument.addEventListener("keydown", (event) =>{if (event.key == "a"){PressLeft = 1;}if (event.key == "w"){PressForward = 1;}if (event.key == "d"){PressRight = 1;}if (event.key == "s"){PressBack = 1;}if (event.keyCode == 32 && onGround){PressUp = 1;}});// Обработчик отжатия клавишdocument.addEventListener("keyup", (event) =>{if (event.key == "a"){PressLeft = 0;}if (event.key == "w"){PressForward = 0;}if (event.key == "d"){PressRight = 0;}if (event.key == "s"){PressBack = 0;}if (event.keyCode == 32){PressUp = 0;}});// Обработчик движения мышиdocument.addEventListener("mousemove", (event)=>{MouseX = event.movementX;MouseY = event.movementY;});// Создаем новый объектvar pawn = new player(-900,0,-900,0,0);// Привяжем новую переменную к worldvar world = document.getElementById("world");function update(){// Задаем локальные переменные смещенияdx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);dy = - PressUp;drx = MouseY;dry = - MouseX;// Обнулим смещения мыши:MouseX = MouseY = 0;// Проверяем коллизию с прямоугольникамиcollision();// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;console.log(pawn.x + ":" + pawn.y + ":" + pawn.z);// Если курсор захвачен, разрешаем вращениеif (lock){pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;};// Изменяем координаты мира (для отображения)world.style.transform = "translateZ(" + (600 - 0) + "px)" +"rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";};function CreateNewWorld(){for (let i = 0; i < map.length; i++){// Создание прямоугольника и придание ему стилейlet newElement = document.createElement("div");newElement.className = "square";newElement.id = "square" + i;newElement.style.width = map[i][6] + "px";newElement.style.height = map[i][7] + "px";newElement.style.background = map[i][8];newElement.style.transform = "translate3d(" +(600 - map[i][6]/2 + map[i][0]) + "px," +(400 - map[i][7]/2 + map[i][1]) + "px," +(map[i][2]) + "px)" +"rotateX(" + map[i][3] + "deg)" +"rotateY(" + map[i][4] + "deg)" +"rotateZ(" + map[i][5] + "deg)";// Вставка прямоугольника в worldworld.append(newElement);}}function collision(){for(let i = 0; i < map.length; i++){// рассчитываем координаты игрока в системе координат прямоугольникаlet x0 = (pawn.x - map[i][0]);let y0 = (pawn.y - map[i][1]);let z0 = (pawn.z - map[i][2]);if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][6]**2 + map[i][7]**2)){let x1 = x0 + dx;let y1 = y0 + dy;let z1 = z0 + dz;let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);let point2 = new Array();// Условие коллизии и действия при немif (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){point1[2] = Math.sign(point0[2])*50;point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);dx = point2[0] - x0;dy = point2[1] - y0;dz = point2[2] - z0;}}};}function coorTransform(x0,y0,z0,rxc,ryc,rzc){let x1 =  x0;let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);let y2 =  y1;let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg); let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);let z3 =  z2;return [x3,y3,z3];}function coorReTransform(x3,y3,z3,rxc,ryc,rzc){let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);let z2 =  z3let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);let y1 =  y2;let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);let x0 =  x1;let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);return [x0,y0,z0];}CreateNewWorld();TimerGame = setInterval(update,10);



1. Реализация гравитации и физики прыжка


У нас есть несколько переменных, которые создаются в разных частях файла javascript. Будет лучше, если мы перенесем их в одно место:

// Создадим переменныеvar lock = false;var onGround = true;var container = document.getElementById("container");var world = document.getElementById("world");


Добавим ускорение свободного падения к ним:

var g = 0.1;


В конструктор player добавим 3 переменные vx, vy и vz:

function player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;this.vx = 3;this.vy = 5;this.vz = 3;}


Это переменные скорости движения. Меняя их, мы можем изменять скорость бега и начальную скорость прыжка игрока. Пока применим новые переменные в update():

        dx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx;dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz;dy = PressUp*pawn.vy;drx = MouseY;dry = - MouseX;


Теперь игрок движется быстрее. Но он не падает и не прыгает. Нужно разрешить прыжок тогда, когда он на чем-то стоит. А стоять он будет тогда, когда столкнется с горизонтальной (или почти) поверхностью. Как определить горизонтальность? Нужно найти нормаль плоскости прямоугольника. Делается это просто. Относительно координат прямоугольника нормаль направлена вдоль оси z. Тогда в мировых координатах нормаль имеет преобразованные координаты. Найдем нормаль (добавим локальную переменную normal):

let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);let point2 = new Array();let normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]);


Чтобы поверхность была горизонтальной, скалярное произведение нормали на ось y в мировых координатах должно равняться 1 или -1, а почти горизонтальная плоскость близко к 1 или -1. Зададим условие почти горизонтальной плоскости:

if (Math.abs(normal[1]) > 0.8){onGround = true;}


Не забудем, что при отсутствии столкновений игрок точно не будет на земле, поэтому по умолчанию в начале функции collision() зададим onGround = false:

function collision(){onGround = false;for(let i = 0; i < map.length; i++){


Однако, если игрок столкнется с поверхностью снизу, то он тоже окажется как бы на земле. Чтобы предотвратить это, проверим игрока на нахождение сверху плоскости (point3[1] должна быть меньше point2[1]):

let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]);dx = point2[0] - x0;dy = point2[1] - y0;dz = point2[2] - z0;if (Math.abs(normal[1]) > 0.8){if (point3[1] > point2[1]) onGround = true;}else dy = y1 - y0;


Что мы делаем? взгляните на картинку:



Красный крест должен находиться ниже оранжевого в мировой системе координат (или y-координата должна быть больше). Это мы и проверяем в point3[1] > point2[1]. А point3 есть как раз координаты красной точки. Перенесем инициализацию point2 внутрь условии коллизии:

let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);let normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]);// Условие коллизии и действия при немif (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){point1[2] = Math.sign(point0[2])*50;let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]);dx = point2[0] - x0;dy = point2[1] - y0;dz = point2[2] - z0;if (Math.abs(normal[1]) > 0.8){if (point3[1] > point2[1]) onGround = true;}}


Перенесемся в update(). Здесь мы тоже сделаем изменения. Во первых, добавим гравитацию и уберем смещение по y при нажатии на пробел:

// Задаем локальные переменные смещенияdx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx;dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz;dy = dy + g;drx = MouseY;dry = - MouseX;  


Во вторых, если игрок находится на земле, запрещаем гравитацию, запрещаем смещения по y (иначе после хождения по наклонной поверхности игрок будет взлетать) и добавляем возможность прыжка (условие if (onGround)):

// Задаем локальные переменные смещенияdx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx;dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz;dy = dy + g;if (onGround){dy = 0;if (PressUp){dy = - PressUp*pawn.vy;onGround = false;}};drx = MouseY;dry = - MouseX;


Естественно, сразу после произведения прыжка запрещаем повторный прыжок, переведя параметр onGround в false. В условии нажатия пробела правдивость этого параметра больше не нужна:

if (event.keyCode == 32){PressUp = 1;}


Для проверки изменений изменим мир:

var map = [   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,0,-300,70,0,0,200,500,"#F000FF"],   [0,-86,-786,90,0,0,200,500,"#F000FF"],   [-500,0,-300,20,0,0,200,500,"#00FF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];


Если мы запустим игру, то увидим, что игрок может взбираться по почти вертикальной зеленой стене. Запретим это, добавив else dy = y1 y0:

if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){point1[2] = Math.sign(point0[2])*50;let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]);dx = point2[0] - x0;dy = point2[1] - y0;dz = point2[2] - z0;if (Math.abs(normal[1]) > 0.8){if (point3[1] > point2[1]) onGround = true;}else dy = y1 - y0;}


Итак, столкновения с сильно вертикальными стенками не изменяют смещения по y. Поэтому разгон на таких стенках теперь полностью исключается. Попробуем взобраться на зеленую стену. У нас это теперь не получится. Итак, мы разобрались с гравитацией и прыжками и теперь мы можем достаточно реалистично взбираться по слабо наклоненным поверхностям. Проверим код:

index.html
<!DOCTYPE HTML><HTML><HEAD><TITLE>Игра</TITLE><LINK rel="stylesheet" href="style.css"><meta charset="utf-8"></HEAD><BODY><div id="container"><div id="world"></div><div id="pawn"></div></div></BODY></HTML><script src="script.js"></script>


style.css
#container{position:absolute;width:1200px;height:800px;border:2px solid #000000;perspective:600px;overflow:hidden;}#world{position:absolute;width:inherit;height:inherit;transform-style:preserve-3d;}.square{position:absolute;}#pawn{display:none;position:absolute;top:400px;left:600px;transform:translate(-50%,-50%);width:100px;height:100px;}


script.js
// Мировые константыvar pi = 3.141592;var deg = pi/180;// Конструктор playerfunction player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;this.vx = 3;this.vy = 5;this.vz = 3;}// Массив прямоугольниковvar map = [   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,0,-300,70,0,0,200,500,"#F000FF"],   [0,-86,-786,90,0,0,200,500,"#F000FF"],   [-500,0,-300,20,0,0,200,500,"#00FF00"],   [0,-800,0,90,0,0,500,500,"#00FF00"],   [0,-400,700,60,0,0,500,900,"#FFFF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];// Нажата ли клавиша и двигается ли мышь?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;var MouseX = 0;var MouseY = 0;// Создадим переменныеvar lock = false;var onGround = false;var container = document.getElementById("container");var world = document.getElementById("world");var g = 0.1;var dx = dy = dz = 0; // Обработчик изменения состояния захвата курсораdocument.addEventListener("pointerlockchange", (event)=>{lock = !lock;});// Обработчик захвата курсора мышиcontainer.onclick = function(){if (!lock) container.requestPointerLock();};// Обработчик нажатия клавишdocument.addEventListener("keydown", (event) =>{if (event.key == "a"){PressLeft = 1;}if (event.key == "w"){PressForward = 1;}if (event.key == "d"){PressRight = 1;}if (event.key == "s"){PressBack = 1;}if (event.keyCode == 32){PressUp = 1;}});// Обработчик отжатия клавишdocument.addEventListener("keyup", (event) =>{if (event.key == "a"){PressLeft = 0;}if (event.key == "w"){PressForward = 0;}if (event.key == "d"){PressRight = 0;}if (event.key == "s"){PressBack = 0;}if (event.keyCode == 32){PressUp = 0;}});// Обработчик движения мышиdocument.addEventListener("mousemove", (event)=>{MouseX = event.movementX;MouseY = event.movementY;});// Создаем новый объектvar pawn = new player(0,-900,0,0,0);function update(){// Задаем локальные переменные смещенияdx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx;dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz;dy = dy + g;if (onGround){dy = 0;if (PressUp){dy = - PressUp*pawn.vy;onGround = false;}};drx = MouseY;dry = - MouseX;// Обнулим смещения мыши:MouseX = MouseY = 0;// Проверяем коллизию с прямоугольникамиcollision();// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;// Если курсор захвачен, разрешаем вращениеif (lock){pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;};// Изменяем координаты мира (для отображения)world.style.transform = "translateZ(" + (600 - 0) + "px)" +"rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";};function CreateNewWorld(){for (let i = 0; i < map.length; i++){// Создание прямоугольника и придание ему стилейlet newElement = document.createElement("div");newElement.className = "square";newElement.id = "square" + i;newElement.style.width = map[i][6] + "px";newElement.style.height = map[i][7] + "px";newElement.style.background = map[i][8];newElement.style.transform = "translate3d(" +(600 - map[i][6]/2 + map[i][0]) + "px," +(400 - map[i][7]/2 + map[i][1]) + "px," +(map[i][2]) + "px)" +"rotateX(" + map[i][3] + "deg)" +"rotateY(" + map[i][4] + "deg)" +"rotateZ(" + map[i][5] + "deg)";// Вставка прямоугольника в worldworld.append(newElement);}}function collision(){onGround = false;for(let i = 0; i < map.length; i++){// рассчитываем координаты игрока в системе координат прямоугольникаlet x0 = (pawn.x - map[i][0]);let y0 = (pawn.y - map[i][1]);let z0 = (pawn.z - map[i][2]);if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][6]**2 + map[i][7]**2)){let x1 = x0 + dx;let y1 = y0 + dy;let z1 = z0 + dz;let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);let normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]);// Условие коллизии и действия при немif (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){point1[2] = Math.sign(point0[2])*50;let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]);dx = point2[0] - x0;dy = point2[1] - y0;dz = point2[2] - z0;if (Math.abs(normal[1]) > 0.8){if (point3[1] > point2[1]) onGround = true;}else dy = y1 - y0;}}};}function coorTransform(x0,y0,z0,rxc,ryc,rzc){let x1 =  x0;let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);let y2 =  y1;let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg); let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);let z3 =  z2;return [x3,y3,z3];}function coorReTransform(x3,y3,z3,rxc,ryc,rzc){let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);let z2 =  z3let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);let y1 =  y2;let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);let x0 =  x1;let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);return [x0,y0,z0];}CreateNewWorld();TimerGame = setInterval(update,10);



2. Создадим меню



Меню создадим в виде html-панелей и html-блоков. Оформление у всего меню будет примерно одинаковым: фон и стиль кнопок можно задать общими для всех. Итак, зададим три панели меню: главное меню, инструкция и вывод результатов по завершению игры. Переходы между меню, переход в мир и обратно будет выполняться скриптами javascript. Чтобы не нагромождать файл script.js, для переходов меню создадим новый файл menu.js, а в index.html подключим его:

<script src="menu.js"></script>


В контейнере создадим 3 элемента, которые будут панелями меню:

<div id="container">    <div id = "world"></div>    <div id = "pawn"></div>    <div id = "menu1"></div>    <div id = "menu2"></div>    <div id = "menu3"></div></div>


Оформим их, добавив в style.css свойства для класса menu:

.menu{display:none;position:absolute;width:inherit;height:inherit;background-color:#C0FFFF;}


В меню (в файле index.html) добавим кнопки с соответствующими надписями:

                 <div class = "menu" id = "menu1"><div id="button1" class="button"><p>Начать игру</p></div><div id="button2" class="button"><p>Инструкция</p></div></div><div class = "menu" id = "menu2"><p style="font-size:30px; top:200px"><strong>Управление:</strong> <br>w - вперед <br>s - назад <br>d - вправо <br>a - влево <br>пробел - прыжок <br>!!! Включите английскую раскладку !!!<br><strong>Задача:</strong> <br>Взять красный квадрат и найти голубой квадрат</p><div id="button3" class="button"><p>Назад</p></div></div><div class = "menu" id = "menu3"><p id = "result" style="top:100px"></p><div id="button4" class="button"><p>Вернуться назад</p></div></div>


Для кнопок тоже зададим стили в style.css:

.button{margin:0px;position:absolute;width:900px;height:250px;background-color:#FFF;cursor:pointer;}.button:hover{background-color:#DDD;}#button1{top:100px;left:150px;}#button2{top:450px;left:150px;}#button3{top:450px;left:150px;}#button4{top:450px;left:150px;}


Но мы не видим меню, так как у них задан стиль display:none, При запуске же игры один из пунктов меню должен быть виден, поэтому в html для 1-го меню добавим запись style = display:block;, а выглядеть это будет следующим образом:

<div class = "menu" id = "menu1" style = "display:block;">


Меню стало выглядеть вот так:



Отлично. Но если мы нажмем на кнопку, то курсор у нас захватится. Значит нам нужно разрешить захват мыши только в случае игры. Для этого введем в script.js переменную canlock и добавим ее в пункт создадим переменные:

// Создадим переменныеvar lock = false;var onGround = false;var container = document.getElementById("container");var world = document.getElementById("world");var g = 0.1;var dx = dy = dz = 0;var canlock = false;А в обработчик захвата мыши изменим условие:// Обработчик захвата курсора мышиcontainer.onclick = function(){if (!lock && canlock) container.requestPointerLock();};


Теперь мы можем щелкать меню. Настроим переходы с помощью скриптов в файле menu.js:

// Создаем переменныеvar menu1 = document.getElementById("menu1");var menu2 = document.getElementById("menu2");var menu3 = document.getElementById("menu3");var button1 = document.getElementById("button1");var button2 = document.getElementById("button2");var button3 = document.getElementById("button3");var button4 = document.getElementById("button4");// Настроим переходыbutton2.onclick = function(){menu1.style.display = "none";menu2.style.display = "block";}button3.onclick = function(){menu1.style.display = "block";menu2.style.display = "none";}button4.onclick = function(){menu1.style.display = "block";menu3.style.display = "none";}


Теперь все кнопки меню, за исключением начать игру, работают. Настроим теперь кнопку button1. Если вы помните, в файле script.js функции CreateNewWorld() и setInterval() запускаются при загрузке веб-страницы. Удалим их оттуда. Вызывать их будем только при нажатии кнопки button1. Сделаем это:

button1.onclick = function(){menu1.style.display = "none";CreateNewWorld();TimerGame = setInterval(update,10);}


Меню мы создали. Да, оно еще некрасивое, но это легко поправляется.

3. Создадим предметы и переход уровней.



Для начала определимся с правилами игры. У нас есть три типа предметов: монеты (желтые квадраты), ключи (красные квадраты) и финиш (голубой квадрат). Монеты приносят очки. Игроку необходимо найти ключ, и только потом прийти к финишу. Если он придет к финишу без ключа, то получит сообщение о необходимости сначала найти ключ. Предметы у нас будут создаваться также, как и карта. Записывать их мы будем с помощью массивов. Но делать для них отдельную функцию мы не будем. Мы просто напишем новую функцию, которая расставляет и элементы карты, и прямоугольника и перенесем команды из CreateNewWorld(). Назовем ее CreateSquares(). Итак, добавим в конец файла script.js следующую запись:

function CreateSquares(squares,string){for (let i = 0; i < squares.length; i++){// Создание прямоугольника и придание ему стилейlet newElement = document.createElement("div");newElement.className = string + " square";newElement.id = string + i;newElement.style.width = squares[i][6] + "px";newElement.style.height = squares[i][7] + "px";newElement.style.background = squares[i][8];newElement.style.transform = "translate3d(" +(600 - squares[i][6]/2 + squares[i][0]) + "px," +(400 - squares[i][7]/2 + squares[i][1]) + "px," +(squares[i][2]) + "px)" +"rotateX(" + squares[i][3] + "deg)" +"rotateY(" + squares[i][4] + "deg)" +"rotateZ(" + squares[i][5] + "deg)";// Вставка прямоугольника в worldworld.append(newElement);}}


А содержимое createNewWorld() изменим:

function CreateNewWorld(){CreateSquares(map,map);}


Строка нужна для того, чтобы задавать имя id. Игра пока ничуть не изменилась. Теперь добавим 3 массива: монеты (things), ключи (keys) и финиш (finish). Вставим их сразу после массива карты:

var things = [[900,50,-900,0,0,0,50,50,"#FFFF00"],    [-400,50,900,0,0,0,50,50,"#FFFF00"],    [-400,50,-300,0,0,0,50,50,"#FFFF00"]];  var keys = [[-100,50,600,0,0,0,50,50,"#FF0000"]];var start = [[-900,0,-900,0,0]];var finish = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];


А в menu.js применим функцию CreateSquares() внутри обработчика нажатия кнопки button1:

button1.onclick = function(){menu1.style.display = "none";CreateNewWorld();CreateSquares(things,thing);CreateSquares(keys,key);CreateSquares(finish,finish);TimerGame = setInterval(update,10);canlock = true;}


Теперь настроим исчезновение предметов. В menu.js создадим функцию проверки расстояний от игрока до предметов:

function interact(objects,string){for (i = 0; i < objects.length; i++){let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2;if(r < (objects[i][7]**2)/4){document.getElementById(string + i).style.display = "none";                        document.getElementById(string + i).style.transform = "translate3d(1000000px,1000000px,1000000px)";};};}


Также в этом же файле создадим функцию repeatFunction() и добавим в нее команды:

function repeatFunction(){update();interact(things,"thing");interact(keys,"key");}


А ее циклический вызов запустим в setInterval внутри button1:

TimerGame = setInterval(repeatFunction,10);


Теперь предметы исчезают, когда мы к ним подходим. Однако они ровно ничего не делают. А мы хотим, чтобы при взятии желтых квадратов нам добавлялись очки, при взятии красных появлялась возможность взять синий и закончить игру. Модифицируем функцию interact():

function interact(objects,string,num){for (i = 0; i < objects.length; i++){let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2;if(r < (objects[i][7]**2)){document.getElementById(string + i).style.display = "none";objects[i][0] = 1000000;objects[i][1] = 1000000;objects[i][2] = 1000000;document.getElementById(string + i).style.transform = "translate3d(1000000px,1000000px,1000000px)";num[0]++;};};}


Изменим входные параметры для вызовов этой функции:

function repeatFunction(){update();interact(things,"thing",m);interact(keys,"key",k);}


А в начале файла добавим четыре новые переменные:

var m = [0];var k = [0];var f = [0];var score = 0;


Вы спросите, почему мы создали массивы из одного элемента а не просто переменные? Дело в том, что мы хотели передать эти переменные в interact() по ссылке, а не по значению. В javascript обычные переменные передаются только по значению, а массивы по ссылке. Если мы передадим в interact() просто переменную, то num будет копией переменной. Изменение num не приведет к изменению k или m. А если мы передаем массив, то num будет ссылкой на массив k или m, и когда мы будем менять num[0], то будет меняться k[0] и m[0]. Можно было, конечно, создать 2 почти одинаковые функции, но лучше обойтись одной, чуть более универсальной.

Для финиша все-таки придется создать отдельную функцию:

function finishInteract(){let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2;if(r < (finish[0][7]**2)){if (k[0] == 0){console.log("найдите ключ");}else{clearWorld();                        clearInterval(TimerGame);document.exitPointerLock();                        score = score + m[0];k[0] = 0;                        m[0] = 0;menu1.style.display = "block";};};};


А clearWorld() настроим в script.js:

function clearWorld(){world.innerHTML = "";}


Как видите, очистка мира проводится довольно просто. В repeatFunction() добавим finishInteract():

function repeatFunction(){update();interact(things,"thing",m);interact(keys,"key",k);finishInteract();}


Что происходит в finishInteract()? Если мы не взяли ключ (k[0] == 0), то пока ничего не происходит. Если взяли, то игра заканчивается, а происходит следующее: очищается мир, останавливается функция repeatFunction(), курсор перестает быть захваченным, счетчик ключей обнуляется, а мы переходим в главное меню. Проверим, запустив игру. Все работает. Однако после нажатия снова на игру, мы оказываемся сразу на финише, а некоторые предметы исчезают. Все потому что мы не ввели место первоначального спауна игрока, а массивы изменяются в течение игры. Давайте добавим в button1 точку спауна для игрока, а именно, приравняем его координаты к элементам массива start[0]:

button1.onclick = function(){menu1.style.display = "none";CreateNewWorld();pawn.x = start[0][0];pawn.y = start[0][1];pawn.z = start[0][2];pawn.rx = start[0][3];pawn.rx = start[0][4];CreateSquares(things,"thing");CreateSquares(keys,"key");CreateSquares(finish,"finish");TimerGame = setInterval(repeatFunction,10);canlock = true;}


Теперь игрок появляется в начале координат. Но вот вопрос: а если уровней в игре будет несколько? Добавим переменную уровней в menu.js:

// Создаем переменныеvar menu1 = document.getElementById("menu1");var menu2 = document.getElementById("menu2");var menu3 = document.getElementById("menu3");var button1 = document.getElementById("button1");var button2 = document.getElementById("button2");var button3 = document.getElementById("button3");var button4 = document.getElementById("button4");var m = [0];var k = [0];var f = [0];var score = 0;var level = 0;


Переделаем переменные map, things, keys, start, finish внутри script.js в массивы, слегка изменив их название:

// 1 уровеньmapArray[0] = [   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,0,-300,70,0,0,200,500,"#F000FF"],   [0,-86,-786,90,0,0,200,500,"#F000FF"],   [-500,0,-300,20,0,0,200,500,"#00FF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];thingsArray [0] = [[900,50,-900,0,0,0,50,50,"#FFFF00"],  [-400,50,900,0,0,0,50,50,"#FFFF00"],  [-400,50,-300,0,0,0,50,50,"#FFFF00"]];  keysArray [0] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];startArray[0] = [[-900,0,-900,0,0]];finishArray [0] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];


Добавим 2-й уровень:

// 2 уровеньmapArray [1] = [   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,0,-300,70,0,0,200,500,"#F000FF"],   [0,-86,-786,90,0,0,200,500,"#F000FF"],   [-500,0,-300,20,0,0,200,500,"#00FF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];thingsArray [1] = [[900,50,-900,0,0,0,50,50,"#FFFF00"],  [-400,50,900,0,0,0,50,50,"#FFFF00"],  [-400,50,-300,0,0,0,50,50,"#FFFF00"]];  keysArray [1] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];startArray[1] = [[0,0,0,0,0]];finishArray [1] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];


А сами массивы инициализируем перед уровнями:

// Инициализация массива уровнейvar mapArray = new Array();var thingsArray = new Array();var keysArray = new Array();var startArray = new Array();var finishArray = new Array();


функцию CreateNewWorld() придется изменить, добавив туда аргумент:

function CreateNewWorld(map){CreateSquares(map,"map");}


Изменим вызов CreateNewWorld() в файле menu.js:

button1.onclick = function(){menu1.style.display = "none";CreateNewWorld(map);pawn.x = start[0][0];pawn.y = start[0][1];pawn.z = start[0][2];pawn.rx = start[0][3];pawn.rx = start[0][4];CreateSquares(things,"thing");CreateSquares(keys,"key");CreateSquares(finish,"finish");TimerGame = setInterval(repeatFunction,10);canlock = true;}


Теперь при запуске консоль выдаст ошибку. Верно, ведь мы переименовали переменные map, things, keys и finish, теперь javascript не может понять, что это за переменные. Заново их инициализируем в script.js:

// Инициализация переменных уровнейvar map;var things;var keys;var start;var finish;


А в button1 (в menu.js) этим переменным присвоим копии элементов массивов mapArray, thingsArray, keysArray и finishArray (для лучшей читабельности поставим комментарии):

button1.onclick = function(){// Присвоение копий массивовmap = userSlice(mapArray[level]);things = userSlice(thingsArray[level]);keys = userSlice(keysArray[level]);        start = userSlice(startArray[level]);finish = userSlice(finishArray[level]);// Создание мира и расстановка предметовmenu1.style.display = "none";CreateNewWorld(map);pawn.x = start[0][0];pawn.y = start[0][1];pawn.z = start[0][2];pawn.rx = start[0][3];pawn.rx = start[0][4];CreateSquares(things,"thing");CreateSquares(keys,"key");CreateSquares(finish,"finish");// Запуск игрыTimerGame = setInterval(repeatFunction,10);canlock = true;}


Где userSlice() функция, которая копирует массив:

function userSlice(array){let NewArray = new Array();for (let i = 0; i < array.length; i++){NewArray[i] = new Array();for (let j = 0; j < array[i].length; j++){NewArray[i][j] = array[i][j];}}return NewArray;}


Если бы мы просто написали, к примеру, keys = keysArray[level], то в переменные были бы переданы не копии массивов, а указатели на них, а значит, они изменялись бы в процессе игры, что недопустимо, ибо при повторном запуске ключа на исходном месте уже не было бы. Вероятно, вы спросите, почему я не применил просто keysArray[level].slice(), а изобрел свои функции? Ведь slice() тоже копирует массивы. Я пробовал так сделать, однако он копировал именно ссылку на массив, а не сам массив, в результате чего изменение keys приводило к изменению keysArray[level], что означало пропадание ключа при повторном запуске. Дело в том, что в документации написано, что в одних случаях он воспринимает массивы как массивы и копирует их, в других же он воспринимает массивы как объекты и копирует лишь указатели на них. Как он это определяет, для меня загадка, поэтому если мне кто-нибудь подскажет, почему slice() не работает как планировалось, то я буду ему сильно благодарен.

Сделаем переход уровней. Это довольно просто. Изменим finishInteract(), добавив внутрь else следующие строки:

level++;if(level >= 2){level = 0;score = 0;};


То есть, значение уровня прибавляется на 1, а если все уровни пройдены (у нас их 2), то уровни сбрасываются и очки score сбрасываются. Проверить это трудно, так как наши уровни сейчас ничем не отличаются. Изменим тогда mapArray[1]:

mapArray[1] = [   [0,0,1000,0,180,0,2000,200,"#00FF00"],   [0,0,-1000,0,0,0,2000,200,"#00FF00"],   [1000,0,0,0,-90,0,2000,200,"#00FF00"],   [-1000,0,0,0,90,0,2000,200,"#00FF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];


Мы поменяли цвет стен. Поиграем в игру. Видим, что после прохождения первого уровня (с фиолетовыми стенками и несколькими прямоугольниками) мы переходим ко второму (с зелеными стенками), а когда проходим второй, то возвращаемся обратно к первому. Итак, переход уровней мы закончили. Осталось только оформить игру, изменив шрифты, подкрасив мир, да и уровни сделать просто чуть посложнее. файлы index.html и style.css мы не изменяли, поэтому проверьте скрипты:

script.js
// Мировые константыvar pi = 3.141592;var deg = pi/180;// Конструктор playerfunction player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;this.vx = 3;this.vy = 5;this.vz = 3;}// Инициализация массива уровнейvar mapArray = new Array();var thingsArray = new Array();var keysArray = new Array();var startArray = new Array();var finishArray = new Array();// 1 уровеньmapArray[0] = [   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,0,-300,70,0,0,200,500,"#F000FF"],   [0,-86,-786,90,0,0,200,500,"#F000FF"],   [-500,0,-300,20,0,0,200,500,"#00FF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];thingsArray[0] = [[900,50,-900,0,0,0,50,50,"#FFFF00"],  [-400,50,900,0,0,0,50,50,"#FFFF00"],  [-400,50,-300,0,0,0,50,50,"#FFFF00"]];  keysArray[0] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];startArray[0] = [[-900,0,-900,0,0]];finishArray[0] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];// 2 уровеньmapArray[1] = [   [0,0,1000,0,180,0,2000,200,"#00FF00"],   [0,0,-1000,0,0,0,2000,200,"#00FF00"],   [1000,0,0,0,-90,0,2000,200,"#00FF00"],   [-1000,0,0,0,90,0,2000,200,"#00FF00"],   [0,100,0,90,0,0,2000,2000,"#666666"]];thingsArray[1] = [[900,50,-900,0,0,0,50,50,"#FFFF00"],  [-400,50,900,0,0,0,50,50,"#FFFF00"],  [-400,50,-300,0,0,0,50,50,"#FFFF00"]];  keysArray[1] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];startArray[1] = [[0,0,0,0,0]];finishArray[1] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];// Инициализация переменных уровнейvar map = new Array();var things = new Array();var keys = new Array();var start = new Array();var finish = new Array();// Нажата ли клавиша и двигается ли мышь?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;var MouseX = 0;var MouseY = 0;// Создадим переменныеvar lock = false;var onGround = false;var container = document.getElementById("container");var world = document.getElementById("world");var g = 0.1;var dx = dy = dz = 0;var canlock = false; // Обработчик проверки изменения состояния захвата курсораdocument.addEventListener("pointerlockchange", (event)=>{lock = !lock;});// Обработчик захвата курсора мышиcontainer.onclick = function(){if (!lock && canlock) container.requestPointerLock();};// Обработчик нажатия клавишdocument.addEventListener("keydown", (event) =>{if (event.key == "a"){PressLeft = 1;}if (event.key == "w"){PressForward = 1;}if (event.key == "d"){PressRight = 1;}if (event.key == "s"){PressBack = 1;}if (event.keyCode == 32){PressUp = 1;}});// Обработчик отжатия клавишdocument.addEventListener("keyup", (event) =>{if (event.key == "a"){PressLeft = 0;}if (event.key == "w"){PressForward = 0;}if (event.key == "d"){PressRight = 0;}if (event.key == "s"){PressBack = 0;}if (event.keyCode == 32){PressUp = 0;}});// Обработчик движения мышиdocument.addEventListener("mousemove", (event)=>{MouseX = event.movementX;MouseY = event.movementY;});// Создаем новый объектvar pawn = new player(0,0,0,0,0);function update(){// Задаем локальные переменные смещенияdx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx;dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz;dy = dy + g;if (onGround){dy = 0;if (PressUp){dy = - PressUp*pawn.vy;onGround = false;}};drx = MouseY;dry = - MouseX;// Обнулим смещения мыши:MouseX = MouseY = 0;// Проверяем коллизию с прямоугольникамиcollision();// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;// Если курсор захвачен, разрешаем вращениеif (lock){pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;};// Изменяем координаты мира (для отображения)world.style.transform = "translateZ(" + (600 - 0) + "px)" +"rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";};function CreateNewWorld(map){CreateSquares(map,"map");}function clearWorld(){world.innerHTML = "";}function collision(){onGround = false;for(let i = 0; i < map.length; i++){// рассчитываем координаты игрока в системе координат прямоугольникаlet x0 = (pawn.x - map[i][0]);let y0 = (pawn.y - map[i][1]);let z0 = (pawn.z - map[i][2]);if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][6]**2 + map[i][7]**2)){let x1 = x0 + dx;let y1 = y0 + dy;let z1 = z0 + dz;let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);let normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]);// Условие коллизии и действия при немif (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){point1[2] = Math.sign(point0[2])*50;let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]);dx = point2[0] - x0;dy = point2[1] - y0;dz = point2[2] - z0;if (Math.abs(normal[1]) > 0.8){if (point3[1] > point2[1]) onGround = true;}else dy = y1 - y0;}}};}function coorTransform(x0,y0,z0,rxc,ryc,rzc){let x1 =  x0;let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);let y2 =  y1;let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg); let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);let z3 =  z2;return [x3,y3,z3];}function coorReTransform(x3,y3,z3,rxc,ryc,rzc){let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);let z2 =  z3let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);let y1 =  y2;let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);let x0 =  x1;let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);return [x0,y0,z0];};function CreateSquares(squares,string){for (let i = 0; i < squares.length; i++){// Создание прямоугольника и придание ему стилейlet newElement = document.createElement("div");newElement.className = string + " square";newElement.id = string + i;newElement.style.width = squares[i][6] + "px";newElement.style.height = squares[i][7] + "px";newElement.style.background = squares[i][8];newElement.style.transform = "translate3d(" +(600 - squares[i][6]/2 + squares[i][0]) + "px," +(400 - squares[i][7]/2 + squares[i][1]) + "px," +(squares[i][2]) + "px)" +"rotateX(" + squares[i][3] + "deg)" +"rotateY(" + squares[i][4] + "deg)" +"rotateZ(" + squares[i][5] + "deg)";// Вставка прямоугольника в worldworld.append(newElement);}}


menu.js
// Создаем переменныеvar menu1 = document.getElementById("menu1");var menu2 = document.getElementById("menu2");var menu3 = document.getElementById("menu3");var button1 = document.getElementById("button1");var button2 = document.getElementById("button2");var button3 = document.getElementById("button3");var button4 = document.getElementById("button4");var m = [0];var k = [0];var f = [0];var level = 0;// Настроим переходыbutton1.onclick = function(){// Присвоение копий массивовmap = userSlice(mapArray[level]);things = userSlice(thingsArray[level]);keys = userSlice(keysArray[level]);start = userSlice(startArray[level]);finish = userSlice(finishArray[level]);// Создание мира и расстановка предметовmenu1.style.display = "none";CreateNewWorld(map);pawn.x = start[0][0];pawn.y = start[0][1];pawn.z = start[0][2];pawn.rx = start[0][3];pawn.rx = start[0][4];CreateSquares(things,"thing");CreateSquares(keys,"key");CreateSquares(finish,"finish");// Запуск игрыTimerGame = setInterval(repeatFunction,10);canlock = true;}button2.onclick = function(){menu1.style.display = "none";menu2.style.display = "block";}button3.onclick = function(){menu1.style.display = "block";menu2.style.display = "none";}button4.onclick = function(){menu1.style.display = "block";menu3.style.display = "none";}// Функция проверки взаимодействияfunction interact(objects,string,num){for (i = 0; i < objects.length; i++){let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2;if(r < (objects[i][7]**2)){document.getElementById(string + i).style.display = "none";objects[i][0] = 1000000;objects[i][1] = 1000000;objects[i][2] = 1000000;document.getElementById(string + i).style.transform = "translate3d(1000000px,1000000px,1000000px)";num[0]++;};};}// Функция проверки взаимодействия с финишомfunction finishInteract(){let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2;if(r < (finish[0][7]**2)){if (k[0] == 0){console.log("найдите ключ");}else{clearWorld();clearInterval(TimerGame);document.exitPointerLock();score = score + m[0];k[0] = 0;m[0] = 0;menu1.style.display = "block";level++;if(level >= 2){level = 0;score = 0;};};};};// Функция, повторяющаяся в игреfunction repeatFunction(){update();interact(things,"thing",m);interact(keys,"key",k);finishInteract();} // Пользовательский slicefunction userSlice(array){let NewArray = new Array();for (let i = 0; i < array.length; i++){NewArray[i] = new Array();for (let j = 0; j < array[i].length; j++){NewArray[i][j] = array[i][j];}}return NewArray;}



4. Оформим игру.



4.1 Изменим уровни



Создание уровней очень интересное занятие. Как правило, этим занимаются отдельные люди, которых называют дизайнерами уровней. У нас уровень представляет из себя массивы чисел, которые скриптами из script.js преобразуются в трехмерный мир. Можно написать отдельную программу, упрощающую создание миров, но сейчас мы это делать не будем. Откроем файл script.js и загрузим туда массивы готовых лабиринтов:

Массивы уровней
// 1 уровеньmapArray[0] = [   //основание   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],   [0,100,0,90,0,0,2000,2000,"#EEEEEE"],      //1   [-700,0,-800,0,180,0,600,200,"#F0C0FF"],   [-700,0,-700,0,0,0,600,200,"#F0C0FF"],   [-400,0,-750,0,90,0,100,200,"#F0C0FF"],      //2   [100,0,-800,0,180,0,600,200,"#F0C0FF"],   [50,0,-700,0,0,0,500,200,"#F0C0FF"],   [400,0,-550,0,90,0,500,200,"#F0C0FF"],   [-200,0,-750,0,-90,0,100,200,"#F0C0FF"],   [300,0,-500,0,-90,0,400,200,"#F0C0FF"],   [350,0,-300,0,0,0,100,200,"#F0C0FF"],      //3   [700,0,-800,0,180,0,200,200,"#F0C0FF"],   [700,0,500,0,0,0,200,200,"#F0C0FF"],   [700,0,-150,0,90,0,1100,200,"#F0C0FF"],   [600,0,-150,0,-90,0,1300,200,"#F0C0FF"],   [800,0,-750,0,90,0,100,200,"#F0C0FF"],   [800,0,450,0,90,0,100,200,"#F0C0FF"],   [750,0,400,0,180,0,100,200,"#F0C0FF"],   [750,0,-700,0,0,0,100,200,"#F0C0FF"],      //4   [850,0,-100,0,180,0,300,200,"#F0C0FF"],   [850,0,0,0,0,0,300,200,"#F0C0FF"],      //5   [400,0,300,0,90,0,800,200,"#F0C0FF"],   [300,0,300,0,-90,0,800,200,"#F0C0FF"],   [350,0,-100,0,180,0,100,200,"#F0C0FF"],      //6   [400,0,800,0,0,0,800,200,"#F0C0FF"],   [450,0,700,0,180,0,700,200,"#F0C0FF"],   [800,0,750,0,90,0,100,200,"#F0C0FF"],   [100,0,550,0,90,0,300,200,"#F0C0FF"],   [0,0,650,0,-90,0,300,200,"#F0C0FF"],   [-100,0,500,0,0,0,200,200,"#F0C0FF"],   [-100,0,400,0,180,0,400,200,"#F0C0FF"],   [-200,0,750,0,90,0,500,200,"#F0C0FF"],   [-300,0,700,0,-90,0,600,200,"#F0C0FF"],      //7   [100,0,-250,0,90,0,900,200,"#F0C0FF"],   [0,0,-300,0,-90,0,800,200,"#F0C0FF"],   [-350,0,200,0,0,0,900,200,"#F0C0FF"],   [-350,0,100,0,180,0,700,200,"#F0C0FF"],   [-700,0,-50,0,90,0,300,200,"#F0C0FF"],   [-800,0,0,0,-90,0,400,200,"#F0C0FF"],   [-750,0,-200,0,180,0,100,200,"#F0C0FF"],      //8   [-500,0,600,0,90,0,800,200,"#F0C0FF"],   [-600,0,600,0,-90,0,800,200,"#F0C0FF"],      //9   [-600,0,-500,0,180,0,800,200,"#F0C0FF"],   [-650,0,-400,0,0,0,700,200,"#F0C0FF"],   [-200,0,-300,0,90,0,400,200,"#F0C0FF"],   [-300,0,-300,0,-90,0,200,200,"#F0C0FF"],   [-350,0,-100,0,0,0,300,200,"#F0C0FF"],   [-400,0,-200,0,180,0,200,200,"#F0C0FF"],   [-500,0,-150,0,-90,0,100,200,"#F0C0FF"],      //10   [-900,0,500,0,0,0,200,200,"#F0C0FF"],   [-900,0,400,0,180,0,200,200,"#F0C0FF"],   [-800,0,450,0,90,0,100,200,"#F0C0FF"]   ];thingsArray[0] = [[900,50,-900,0,0,0,50,50,"#FFFF00"],  [-400,50,900,0,0,0,50,50,"#FFFF00"],  [-400,50,-300,0,0,0,50,50,"#FFFF00"]];  keysArray[0] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];startArray[0] = [[-900,0,-900,0,0]];finishArray[0] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];// 2 уровеньmapArray[1] = [   //основание   [0,0,1200,0,180,0,2400,200,"#C0FFE0"],   [0,0,-1200,0,0,0,2400,200,"#C0FFE0"],   [1200,0,0,0,-90,0,2400,200,"#C0FFE0"],   [-1200,0,0,0,90,0,2400,200,"#C0FFE0"],   [0,100,0,90,0,0,2400,2400,"#EEEEEE"],      //1   [1100,0,-800,0,180,0,200,200,"#C0FFE0"],   [1000,0,-900,0,90,0,200,200,"#C0FFE0"],   [850,0,-1000,0,180,0,300,200,"#C0FFE0"],   [700,0,-950,0,-90,0,100,200,"#C0FFE0"],   [800,0,-900,0,0,0,200,200,"#C0FFE0"],   [900,0,-700,0,-90,0,400,200,"#C0FFE0"],   [750,0,-500,0,180,0,300,200,"#C0FFE0"],   [600,0,-450,0,-90,0,100,200,"#C0FFE0"],   [800,0,-400,0,0,0,400,200,"#C0FFE0"],   [1000,0,-550,0,90,0,300,200,"#C0FFE0"],   [1100,0,-700,0,0,0,200,200,"#C0FFE0"],      //2   [800,0,-200,0,180,0,800,200,"#C0FFE0"],   [400,0,-300,0,90,0,200,200,"#C0FFE0"],   [300,0,-400,0,180,0,200,200,"#C0FFE0"],   [200,0,-700,0,90,0,600,200,"#C0FFE0"],   [50,0,-1000,0,180,0,300,200,"#C0FFE0"],   [-100,0,-950,0,-90,0,100,200,"#C0FFE0"],   [0,0,-900,0,0,0,200,200,"#C0FFE0"],   [100,0,-600,0,-90,0,600,200,"#C0FFE0"],   [200,0,-300,0,0,0,200,200,"#C0FFE0"],   [300,0,-200,0,-90,0,200,200,"#C0FFE0"],   [750,0,-100,0,0,0,900,200,"#C0FFE0"],      //3   [500,0,-950,0,90,0,500,200,"#C0FFE0"],   [450,0,-700,0,0,0,100,200,"#C0FFE0"],   [400,0,-950,0,-90,0,500,200,"#C0FFE0"],      //4   [-700,0,-600,0,0,0,1000,200,"#C0FFE0"],   [-200,0,-500,0,-90,0,200,200,"#C0FFE0"],   [-300,0,-400,0,180,0,200,200,"#C0FFE0"],   [-400,0,-250,0,-90,0,300,200,"#C0FFE0"],   [-350,0,-100,0,0,0,100,200,"#C0FFE0"],   [-300,0,-200,0,90,0,200,200,"#C0FFE0"],   [-200,0,-300,0,0,0,200,200,"#C0FFE0"],   [-100,0,-500,0,90,0,400,200,"#C0FFE0"],   [-650,0,-700,0,180,0,1100,200,"#C0FFE0"],      //5   [-300,0,-850,0,90,0,300,200,"#C0FFE0"],   [-350,0,-1000,0,180,0,100,200,"#C0FFE0"],   [-400,0,-850,0,-90,0,300,200,"#C0FFE0"],      //6   [-600,0,-1050,0,90,0,300,200,"#C0FFE0"],   [-650,0,-900,0,0,0,100,200,"#C0FFE0"],   [-700,0,-1050,0,-90,0,300,200,"#C0FFE0"],      //7   [-900,0,-850,0,90,0,300,200,"#C0FFE0"],   [-950,0,-1000,0,180,0,100,200,"#C0FFE0"],   [-1000,0,-850,0,-90,0,300,200,"#C0FFE0"],      //8   [-600,0,-250,0,90,0,700,200,"#C0FFE0"],   [-650,0,100,0,0,0,100,200,"#C0FFE0"],   [-700,0,-250,0,-90,0,700,200,"#C0FFE0"],      //9   [-900,0,-150,0,90,0,900,200,"#C0FFE0"],   [-500,0,300,0,180,0,800,200,"#C0FFE0"],   [-100,0,650,0,90,0,700,200,"#C0FFE0"],   [-300,0,1000,0,0,0,400,200,"#C0FFE0"],   [-500,0,950,0,-90,0,100,200,"#C0FFE0"],   [-350,0,900,0,180,0,300,200,"#C0FFE0"],   [-200,0,650,0,-90,0,500,200,"#C0FFE0"],   [-600,0,400,0,0,0,800,200,"#C0FFE0"],   [-1000,0,-100,0,-90,0,1000,200,"#C0FFE0"],      //10   [-300,0,200,0,90,0,200,200,"#C0FFE0"],   [-350,0,100,0,180,0,100,200,"#C0FFE0"],   [-400,0,200,0,-90,0,200,200,"#C0FFE0"],      //11   [-800,0,600,0,180,0,800,200,"#C0FFE0"],   [-400,0,650,0,90,0,100,200,"#C0FFE0"],   [-800,0,700,0,0,0,800,200,"#C0FFE0"],      //12   [-700,0,1050,0,90,0,300,200,"#C0FFE0"],   [-850,0,900,0,180,0,300,200,"#C0FFE0"],   [-1000,0,950,0,-90,0,100,200,"#C0FFE0"],   [-900,0,1000,0,0,0,200,200,"#C0FFE0"],   [-800,0,1100,0,-90,0,200,200,"#C0FFE0"],      //13   [1050,0,700,0,180,0,300,200,"#C0FFE0"],   [900,0,800,0,-90,0,200,200,"#C0FFE0"],   [550,0,900,0,180,0,700,200,"#C0FFE0"],   [200,0,650,0,90,0,500,200,"#C0FFE0"],   [300,0,400,0,0,0,200,200,"#C0FFE0"],   [400,0,300,0,90,0,200,200,"#C0FFE0"],   [550,0,200,0,0,0,300,200,"#C0FFE0"],   [700,0,150,0,90,0,100,200,"#C0FFE0"],   [500,0,100,0,180,0,400,200,"#C0FFE0"],   [300,0,200,0,-90,0,200,200,"#C0FFE0"],   [200,0,300,0,180,0,200,200,"#C0FFE0"],   [100,0,650,0,-90,0,700,200,"#C0FFE0"],   [550,0,1000,0,0,0,900,200,"#C0FFE0"],   [1000,0,900,0,90,0,200,200,"#C0FFE0"],   [1100,0,800,0,0,0,200,200,"#C0FFE0"],      //14   [700,0,700,0,90,0,400,200,"#C0FFE0"],   [850,0,500,0,0,0,300,200,"#C0FFE0"],   [1000,0,300,0,90,0,400,200,"#C0FFE0"],   [950,0,100,0,180,0,100,200,"#C0FFE0"],   [900,0,250,0,-90,0,300,200,"#C0FFE0"],   [750,0,400,0,180,0,300,200,"#C0FFE0"],   [600,0,650,0,-90,0,500,200,"#C0FFE0"],      //15   [500,0,600,0,180,0,200,200,"#C0FFE0"],   [400,0,650,0,-90,0,100,200,"#C0FFE0"],   [500,0,700,0,0,0,200,200,"#C0FFE0"]   ];thingsArray[1] = [[1100,50,900,0,0,0,50,50,"#FFFF00"],  [500,50,800,0,0,0,50,50,"#FFFF00"],  [-800,50,-500,0,0,0,50,50,"#FFFF00"],  [-900,50,1100,0,0,0,50,50,"#FFFF00"],  [-1100,50,-800,0,0,0,50,50,"#FFFF00"]  ];  keysArray[1] = [[1100,50,-900,0,0,0,50,50,"#FF0000"]];startArray[1] = [[0,0,0,0,0]];finishArray[1] = [[-1100,50,-500,0,0,0,50,50,"#00FFFF"]];



Теперь мы можем поиграть в игру. В результате уровни выглядят вот так:



Ориентироваться в таком мире крайне сложно. Плюс передвижение вдоль стенок содержит баги, так как на углах стенок игрок может застрять. Исправим это в collision(), заменив числа 98 на 90:

// Условие коллизии и действия при немif (Math.abs(point1[0])<(map[i][6]+90)/2 && Math.abs(point1[1])<(map[i][7]+90)/2 && Math.abs(point1[2]) < 50){


4.2 Добавим статическое освещение



Чтобы ориентироваться стало проще, реализуем статическое солнечное освещение (без теней). Добавим вектор солнечного света:

var sun = [0.48,0.8,0.36];


Как создать освещенность? Посмотрите на рисунок:



Если вектор sun точно противонаправлен вектору n, то освещение максимально. Интенсивность освещенности зависит от угла падения света на поверхность. Если же луч света падает параллельно плоскости или падает с противоположной его стороны, то плоскость не освещается. Посчитать угол падения можно с помощью скалярного произведения n*sun: если оно отрицательно, то освещенность зависит от модуля скалярного произведения, а если положительно, то освещенность отсутствует. Освещенность поверхностей создадим при генерации мира, то есть, в CreateNewWorld(). А так как там есть только функция CreateSquare(), то и освещенность будем применять там. Но овещенность мы применим, пожалуй, только к миру, но не к вещам, так что добавим туда аргумент освещенности, да и сам CreateSquare() изменим:

function CreateSquares(squares,string,havelight){for (let i = 0; i < squares.length; i++){// Создание прямоугольника и придание ему стилейlet newElement = document.createElement("div");newElement.className = string + " square";newElement.id = string + i;newElement.style.width = squares[i][6] + "px";newElement.style.height = squares[i][7] + "px";if (havelight){let normal = coorReTransform(0,0,1,squares[i][3],squares[i][4],squares[i][5]);let light = -(normal[0]*sun[0] + normal[1]*sun[1] + normal[2]*sun[2]);if (light < 0){light = 0;};newElement.style.background = "linear-gradient(rgba(0,0,0," + (0.2 - light*0.2) + "),rgba(0,0,0," + (0.2 - light*0.2) + ")), " +  squares[i][8];}else{newElement.style.background = squares[i][8];}newElement.style.transform = "translate3d(" +(600 - squares[i][6]/2 + squares[i][0]) + "px," +(400 - squares[i][7]/2 + squares[i][1]) + "px," +(squares[i][2]) + "px)" +"rotateX(" + squares[i][3] + "deg)" +"rotateY(" + squares[i][4] + "deg)" +"rotateZ(" + squares[i][5] + "deg)";// Вставка прямоугольника в worldworld.append(newElement);}}


Включим освещенность при генерации мира в CreateNewWorld():

function CreateNewWorld(map){CreateSquares(map,"map",true);}


И добавим отключение освещенности для предметов в button1.onclick (в CreateSquares последний параметр для них false):

// Создание мира и расстановка предметовmenu1.style.display = "none";CreateNewWorld(map);pawn.x = start[0][0];pawn.y = start[0][1];pawn.z = start[0][2];pawn.rx = start[0][3];pawn.rx = start[0][4];CreateSquares(things,"thing",false);CreateSquares(keys,"key",false);CreateSquares(finish,"finish",false);


Запустим игру и заметим, что освещение стало более реалистичным, а ориентироваться в пространстве намного проще:



Добавим голубое небо. Зададим фон для #container в style.css:

background-color:#C0FFFF;


Небо стало голубым:



Мы оформили уровни. Но искать предметы все равно сложно, так как они статичны, а игроку интуитивно сложно понять, что их можно собирать.

4.3 Добавим вращение и свет предметам



В menu.js создадим отельную функцию вращения:

function rotate(objects,string,wy){for (i = 0; i < objects.length; i++){objects[i][4] = objects[i][4] + wy;document.getElementById(string + i).style.transform = "translate3d(" +(600 - objects[i][6]/2 + objects[i][0]) + "px," +(400 - objects[i][7]/2 + objects[i][1]) + "px," +(objects[i][2]) + "px)" +"rotateX(" + objects[i][3] + "deg)" +"rotateY(" + objects[i][4] + "deg)" +"rotateZ(" + objects[i][5] + "deg)";};}


А вызывать ее будем из repeatFunction():

function repeatFunction(){update();interact(things,"thing",m);interact(keys,"key",k);rotate(things,"thing",0.5);rotate(keys,"key",0.5);rotate(finish,"finish",0.5);        finishInteract();}


Правда функцию rotate можно использовать не только для вращения предметов, но и их передвижения. Итак, предметы вращаются. Но если мы сделаем эти предметы светящимися, то будет вообще супер. Зададим для них цветные тени в style.css:

.thing{box-shadow: 0 0 10px #FFFF00;}.key{box-shadow: 0 0 10px #FF0000;}.finish{box-shadow: 0 0 10px #00FFFF;}


Теперь игрок точно понимает, что с этими предметами можно взаимодействовать.

4.4 Добавим виджеты



Обычно виджеты показывают количество очков, здоровье и другие необходимые числовые данные. У нас они будут показывать количество собранных монет (желтых квадратов) и ключей (красных квадратов), а изменять их можно из javascript. Сначала добавим в html новые элементы:

<div id="container"><div id="world"></div><div id="pawn"></div><div class = "widget" id = "widget1"></div><div class = "widget" id = "widget2"></div>                <div class = "widget" id = "widget3"></div>


В menu.js привяжем к ним переменные:

var widget1 = document.getElementById("widget1");var widget2 = document.getElementById("widget2");var widget3 = document.getElementById("widget3");


А внутри button1.onclick() к ним добавим текст:

widget1.innerHTML = "<p style='font-size:30px'>Монеты: 0 из 0" </p>";widget2.innerHTML = "<p style='font-size:30px'>Ключи:0</p>";widget3.innerHTML = "<p style='font-size:40px'>Найдите красный квадрат!</p>";


Зададим стили для них в style.css():

/* Оформление виджетов */.widget{display:none;position:absolute;background-color:#FFF;opacity:0.8;z-index:300;}#widget1{top:0px;left:0px;width:300px;height:100px;}#widget2{top:0px;right:0px;width:300px;height:100px;}#widget3{bottom:0px;left:0px;width:500px;height:200px;}


Изначально они невидимы. Сделаем видимыми первые 2 виджета при запуске уровня внутри button1.onclick:

       // Вывод виджетов на экран и их настройкаwidget1.style.display = "block";widget2.style.display = "block";widget1.innerHTML = "<p style='font-size:30px'>Монеты: 0 из " + things.length + " </p>";widget2.innerHTML = "<p style='font-size:30px'>Ключи:0</p>";widget3.innerHTML = "<p style='font-size:40px'>Найдите красный квадрат!</p>";


Виджеты есть, но при взаимодействии с предметами еще ничего не происходит. Будем менять надписи виджетов при взаимодействии из функций interact (внутри if(r < (objects[i][7]**2)){}):

widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>";widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>";


Теперь при взятии монет и ключа информация в виджетах меняется. Но при завершении игры виджеты не скрываются. Скроим их по окончании игры, добавив в finishInteract() внутрь else следующие строки:

widget1.style.display = none;
widget2.style.display = none;
widget3.style.display = none;

Виджеты скрыты. Осталось настроить виджет, который просит взять ключ в случае прихода к финишу без него. В finishInteract() вместо console.log(найдите ключ) вставим следующие строки:

widget3.style.display = "block";setTimeout(() => widget3.style.display = "none",5000);


При неудачной попытки окончания игры мы, получаем сообщение, которое скрывается через 5 секунд. Наша игра сейчас выглядит вот так:





4.5 Оформим текст.



Создадим в папке с файлами папку Fonts. Скачаем отсюда файл font1.woff и вставим его в Fonts. В style.css добавим стили текста:

/* Оформление текста */p{margin:0px;font-size:60px;position:absolute;display:block;top:50%;left:50%;transform:translate(-50%,-50%);user-select:none;font-family:fontlab;}@font-face{font-family:fontlab;src:url("Fonts/font1.woff");}


Меню и игра преобразились:





4.6 Добавим звуки.



Скачаем отсюда архив со звуками Sounds.zip. Создадим в папке с проектом папку Sounds и вставьте туда звуки (они находятся в формате mp3). Сделаем переменные-ссылки на эти звуки:

// Загрузка звуковvar clickSound = new Audio;clickSound.src = "Sounds/click.mp3";var keySound = new Audio;keySound.src = "Sounds/key.mp3";var mistakeSound = new Audio;mistakeSound.src = "Sounds/mistake.mp3";var thingSound = new Audio;thingSound.src = "Sounds/thing.mp3";var winSound = new Audio;winSound.src = "Sounds/win.mp3";


В функции interact добавим аргумент звукового файла и проигрывание звука (soundObject.play()):

function interact(objects,string,num,soundObject){for (i = 0; i < objects.length; i++){let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2;if(r < (objects[i][7]**2)){soundObject.play();document.getElementById(string + i).style.display = "none";objects[i][0] = 1000000;objects[i][1] = 1000000;objects[i][2] = 1000000;document.getElementById(string + i).style.transform = "translate3d(1000000px,1000000px,1000000px)";num[0]++;widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>";widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>";};};}


В repeatFunction() изменим, соответственно, вызовы этой функции:

interact(things,"thing",m,thingSound);interact(keys,"key",k,keySound);


А в finishInteract() добавим звуки mistakeSound и winSound:

function finishInteract(){let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2;if(r < (finish[0][7]**2)){if (k[0] == 0){widget3.style.display = "block";setTimeout(() => widget3.style.display = "none",5000);mistakeSound.play();}else{clearWorld();clearInterval(TimerGame);document.exitPointerLock();score = score + m[0];k[0] = 0;m[0] = 0;level++;menu1.style.display = "block";widget1.style.display = "none";widget2.style.display = "none";widget3.style.display = "none";winSound.play();if(level >= 2){level = 0;score = 0;};};};};


При клике любой кнопки меню проиграем звук clickSound:

button1.onclick = function(){clickSound.play();...}button2.onclick = function(){clickSound.play();menu1.style.display = "none";menu2.style.display = "block";}button3.onclick = function(){clickSound.play();menu1.style.display = "block";menu2.style.display = "none";}button4.onclick = function(){clickSound.play();menu1.style.display = "block";menu3.style.display = "none";}


Игра заиграла ярче. Осталось настроить вывод результатов после прохождения всех уровней:

4.7 Вывод результатов.



В menu.js в finishInteract() внутрь if(level >= 2){} добавим строки:

if(level >= 2){menu1.style.display = "none";menu3.style.display = "block";document.getElementById("result").innerHTML = "Вы набрали " + score + " очков";level = 0;score = 0;};


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

canlock = false;


А также:

button1.innerHTML = "<p>Продолжить</p>";


и

button1.innerHTML = "<p>Начать игру</p>";


В результате:

function finishInteract(){
let r = (finish[0][0] pawn.x)**2 + (finish[0][1] pawn.y)**2 + (finish[0][2] pawn.z)**2;
if(r < (finish[0][7]**2)){
if (k[0] == 0){

}
else{

canlock = false;
button1.innerHTML = "

Продолжить

";
if(level >= 2){
menu1.style.display = none;
menu3.style.display = block;
document.getElementById(result).innerHTML = Вы набрали + score + " очков";
level = 0;
score = 0;
button1.innerHTML = "

Начать игру

";
};
};
};
};

Теперь кнопка запуска игры меняется в зависимости от прохождения уровней. Также передвинем container в центр окна, добавив в стили для него следующие строки:

top:50%;left:50%;transform: translate(-50%,-50%);


А в body уберем отступы:

body{margin:0px;}


Итак, мы полностью написали браузерную трехмерную игру лабиринт. Благодаря ей мы обратили внимание на некоторые аспекты языка javascript, узнали о функциях, о которых вы раньше может быть и не слышали. А главное, мы показали, что делать простые игрушки для браузера даже на чистом коде не так уж и сложно. Полный исходный код вы можете скачать отсюда (исходники.zip). Сами скрипты можно существенно улучшить, добавив туда разные библиотеки, написать новые конструкторы или сделать что-нибудь еще. Спасибо за внимание!
Источник: habr.com
К списку статей
Опубликовано: 17.07.2020 14:11:33
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Css

Html

Javascript

Разработка игр

3d-игры

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru