[CSS, HTML, JavaScript, Программирование, Разработка игр] Создание браузерных 3d-игр с нуля на чистом html, css и js. Часть 1/2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Современная вычислительная техника позволяет создавать классные компьютерные игры! И сейчас, достаточно популярны игры с 3d-графикой, так как, играя в них, ты окунаешься в вымышленный мир и теряешь всякую связь с реальностью. Развитие интернета и браузерных технологий сделало возможным запускать головоломки и стрелялки в любимом Хроме, Мозилле или еще в чем-то там (про Эксплорер помолчим) в онлайн-режиме, без загрузки. Так вот, здесь я расскажу о том, как создать простую трехмерную браузерную игру.
Выбор жанра, сюжета и стилистики игры является достаточно интересной задачей, и от решения этих вопросов может зависеть успех игры. Кроме этого, свои нюансы вносит и выбор технологии, на основе которой будет создаваться продукт. Моя цель – показать элементарные основы этого увлекательного процесса, поэтому я буду делать 3-мерный лабиринт с незамысловатым оформлением. Более того, я это сделаю на чистом коде без использования библиотек и движков, типа three.js (хотя большие проекты лучше делать все-таки на нем), чтобы показать, как можно создать движок для своих нужд. Полностью самописная игра может быть оригинальной, а потому интересной. В общем, оба подхода имеют свои плюсы и минусы.
Я полагаю, если вы читаете эту статью, то вам интересна тема создания игр для гугл Хром, а, значит, понимаете, как работает связка html-css-javaScript, поэтому не буду останавливаться на основах, а сразу приступлю к разработке. В html5 и css3, которые поддерживают все современные браузеры (Эксплорер не в счет), есть возможность расположения блоков в 3-мерном пространстве. Также есть элемент , в котором можно рисовать линии и графические примитивы. Большинство браузерных движков используют <сanvas>, так как на нем можно сделать больше вещей, да и производительность на нем выше. Но для простых вещей вполне можно использовать методы transform-3d, которые будут занимать меньше кода.
1. Инструменты для разработки
Я использую для проверки сайтов и игр только 2 браузера: Chrome и Mozilla. Все остальные браузеры (кроме того самого Эксплорера) построены на движке первого, поэтому использовать их я не вижу смысла, ибо результаты точно такие же, как и в Chrome. Для написания кода достаточно Notepad++.
2. Как реализуется трехмерное пространство в html?
Посмотрим на систему координат блока:
По умолчанию, дочерний блок имеет координаты (left и top) 0 пикселей по x и 0 пикселей по y. Смещение (translate), также 0 пикселей по всем трем осям. Покажем это на примере, для чего создадим новую папку. В нем создадим файлы index.html, style.css и script.js. Откроем index.html и запишем туда следующее:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE>Игра</TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
</div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
В файле style.css зададим стили для элементов “container” и “world”.
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
}
#world{
width:300px;
height:300px;
background-color:#C0FFFF;
}
Сохраним. Откроем index.html c помощью Chrome, получим:
Попробуем применить translate3d к элементу “world”:
#world{
width:300px;
height:300px;
background-color:#C0FFFF;
transform:translate3d(200px,100px,0px);
}
Как вы поняли, я перешел в полноэкранный режим. Теперь зададим смещение по оси Z:
transform:translate3d(200px,100px,-1000px);
Если вы снова откроете html-файл в браузере, то никаких изменений вы не увидите. Чтобы увидеть изменения, нужно задать перспективу для объекта “container”:
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
}
В результате:
Квадрат отдалился от нас. Как работает перспектива в html? Взглянем на картинку:
d – расстояние от пользователя до объекта, а z – его координата. Отрицательный z (в html это translateZ) означает, что мы отдалили объект, а положительный – наоборот. Значение perspective определяет величину d. Если же свойство perspective не задано, то значение d принимается за бесконечность, а в этом случае объект визуально не изменяется для пользователя с изменением z. В нашем случае мы задали d = 600px. По умолчанию, точка взгляда перспективы находится в центре элемента, однако ее можно изменить путем задания свойства perspective-origin: .
Теперь повернем “world” вокруг какой-нибудь оси. В сss можно использовать 2 способа вращения. Первый – вращение вокруг осей x,y и z. Для этого используются transform-свойства rotateX(), rotateY() и rotateZ(). Второй – вращение вокруг заданной оси с помощью свойства rotate3d(). Мы будем использовать первый способ, так как он больше подходит для наших задач. Обратите внимание, что оси вращения выходят из центра прямоугольника!
Точка, относительно которой происходят трансформации, может быть изменена путем задания свойства translate-origin: . Итак, зададим вращение “world” по оси x:
#world{
width:300px;
height:300px;
background-color:#C0FFFF;
transform:translate3d(200px,100px,0px) rotateX(45deg);
}
Получим:
Заметно смещение против часовой стрелки. Если же мы добавим rotateY(), то получим смещение уже по оси Y. Важно заметить, что при вращении блока оси вращения также поворачиваются. Вы также можете поэкспериментировать с различными значениями вращения.
Теперь внутри блока “world” создадим еще один блок, для этого добавим тег в html-файл:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE>Игра</TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
<div id="square1"></div>
</div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
В style.css добавим стили к этому блоку:
#square1{
position:absolute;
width:200px;
height:200px;
background-color:#FF0000;
}
Получим:
То есть, элементы внутри блока “world” будут трансформироваться в составе этого блока. Попробуем повернуть “square1” по оси y, добавив к нему стиль вращения:
transform: rotateY(30deg);
В итоге:
«Где вращение?» — спросите вы? На самом деле именно так выглядит проекция блока “square1” на плоскость, образуемую элементом “world”. Но нам нужна не проекция, а настоящее вращение. Чтобы все элементы внутри “world” стали объемными, необходимо применить к нему свойство transform-style:preserve-3d. После подстановки свойства внутрь списка стилей “world” проверим изменения:
Отлично! Половина блока “square” скрылась за голубым блоком. Чтобы его полностью показать, уберем цвет блока “world”, а именно, удалим строку background-color:#C0FFFF; Если мы добавим еще прямоугольников внутрь блока “world”, то мы можем создать трехмерный мир. Сейчас же уберем смещение мира “world”, удалив строку со свойством transform в стилях для этого элемента.
3. Создаем движение в трехмерном мире
Для того, чтобы пользователь мог по этому миру передвигаться, нужно задать обработчики нажатия клавиш и перемещения мыши. Управление будет стандартным, какое присутствует в большинстве 3д-шутеров. Клавишами W, S, A, D мы будем перемещаться вперед, назад, влево, вправо, пробелом мы будем прыгать (проще говоря – перемещаться вверх), а мышью мы будем менять направление взгляда. Для этого откроем пока еще пустой файл script.js. Сначала впишем туда такие переменные:
// Нажата ли клавиша?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
Изначально клавиши не нажаты. Если мы нажмем клавишу, то значение определенной переменной изменится на 1. Если отпустим ее, то она снова станет 0. Реализуем это посредством добавления обработчиков нажатия и отжатия клавиш:
// Обработчик нажатия клавиш
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
// Обработчик отжатия клавиш
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
Номер 32 – код пробела. Как видите, тут появилась переменная onGround, указывающая на то, находимся ли мы на земле. Пока разрешим движение вверх, добавив после переменных press… переменную onGround:
// На земле ли игрок?
var onGround = true;
Итак, мы добавили алгоритм нажатия и отжатия. Теперь необходимо добавить само передвижение. Что, собственно, мы передвигаем. Представим, что у нас есть объект, который мы двинаем. Назовем его “pawn”. Как и принято у нормальных разработчиков, для него мы создадим отдельный класс “Player”. Классы в javaScript создаются, как ни странно, с помощью функций:
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
Вставим этот код в script.js в самом начале файла. В конце же файла создадим объект данного типа:
// Создаем новый объект
var pawn = new player(0,0,0,0,0);
Распишем, что означают эти переменные. x, y, z – это начальные координаты игрока, rx, ry – углы его поворота относительно осей x и y в градусах. Последняя записанная строка означает, что мы создаем объект “pawn” типа “player” (специально пишу тип, а не класс, так как классы в javascript означают несколько другие вещи) с нулевыми начальными координатами. Когда мы двигаем объект, координата мира изменяться не должна, а должна изменяться координата «pawn». Это с точки зрения переменных. А с точки зрения пользователя, игрок находится на одном месте, а вот мир двигается. Таким образом, нужно заставить программу изменять координаты игрока, обрабатывать эти изменения и двигать, в конце концов, мир. На деле это проще, чем кажется.
Итак, после загрузки документа в браузер мы запустим функцию, которая перерисовывает мир. Напишем функцию перерисовки:
function update(){
// Высчитываем смещения
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = PressUp;
// Прибавляем смещения к координатам
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
// Изменяем координаты мира (для отображения)
world.style.transform =
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
В новых браузерах world будет соответствовать элементу с id=«world», однако надежнее ее присвоить перед функцией update() с помощью следующей конструкции:
var world = document.getElementById("world");
Мы будем изменять положение мира каждые 10 мс (100 обновлений в секунду), для чего запустим бесконечный цикл:
TimerGame = setInterval(update,10);
Запустим игру. Ура, теперь мы можем двигаться! Однако мир вылазит за пределы рамок элемента «container». Чтобы этого не происходило, зададим css-свойство для него в style.css. Добавим строку overflow:hidden; и посмотрим на изменения. Теперь мир остается в пределах контейнера.
Вполне возможно, что вы не всегда понимаете, куда нужно записывать те или иные строчки кода, поэтому сейчас я вам представлю файлы, которые, как я полагаю, у вас должны получиться:
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE>Игра</TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
<div id="square1"></div>
</div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css:
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:300px;
height:300px;
transform-style:preserve-3d;
}
#square1{
position:absolute;
width:200px;
height:200px;
background-color:#FF0000;
transform:rotateY(30deg);
}
script.js:
// Конструктор Pawn
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
// Нажата ли клавиша?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
// На земле ли игрок?
var onGround = true;
// Обработчик нажатия клавиш
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
// Обработчик отжатия клавиш
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
// Создаем новый объект
var pawn = new player(0,0,0,0,0);
// Привяжем новую переменную к world
var world = document.getElementById("world");
function update(){
// Задаем локальные переменные смещения
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = - PressUp;
// Прибавляем смещения к координатам
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
// Изменяем координаты мира (для отображения)
world.style.transform =
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
TimerGame = setInterval(update,10);
Если у вас что-то по-другому, обязательно поправьте!
Мы научились двигать персонажа, однако мы еще не умеем поворачивать его! Поворот персонажа, конечно же, будет осуществляться с помощью мыши. Для мыши к переменным состояния клавиш press… мы добавим переменные состояния движения мыши:
// Нажата ли клавиша и двигается ли мышь?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;
А после обработчиков нажатия-отжатия вставим обработчик движения:
// Обработчик движения мыши
document.addEventListener("mousemove", (event)=>{
MouseX = event.movementX;
MouseY = event.movementY;
});
В функцию update добавим поворот:
// Задаем локальные переменные смещения
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// Прибавляем смещения к координатам
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
Обратите внимание на то, что движение мыши по оси y вращает pawn по оси x и наоборот. Если мы посмотрим на результат, то ужаснемся от увиденного. Дело в том, что если смещения нет, то MouseX и MouseY остаются прежними, а не приравниваются к нулю. Значит, после каждой итерации update смещения миши должно обнуляться:
// Задаем локальные переменные смещения
let dx = (PressRight - PressLeft);
let dz = - (PressForward - PressBack);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// Обнулим смещения мыши:
MouseX = MouseY = 0;
// Прибавляем смещения к координатам
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
Уже лучше, мы избавились от инерции вращения, однако вращение происходит все равно странно! Чтобы понять, что все-таки происходит, добавим div-элемент «pawn» внутрь «container»:
<div id="container">
<div id="world">
<div id="square1"></div>
</div>
<div id="pawn"></div>
</div>
Зададим ему стили в style.css:
#pawn{
position:absolute;
width:100px;
height:100px;
top:400px;
left:600px;
transform:translate(-50%,-50%);
background-color:#0000FF;
}
Проверим результат. Теперь все ровно! Единственное — синий квадрат остается впереди, но пока оставим это. Чтобы сделать игру от первого лица, а не от третьего, нужно приблизить мир к нам на значение perspective. Сделаем это в script.js в функции update():
world.style.transform =
"translateZ(600px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
Теперь можно делать игру от первого лица. Скроем pawn добавив строку в style.css:
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
width:100px;
height:100px;
transform:translate(-50%,-50%);
background-color:#0000FF;
}
Отлично. Сразу скажу, что ориентироваться в мире с одним квадратом крайне тяжело, поэтому создадим площадку. Добавим в «world» блок «square2»:
<div id="world">
<div id="square1"></div>
<div id="square2"></div>
</div>
А в style.css добавим стили для него:
#square2{
position:absolute;
width:1000px;
height:1000px;
top:400px;
left:600px;
background-color:#00FF00;
transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}
Теперь все четко. Ну… не совсем. Когда мы нажимаем по клавишам, мы движемся строго по осям X и Z. А мы хотим сделать движение по направлению взгляда. Сделаем следующее: в самом начале файла script.js добавим 2 переменные:
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
Градус — это pi/180 от радиана. Нам придется применить синусы и косинусы, которые считаются от радиан. Что нужно сделать? Взгляните на рисунок:
Когда наш взгляд направлен под углом и мы хотим пойти вперед, то изменятся обе координаты: X и Z. В случае перемещения в сторону тригонометрические функции просто поменяются местами, а перед образовавшимся синусом изменится знак. Изменим уравнения смещений в update():
// Задаем локальные переменные смещения
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
let dy = -PressUp;
let drx = MouseY;
let dry = - MouseX;
Внимательно просмотрите все файлы полностью! Если у вас что-то оказалось не так, то потом обязательно буду ошибки, из-за которых вы сломаете голову!
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE>Игра</TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world">
<div id="square1"></div>
<div id="square2"></div>
</div>
<div id="pawn"></div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css:
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:inherit;
height:inherit;
transform-style:preserve-3d;
}
#square1{
position:absolute;
width:200px;
height:200px;
top:400px;
left:600px;
background-color:#FF0000;
transform:translate(-50%,-50%) rotateY(30deg);
}
#square2{
position:absolute;
width:1000px;
height:1000px;
top:400px;
left:600px;
background-color:#00FF00;
transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
transform:translate(-50%,-50%);
width:100px;
height:100px;
background-color:#0000FF;
}
script.js:
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
// Конструктор Pawn
function player(x,y,z,rx,ry) {
this.x = x;
this.y = y;
this.z = z;
this.rx = rx;
this.ry = ry;
}
// Нажата ли клавиша и двигается ли мышь?
var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;
// На земле ли игрок?
var onGround = true;
// Обработчик нажатия клавиш
document.addEventListener("keydown", (event) =>{
if (event.key == "a"){
PressLeft = 1;
}
if (event.key == "w"){
PressForward = 1;
}
if (event.key == "d"){
PressRight = 1;
}
if (event.key == "s"){
PressBack = 1;
}
if (event.keyCode == 32 && onGround){
PressUp = 1;
}
});
// Обработчик отжатия клавиш
document.addEventListener("keyup", (event) =>{
if (event.key == "a"){
PressLeft = 0;
}
if (event.key == "w"){
PressForward = 0;
}
if (event.key == "d"){
PressRight = 0;
}
if (event.key == "s"){
PressBack = 0;
}
if (event.keyCode == 32){
PressUp = 0;
}
});
// Обработчик движения мыши
document.addEventListener("mousemove", (event)=>{
MouseX = event.movementX;
MouseY = event.movementY;
});
// Создаем новый объект типа player
var pawn = new player(0,0,0,0,0);
// Привяжем новую переменную к world
var world = document.getElementById("world");
function update(){
// Задаем локальные переменные смещения
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// Обнулим смещения мыши:
MouseX = MouseY = 0;
// Прибавляем смещения к координатам
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
// Изменяем координаты мира (для отображения)
world.style.transform =
"translateZ(600px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
TimerGame = setInterval(update,10);
С движением мы почти разобрались. Но осталось неудобство: курсор мыши может двигаться только в пределах экрана. В трехмерных шутерах можно вращать мышью сколь угодно долго и сколь угодно далеко. Сделаем также: при нажатии на экран игры (на “container”) курсор будет пропадать, и мы сможем вращать мышью без ограничений на размер экрана. Активируем захват мыши при нажатии на экран, для чего перед обработчиками нажатия клавиш поставим обработчик нажатия мыши на “container”:
// Привяжем новую переменную к container
var container = document.getElementById("container");
// Обработчик захвата курсора мыши
container.onclick = function(){
container.requestPointerLock();
};
Теперь совсем другое дело. Однако лучше вообще сделать так, чтобы вращение производилось только тогда, когда курсор захвачен. Введем новую переменную после переменных нажатия клавиш press…
// Введен ли захват мыши?
var lock = false;
Добавим обработчик изменения состояния захвата курсора (захвачен или нет) перед обработчиком захвата курсора (извините за тавтологию):
// Обработчик изменения состояния захвата курсора
document.addEventListener("pointerlockchange", (event)=>{
lock = !lock;
});
А в update() добавим условие вращения “pawn”:
// Если курсор захвачен, разрешаем вращение
if (lock){
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
};
А сам захват мыши при клике по контейнеру разрешим только тогда, когда курсор еще не захвачен:
// Обработчик захвата курсора мыши
container.onclick = function(){
if (!lock) container.requestPointerLock();
};
С движением мы полностью разобрались. Перейдем к генерации мира
4. Загрузка карты
Мир в нашем случае удобнее всего представить в виде множества прямоугольников, имеющих разное местоположение, поворот, размеры и цвет. Вместо цвета также можно использовать текстуры. На самом деле, все современные трехмерные миры в играх – это набор треугольников и прямоугольников, которые называют полигонами. В крутых играх их количество может достигать десятков тысяч в одном только кадре. У нас же их будет около сотни, так как браузер сам по себе имеет невысокую графическую производительность. В предыдущих пунктах мы вставляли блоки “div” внутрь “world”. Но если таких блоков много (сотни), то вставлять каждый из них в контейнер очень утомительно. Да и уровней может быть много. Поэтому пусть эти прямоугольники вставляет javaScript, а не мы. Для него же мы будем создавать специальный массив.
Откроем index.html и удалим из блока “world” все внутренние блоки:
<BODY>
<div id="container">
<div id="world"></div>
<div id="pawn"></div>
</div>
</BODY>
Как видим, в “world” теперь ничего нет. В style.css удалим стили для #square1 и #square2 (вообще удалим #square1 и #square2 из этого файла), а вместо них создадим стили для класса .square, который будет общим для всех прямоугольников. Причем зададим для него только одно свойство:
.square{
position:absolute;
}
Теперь создадим массив прямоугольников (запихнем его, примеру, между конструктором player и переменными press… в script.js):
// Массив прямоугольников
var map = [
[0,0,1000,0,180,0,2000,200,"#F0C0FF"],
[0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
[1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
[-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
[0,100,0,90,0,0,2000,2000,"#666666"]
]
Можно было это сделать в виде конструктора, но пока обойдемся чисто массивом, так как запуск цикла расстановки прямоугольников проще реализовать именно через массивы, а не через конструкторы. Я же поясню, что означают цифры в нем. Массив map содержит одномерные массивы из 9 переменных: [,,,,,,,,]. Я думаю, вы понимаете, что первые три числа – это координаты центра прямоугольника, вторые три числа – углы поворота в градусах (относительно того же центра), затем два числа – его размеры и последнее число – фон. Причем фон может быть сплошным цветом, градиентом или фотографией. Последнее очень удобно использовать в качестве текстур.
Массив мы записали, теперь запишем функцию, которая переделает этот массив в собственно прямоугольники:
function CreateNewWorld(){
for (let i = 0; i < map.length; i++){
// Создание прямоугольника и придание ему стилей
let newElement = document.createElement("div");
newElement.className = "square";
newElement.id = "square" + i;
newElement.style.width = map[i][6] + "px";
newElement.style.height = map[i][7] + "px";
newElement.style.background = map[i][8];
newElement.style.transform = "translate3d(" +
(600 - map[i][6]/2 + map[i][0]) + "px," +
(400 - map[i][7]/2 + map[i][1]) + "px," +
(map[i][2]) + "px)" +
"rotateX(" + map[i][3] + "deg)" +
"rotateY(" + map[i][4] + "deg)" +
"rotateZ(" + map[i][5] + "deg)";
// Вставка прямоугольника в world
world.append(newElement);
}
}
Поясню, что происходит: мы создаем новую переменную, которая указывает на только что созданный элемент. Ему мы присваиваем id и css-класс (именно это и имеется ввиду под словом класс в языке javaScript), задаем ширину с высотой, фон и трансформацию. Примечательно, что в трансформации помимо координат центра прямоугольника мы указываем смещение на 600 и 400 и половины размеров для того, чтобы центр прямоугольника точно оказался в точке с нужными координатами. Запустим генератор мира перед таймером:
CreateNewWorld();
TimerGame = setInterval(update,10);
Теперь мы видим площадку с розовыми стенами и серым полом. Как видите, создание карты технически несложно реализовать. А в результате ваш код в трех файлах должен получиться примерно таким:
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE>Игра</TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world"></div>
<div id="pawn"></div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:inherit;
height:inherit;
transform-style:preserve-3d;
}
.square{
position:absolute;
}
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
transform:translate(-50%,-50%);
width:100px;
height:100px;
}
script.js:
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
// Конструктор 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(0,0,0,0,0);
// Привяжем новую переменную к world
var world = document.getElementById("world");
function update(){
// Задаем локальные переменные смещения
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
let dy = - PressUp;
let drx = MouseY;
let dry = - MouseX;
// Обнулим смещения мыши:
MouseX = MouseY = 0;
// Прибавляем смещения к координатам
pawn.x = pawn.x + dx;
pawn.y = pawn.y + dy;
pawn.z = pawn.z + dz;
// Если курсор захвачен, разрешаем вращение
if (lock){
pawn.rx = pawn.rx + drx;
pawn.ry = pawn.ry + dry;
};
// Изменяем координаты мира (для отображения)
world.style.transform =
"translateZ(" + (600 - 0) + "px)" +
"rotateX(" + (-pawn.rx) + "deg)" +
"rotateY(" + (-pawn.ry) + "deg)" +
"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
};
function CreateNewWorld(){
for (let i = 0; i < map.length; i++){
// Создание прямоугольника и придание ему стилей
let newElement = document.createElement("div");
newElement.className = "square";
newElement.id = "square" + i;
newElement.style.width = map[i][6] + "px";
newElement.style.height = map[i][7] + "px";
newElement.style.background = map[i][8];
newElement.style.transform = "translate3d(" +
(600 - map[i][6]/2 + map[i][0]) + "px," +
(400 - map[i][7]/2 + map[i][1]) + "px," +
(map[i][2]) + "px)" +
"rotateX(" + map[i][3] + "deg)" +
"rotateY(" + map[i][4] + "deg)" +
"rotateZ(" + map[i][5] + "deg)";
// Вставка прямоугольника в world
world.append(newElement);
}
}
CreateNewWorld();
TimerGame = setInterval(update,10);
Если все хорошо, переходим к следующему пункту.
5. Столкновения игрока с объектами мира
Мы создали технику движения, генератор мира из массива. Мы можем передвигаться по миру, который может быть красивым. Однако наш игрок еще никак не взаимодействует с ним. Чтобы это взаимодействие происходило, нам необходимо проверять, сталкивается ли игрок с каким-нибудь прямоугольником или нет? То есть, мы будем проверять наличие коллизий. Для начала вставим пустую функцию:
function collision(){
}
А вызывать ее будем в update():
// Обнулим смещения мыши:
MouseX = MouseY = 0;
// Проверяем коллизию с прямоугольниками
collision();
Как это происходит? Представим себе, что игрок – это шар с радиусом r. И он движется в сторону прямоугольника:
Очевидно, что если расстояние от шара до плоскости прямоугольника больше r, то коллизии точно не происходит. Чтобы узнать это расстояние, можно перевести координаты игрока в систему координат прямоугольника. Напишем функцию перевода из мировой системы в систему прямоугольника:
function coorTransform(x0,y0,z0,rxc,ryc,rzc){
let x1 = x0;
let y1 = y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
let x2 = x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
let y2 = y1;
let z2 = x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
let x3 = x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
let z3 = z2;
return [x3,y3,z3];
}
И обратную функцию:
function coorReTransform (x3,y3,z3,rxc,ryc,rzc){
let x2 = x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
let y2 = x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
let z2 = 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];
}
Вставим эти функции после функции update(). Я не буду объяснять, как это работает, потому что мне не хочется рассказывать курс аналитической геометрии. Скажу, что есть такие формулы перевода координат при вращении и мы просто ими воспользовались. С точки зрения прямоугольника наш игрок расположен вот так:
В этом случае условие коллизии становится таким: если после смещения шара на величину v (v – это вектор) координата z между –r и r, а координаты x и y лежат в пределах прямоугольника или отстоят от него на величину, не большую r, то объявляется коллизия. В этом случае координата игрока по z после смещения будет составлять r или – r (в зависимости от того, с какой стороны придет игрок). В соответствии с этим, смещение игрока изменяется. Мы специально вызываем коллизию перед тем, как в update() координаты игрока будут обновлены, чтобы вовремя изменить смещение. Таким образом, шар никогда не пересечется с прямоугольником, как бывает в других алгоритмах коллизии. Хотя физически игрок будет представлять собой, скорее, случае куб, мы не будем обращать на это внимание. Итак, реализуем это в javaScript:
function collision(){
for(let i = 0; i < map.length; i++){
// рассчитываем координаты игрока в системе координат прямоугольника
let x0 = (pawn.x - map[i][0]);
let y0 = (pawn.y - map[i][1]);
let z0 = (pawn.z - map[i][2]);
let x1 = x0 + dx;
let y1 = y0 + dy;
let z1 = z0 + dz;
let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
let point2 = new Array();
// Условие коллизии и действия при нем
if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
point1[2] = Math.sign(point0[2])*50;
point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
dx = point2[0] - x0;
dy = point2[1] - y0;
dz = point2[2] - z0;
}
};
}
x0,y0 и z0 – начальные координаты игрока в системе координат прямоугольника (без поворотов. x1,y1 и z1 – координаты игрока после смещения без учета коллизии. point0, point0, point1 и point2 – начальный радиус-вектор, радиус-вектор после смещения без коллизии и радиус-вектор с коллизией соответственно. map[3] и другие, если вы помните, это углы поворота прямоугольника. Заметим, что в условии мы к размерам прямоугольника прибавляем не 100, а 98. Это костыль, зачем, подумайте сами. Запустите игру и вы увидите довольно качественные столкновения.
Как видим, все эти действия происходят в цикле for для всех прямоугольников. При их большом количестве такая операция становится очень дорогой, так как тут и так есть 3 вызова функций преобразований координат, которые тоже производят достаточно много математических операций. Очевидно, что если прямоугольники находятся очень далеко от игрока, то коллизию считать не имеет смысла. Добавим это условие:
if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**2)){
let x1 = x0 + dx;
let y1 = y0 + dy;
let z1 = z0 + dz;
let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
let point2 = new Array();
// Условие коллизии и действия при нем
if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
point1[2] = Math.sign(point0[2])*50;
point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
dx = point2[0] - x0;
dy = point2[1] - y0;
dz = point2[2] - z0;
}
}
Итак, с коллизиями мы разобрались. Мы спокойно можем взбираться и по наклонным поверхностям, а возникновение багов возможно только на медленных системах, если, конечно, возможно. По сути, вся основная техническая часть на этом закончилась. Нам осталось лишь добавить частные вещи, такие как гравитация, вещи, меню, звуки, красивую графику. Но это достаточно легко сделать, а к самому движку, который мы только что сделали, это отношения не имеет. Поэтому об этом я расскажу в следующей части. А сейчас проверьте то, что у вас получилось с моим кодом:
index.html:
<!DOCTYPE HTML>
<HTML>
<HEAD>
<TITLE>Игра</TITLE>
<LINK rel="stylesheet" href="style.css">
<meta charset="utf-8">
</HEAD>
<BODY>
<div id="container">
<div id="world"></div>
<div id="pawn"></div>
</div>
</BODY>
</HTML>
<script src="script.js"></script>
style.css
#container{
position:absolute;
width:1200px;
height:800px;
border:2px solid #000000;
perspective:600px;
overflow:hidden;
}
#world{
position:absolute;
width:inherit;
height:inherit;
transform-style:preserve-3d;
}
.square{
position:absolute;
}
#pawn{
display:none;
position:absolute;
top:400px;
left:600px;
transform:translate(-50%,-50%);
width:100px;
height:100px;
}
script.js:
// Мировые константы
var pi = 3.141592;
var deg = pi/180;
// Конструктор 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][1]**2 + map[i][2]**2)){
let x1 = x0 + dx;
let y1 = y0 + dy;
let z1 = z0 + dz;
let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
let point2 = new Array();
// Условие коллизии и действия при нем
if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
point1[2] = Math.sign(point0[2])*50;
point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
dx = point2[0] - x0;
dy = point2[1] - y0;
dz = point2[2] - z0;
}
}
};
}
function coorTransform(x0,y0,z0,rxc,ryc,rzc){
let x1 = x0;
let y1 = y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
let x2 = x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
let y2 = y1;
let z2 = x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
let x3 = x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
let z3 = z2;
return [x3,y3,z3];
}
function coorReTransform(x3,y3,z3,rxc,ryc,rzc){
let x2 = x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
let y2 = x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
let z2 = 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);
===========
Источник:
habr.com
===========
Похожие новости:
- [SQL, Программирование] OLAP-отчеты. Построение для любой базы на SQL
- [C, Компьютерная анимация, Программирование, Реверс-инжиниринг] Разжимаем древний формат сжатия анимаций
- [JavaScript, Программирование, Разработка веб-сайтов, Учебный процесс в IT] Задачки для фронтенд-тренировки: doodle-place, Apple Podcasts, Site Blocker, парсинг CSV-файлов (перевод)
- [FPGA, Haskell, Компиляторы, Программирование микроконтроллеров] Встраивание Haskell: компиляторы и компиляция компиляторов (перевод)
- [Дизайн, Программирование микроконтроллеров] Интеграция в проект LVGL графической библиотеки для микроконтроллеров
- [JavaScript, ReactJS, TypeScript] Todolist на React Hooks + TypeScript: от сборки до тестирования
- [CSS, JavaScript] Atomizer vs Minimalist Notation (MN)
- [Разработка игр] Страх и ненависть в геймдеве: от первых шагов до первых денег
- [Администрирование баз данных, Микросервисы, Программирование] Использование Camunda для удобной оркестровки на основе REST и Workflow Engine (без Java)
- [C++, Программирование] Отображение данных в формате json на структуру c++ и обратно (работа над ошибками)
Теги для поиска: #_css, #_html, #_javascript, #_programmirovanie (Программирование), #_razrabotka_igr (Разработка игр), #_3digry (3d-игры), #_html, #_css, #_javascript, #_css, #_html, #_javascript, #_programmirovanie (
Программирование
), #_razrabotka_igr (
Разработка игр
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:20
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Современная вычислительная техника позволяет создавать классные компьютерные игры! И сейчас, достаточно популярны игры с 3d-графикой, так как, играя в них, ты окунаешься в вымышленный мир и теряешь всякую связь с реальностью. Развитие интернета и браузерных технологий сделало возможным запускать головоломки и стрелялки в любимом Хроме, Мозилле или еще в чем-то там (про Эксплорер помолчим) в онлайн-режиме, без загрузки. Так вот, здесь я расскажу о том, как создать простую трехмерную браузерную игру. Выбор жанра, сюжета и стилистики игры является достаточно интересной задачей, и от решения этих вопросов может зависеть успех игры. Кроме этого, свои нюансы вносит и выбор технологии, на основе которой будет создаваться продукт. Моя цель – показать элементарные основы этого увлекательного процесса, поэтому я буду делать 3-мерный лабиринт с незамысловатым оформлением. Более того, я это сделаю на чистом коде без использования библиотек и движков, типа three.js (хотя большие проекты лучше делать все-таки на нем), чтобы показать, как можно создать движок для своих нужд. Полностью самописная игра может быть оригинальной, а потому интересной. В общем, оба подхода имеют свои плюсы и минусы. Я полагаю, если вы читаете эту статью, то вам интересна тема создания игр для гугл Хром, а, значит, понимаете, как работает связка html-css-javaScript, поэтому не буду останавливаться на основах, а сразу приступлю к разработке. В html5 и css3, которые поддерживают все современные браузеры (Эксплорер не в счет), есть возможность расположения блоков в 3-мерном пространстве. Также есть элемент , в котором можно рисовать линии и графические примитивы. Большинство браузерных движков используют <сanvas>, так как на нем можно сделать больше вещей, да и производительность на нем выше. Но для простых вещей вполне можно использовать методы transform-3d, которые будут занимать меньше кода. 1. Инструменты для разработки Я использую для проверки сайтов и игр только 2 браузера: Chrome и Mozilla. Все остальные браузеры (кроме того самого Эксплорера) построены на движке первого, поэтому использовать их я не вижу смысла, ибо результаты точно такие же, как и в Chrome. Для написания кода достаточно Notepad++. 2. Как реализуется трехмерное пространство в html? Посмотрим на систему координат блока: По умолчанию, дочерний блок имеет координаты (left и top) 0 пикселей по x и 0 пикселей по y. Смещение (translate), также 0 пикселей по всем трем осям. Покажем это на примере, для чего создадим новую папку. В нем создадим файлы index.html, style.css и script.js. Откроем index.html и запишем туда следующее: <!DOCTYPE HTML>
<HTML> <HEAD> <TITLE>Игра</TITLE> <LINK rel="stylesheet" href="style.css"> <meta charset="utf-8"> </HEAD> <BODY> <div id="container"> <div id="world"> </div> </div> </BODY> </HTML> <script src="script.js"></script> В файле style.css зададим стили для элементов “container” и “world”. #container{
position:absolute; width:1200px; height:800px; border:2px solid #000000; } #world{ width:300px; height:300px; background-color:#C0FFFF; } Сохраним. Откроем index.html c помощью Chrome, получим: Попробуем применить translate3d к элементу “world”: #world{
width:300px; height:300px; background-color:#C0FFFF; transform:translate3d(200px,100px,0px); } Как вы поняли, я перешел в полноэкранный режим. Теперь зададим смещение по оси Z: transform:translate3d(200px,100px,-1000px); Если вы снова откроете html-файл в браузере, то никаких изменений вы не увидите. Чтобы увидеть изменения, нужно задать перспективу для объекта “container”: #container{
position:absolute; width:1200px; height:800px; border:2px solid #000000; perspective:600px; } В результате: Квадрат отдалился от нас. Как работает перспектива в html? Взглянем на картинку: d – расстояние от пользователя до объекта, а z – его координата. Отрицательный z (в html это translateZ) означает, что мы отдалили объект, а положительный – наоборот. Значение perspective определяет величину d. Если же свойство perspective не задано, то значение d принимается за бесконечность, а в этом случае объект визуально не изменяется для пользователя с изменением z. В нашем случае мы задали d = 600px. По умолчанию, точка взгляда перспективы находится в центре элемента, однако ее можно изменить путем задания свойства perspective-origin: . Теперь повернем “world” вокруг какой-нибудь оси. В сss можно использовать 2 способа вращения. Первый – вращение вокруг осей x,y и z. Для этого используются transform-свойства rotateX(), rotateY() и rotateZ(). Второй – вращение вокруг заданной оси с помощью свойства rotate3d(). Мы будем использовать первый способ, так как он больше подходит для наших задач. Обратите внимание, что оси вращения выходят из центра прямоугольника! Точка, относительно которой происходят трансформации, может быть изменена путем задания свойства translate-origin: . Итак, зададим вращение “world” по оси x: #world{
width:300px; height:300px; background-color:#C0FFFF; transform:translate3d(200px,100px,0px) rotateX(45deg); } Получим: Заметно смещение против часовой стрелки. Если же мы добавим rotateY(), то получим смещение уже по оси Y. Важно заметить, что при вращении блока оси вращения также поворачиваются. Вы также можете поэкспериментировать с различными значениями вращения. Теперь внутри блока “world” создадим еще один блок, для этого добавим тег в html-файл: <!DOCTYPE HTML>
<HTML> <HEAD> <TITLE>Игра</TITLE> <LINK rel="stylesheet" href="style.css"> <meta charset="utf-8"> </HEAD> <BODY> <div id="container"> <div id="world"> <div id="square1"></div> </div> </div> </BODY> </HTML> <script src="script.js"></script> В style.css добавим стили к этому блоку: #square1{
position:absolute; width:200px; height:200px; background-color:#FF0000; } Получим: То есть, элементы внутри блока “world” будут трансформироваться в составе этого блока. Попробуем повернуть “square1” по оси y, добавив к нему стиль вращения: transform: rotateY(30deg); В итоге: «Где вращение?» — спросите вы? На самом деле именно так выглядит проекция блока “square1” на плоскость, образуемую элементом “world”. Но нам нужна не проекция, а настоящее вращение. Чтобы все элементы внутри “world” стали объемными, необходимо применить к нему свойство transform-style:preserve-3d. После подстановки свойства внутрь списка стилей “world” проверим изменения: Отлично! Половина блока “square” скрылась за голубым блоком. Чтобы его полностью показать, уберем цвет блока “world”, а именно, удалим строку background-color:#C0FFFF; Если мы добавим еще прямоугольников внутрь блока “world”, то мы можем создать трехмерный мир. Сейчас же уберем смещение мира “world”, удалив строку со свойством transform в стилях для этого элемента. 3. Создаем движение в трехмерном мире Для того, чтобы пользователь мог по этому миру передвигаться, нужно задать обработчики нажатия клавиш и перемещения мыши. Управление будет стандартным, какое присутствует в большинстве 3д-шутеров. Клавишами W, S, A, D мы будем перемещаться вперед, назад, влево, вправо, пробелом мы будем прыгать (проще говоря – перемещаться вверх), а мышью мы будем менять направление взгляда. Для этого откроем пока еще пустой файл script.js. Сначала впишем туда такие переменные: // Нажата ли клавиша?
var PressBack = 0; var PressForward = 0; var PressLeft = 0; var PressRight = 0; var PressUp = 0; Изначально клавиши не нажаты. Если мы нажмем клавишу, то значение определенной переменной изменится на 1. Если отпустим ее, то она снова станет 0. Реализуем это посредством добавления обработчиков нажатия и отжатия клавиш: // Обработчик нажатия клавиш
document.addEventListener("keydown", (event) =>{ if (event.key == "a"){ PressLeft = 1; } if (event.key == "w"){ PressForward = 1; } if (event.key == "d"){ PressRight = 1; } if (event.key == "s"){ PressBack = 1; } if (event.keyCode == 32 && onGround){ PressUp = 1; } }); // Обработчик отжатия клавиш document.addEventListener("keyup", (event) =>{ if (event.key == "a"){ PressLeft = 0; } if (event.key == "w"){ PressForward = 0; } if (event.key == "d"){ PressRight = 0; } if (event.key == "s"){ PressBack = 0; } if (event.keyCode == 32){ PressUp = 0; } }); Номер 32 – код пробела. Как видите, тут появилась переменная onGround, указывающая на то, находимся ли мы на земле. Пока разрешим движение вверх, добавив после переменных press… переменную onGround: // На земле ли игрок?
var onGround = true; Итак, мы добавили алгоритм нажатия и отжатия. Теперь необходимо добавить само передвижение. Что, собственно, мы передвигаем. Представим, что у нас есть объект, который мы двинаем. Назовем его “pawn”. Как и принято у нормальных разработчиков, для него мы создадим отдельный класс “Player”. Классы в javaScript создаются, как ни странно, с помощью функций: function player(x,y,z,rx,ry) {
this.x = x; this.y = y; this.z = z; this.rx = rx; this.ry = ry; } Вставим этот код в script.js в самом начале файла. В конце же файла создадим объект данного типа: // Создаем новый объект
var pawn = new player(0,0,0,0,0); Распишем, что означают эти переменные. x, y, z – это начальные координаты игрока, rx, ry – углы его поворота относительно осей x и y в градусах. Последняя записанная строка означает, что мы создаем объект “pawn” типа “player” (специально пишу тип, а не класс, так как классы в javascript означают несколько другие вещи) с нулевыми начальными координатами. Когда мы двигаем объект, координата мира изменяться не должна, а должна изменяться координата «pawn». Это с точки зрения переменных. А с точки зрения пользователя, игрок находится на одном месте, а вот мир двигается. Таким образом, нужно заставить программу изменять координаты игрока, обрабатывать эти изменения и двигать, в конце концов, мир. На деле это проще, чем кажется. Итак, после загрузки документа в браузер мы запустим функцию, которая перерисовывает мир. Напишем функцию перерисовки: function update(){
// Высчитываем смещения let dx = (PressRight - PressLeft); let dz = - (PressForward - PressBack); let dy = PressUp; // Прибавляем смещения к координатам pawn.x = pawn.x + dx; pawn.y = pawn.y + dy; pawn.z = pawn.z + dz; // Изменяем координаты мира (для отображения) world.style.transform = "rotateX(" + (-pawn.rx) + "deg)" + "rotateY(" + (-pawn.ry) + "deg)" + "translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)"; }; В новых браузерах world будет соответствовать элементу с id=«world», однако надежнее ее присвоить перед функцией update() с помощью следующей конструкции: var world = document.getElementById("world");
Мы будем изменять положение мира каждые 10 мс (100 обновлений в секунду), для чего запустим бесконечный цикл: TimerGame = setInterval(update,10);
Запустим игру. Ура, теперь мы можем двигаться! Однако мир вылазит за пределы рамок элемента «container». Чтобы этого не происходило, зададим css-свойство для него в style.css. Добавим строку overflow:hidden; и посмотрим на изменения. Теперь мир остается в пределах контейнера. Вполне возможно, что вы не всегда понимаете, куда нужно записывать те или иные строчки кода, поэтому сейчас я вам представлю файлы, которые, как я полагаю, у вас должны получиться: index.html: <!DOCTYPE HTML>
<HTML> <HEAD> <TITLE>Игра</TITLE> <LINK rel="stylesheet" href="style.css"> <meta charset="utf-8"> </HEAD> <BODY> <div id="container"> <div id="world"> <div id="square1"></div> </div> </div> </BODY> </HTML> <script src="script.js"></script> style.css: #container{
position:absolute; width:1200px; height:800px; border:2px solid #000000; perspective:600px; overflow:hidden; } #world{ position:absolute; width:300px; height:300px; transform-style:preserve-3d; } #square1{ position:absolute; width:200px; height:200px; background-color:#FF0000; transform:rotateY(30deg); } script.js: // Конструктор Pawn
function player(x,y,z,rx,ry) { this.x = x; this.y = y; this.z = z; this.rx = rx; this.ry = ry; } // Нажата ли клавиша? var PressBack = 0; var PressForward = 0; var PressLeft = 0; var PressRight = 0; var PressUp = 0; // На земле ли игрок? var onGround = true; // Обработчик нажатия клавиш document.addEventListener("keydown", (event) =>{ if (event.key == "a"){ PressLeft = 1; } if (event.key == "w"){ PressForward = 1; } if (event.key == "d"){ PressRight = 1; } if (event.key == "s"){ PressBack = 1; } if (event.keyCode == 32 && onGround){ PressUp = 1; } }); // Обработчик отжатия клавиш document.addEventListener("keyup", (event) =>{ if (event.key == "a"){ PressLeft = 0; } if (event.key == "w"){ PressForward = 0; } if (event.key == "d"){ PressRight = 0; } if (event.key == "s"){ PressBack = 0; } if (event.keyCode == 32){ PressUp = 0; } }); // Создаем новый объект var pawn = new player(0,0,0,0,0); // Привяжем новую переменную к world var world = document.getElementById("world"); function update(){ // Задаем локальные переменные смещения let dx = (PressRight - PressLeft); let dz = - (PressForward - PressBack); let dy = - PressUp; // Прибавляем смещения к координатам pawn.x = pawn.x + dx; pawn.y = pawn.y + dy; pawn.z = pawn.z + dz; // Изменяем координаты мира (для отображения) world.style.transform = "rotateX(" + (-pawn.rx) + "deg)" + "rotateY(" + (-pawn.ry) + "deg)" + "translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)"; }; TimerGame = setInterval(update,10); Если у вас что-то по-другому, обязательно поправьте! Мы научились двигать персонажа, однако мы еще не умеем поворачивать его! Поворот персонажа, конечно же, будет осуществляться с помощью мыши. Для мыши к переменным состояния клавиш press… мы добавим переменные состояния движения мыши: // Нажата ли клавиша и двигается ли мышь?
var PressBack = 0; var PressForward = 0; var PressLeft = 0; var PressRight = 0; var PressUp = 0; var MouseX = 0; var MouseY = 0; А после обработчиков нажатия-отжатия вставим обработчик движения: // Обработчик движения мыши
document.addEventListener("mousemove", (event)=>{ MouseX = event.movementX; MouseY = event.movementY; }); В функцию update добавим поворот: // Задаем локальные переменные смещения
let dx = (PressRight - PressLeft); let dz = - (PressForward - PressBack); let dy = - PressUp; let drx = MouseY; let dry = - MouseX; // Прибавляем смещения к координатам pawn.x = pawn.x + dx; pawn.y = pawn.y + dy; pawn.z = pawn.z + dz; pawn.rx = pawn.rx + drx; pawn.ry = pawn.ry + dry; Обратите внимание на то, что движение мыши по оси y вращает pawn по оси x и наоборот. Если мы посмотрим на результат, то ужаснемся от увиденного. Дело в том, что если смещения нет, то MouseX и MouseY остаются прежними, а не приравниваются к нулю. Значит, после каждой итерации update смещения миши должно обнуляться: // Задаем локальные переменные смещения
let dx = (PressRight - PressLeft); let dz = - (PressForward - PressBack); let dy = - PressUp; let drx = MouseY; let dry = - MouseX; // Обнулим смещения мыши: MouseX = MouseY = 0; // Прибавляем смещения к координатам pawn.x = pawn.x + dx; pawn.y = pawn.y + dy; pawn.z = pawn.z + dz; pawn.rx = pawn.rx + drx; pawn.ry = pawn.ry + dry; Уже лучше, мы избавились от инерции вращения, однако вращение происходит все равно странно! Чтобы понять, что все-таки происходит, добавим div-элемент «pawn» внутрь «container»: <div id="container">
<div id="world"> <div id="square1"></div> </div> <div id="pawn"></div> </div> Зададим ему стили в style.css: #pawn{
position:absolute; width:100px; height:100px; top:400px; left:600px; transform:translate(-50%,-50%); background-color:#0000FF; } Проверим результат. Теперь все ровно! Единственное — синий квадрат остается впереди, но пока оставим это. Чтобы сделать игру от первого лица, а не от третьего, нужно приблизить мир к нам на значение perspective. Сделаем это в script.js в функции update(): world.style.transform =
"translateZ(600px)" + "rotateX(" + (-pawn.rx) + "deg)" + "rotateY(" + (-pawn.ry) + "deg)" + "translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)"; Теперь можно делать игру от первого лица. Скроем pawn добавив строку в style.css: #pawn{
display:none; position:absolute; top:400px; left:600px; width:100px; height:100px; transform:translate(-50%,-50%); background-color:#0000FF; } Отлично. Сразу скажу, что ориентироваться в мире с одним квадратом крайне тяжело, поэтому создадим площадку. Добавим в «world» блок «square2»: <div id="world">
<div id="square1"></div> <div id="square2"></div> </div> А в style.css добавим стили для него: #square2{
position:absolute; width:1000px; height:1000px; top:400px; left:600px; background-color:#00FF00; transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px); } Теперь все четко. Ну… не совсем. Когда мы нажимаем по клавишам, мы движемся строго по осям X и Z. А мы хотим сделать движение по направлению взгляда. Сделаем следующее: в самом начале файла script.js добавим 2 переменные: // Мировые константы
var pi = 3.141592; var deg = pi/180; Градус — это pi/180 от радиана. Нам придется применить синусы и косинусы, которые считаются от радиан. Что нужно сделать? Взгляните на рисунок: Когда наш взгляд направлен под углом и мы хотим пойти вперед, то изменятся обе координаты: X и Z. В случае перемещения в сторону тригонометрические функции просто поменяются местами, а перед образовавшимся синусом изменится знак. Изменим уравнения смещений в update(): // Задаем локальные переменные смещения
let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg); let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg); let dy = -PressUp; let drx = MouseY; let dry = - MouseX; Внимательно просмотрите все файлы полностью! Если у вас что-то оказалось не так, то потом обязательно буду ошибки, из-за которых вы сломаете голову! index.html: <!DOCTYPE HTML>
<HTML> <HEAD> <TITLE>Игра</TITLE> <LINK rel="stylesheet" href="style.css"> <meta charset="utf-8"> </HEAD> <BODY> <div id="container"> <div id="world"> <div id="square1"></div> <div id="square2"></div> </div> <div id="pawn"></div> </div> </BODY> </HTML> <script src="script.js"></script> style.css: #container{
position:absolute; width:1200px; height:800px; border:2px solid #000000; perspective:600px; overflow:hidden; } #world{ position:absolute; width:inherit; height:inherit; transform-style:preserve-3d; } #square1{ position:absolute; width:200px; height:200px; top:400px; left:600px; background-color:#FF0000; transform:translate(-50%,-50%) rotateY(30deg); } #square2{ position:absolute; width:1000px; height:1000px; top:400px; left:600px; background-color:#00FF00; transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px); } #pawn{ display:none; position:absolute; top:400px; left:600px; transform:translate(-50%,-50%); width:100px; height:100px; background-color:#0000FF; } script.js: // Мировые константы
var pi = 3.141592; var deg = pi/180; // Конструктор Pawn function player(x,y,z,rx,ry) { this.x = x; this.y = y; this.z = z; this.rx = rx; this.ry = ry; } // Нажата ли клавиша и двигается ли мышь? var PressBack = 0; var PressForward = 0; var PressLeft = 0; var PressRight = 0; var PressUp = 0; var MouseX = 0; var MouseY = 0; // На земле ли игрок? var onGround = true; // Обработчик нажатия клавиш document.addEventListener("keydown", (event) =>{ if (event.key == "a"){ PressLeft = 1; } if (event.key == "w"){ PressForward = 1; } if (event.key == "d"){ PressRight = 1; } if (event.key == "s"){ PressBack = 1; } if (event.keyCode == 32 && onGround){ PressUp = 1; } }); // Обработчик отжатия клавиш document.addEventListener("keyup", (event) =>{ if (event.key == "a"){ PressLeft = 0; } if (event.key == "w"){ PressForward = 0; } if (event.key == "d"){ PressRight = 0; } if (event.key == "s"){ PressBack = 0; } if (event.keyCode == 32){ PressUp = 0; } }); // Обработчик движения мыши document.addEventListener("mousemove", (event)=>{ MouseX = event.movementX; MouseY = event.movementY; }); // Создаем новый объект типа player var pawn = new player(0,0,0,0,0); // Привяжем новую переменную к world var world = document.getElementById("world"); function update(){ // Задаем локальные переменные смещения let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg); let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg); let dy = - PressUp; let drx = MouseY; let dry = - MouseX; // Обнулим смещения мыши: MouseX = MouseY = 0; // Прибавляем смещения к координатам pawn.x = pawn.x + dx; pawn.y = pawn.y + dy; pawn.z = pawn.z + dz; pawn.rx = pawn.rx + drx; pawn.ry = pawn.ry + dry; // Изменяем координаты мира (для отображения) world.style.transform = "translateZ(600px)" + "rotateX(" + (-pawn.rx) + "deg)" + "rotateY(" + (-pawn.ry) + "deg)" + "translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)"; }; TimerGame = setInterval(update,10); С движением мы почти разобрались. Но осталось неудобство: курсор мыши может двигаться только в пределах экрана. В трехмерных шутерах можно вращать мышью сколь угодно долго и сколь угодно далеко. Сделаем также: при нажатии на экран игры (на “container”) курсор будет пропадать, и мы сможем вращать мышью без ограничений на размер экрана. Активируем захват мыши при нажатии на экран, для чего перед обработчиками нажатия клавиш поставим обработчик нажатия мыши на “container”: // Привяжем новую переменную к container
var container = document.getElementById("container"); // Обработчик захвата курсора мыши container.onclick = function(){ container.requestPointerLock(); }; Теперь совсем другое дело. Однако лучше вообще сделать так, чтобы вращение производилось только тогда, когда курсор захвачен. Введем новую переменную после переменных нажатия клавиш press… // Введен ли захват мыши?
var lock = false; Добавим обработчик изменения состояния захвата курсора (захвачен или нет) перед обработчиком захвата курсора (извините за тавтологию): // Обработчик изменения состояния захвата курсора
document.addEventListener("pointerlockchange", (event)=>{ lock = !lock; }); А в update() добавим условие вращения “pawn”: // Если курсор захвачен, разрешаем вращение
if (lock){ pawn.rx = pawn.rx + drx; pawn.ry = pawn.ry + dry; }; А сам захват мыши при клике по контейнеру разрешим только тогда, когда курсор еще не захвачен: // Обработчик захвата курсора мыши
container.onclick = function(){ if (!lock) container.requestPointerLock(); }; С движением мы полностью разобрались. Перейдем к генерации мира 4. Загрузка карты Мир в нашем случае удобнее всего представить в виде множества прямоугольников, имеющих разное местоположение, поворот, размеры и цвет. Вместо цвета также можно использовать текстуры. На самом деле, все современные трехмерные миры в играх – это набор треугольников и прямоугольников, которые называют полигонами. В крутых играх их количество может достигать десятков тысяч в одном только кадре. У нас же их будет около сотни, так как браузер сам по себе имеет невысокую графическую производительность. В предыдущих пунктах мы вставляли блоки “div” внутрь “world”. Но если таких блоков много (сотни), то вставлять каждый из них в контейнер очень утомительно. Да и уровней может быть много. Поэтому пусть эти прямоугольники вставляет javaScript, а не мы. Для него же мы будем создавать специальный массив. Откроем index.html и удалим из блока “world” все внутренние блоки: <BODY>
<div id="container"> <div id="world"></div> <div id="pawn"></div> </div> </BODY> Как видим, в “world” теперь ничего нет. В style.css удалим стили для #square1 и #square2 (вообще удалим #square1 и #square2 из этого файла), а вместо них создадим стили для класса .square, который будет общим для всех прямоугольников. Причем зададим для него только одно свойство: .square{
position:absolute; } Теперь создадим массив прямоугольников (запихнем его, примеру, между конструктором player и переменными press… в script.js): // Массив прямоугольников
var map = [ [0,0,1000,0,180,0,2000,200,"#F0C0FF"], [0,0,-1000,0,0,0,2000,200,"#F0C0FF"], [1000,0,0,0,-90,0,2000,200,"#F0C0FF"], [-1000,0,0,0,90,0,2000,200,"#F0C0FF"], [0,100,0,90,0,0,2000,2000,"#666666"] ] Можно было это сделать в виде конструктора, но пока обойдемся чисто массивом, так как запуск цикла расстановки прямоугольников проще реализовать именно через массивы, а не через конструкторы. Я же поясню, что означают цифры в нем. Массив map содержит одномерные массивы из 9 переменных: [,,,,,,,,]. Я думаю, вы понимаете, что первые три числа – это координаты центра прямоугольника, вторые три числа – углы поворота в градусах (относительно того же центра), затем два числа – его размеры и последнее число – фон. Причем фон может быть сплошным цветом, градиентом или фотографией. Последнее очень удобно использовать в качестве текстур. Массив мы записали, теперь запишем функцию, которая переделает этот массив в собственно прямоугольники: function CreateNewWorld(){
for (let i = 0; i < map.length; i++){ // Создание прямоугольника и придание ему стилей let newElement = document.createElement("div"); newElement.className = "square"; newElement.id = "square" + i; newElement.style.width = map[i][6] + "px"; newElement.style.height = map[i][7] + "px"; newElement.style.background = map[i][8]; newElement.style.transform = "translate3d(" + (600 - map[i][6]/2 + map[i][0]) + "px," + (400 - map[i][7]/2 + map[i][1]) + "px," + (map[i][2]) + "px)" + "rotateX(" + map[i][3] + "deg)" + "rotateY(" + map[i][4] + "deg)" + "rotateZ(" + map[i][5] + "deg)"; // Вставка прямоугольника в world world.append(newElement); } } Поясню, что происходит: мы создаем новую переменную, которая указывает на только что созданный элемент. Ему мы присваиваем id и css-класс (именно это и имеется ввиду под словом класс в языке javaScript), задаем ширину с высотой, фон и трансформацию. Примечательно, что в трансформации помимо координат центра прямоугольника мы указываем смещение на 600 и 400 и половины размеров для того, чтобы центр прямоугольника точно оказался в точке с нужными координатами. Запустим генератор мира перед таймером: CreateNewWorld();
TimerGame = setInterval(update,10); Теперь мы видим площадку с розовыми стенами и серым полом. Как видите, создание карты технически несложно реализовать. А в результате ваш код в трех файлах должен получиться примерно таким: index.html: <!DOCTYPE HTML>
<HTML> <HEAD> <TITLE>Игра</TITLE> <LINK rel="stylesheet" href="style.css"> <meta charset="utf-8"> </HEAD> <BODY> <div id="container"> <div id="world"></div> <div id="pawn"></div> </div> </BODY> </HTML> <script src="script.js"></script> style.css #container{
position:absolute; width:1200px; height:800px; border:2px solid #000000; perspective:600px; overflow:hidden; } #world{ position:absolute; width:inherit; height:inherit; transform-style:preserve-3d; } .square{ position:absolute; } #pawn{ display:none; position:absolute; top:400px; left:600px; transform:translate(-50%,-50%); width:100px; height:100px; } script.js: // Мировые константы
var pi = 3.141592; var deg = pi/180; // Конструктор 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(0,0,0,0,0); // Привяжем новую переменную к world var world = document.getElementById("world"); function update(){ // Задаем локальные переменные смещения let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg); let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg); let dy = - PressUp; let drx = MouseY; let dry = - MouseX; // Обнулим смещения мыши: MouseX = MouseY = 0; // Прибавляем смещения к координатам pawn.x = pawn.x + dx; pawn.y = pawn.y + dy; pawn.z = pawn.z + dz; // Если курсор захвачен, разрешаем вращение if (lock){ pawn.rx = pawn.rx + drx; pawn.ry = pawn.ry + dry; }; // Изменяем координаты мира (для отображения) world.style.transform = "translateZ(" + (600 - 0) + "px)" + "rotateX(" + (-pawn.rx) + "deg)" + "rotateY(" + (-pawn.ry) + "deg)" + "translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)"; }; function CreateNewWorld(){ for (let i = 0; i < map.length; i++){ // Создание прямоугольника и придание ему стилей let newElement = document.createElement("div"); newElement.className = "square"; newElement.id = "square" + i; newElement.style.width = map[i][6] + "px"; newElement.style.height = map[i][7] + "px"; newElement.style.background = map[i][8]; newElement.style.transform = "translate3d(" + (600 - map[i][6]/2 + map[i][0]) + "px," + (400 - map[i][7]/2 + map[i][1]) + "px," + (map[i][2]) + "px)" + "rotateX(" + map[i][3] + "deg)" + "rotateY(" + map[i][4] + "deg)" + "rotateZ(" + map[i][5] + "deg)"; // Вставка прямоугольника в world world.append(newElement); } } CreateNewWorld(); TimerGame = setInterval(update,10); Если все хорошо, переходим к следующему пункту. 5. Столкновения игрока с объектами мира Мы создали технику движения, генератор мира из массива. Мы можем передвигаться по миру, который может быть красивым. Однако наш игрок еще никак не взаимодействует с ним. Чтобы это взаимодействие происходило, нам необходимо проверять, сталкивается ли игрок с каким-нибудь прямоугольником или нет? То есть, мы будем проверять наличие коллизий. Для начала вставим пустую функцию: function collision(){
} А вызывать ее будем в update(): // Обнулим смещения мыши:
MouseX = MouseY = 0; // Проверяем коллизию с прямоугольниками collision(); Как это происходит? Представим себе, что игрок – это шар с радиусом r. И он движется в сторону прямоугольника: Очевидно, что если расстояние от шара до плоскости прямоугольника больше r, то коллизии точно не происходит. Чтобы узнать это расстояние, можно перевести координаты игрока в систему координат прямоугольника. Напишем функцию перевода из мировой системы в систему прямоугольника: function coorTransform(x0,y0,z0,rxc,ryc,rzc){
let x1 = x0; let y1 = y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg); let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg); let x2 = x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg); let y2 = y1; let z2 = x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg); let x3 = x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg); let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg); let z3 = z2; return [x3,y3,z3]; } И обратную функцию: function coorReTransform (x3,y3,z3,rxc,ryc,rzc){
let x2 = x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg); let y2 = x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg); let z2 = 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]; } Вставим эти функции после функции update(). Я не буду объяснять, как это работает, потому что мне не хочется рассказывать курс аналитической геометрии. Скажу, что есть такие формулы перевода координат при вращении и мы просто ими воспользовались. С точки зрения прямоугольника наш игрок расположен вот так: В этом случае условие коллизии становится таким: если после смещения шара на величину v (v – это вектор) координата z между –r и r, а координаты x и y лежат в пределах прямоугольника или отстоят от него на величину, не большую r, то объявляется коллизия. В этом случае координата игрока по z после смещения будет составлять r или – r (в зависимости от того, с какой стороны придет игрок). В соответствии с этим, смещение игрока изменяется. Мы специально вызываем коллизию перед тем, как в update() координаты игрока будут обновлены, чтобы вовремя изменить смещение. Таким образом, шар никогда не пересечется с прямоугольником, как бывает в других алгоритмах коллизии. Хотя физически игрок будет представлять собой, скорее, случае куб, мы не будем обращать на это внимание. Итак, реализуем это в javaScript: function collision(){
for(let i = 0; i < map.length; i++){ // рассчитываем координаты игрока в системе координат прямоугольника let x0 = (pawn.x - map[i][0]); let y0 = (pawn.y - map[i][1]); let z0 = (pawn.z - map[i][2]); let x1 = x0 + dx; let y1 = y0 + dy; let z1 = z0 + dz; let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]); let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]); let point2 = new Array(); // Условие коллизии и действия при нем if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){ point1[2] = Math.sign(point0[2])*50; point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); dx = point2[0] - x0; dy = point2[1] - y0; dz = point2[2] - z0; } }; } x0,y0 и z0 – начальные координаты игрока в системе координат прямоугольника (без поворотов. x1,y1 и z1 – координаты игрока после смещения без учета коллизии. point0, point0, point1 и point2 – начальный радиус-вектор, радиус-вектор после смещения без коллизии и радиус-вектор с коллизией соответственно. map[3] и другие, если вы помните, это углы поворота прямоугольника. Заметим, что в условии мы к размерам прямоугольника прибавляем не 100, а 98. Это костыль, зачем, подумайте сами. Запустите игру и вы увидите довольно качественные столкновения. Как видим, все эти действия происходят в цикле for для всех прямоугольников. При их большом количестве такая операция становится очень дорогой, так как тут и так есть 3 вызова функций преобразований координат, которые тоже производят достаточно много математических операций. Очевидно, что если прямоугольники находятся очень далеко от игрока, то коллизию считать не имеет смысла. Добавим это условие: if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**2)){
let x1 = x0 + dx; let y1 = y0 + dy; let z1 = z0 + dz; let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]); let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]); let point2 = new Array(); // Условие коллизии и действия при нем if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){ point1[2] = Math.sign(point0[2])*50; point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); dx = point2[0] - x0; dy = point2[1] - y0; dz = point2[2] - z0; } } Итак, с коллизиями мы разобрались. Мы спокойно можем взбираться и по наклонным поверхностям, а возникновение багов возможно только на медленных системах, если, конечно, возможно. По сути, вся основная техническая часть на этом закончилась. Нам осталось лишь добавить частные вещи, такие как гравитация, вещи, меню, звуки, красивую графику. Но это достаточно легко сделать, а к самому движку, который мы только что сделали, это отношения не имеет. Поэтому об этом я расскажу в следующей части. А сейчас проверьте то, что у вас получилось с моим кодом: index.html: <!DOCTYPE HTML>
<HTML> <HEAD> <TITLE>Игра</TITLE> <LINK rel="stylesheet" href="style.css"> <meta charset="utf-8"> </HEAD> <BODY> <div id="container"> <div id="world"></div> <div id="pawn"></div> </div> </BODY> </HTML> <script src="script.js"></script> style.css #container{
position:absolute; width:1200px; height:800px; border:2px solid #000000; perspective:600px; overflow:hidden; } #world{ position:absolute; width:inherit; height:inherit; transform-style:preserve-3d; } .square{ position:absolute; } #pawn{ display:none; position:absolute; top:400px; left:600px; transform:translate(-50%,-50%); width:100px; height:100px; } script.js: // Мировые константы
var pi = 3.141592; var deg = pi/180; // Конструктор 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][1]**2 + map[i][2]**2)){ let x1 = x0 + dx; let y1 = y0 + dy; let z1 = z0 + dz; let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]); let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]); let point2 = new Array(); // Условие коллизии и действия при нем if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){ point1[2] = Math.sign(point0[2])*50; point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); dx = point2[0] - x0; dy = point2[1] - y0; dz = point2[2] - z0; } } }; } function coorTransform(x0,y0,z0,rxc,ryc,rzc){ let x1 = x0; let y1 = y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg); let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg); let x2 = x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg); let y2 = y1; let z2 = x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg); let x3 = x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg); let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg); let z3 = z2; return [x3,y3,z3]; } function coorReTransform(x3,y3,z3,rxc,ryc,rzc){ let x2 = x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg); let y2 = x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg); let z2 = 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); =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_razrabotka_igr ( Разработка игр ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:20
Часовой пояс: UTC + 5