[CSS, HTML, JavaScript, Разработка игр] Создание браузерных 3d-игр с нуля на чистом html, css и js. Часть 2/2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В данной статье мы продолжим создавать трехмерную браузерную игру лабиринт на чистом html, css и javascript. В предыдущей части мы сделали простой 3-мерный мир, реализовали движение, управление, столкновения игрока со статическими объектами. В этой части мы будем добавлять гравитацию, статическое солнечное освещение (без теней), загружать звуки и делать меню. Увы, как и в первой части, демок здесь не будет.
Вспомним код, который мы сделали в предыдущей части. У нас имеются 3 файла:
index.html
SPL
<!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
SPL
#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
SPL
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
// Конструктор player
function 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;
// Привяжем новую переменную к container
var 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);
// Привяжем новую переменную к world
var 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)";
// Вставка прямоугольника в world
world.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 = z3
let 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
SPL
<!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
SPL
#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
SPL
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
// Конструктор player
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;
}
// Массив прямоугольников
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)";
// Вставка прямоугольника в world
world.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 = z3
let 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)";
// Вставка прямоугольника в world
world.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
SPL
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
// Конструктор player
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;
}
// Инициализация массива уровней
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 = z3
let 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)";
// Вставка прямоугольника в world
world.append(newElement);
}
}
menu.js
SPL
// Создаем переменные
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();
}
// Пользовательский slice
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;
}
4. Оформим игру.
4.1 Изменим уровни
Создание уровней – очень интересное занятие. Как правило, этим занимаются отдельные люди, которых называют дизайнерами уровней. У нас уровень представляет из себя массивы чисел, которые скриптами из script.js преобразуются в трехмерный мир. Можно написать отдельную программу, упрощающую создание миров, но сейчас мы это делать не будем. Откроем файл script.js и загрузим туда массивы готовых лабиринтов:
Массивы уровней
SPL
// 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)";
// Вставка прямоугольника в world
world.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[7]**2)){…}):
widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>";
widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>";
Теперь при взятии монет и ключа информация в виджетах меняется. Но при завершении игры виджеты не скрываются. Скроим их по окончании игры, добавив в finishInteract() внутрь else следующие строки:
widget1.style.display = «none»;
widget2.style.display = «none»;
widget3.style.display = «none»;
Виджеты скрыты. Осталось настроить виджет, который просит взять ключ в случае прихода к финишу без него. В finishInteract() вместо console.log(«найдите ключ») вставим следующие строки:
widget3.style.display = "block";
setTimeout(() => widget3.style.display = "none",5000);
При неудачной попытки окончания игры мы, получаем сообщение, которое скрывается через 5 секунд. Наша игра сейчас выглядит вот так:
4.5 Оформим текст.
Создадим в папке с файлами папку Fonts. Скачаем отсюда файл font1.woff и вставим его в Fonts. В style.css добавим стили текста:
/* Оформление текста */
p{
margin:0px;
font-size:60px;
position:absolute;
display:block;
top:50%;
left:50%;
transform:translate(-50%,-50%);
user-select:none;
font-family:fontlab;
}
@font-face{
font-family:fontlab;
src:url("Fonts/font1.woff");
}
Меню и игра преобразились:
4.6 Добавим звуки.
Скачаем отсюда архив со звуками Sounds.zip. Создадим в папке с проектом папку Sounds и вставьте туда звуки (они находятся в формате mp3). Сделаем переменные-ссылки на эти звуки:
// Загрузка звуков
var clickSound = new Audio;
clickSound.src = "Sounds/click.mp3";
var keySound = new Audio;
keySound.src = "Sounds/key.mp3";
var mistakeSound = new Audio;
mistakeSound.src = "Sounds/mistake.mp3";
var thingSound = new Audio;
thingSound.src = "Sounds/thing.mp3";
var winSound = new Audio;
winSound.src = "Sounds/win.mp3";
В функции interact добавим аргумент звукового файла и проигрывание звука (soundObject.play()):
function interact(objects,string,num,soundObject){
for (i = 0; i < objects.length; i++){
let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2;
if(r < (objects[i][7]**2)){
soundObject.play();
document.getElementById(string + i).style.display = "none";
objects[i][0] = 1000000;
objects[i][1] = 1000000;
objects[i][2] = 1000000;
document.getElementById(string + i).style.transform =
"translate3d(1000000px,1000000px,1000000px)";
num[0]++;
widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>";
widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>";
};
};
}
В repeatFunction() изменим, соответственно, вызовы этой функции:
interact(things,"thing",m,thingSound);
interact(keys,"key",k,keySound);
А в finishInteract() добавим звуки mistakeSound и winSound:
function finishInteract(){
let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2;
if(r < (finish[0][7]**2)){
if (k[0] == 0){
widget3.style.display = "block";
setTimeout(() => widget3.style.display = "none",5000);
mistakeSound.play();
}
else{
clearWorld();
clearInterval(TimerGame);
document.exitPointerLock();
score = score + m[0];
k[0] = 0;
m[0] = 0;
level++;
menu1.style.display = "block";
widget1.style.display = "none";
widget2.style.display = "none";
widget3.style.display = "none";
winSound.play();
if(level >= 2){
level = 0;
score = 0;
};
};
};
};
При клике любой кнопки меню проиграем звук clickSound:
button1.onclick = function(){
clickSound.play();
...
}
button2.onclick = function(){
clickSound.play();
menu1.style.display = "none";
menu2.style.display = "block";
}
button3.onclick = function(){
clickSound.play();
menu1.style.display = "block";
menu2.style.display = "none";
}
button4.onclick = function(){
clickSound.play();
menu1.style.display = "block";
menu3.style.display = "none";
}
Игра заиграла ярче. Осталось настроить вывод результатов после прохождения всех уровней:
4.7 Вывод результатов.
В menu.js в finishInteract() внутрь if(level >= 2){…} добавим строки:
if(level >= 2){
menu1.style.display = "none";
menu3.style.display = "block";
document.getElementById("result").innerHTML = "Вы набрали " + score + " очков";
level = 0;
score = 0;
};
Мы видим количество набранных очков по прохождении всех уровней.
Кстати, не забудем добавить в эту же функцию строку:
canlock = false;
А также:
button1.innerHTML = "<p>Продолжить</p>";
и
button1.innerHTML = "<p>Начать игру</p>";
В результате:
function finishInteract(){
let r = (finish[0][0] — pawn.x)**2 + (finish[0][1] — pawn.y)**2 + (finish[0][2] — pawn.z)**2;
if(r < (finish[0][7]**2)){
if (k[0] == 0){
…
}
else{
…
canlock = false;
button1.innerHTML = "Продолжить";
if(level >= 2){
menu1.style.display = «none»;
menu3.style.display = «block»;
document.getElementById(«result»).innerHTML = «Вы набрали » + score + " очков";
level = 0;
score = 0;
button1.innerHTML = "Начать игру";
};
};
};
};
Теперь кнопка запуска игры меняется в зависимости от прохождения уровней. Также передвинем “container” в центр окна, добавив в стили для него следующие строки:
top:50%;
left:50%;
transform: translate(-50%,-50%);
А в body уберем отступы:
body{
margin:0px;
}
Итак, мы полностью написали браузерную трехмерную игру лабиринт. Благодаря ей мы обратили внимание на некоторые аспекты языка javascript, узнали о функциях, о которых вы раньше может быть и не слышали. А главное, мы показали, что делать простые игрушки для браузера даже на чистом коде не так уж и сложно. Полный исходный код вы можете скачать отсюда (исходники.zip). Сами скрипты можно существенно улучшить, добавив туда разные библиотеки, написать новые конструкторы или сделать что-нибудь еще. Спасибо за внимание!
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Node.JS, Интерфейсы, Программирование] Наилучшие практики создания REST API (перевод)
- [Программирование, Разработка под e-commerce, Управление e-commerce] Интеграция интернет-магазина на 1С-Битрикс с Mindbox
- [IT-компании, JavaScript, Python, Управление разработкой] Практики хорошего code review, или что такое code review за 15 минут. Доклад Никиты Соболева на DUMP в Казани
- [JavaScript, Программирование, Разработка веб-сайтов] Что за черт, JavaScript?
- [Клиентская оптимизация, Поисковая оптимизация, Разработка веб-сайтов] Проверка разметки сайтов-участников W3C
- [JavaScript, Python, Программирование, Разработка веб-сайтов] Надоел JavaScript — используй браузерный Python (перевод)
- [C++, Разработка игр] Source Modding — Часть 2 — Всё есть сущность
- [CSS, HTML, Разработка веб-сайтов] Еще раз о визуализации input типа checkbox и radio. Для тех, кто забыл как
- [Дизайн игр, Разработка игр] В играх детям нравятся атрибуты взрослой жизни: как мы разрабатывали обучающую игру для младших школьников
- [C++, Разработка игр] Source Modding — Часть 1 — Основы основ
Теги для поиска: #_css, #_html, #_javascript, #_razrabotka_igr (Разработка игр), #_html, #_css, #_javascript, #_3digry (3d-игры), #_css, #_html, #_javascript, #_razrabotka_igr (
Разработка игр
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:36
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В данной статье мы продолжим создавать трехмерную браузерную игру лабиринт на чистом html, css и javascript. В предыдущей части мы сделали простой 3-мерный мир, реализовали движение, управление, столкновения игрока со статическими объектами. В этой части мы будем добавлять гравитацию, статическое солнечное освещение (без теней), загружать звуки и делать меню. Увы, как и в первой части, демок здесь не будет. Вспомним код, который мы сделали в предыдущей части. У нас имеются 3 файла: index.htmlSPL<!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.cssSPL#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.jsSPL// Мировые константы
var pi = 3.141592; var deg = pi/180; // Конструктор player function 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; // Привяжем новую переменную к container var 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); // Привяжем новую переменную к world var 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)"; // Вставка прямоугольника в world world.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 = z3 let 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.htmlSPL<!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.cssSPL#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.jsSPL// Мировые константы
var pi = 3.141592; var deg = pi/180; // Конструктор player 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; } // Массив прямоугольников 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)"; // Вставка прямоугольника в world world.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 = z3 let 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)"; // Вставка прямоугольника в world world.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.jsSPL// Мировые константы
var pi = 3.141592; var deg = pi/180; // Конструктор player 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; } // Инициализация массива уровней 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 = z3 let 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)"; // Вставка прямоугольника в world world.append(newElement); } } menu.jsSPL// Создаем переменные
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(); } // Пользовательский slice 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; } 4. Оформим игру. 4.1 Изменим уровни Создание уровней – очень интересное занятие. Как правило, этим занимаются отдельные люди, которых называют дизайнерами уровней. У нас уровень представляет из себя массивы чисел, которые скриптами из script.js преобразуются в трехмерный мир. Можно написать отдельную программу, упрощающую создание миров, но сейчас мы это делать не будем. Откроем файл script.js и загрузим туда массивы готовых лабиринтов: Массивы уровнейSPL// 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)"; // Вставка прямоугольника в world world.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[7]**2)){…}): widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>";
widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>"; Теперь при взятии монет и ключа информация в виджетах меняется. Но при завершении игры виджеты не скрываются. Скроим их по окончании игры, добавив в finishInteract() внутрь else следующие строки: widget1.style.display = «none»; widget2.style.display = «none»; widget3.style.display = «none»; Виджеты скрыты. Осталось настроить виджет, который просит взять ключ в случае прихода к финишу без него. В finishInteract() вместо console.log(«найдите ключ») вставим следующие строки: widget3.style.display = "block";
setTimeout(() => widget3.style.display = "none",5000); При неудачной попытки окончания игры мы, получаем сообщение, которое скрывается через 5 секунд. Наша игра сейчас выглядит вот так: 4.5 Оформим текст. Создадим в папке с файлами папку Fonts. Скачаем отсюда файл font1.woff и вставим его в Fonts. В style.css добавим стили текста: /* Оформление текста */
p{ margin:0px; font-size:60px; position:absolute; display:block; top:50%; left:50%; transform:translate(-50%,-50%); user-select:none; font-family:fontlab; } @font-face{ font-family:fontlab; src:url("Fonts/font1.woff"); } Меню и игра преобразились: 4.6 Добавим звуки. Скачаем отсюда архив со звуками Sounds.zip. Создадим в папке с проектом папку Sounds и вставьте туда звуки (они находятся в формате mp3). Сделаем переменные-ссылки на эти звуки: // Загрузка звуков
var clickSound = new Audio; clickSound.src = "Sounds/click.mp3"; var keySound = new Audio; keySound.src = "Sounds/key.mp3"; var mistakeSound = new Audio; mistakeSound.src = "Sounds/mistake.mp3"; var thingSound = new Audio; thingSound.src = "Sounds/thing.mp3"; var winSound = new Audio; winSound.src = "Sounds/win.mp3"; В функции interact добавим аргумент звукового файла и проигрывание звука (soundObject.play()): function interact(objects,string,num,soundObject){
for (i = 0; i < objects.length; i++){ let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2; if(r < (objects[i][7]**2)){ soundObject.play(); document.getElementById(string + i).style.display = "none"; objects[i][0] = 1000000; objects[i][1] = 1000000; objects[i][2] = 1000000; document.getElementById(string + i).style.transform = "translate3d(1000000px,1000000px,1000000px)"; num[0]++; widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>"; widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>"; }; }; } В repeatFunction() изменим, соответственно, вызовы этой функции: interact(things,"thing",m,thingSound);
interact(keys,"key",k,keySound); А в finishInteract() добавим звуки mistakeSound и winSound: function finishInteract(){
let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2; if(r < (finish[0][7]**2)){ if (k[0] == 0){ widget3.style.display = "block"; setTimeout(() => widget3.style.display = "none",5000); mistakeSound.play(); } else{ clearWorld(); clearInterval(TimerGame); document.exitPointerLock(); score = score + m[0]; k[0] = 0; m[0] = 0; level++; menu1.style.display = "block"; widget1.style.display = "none"; widget2.style.display = "none"; widget3.style.display = "none"; winSound.play(); if(level >= 2){ level = 0; score = 0; }; }; }; }; При клике любой кнопки меню проиграем звук clickSound: button1.onclick = function(){
clickSound.play(); ... } button2.onclick = function(){ clickSound.play(); menu1.style.display = "none"; menu2.style.display = "block"; } button3.onclick = function(){ clickSound.play(); menu1.style.display = "block"; menu2.style.display = "none"; } button4.onclick = function(){ clickSound.play(); menu1.style.display = "block"; menu3.style.display = "none"; } Игра заиграла ярче. Осталось настроить вывод результатов после прохождения всех уровней: 4.7 Вывод результатов. В menu.js в finishInteract() внутрь if(level >= 2){…} добавим строки: if(level >= 2){
menu1.style.display = "none"; menu3.style.display = "block"; document.getElementById("result").innerHTML = "Вы набрали " + score + " очков"; level = 0; score = 0; }; Мы видим количество набранных очков по прохождении всех уровней. Кстати, не забудем добавить в эту же функцию строку: canlock = false;
А также: button1.innerHTML = "<p>Продолжить</p>";
и button1.innerHTML = "<p>Начать игру</p>";
В результате: function finishInteract(){ let r = (finish[0][0] — pawn.x)**2 + (finish[0][1] — pawn.y)**2 + (finish[0][2] — pawn.z)**2; if(r < (finish[0][7]**2)){ if (k[0] == 0){ … } else{ … canlock = false; button1.innerHTML = "Продолжить"; if(level >= 2){ menu1.style.display = «none»; menu3.style.display = «block»; document.getElementById(«result»).innerHTML = «Вы набрали » + score + " очков"; level = 0; score = 0; button1.innerHTML = "Начать игру"; }; }; }; }; Теперь кнопка запуска игры меняется в зависимости от прохождения уровней. Также передвинем “container” в центр окна, добавив в стили для него следующие строки: top:50%;
left:50%; transform: translate(-50%,-50%); А в body уберем отступы: body{
margin:0px; } Итак, мы полностью написали браузерную трехмерную игру лабиринт. Благодаря ей мы обратили внимание на некоторые аспекты языка javascript, узнали о функциях, о которых вы раньше может быть и не слышали. А главное, мы показали, что делать простые игрушки для браузера даже на чистом коде не так уж и сложно. Полный исходный код вы можете скачать отсюда (исходники.zip). Сами скрипты можно существенно улучшить, добавив туда разные библиотеки, написать новые конструкторы или сделать что-нибудь еще. Спасибо за внимание! =========== Источник: habr.com =========== Похожие новости:
Разработка игр ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:36
Часовой пояс: UTC + 5