Здравствуйте. В этой серии постов я расскажу, как я создавал браузерный клон игры Tron на node.js. В игре Tron вы управляете мотоциклом, который постоянно движется и оставляет за собой непроходимый шлейф. Вы можете только изменять направление движения вашего мотоцикла. На той же самой карте вместе с вами играют другие участники с теме же условиями. Так как игровая площадь ограничена, то со временем избегать столкновений с мотоциклами, стенами и шлейфами становится с каждой секундой сложнее. Выигрывает тот, кто продержится дольше всех.
Это вводная часть, в ней нет ничего о самом node.js, зато есть о много о клиентской части. Объяснение постараюсь дать детальное, особенно в плане написания функций.
Каждый подход имеет свои преимущества и недостатки. Я знал о сложностях работы с canvas, когда работал над Maze; о сложностях с WebGL узнал, когда работал над Amoeba; а вот способ с html-тегами мне попробовать еще не удалось, отчего он мне казался наиболее простым и подходящим. Изначально я планировал игру с очень простой реализацией, особенно в плане графики, так как много элементов отображать и не придется.
Кроме того, я привык работать с тегами, используя jQuery, но при портировании игры Web Developer на мобильные платформы с использованием phonegap, производительность конечного приложения оказывалась слишком низкая. Я винил jQuery, поэтому решил в этот раз освежить свои знания и попробовать сделать все на чистом js.
Теперь, когда у нас есть игровое поле, добавим мотоцикл. Создадим для него класс, который позволит нам создавать несколько экземпляров мотоциклов для их отображения.
Опишу подробнее каждое добавленное поле, чтобы было понятно, чем я думал, когда это делал. Оставляемые шлейфы я решил делать с помощью тех же тегов div, изменяя им положение относительно игрового поля, а так же меняя ширину и высоту, если нужно. Поля:
number - это своего рода идентификатор мотоцикла, то есть, некоторое уникальное для игрового мира число, чтобы можно было отличить один мотоцикл от другого;
Метод allocate для первоначального размещения мотоцикла на игровом поле. Самому элементу мы назначаем дополнительный css-класс с суффиксом номера мотоцикла, чтобы с помощью css можно было выставить цвета всем шлейфам мотоцикла.
Метод createHtml, который создает html-элемент нашего шлейфа и назначает ему ширину, высоту и положение. Нам нужно создавать новый блок каждый раз, когда мотоцикл поворачивает.
Метод move, который двигает мотоцикл в текущем направлении на некоторую величину stepSize. Фактически, мы изменяем положение блока шлейфа и его высоту или ширину в зависимости от направления:
И два последних метода turnRight и turnLeft, которые создают новый блок шлейфа и изменяют направление движения мотоцикла:
Класс для мотоцикла у нас готов. Теперь его можно добавить на игровое поле. Для управления мотоциклом добавим обработчики события нажатия клавиш. Для удобства продублируем стрелки с кнопками A и D. Движение мотоцикла будем осуществлять по интервалу.
Мы решили украшать всё с помощью css, поэтому нам нужно подключить такие стили:
Эти стили дадут нам такую картинку:
Теперь нужно только вызвать функцию startGame() и наш мотоцикл поедет, вместе с двумя другими. Но у нас нет никаких ограничений на движение, а они нужны. Мы будем останавливать движение, если мотоцикл столкнулся с шлейфом от любого мотоцикла, либо с границами игрового поля. Для этого нам нужна функция определения столкновения.
Теперь задача состоит в том, чтобы определить, пересёк ли мотоцикл какой-либо отрезок прямой. Мы упростим себе задачу и сделаем шаг движения мотоциклов таким, чтобы свести задачу к определению лежит ли мотоцикл на отрезке в данный момент. Этого нетрудно добиться, если у нас ширина поля 800, высота 600. В таком случае шаг движения stepSize для мотоциклов можно брать любой из общих делителей 800 и 600. Возьмем 10.
Функция определения, лежит ли точка на отрезке, содержит в себе только сравнения.
1. Если y-координаты мотоцикла и начала отрезка равны, то это горизонтальный отрезок, и нам нужно сравнить x-координаты. Координата x мотоцикла должна быть больше наименьшей x-координаты (начала) отрезка и меньше наибольшей (конца) y-координаты отрезка.
2. Если x-координаты мотоцикла и начала отрезка равны, то это вертикальный отрезок, и нам нужно сравнить y-координаты аналогично первому случаю.
Стоит отметить, что y-координата у нас идет вниз. Хоть мы и подобрали числа так, чтобы координаты получались только целыми числами, мы все же будем сравнивать их с некоторой поправкой на точность. Для этого введем переменную eps и положим её равной какому-либо малому числу, например, 0.01. Вместо if (y1 == y), будем писать if (y1 - y < eps). Это позволит нам сравнивать на равенство числа, которые отличаются на величину порядка 0,001. Хотя, конечно, сильно усложняет читаемость кода.
В итоге нам нужно пробежать по всем мотоциклам и определить, находится ли он в каком либо отрезке из массива lines:
Как видно по коду, при сборке массива lines мы храним в последнем отрезке мотоцикла номер самого мотоцикла, а в цикле по мотоциклам мы проверяем равенство номеров. Это нужно, потому что последний отрезок не должен участвовать в проверке для того же самого мотоцикла. Если мы выясняем, что мотоцикл столкнулся с чем-то, то мы вызываем метод bike.collide(), который нужно добавить:
Так как мы обнуляем направление мотоцикла, то он не сможет больше двигаться.
Вызов функции проверки столкновений нужно поместить в основной цикл после вызова движения:
html-тегов для отображения игры в нашем случае не принесло проблем и не создало трудностей, а значит можно перейти к клиент-серверному взаимодействую для многопользовательской игры на node.js.
Это вводная часть, в ней нет ничего о самом node.js, зато есть о много о клиентской части. Объяснение постараюсь дать детальное, особенно в плане написания функций.
Выбор способа отображения
Для отображения игровой графики в браузере мне известно несколько способов:- рисование на canvas;
- использование WebGL;
- использование html-тегов и раскрашивание их с помощью css
Каждый подход имеет свои преимущества и недостатки. Я знал о сложностях работы с canvas, когда работал над Maze; о сложностях с WebGL узнал, когда работал над Amoeba; а вот способ с html-тегами мне попробовать еще не удалось, отчего он мне казался наиболее простым и подходящим. Изначально я планировал игру с очень простой реализацией, особенно в плане графики, так как много элементов отображать и не придется.
Кроме того, я привык работать с тегами, используя jQuery, но при портировании игры Web Developer на мобильные платформы с использованием phonegap, производительность конечного приложения оказывалась слишком низкая. Я винил jQuery, поэтому решил в этот раз освежить свои знания и попробовать сделать все на чистом js.
Создаем игровое поле и мотоцикл
Для начала создадим контейнер с помощью добавления обычного div. Он же и будет игровым полем.var body = document.getElementsByTagName('body')[0]; var gameContainer = document.createElement("div"); gameContainer.className = "game"; body.appendChild(gameContainer);В css добавим пару стилей для установки ширины, высоты, фона, но это не стоит внимания.
Теперь, когда у нас есть игровое поле, добавим мотоцикл. Создадим для него класс, который позволит нам создавать несколько экземпляров мотоциклов для их отображения.
function Bike(number, pos) { this.number = number; this.x = pos[0] || 0; this.y = pos[1] || 0; this.direction = "r"; this.turnPoints = []; this.currentHtml = null; this.defaultWidth = 5; this.defaultHeight = 5; this.currentHtmlWidth = this.defaultWidth; this.currentHtmlHeight = this.defaultHeight; this.container = null; this.turnPoints.push(pos); this.headHtml = null; }
Опишу подробнее каждое добавленное поле, чтобы было понятно, чем я думал, когда это делал. Оставляемые шлейфы я решил делать с помощью тех же тегов div, изменяя им положение относительно игрового поля, а так же меняя ширину и высоту, если нужно. Поля:
number - это своего рода идентификатор мотоцикла, то есть, некоторое уникальное для игрового мира число, чтобы можно было отличить один мотоцикл от другого;
- x,y - позиция мотоцикла;
- direction - направление движения мотоцикла, а чтобы было понятнее, я решил использовать значения u, r, d, l, которые сосуществуют up, right, down, left;
- turnPoints - массив, который содержит координаты точек поворотов; они нужны мне для определения столкновений, т. к. по ним можно построить полные траектории;
- defaultWidth, defaultHeight - это ширина и высота шлейфа по умолчанию;
- currentHtmlWidth, currentHtmlHeight - это ширина и высота шлейфа в данный момент; т. к. мы изменяем высоту блока шлейфа, когда движемся по вертикали, и изменяем ширину, когда движемся по горизонтали;
- container - это еще один div-блок, в который мы будем прикреплять все шлейфы и голову мотоцикла
- headHtml - div-блок для обозначения головы (самого мотоцикла)
Метод allocate для первоначального размещения мотоцикла на игровом поле. Самому элементу мы назначаем дополнительный css-класс с суффиксом номера мотоцикла, чтобы с помощью css можно было выставить цвета всем шлейфам мотоцикла.
Bike.prototype.allocate = function(parent) { this.container = document.createElement("div"); this.container.className = "bike bike-" + this.number; parent.appendChild(this.container); this.headHtml = document.createElement("div"); this.headHtml.className = "bike-head"; this.container.appendChild(this.headHtml) this.headHtml.style.left = (this.x - 1) + "px"; this.headHtml.style.top = (this.y - 1) + "px"; }
Метод createHtml, который создает html-элемент нашего шлейфа и назначает ему ширину, высоту и положение. Нам нужно создавать новый блок каждый раз, когда мотоцикл поворачивает.
Bike.prototype.createHtml = function() { this.currentHtml = document.createElement("div"); this.currentHtml.className = "bike-trail"; this.currentHtml.style.left = this.x + "px"; this.currentHtml.style.top = this.y + "px"; this.currentHtml.style.width = this.currentHtmlWidth + "px"; this.currentHtml.style.height = this.currentHtmlHeight + "px"; this.container.appendChild(this.currentHtml); }
Метод move, который двигает мотоцикл в текущем направлении на некоторую величину stepSize. Фактически, мы изменяем положение блока шлейфа и его высоту или ширину в зависимости от направления:
Bike.prototype.move = function(stepSize) { if (!this.currentHtml) { this.createHtml(); } switch (this.direction) { case "u": this.y -= stepSize; this.currentHtmlHeight += stepSize; this.currentHtml.style.height = this.currentHtmlHeight + "px"; this.currentHtml.style.top = this.y + "px"; break; case "r": this.x += stepSize; this.currentHtmlWidth += stepSize; this.currentHtml.style.width = this.currentHtmlWidth + "px"; break; case "d": this.y += stepSize; this.currentHtmlHeight += stepSize; this.currentHtml.style.height = this.currentHtmlHeight + "px"; break; case "l": this.x -= stepSize; this.currentHtmlWidth += stepSize; this.currentHtml.style.width = this.currentHtmlWidth + "px"; this.currentHtml.style.left = this.x + "px"; break; } this.headHtml.style.left = (this.x - 1) + "px"; this.headHtml.style.top = (this.y - 1) + "px"; }
И два последних метода turnRight и turnLeft, которые создают новый блок шлейфа и изменяют направление движения мотоцикла:
Bike.prototype.turnRight = function() { this.currentHtml = null; this.currentHtmlWidth = this.defaultWidth; this.currentHtmlHeight = this.defaultHeight; this.turnPoints.push([this.x, this.y]); switch (this.direction) { case "r": this.direction = "d"; break; case "d": this.direction = "l"; break; case "l": this.direction = "u"; break; case "u": this.direction = "r"; break; } } Bike.prototype.turnLeft = function() { this.currentHtml = null; this.currentHtmlWidth = this.defaultWidth; this.currentHtmlHeight = this.defaultHeight; this.turnPoints.push([this.x, this.y]); switch (this.direction) { case "r": this.direction = "u"; break; case "d": this.direction = "r"; break; case "l": this.direction = "d"; break; case "u": this.direction = "l"; break; } }
Класс для мотоцикла у нас готов. Теперь его можно добавить на игровое поле. Для управления мотоциклом добавим обработчики события нажатия клавиш. Для удобства продублируем стрелки с кнопками A и D. Движение мотоцикла будем осуществлять по интервалу.
var keyRight = 39; var keyD = 68; var keyLeft = 37; var keyA = 65; var myBike; var moveInterval = 50; function startGame() { var body = document.getElementsByTagName('body')[0]; var gameContainer = document.createElement("div"); gameContainer.className = "game"; body.appendChild(gameContainer); bindEvents(body); var bike1 = new Bike(1, [100, 100]); bike1.allocate(gameContainer); bikes.push(bike1); var bike2 = new Bike(2, [700, 300]); bike2.allocate(gameContainer); bike2.direction = "l"; bikes.push(bike2); var bike3 = new Bike(3, [50, 500]); bike3.allocate(gameContainer); bike3.direction = "u"; bikes.push(bike3); myBike = bike1; window.setInterval(mainLoop, mainLoopInterval); } function mainLoop() { for (var b in bikes) { var bike = bikes[b]; bike.move(10); } } function bindEvents(container) { container.onkeypress = function(e) { e = e || window.event; switch (e.keyCode) { case keyRight: case keyD: myBike.turnRight(); break; case keyLeft: case keyA: myBike.turnLeft(); break; } } }где bikes - это массив, который мы предварительно создали пустым.
Мы решили украшать всё с помощью css, поэтому нам нужно подключить такие стили:
.game { height: 600px; width: 800px; background: #eee; border: 1px solid #000; } .game .bike { position: relative; } .game .bike .bike-trail { position: absolute; } .game .bike-1 .bike-trail { background-color: #f00; } .game .bike-2 .bike-trail { background-color: #0f0; } .game .bike-3 .bike-trail { background-color: #00f; } .game .bike .bike-head { position: absolute; z-index: 10; width: 5px; height: 5px; border: 1px solid #000; }
Эти стили дадут нам такую картинку:
Теперь нужно только вызвать функцию startGame() и наш мотоцикл поедет, вместе с двумя другими. Но у нас нет никаких ограничений на движение, а они нужны. Мы будем останавливать движение, если мотоцикл столкнулся с шлейфом от любого мотоцикла, либо с границами игрового поля. Для этого нам нужна функция определения столкновения.
Определение столкновений
Все объекты, с которыми нам нужно находить столкновения, в этой игре являются прямыми линиями - это границы игрового поля и шлейфы мотоциклов. Любую прямую на плоскости мы можем построить по двум точкам. Но нам нужна не сама прямая, а именно отрезки с началом и концом. Для начала соберем координаты начала и конца для каждого отрезка в один массив./* line = [x1, y1, x2, y2] */ var lines = []; lines.push([0, 0, gameWidth, 0]); lines.push([gameWidth, 0, gameWidth, gameHeight]); lines.push([gameWidth, gameHeight, 0, gameHeight]); lines.push([0, gameHeight, 0, 0]); /* add lines from bikes' trails */ for (var b in bikes) { var bike = bikes[b]; if (bike.turnPoints.length > 1) { for (var i = 1; i < bike.turnPoints.length; i++) { lines.push([ bike.turnPoints[i - 1][0], bike.turnPoints[i - 1][1], bike.turnPoints[i][0], bike.turnPoints[i][1] ]); } } var lastPoint = bike.turnPoints[bike.turnPoints.length - 1]; lines.push([ lastPoint[0], lastPoint[1], bike.x, bike.y, bike.number ]); }
Теперь задача состоит в том, чтобы определить, пересёк ли мотоцикл какой-либо отрезок прямой. Мы упростим себе задачу и сделаем шаг движения мотоциклов таким, чтобы свести задачу к определению лежит ли мотоцикл на отрезке в данный момент. Этого нетрудно добиться, если у нас ширина поля 800, высота 600. В таком случае шаг движения stepSize для мотоциклов можно брать любой из общих делителей 800 и 600. Возьмем 10.
Функция определения, лежит ли точка на отрезке, содержит в себе только сравнения.
1. Если y-координаты мотоцикла и начала отрезка равны, то это горизонтальный отрезок, и нам нужно сравнить x-координаты. Координата x мотоцикла должна быть больше наименьшей x-координаты (начала) отрезка и меньше наибольшей (конца) y-координаты отрезка.
2. Если x-координаты мотоцикла и начала отрезка равны, то это вертикальный отрезок, и нам нужно сравнить y-координаты аналогично первому случаю.
Стоит отметить, что y-координата у нас идет вниз. Хоть мы и подобрали числа так, чтобы координаты получались только целыми числами, мы все же будем сравнивать их с некоторой поправкой на точность. Для этого введем переменную eps и положим её равной какому-либо малому числу, например, 0.01. Вместо if (y1 == y), будем писать if (y1 - y < eps). Это позволит нам сравнивать на равенство числа, которые отличаются на величину порядка 0,001. Хотя, конечно, сильно усложняет читаемость кода.
В итоге нам нужно пробежать по всем мотоциклам и определить, находится ли он в каком либо отрезке из массива lines:
var eps = 0.01; for (var b in bikes) { var bike = bikes[b]; var x = bike.x, y = bike.y; for (var li in lines) { var line = lines[li]; var x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3]; if (typeof line[4] === "number" && line[4] === bike.number) { /* last line of current bike */ continue; } if ((Math.abs(y1 - y) < eps && ((x > x1 - eps && x < x2 + eps) || (x > x2 - eps && x < x1 + eps))) || (Math.abs(x1 - x) < eps && ((y > y1 - eps && y < y2 + eps) || (y > y2 - eps && y < y1 + eps)))) { bike.collide(); } } }
Как видно по коду, при сборке массива lines мы храним в последнем отрезке мотоцикла номер самого мотоцикла, а в цикле по мотоциклам мы проверяем равенство номеров. Это нужно, потому что последний отрезок не должен участвовать в проверке для того же самого мотоцикла. Если мы выясняем, что мотоцикл столкнулся с чем-то, то мы вызываем метод bike.collide(), который нужно добавить:
Bike.prototype.collide = function() { console.log("collide"); this.direction = null; }
Так как мы обнуляем направление мотоцикла, то он не сможет больше двигаться.
Вызов функции проверки столкновений нужно поместить в основной цикл после вызова движения:
function mainLoop() { for (var b in bikes) { var bike = bikes[b]; bike.move(10); } detectCollisions(); }
Заключение
Мы создали класс для мотоцикла, добавили игровой контейнер и поместили в него несколько экземпляров мотоциклов. Мы научили игру определять столкновения и останавливать движения столкнувшихся мотоциклов. Таким образом мы проверили, что использованиеhtml-тегов для отображения игры в нашем случае не принесло проблем и не создало трудностей, а значит можно перейти к клиент-серверному взаимодействую для многопользовательской игры на node.js.
Комментариев нет:
Отправить комментарий