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

3d-игры

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

13.07.2020 18:21:24 | Автор: admin
Современная вычислительная техника позволяет создавать классные компьютерные игры! И сейчас, достаточно популярны игры с 3d-графикой, так как, играя в них, ты окунаешься в вымышленный мир и теряешь всякую связь с реальностью. Развитие интернета и браузерных технологий сделало возможным запускать головоломки и стрелялки в любимом Хроме, Мозилле или еще в чем-то там (про Эксплорер помолчим) в онлайн-режиме, без загрузки. Так вот, здесь я расскажу о том, как создать простую трехмерную браузерную игру.

Выбор жанра, сюжета и стилистики игры является достаточно интересной задачей, и от решения этих вопросов может зависеть успех игры. Кроме этого, свои нюансы вносит и выбор технологии, на основе которой будет создаваться продукт. Моя цель показать элементарные основы этого увлекательного процесса, поэтому я буду делать 3-мерный лабиринт с незамысловатым оформлением. Более того, я это сделаю на чистом коде без использования библиотек и движков, типа three.js (хотя большие проекты лучше делать все-таки на нем), чтобы показать, как можно создать движок для своих нужд. Полностью самописная игра может быть оригинальной, а потому интересной. В общем, оба подхода имеют свои плюсы и минусы.

Я полагаю, если вы читаете эту статью, то вам интересна тема создания игр для гугл Хром, а, значит, понимаете, как работает связка html-css-javaScript, поэтому не буду останавливаться на основах, а сразу приступлю к разработке. В html5 и css3, которые поддерживают все современные браузеры (Эксплорер не в счет), есть возможность расположения блоков в 3-мерном пространстве. Также есть элемент , в котором можно рисовать линии и графические примитивы. Большинство браузерных движков используют <сanvas>, так как на нем можно сделать больше вещей, да и производительность на нем выше. Но для простых вещей вполне можно использовать методы transform-3d, которые будут занимать меньше кода.

1. Инструменты для разработки


Я использую для проверки сайтов и игр только 2 браузера: Chrome и Mozilla. Все остальные браузеры (кроме того самого Эксплорера) построены на движке первого, поэтому использовать их я не вижу смысла, ибо результаты точно такие же, как и в Chrome. Для написания кода достаточно Notepad++.

2. Как реализуется трехмерное пространство в html?


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



По умолчанию, дочерний блок имеет координаты (left и top) 0 пикселей по x и 0 пикселей по y. Смещение (translate), также 0 пикселей по всем трем осям. Покажем это на примере, для чего создадим новую папку. В нем создадим файлы index.html, style.css и script.js. Откроем 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></BODY></HTML><script src="script.js"></script>

В файле style.css зададим стили для элементов container и world.

#container{position:absolute;width:1200px;height:800px;border:2px solid #000000;}#world{width:300px;height:300px;        background-color:#C0FFFF;}

Сохраним. Откроем index.html c помощью Chrome, получим:



Попробуем применить translate3d к элементу world:

#world{width:300px;height:300px;        background-color:#C0FFFF;        transform:translate3d(200px,100px,0px);}



Как вы поняли, я перешел в полноэкранный режим. Теперь зададим смещение по оси Z:
transform:translate3d(200px,100px,-1000px);

Если вы снова откроете html-файл в браузере, то никаких изменений вы не увидите. Чтобы увидеть изменения, нужно задать перспективу для объекта container:

#container{position:absolute;width:1200px;height:800px;border:2px solid #000000;perspective:600px;}

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



Квадрат отдалился от нас. Как работает перспектива в html? Взглянем на картинку:



d расстояние от пользователя до объекта, а z его координата. Отрицательный z (в html это translateZ) означает, что мы отдалили объект, а положительный наоборот. Значение perspective определяет величину d. Если же свойство perspective не задано, то значение d принимается за бесконечность, а в этом случае объект визуально не изменяется для пользователя с изменением z. В нашем случае мы задали d = 600px. По умолчанию, точка взгляда перспективы находится в центре элемента, однако ее можно изменить путем задания свойства perspective-origin: .

Теперь повернем world вокруг какой-нибудь оси. В сss можно использовать 2 способа вращения. Первый вращение вокруг осей x,y и z. Для этого используются transform-свойства rotateX(), rotateY() и rotateZ(). Второй вращение вокруг заданной оси с помощью свойства rotate3d(). Мы будем использовать первый способ, так как он больше подходит для наших задач. Обратите внимание, что оси вращения выходят из центра прямоугольника!



Точка, относительно которой происходят трансформации, может быть изменена путем задания свойства translate-origin: . Итак, зададим вращение world по оси x:

#world{width:300px;height:300px;background-color:#C0FFFF;transform:translate3d(200px,100px,0px) rotateX(45deg);}

Получим:



Заметно смещение против часовой стрелки. Если же мы добавим rotateY(), то получим смещение уже по оси Y. Важно заметить, что при вращении блока оси вращения также поворачиваются. Вы также можете поэкспериментировать с различными значениями вращения.
Теперь внутри блока world создадим еще один блок, для этого добавим тег в 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 id="square1"></div></div></div></BODY></HTML><script src="script.js"></script>

В style.css добавим стили к этому блоку:

#square1{position:absolute;width:200px;height:200px;background-color:#FF0000;}

Получим:



То есть, элементы внутри блока world будут трансформироваться в составе этого блока. Попробуем повернуть square1 по оси y, добавив к нему стиль вращения:
transform: rotateY(30deg);

В итоге:



Где вращение? спросите вы? На самом деле именно так выглядит проекция блока square1 на плоскость, образуемую элементом world. Но нам нужна не проекция, а настоящее вращение. Чтобы все элементы внутри world стали объемными, необходимо применить к нему свойство transform-style:preserve-3d. После подстановки свойства внутрь списка стилей world проверим изменения:



Отлично! Половина блока square скрылась за голубым блоком. Чтобы его полностью показать, уберем цвет блока world, а именно, удалим строку background-color:#C0FFFF; Если мы добавим еще прямоугольников внутрь блока world, то мы можем создать трехмерный мир. Сейчас же уберем смещение мира world, удалив строку со свойством transform в стилях для этого элемента.

3. Создаем движение в трехмерном мире


Для того, чтобы пользователь мог по этому миру передвигаться, нужно задать обработчики нажатия клавиш и перемещения мыши. Управление будет стандартным, какое присутствует в большинстве 3д-шутеров. Клавишами W, S, A, D мы будем перемещаться вперед, назад, влево, вправо, пробелом мы будем прыгать (проще говоря перемещаться вверх), а мышью мы будем менять направление взгляда. Для этого откроем пока еще пустой файл script.js. Сначала впишем туда такие переменные:

// Нажата ли клавиша?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;

Изначально клавиши не нажаты. Если мы нажмем клавишу, то значение определенной переменной изменится на 1. Если отпустим ее, то она снова станет 0. Реализуем это посредством добавления обработчиков нажатия и отжатия клавиш:

// Обработчик нажатия клавиш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;}});

Номер 32 код пробела. Как видите, тут появилась переменная onGround, указывающая на то, находимся ли мы на земле. Пока разрешим движение вверх, добавив после переменных press переменную onGround:

// На земле ли игрок?var onGround = true;

Итак, мы добавили алгоритм нажатия и отжатия. Теперь необходимо добавить само передвижение. Что, собственно, мы передвигаем. Представим, что у нас есть объект, который мы двинаем. Назовем его pawn. Как и принято у нормальных разработчиков, для него мы создадим отдельный класс Player. Классы в javaScript создаются, как ни странно, с помощью функций:

function player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;}

Вставим этот код в script.js в самом начале файла. В конце же файла создадим объект данного типа:

// Создаем новый объектvar pawn = new player(0,0,0,0,0);

Распишем, что означают эти переменные. x, y, z это начальные координаты игрока, rx, ry углы его поворота относительно осей x и y в градусах. Последняя записанная строка означает, что мы создаем объект pawn типа player (специально пишу тип, а не класс, так как классы в javascript означают несколько другие вещи) с нулевыми начальными координатами. Когда мы двигаем объект, координата мира изменяться не должна, а должна изменяться координата pawn. Это с точки зрения переменных. А с точки зрения пользователя, игрок находится на одном месте, а вот мир двигается. Таким образом, нужно заставить программу изменять координаты игрока, обрабатывать эти изменения и двигать, в конце концов, мир. На деле это проще, чем кажется.

Итак, после загрузки документа в браузер мы запустим функцию, которая перерисовывает мир. Напишем функцию перерисовки:

function update(){// Высчитываем смещенияlet dx = (PressRight - PressLeft);let dz = - (PressForward - PressBack);let dy = PressUp;// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;// Изменяем координаты мира (для отображения)world.style.transform = "rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";};

В новых браузерах world будет соответствовать элементу с id=world, однако надежнее ее присвоить перед функцией update() с помощью следующей конструкции:

var world = document.getElementById("world");

Мы будем изменять положение мира каждые 10 мс (100 обновлений в секунду), для чего запустим бесконечный цикл:

TimerGame = setInterval(update,10);

Запустим игру. Ура, теперь мы можем двигаться! Однако мир вылазит за пределы рамок элемента container. Чтобы этого не происходило, зададим css-свойство для него в style.css. Добавим строку overflow:hidden; и посмотрим на изменения. Теперь мир остается в пределах контейнера.

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

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 id="square1"></div></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:300px;height:300px;transform-style:preserve-3d;}#square1{position:absolute;width:200px;height:200px;background-color:#FF0000;transform:rotateY(30deg);}

script.js:

// Конструктор Pawnfunction player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;}// Нажата ли клавиша?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;// На земле ли игрок?var onGround = true;// Обработчик нажатия клавиш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;}});// Создаем новый объектvar pawn = new player(0,0,0,0,0);// Привяжем новую переменную к worldvar world = document.getElementById("world");function update(){// Задаем локальные переменные смещенияlet dx = (PressRight - PressLeft);let dz = - (PressForward - PressBack);let dy = - PressUp;// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;// Изменяем координаты мира (для отображения)world.style.transform = "rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";};TimerGame = setInterval(update,10);

Если у вас что-то по-другому, обязательно поправьте!

Мы научились двигать персонажа, однако мы еще не умеем поворачивать его! Поворот персонажа, конечно же, будет осуществляться с помощью мыши. Для мыши к переменным состояния клавиш press мы добавим переменные состояния движения мыши:

// Нажата ли клавиша и двигается ли мышь?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;var MouseX = 0;var MouseY = 0;

А после обработчиков нажатия-отжатия вставим обработчик движения:

// Обработчик движения мышиdocument.addEventListener("mousemove", (event)=>{MouseX = event.movementX;MouseY = event.movementY;});

В функцию update добавим поворот:

// Задаем локальные переменные смещенияlet dx = (PressRight - PressLeft);let dz = - (PressForward - PressBack);let dy = - PressUp;let drx = MouseY;let dry = - MouseX;// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;

Обратите внимание на то, что движение мыши по оси y вращает pawn по оси x и наоборот. Если мы посмотрим на результат, то ужаснемся от увиденного. Дело в том, что если смещения нет, то MouseX и MouseY остаются прежними, а не приравниваются к нулю. Значит, после каждой итерации update смещения миши должно обнуляться:

// Задаем локальные переменные смещенияlet dx = (PressRight - PressLeft);let dz = - (PressForward - PressBack);let dy = - PressUp;let drx = MouseY;let dry = - MouseX;// Обнулим смещения мыши:MouseX = MouseY = 0;// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;

Уже лучше, мы избавились от инерции вращения, однако вращение происходит все равно странно! Чтобы понять, что все-таки происходит, добавим div-элемент pawn внутрь container:

<div id="container"><div id="world"><div id="square1"></div></div><div id="pawn"></div></div>

Зададим ему стили в style.css:

#pawn{position:absolute;width:100px;height:100px;top:400px;left:600px;transform:translate(-50%,-50%);background-color:#0000FF;}

Проверим результат. Теперь все ровно! Единственное синий квадрат остается впереди, но пока оставим это. Чтобы сделать игру от первого лица, а не от третьего, нужно приблизить мир к нам на значение perspective. Сделаем это в script.js в функции update():

world.style.transform = "translateZ(600px)" +"rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";

Теперь можно делать игру от первого лица. Скроем pawn добавив строку в style.css:

#pawn{display:none;position:absolute;top:400px;left:600px;width:100px;height:100px;transform:translate(-50%,-50%);background-color:#0000FF;}

Отлично. Сразу скажу, что ориентироваться в мире с одним квадратом крайне тяжело, поэтому создадим площадку. Добавим в world блок square2:

<div id="world"><div id="square1"></div><div id="square2"></div></div>

А в style.css добавим стили для него:

#square2{position:absolute;width:1000px;height:1000px;top:400px;left:600px;background-color:#00FF00;transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);}

Теперь все четко. Ну не совсем. Когда мы нажимаем по клавишам, мы движемся строго по осям X и Z. А мы хотим сделать движение по направлению взгляда. Сделаем следующее: в самом начале файла script.js добавим 2 переменные:

// Мировые константыvar pi = 3.141592;var deg = pi/180;

Градус это pi/180 от радиана. Нам придется применить синусы и косинусы, которые считаются от радиан. Что нужно сделать? Взгляните на рисунок:



Когда наш взгляд направлен под углом и мы хотим пойти вперед, то изменятся обе координаты: X и Z. В случае перемещения в сторону тригонометрические функции просто поменяются местами, а перед образовавшимся синусом изменится знак. Изменим уравнения смещений в update():

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

Внимательно просмотрите все файлы полностью! Если у вас что-то оказалось не так, то потом обязательно буду ошибки, из-за которых вы сломаете голову!

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 id="square1"></div><div id="square2"></div></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;}#square1{position:absolute;width:200px;height:200px;top:400px;left:600px;background-color:#FF0000;transform:translate(-50%,-50%) rotateY(30deg);}#square2{position:absolute;width:1000px;height:1000px;top:400px;left:600px;background-color:#00FF00;transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);}#pawn{display:none;position:absolute;top:400px;left:600px;transform:translate(-50%,-50%);width:100px;height:100px;background-color:#0000FF;}

script.js:

// Мировые константыvar pi = 3.141592;var deg = pi/180;// Конструктор Pawnfunction player(x,y,z,rx,ry) {this.x = x;this.y = y;this.z = z;this.rx = rx;this.ry = ry;}// Нажата ли клавиша и двигается ли мышь?var PressBack = 0;var PressForward = 0;var PressLeft = 0;var PressRight = 0;var PressUp = 0;var MouseX = 0;var MouseY = 0;// На земле ли игрок?var onGround = true;// Обработчик нажатия клавиш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;});// Создаем новый объект типа playervar pawn = new player(0,0,0,0,0);// Привяжем новую переменную к worldvar world = document.getElementById("world");function update(){// Задаем локальные переменные смещенияlet dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);let dy = - PressUp;let drx = MouseY;let dry = - MouseX;// Обнулим смещения мыши:MouseX = MouseY = 0;// Прибавляем смещения к координатамpawn.x = pawn.x + dx;pawn.y = pawn.y + dy;pawn.z = pawn.z + dz;pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;// Изменяем координаты мира (для отображения)world.style.transform = "translateZ(600px)" +"rotateX(" + (-pawn.rx) + "deg)" +"rotateY(" + (-pawn.ry) + "deg)" +"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";};TimerGame = setInterval(update,10);

С движением мы почти разобрались. Но осталось неудобство: курсор мыши может двигаться только в пределах экрана. В трехмерных шутерах можно вращать мышью сколь угодно долго и сколь угодно далеко. Сделаем также: при нажатии на экран игры (на container) курсор будет пропадать, и мы сможем вращать мышью без ограничений на размер экрана. Активируем захват мыши при нажатии на экран, для чего перед обработчиками нажатия клавиш поставим обработчик нажатия мыши на container:

// Привяжем новую переменную к containervar container = document.getElementById("container");// Обработчик захвата курсора мышиcontainer.onclick = function(){container.requestPointerLock();};

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

// Введен ли захват мыши?var lock = false;

Добавим обработчик изменения состояния захвата курсора (захвачен или нет) перед обработчиком захвата курсора (извините за тавтологию):

// Обработчик изменения состояния захвата курсораdocument.addEventListener("pointerlockchange", (event)=>{lock = !lock;});

А в update() добавим условие вращения pawn:

// Если курсор захвачен, разрешаем вращениеif (lock){pawn.rx = pawn.rx + drx;pawn.ry = pawn.ry + dry;};

А сам захват мыши при клике по контейнеру разрешим только тогда, когда курсор еще не захвачен:

// Обработчик захвата курсора мышиcontainer.onclick = function(){if (!lock) container.requestPointerLock();};

С движением мы полностью разобрались. Перейдем к генерации мира

4. Загрузка карты


Мир в нашем случае удобнее всего представить в виде множества прямоугольников, имеющих разное местоположение, поворот, размеры и цвет. Вместо цвета также можно использовать текстуры. На самом деле, все современные трехмерные миры в играх это набор треугольников и прямоугольников, которые называют полигонами. В крутых играх их количество может достигать десятков тысяч в одном только кадре. У нас же их будет около сотни, так как браузер сам по себе имеет невысокую графическую производительность. В предыдущих пунктах мы вставляли блоки div внутрь world. Но если таких блоков много (сотни), то вставлять каждый из них в контейнер очень утомительно. Да и уровней может быть много. Поэтому пусть эти прямоугольники вставляет javaScript, а не мы. Для него же мы будем создавать специальный массив.

Откроем index.html и удалим из блока world все внутренние блоки:

<BODY><div id="container"><div id="world"></div><div id="pawn"></div></div></BODY>

Как видим, в world теперь ничего нет. В style.css удалим стили для #square1 и #square2 (вообще удалим #square1 и #square2 из этого файла), а вместо них создадим стили для класса .square, который будет общим для всех прямоугольников. Причем зададим для него только одно свойство:

.square{position:absolute;}

Теперь создадим массив прямоугольников (запихнем его, примеру, между конструктором player и переменными press в script.js):

// Массив прямоугольников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"]]

Можно было это сделать в виде конструктора, но пока обойдемся чисто массивом, так как запуск цикла расстановки прямоугольников проще реализовать именно через массивы, а не через конструкторы. Я же поясню, что означают цифры в нем. Массив map содержит одномерные массивы из 9 переменных: [,,,,,,,,]. Я думаю, вы понимаете, что первые три числа это координаты центра прямоугольника, вторые три числа углы поворота в градусах (относительно того же центра), затем два числа его размеры и последнее число фон. Причем фон может быть сплошным цветом, градиентом или фотографией. Последнее очень удобно использовать в качестве текстур.

Массив мы записали, теперь запишем функцию, которая переделает этот массив в собственно прямоугольники:

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);}}

Поясню, что происходит: мы создаем новую переменную, которая указывает на только что созданный элемент. Ему мы присваиваем id и css-класс (именно это и имеется ввиду под словом класс в языке javaScript), задаем ширину с высотой, фон и трансформацию. Примечательно, что в трансформации помимо координат центра прямоугольника мы указываем смещение на 600 и 400 и половины размеров для того, чтобы центр прямоугольника точно оказался в точке с нужными координатами. Запустим генератор мира перед таймером:

CreateNewWorld();TimerGame = setInterval(update,10);

Теперь мы видим площадку с розовыми стенами и серым полом. Как видите, создание карты технически несложно реализовать. А в результате ваш код в трех файлах должен получиться примерно таким:

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(0,0,0,0,0);// Привяжем новую переменную к worldvar world = document.getElementById("world");function update(){// Задаем локальные переменные смещенияlet dx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);let dy = - PressUp;let drx = MouseY;let dry = - MouseX;// Обнулим смещения мыши:MouseX = MouseY = 0;// Прибавляем смещения к координатам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);}}CreateNewWorld();TimerGame = setInterval(update,10);

Если все хорошо, переходим к следующему пункту.

5. Столкновения игрока с объектами мира


Мы создали технику движения, генератор мира из массива. Мы можем передвигаться по миру, который может быть красивым. Однако наш игрок еще никак не взаимодействует с ним. Чтобы это взаимодействие происходило, нам необходимо проверять, сталкивается ли игрок с каким-нибудь прямоугольником или нет? То есть, мы будем проверять наличие коллизий. Для начала вставим пустую функцию:

function collision(){}

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

// Обнулим смещения мыши:MouseX = MouseY = 0;// Проверяем коллизию с прямоугольникамиcollision();

Как это происходит? Представим себе, что игрок это шар с радиусом r. И он движется в сторону прямоугольника:



Очевидно, что если расстояние от шара до плоскости прямоугольника больше r, то коллизии точно не происходит. Чтобы узнать это расстояние, можно перевести координаты игрока в систему координат прямоугольника. Напишем функцию перевода из мировой системы в систему прямоугольника:

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];}

Вставим эти функции после функции update(). Я не буду объяснять, как это работает, потому что мне не хочется рассказывать курс аналитической геометрии. Скажу, что есть такие формулы перевода координат при вращении и мы просто ими воспользовались. С точки зрения прямоугольника наш игрок расположен вот так:



В этом случае условие коллизии становится таким: если после смещения шара на величину v (v это вектор) координата z между r и r, а координаты x и y лежат в пределах прямоугольника или отстоят от него на величину, не большую r, то объявляется коллизия. В этом случае координата игрока по z после смещения будет составлять r или r (в зависимости от того, с какой стороны придет игрок). В соответствии с этим, смещение игрока изменяется. Мы специально вызываем коллизию перед тем, как в update() координаты игрока будут обновлены, чтобы вовремя изменить смещение. Таким образом, шар никогда не пересечется с прямоугольником, как бывает в других алгоритмах коллизии. Хотя физически игрок будет представлять собой, скорее, случае куб, мы не будем обращать на это внимание. Итак, реализуем это в javaScript:

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]);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;}};}

x0,y0 и z0 начальные координаты игрока в системе координат прямоугольника (без поворотов. x1,y1 и z1 координаты игрока после смещения без учета коллизии. point0, point0, point1 и point2 начальный радиус-вектор, радиус-вектор после смещения без коллизии и радиус-вектор с коллизией соответственно. map[i][3] и другие, если вы помните, это углы поворота прямоугольника. Заметим, что в условии мы к размерам прямоугольника прибавляем не 100, а 98. Это костыль, зачем, подумайте сами. Запустите игру и вы увидите довольно качественные столкновения.

Как видим, все эти действия происходят в цикле for для всех прямоугольников. При их большом количестве такая операция становится очень дорогой, так как тут и так есть 3 вызова функций преобразований координат, которые тоже производят достаточно много математических операций. Очевидно, что если прямоугольники находятся очень далеко от игрока, то коллизию считать не имеет смысла. Добавим это условие:

if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**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;}} 

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

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][1]**2 + map[i][2]**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);
Подробнее..

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

17.07.2020 14:11:33 | Автор: admin
В данной статье мы продолжим создавать трехмерную браузерную игру лабиринт на чистом 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). Сами скрипты можно существенно улучшить, добавив туда разные библиотеки, написать новые конструкторы или сделать что-нибудь еще. Спасибо за внимание!
Подробнее..

Категории

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

  • Имя: Макс
    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