Создать игру в HTML5 Canvas совсем не сложно, если вам известен принцип работы создаваемой игры. В этом уроке вы узнаете, как сделать в Canvas игру всех народов: пинг-понг. Прежде всего, давайте взглянем на основную концепцию и разберем логику этой игры.
- Должно быть две дощечки и шарик.
- Шарик должен отскакивать назад (направление изменения) после удара о стены и дощечки.
- Если шарик улетает за пределы игрового поля (мы его не поймали) - игра заканчивается.
- Дощечки должны двигаться с помощью мыши, или клавиатуры.
Это основная концепция нашей игры. так же нам потребуется меню, система подсчета очков, уровни геймплея, эффекты, звуки и т.д., но, давайте сделаем сперва саму игру, а потом уже все остальное.
Шаг 1 - Инициализация холста
Итак, сначала начните с создания HTML-файла с пустым тегом canvas с идентификатором cavas. Затем добавьте некоторые стили, чтобы холст оставался месте при нажатии клавиш со стрелками:
<!-- HTML -->
<canvas id="canvas"></canvas>
/* CSS */
body {padding: 0; margin: 0; overflow: hidden;}
Теперь начнем создавать свою игру в пинг-пинг. В холсте canvas
, все отрисовывается с помощью JavaScript, поэтому вы должны хотя бы немного разбираться в нем. Вы также можете воспользоваться библиотеками типа JQuery, prototype, MooTools и т.д., но лучше всего кодить в 'чистом' JavaScript чтобы код выполнялся быстро и мало весил. Итак, сначала начнем с инициализации холста. Это может быть сделано путем написания следующих строк в вашем JS-файле.
// Инициализация холста
var canvas = document.getElementById("canvas");
При инициализации тег canvas
пуст и для рисования нам необходимо получить доступ к встроенному объекту, который содержит различные методы для рисования :
var ctx = canvas.getContext("2d");
Теперь, после инициализации холста, мы должны установить его высоту и ширину. Я решил заполнить холстом все окно браузера, но вы можете задать постоянные значения, если хотите большего контроля над пространством.
var W = window.innerWidth, // длина окна
H = window.innerHeight, // высота окна
// Установите высоту и ширину холста на весь экран
canvas.width = W;
canvas.height = H;
Сначала мы вычисляем ширину и высоту окна и сохраняем эти значения в переменных W
и H
, а зная их, можно задать высоту и ширину полотна. Теперь попробуйте закрасить холст черным цветом, добавив одну строчку кода: ctx.fillRect(0, 0, W, H);
. Если окно становится черным, то вы все делаете правильно, в противном случае есть какая-то проблема. Убедитесь, что вы подключили файл со стилями и файл со скриптом к вашей web-странице.
Шаг 2 - Определение дощечек и шарика
Теперь займемся наши дощечками и шариком. Сделайте три новые переменные, как показано ниже:
var particles = [], // Массив, содержащий частицы
ball = {}, // шарик
paddles = [2]; // Массив, содержащий две дощечки
Давайте рассмотрим наш шарик. Это будет объект со свойствами: координаты х и у, которые будут определять положение шарика на холсте, радиус, цвет, скорость в х и у-направлениях, и функция отрисовки его на холсте. Вот как это выглядит на javascript:
ball = {
x: 50,
y: 50,
r: 5,
c: "white",
vx: 4,
vy: 8,
// Функция для рисования шарика на холсте
draw: function() {
ctx.beginPath();
ctx.fillStyle = this.c;
ctx.arc(this.x, this.y, this.r, 0, Math.PI*2, false);
ctx.fill();
}
};
Чтобы нарисовать шарик, мы используем некоторые встроенные в canvas api графические функции. Сначала мы запускаем процесс прорисовки траектории на холсте, затем выбираем цвет шарика (здесь можно использовать что угодно, от кода цвета до значения rgba), затем мы используем функции, чтобы нарисовать арку. Эта функция имеет шесть параметров: первые два параметра - координаты центра окружности (да-да, дуга это часть окружности). Они соответствуют позиции нашего шарика. Далее следует радиус дуги, начальный и конечный углы. Последним аргрументом мы передаем направление рисования, по умолчанию оно равно false
, что означает что рисоваться будет по часовой стрелке.
Итак, мы сделали шар, пришло время заняться нашими дощечками. Вместо того чтобы создавать другой объект для каждой доски, создадим класс Paddle
, из которого можно сделать два объекта с помощью всего двух строк! Итак, сначала мы определим класс как функцию с одним параметром, который будет определять позицию дощечки.
// Функция для создания дощечки
function Paddle(pos) {
// длина и высота
this.h = 5;
this.w = 150;
// позиция дощечки
this.x = W/2 - this.w/2;
this.y = (pos == "top") ? 0 : H - this.h;
}
Здесь, мы просто определение высоту и ширину доски, и Х и Y позиции. Взгляните на this.y = (pos == "top") ? 0 : H - this.h;
, это означает, что если позиция top
(дощечка наверху), то у положение доски должна быть 0
противном случае оно должно быть H
минус высота манипулятора. Довольно просто, не так ли?
Теперь нам нужно заполнить массив paddles
нашими верхней и нижней дощечками. Мы можем сделать это с помощью оператора new
, которое будет создавать объект из нашего класса, как показано ниже.
// Добавляем наши дощечки в массив paddles
paddles.push(new Paddle("bottom"));
paddles.push(new Paddle("top"));
Метод массива push
используется для добавления новых значений в конец массива. Конечно, можно было бы также использовать paddles[0] = new Paddle("bottom")
, но это не слишком элегантно)
Шаг 3 - Рисование всех элементов на холсте
Таким образом, теперь у нас есть две дощечки и шарик с нетерпением ждущие того момента, когда они появятся на холсте. Прежде всего, мы создадим функцию, чтобы закрасить холст. Вы, наверное, уже обратили внимание, что мы создаем много функций вместо того, чтобы написать одну большую функцию и засунуть в нее все. Дело в том, что так код становится чище и намного проще отлаживается. Итак, создаем новую функцию под названием paintCanvas
.
// функция закрашивания холста
function paintCanvas() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, W, H);
}
Здесь первая строка используется, чтобы установить цвет заливки, а код второй строки вам уже знаком, не так ли?) Функцию fillRect
вы будете использовать чтобы закрашивать прямоугольную область цветом, определенным свойством fillStyle. Она принимает четыре параметра, первые два - координаты левого верхнего угла прямоугольника и два других - высота и ширина прямоугольника. Чтобы заполнить весь холст, мы использовали начальные координаты (0,0), а остальные два параметра равны ширине и высоте окна соответственно.
Прежде, чем что-либо делать, давайте запустим всю систему в виде цикла. Для этого я использовал requestAnimationFrame
, поскольку этот метод намного лучше, чем setInterval
или setTimeout
. Вставьте следующий текст в верхнюю часть вашего кода.
// RequestAnimFrame: API браузера для получения плавной анимации
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
return window.setTimeout(callback, 1000 / 60);
};
})();
Давайте создадим функцию draw
, которая будет использоваться в массиве и повторяться 60 раз в секунду, чтобы получить плавную анимацию. К анимации мы вернемся позже, а сейчас займемся нашей функцией. В функции Draw, мы сначала закрасим холст с помощью функции paintCanvas()
, затем нарисуем дощечки и шарик.
// Нарисовать все на холсте
function draw() {
paintCanvas();
for(var i = 0; i < paddles.length; i++) {
p = paddles[i];
ctx.fillStyle = "white";
ctx.fillRect(p.x, p.y, p.w, p.h);
}
ball.draw();
}
Чтобы нарисовать дощечки мы используем тот же метод, что мы использовали когда рисовали на холсте. Мы используем цикл for
, чтобы обойти масив paddles
, а потом снова используем метод fillRect
. Параметры, используемые здесь говорят сами за себя. Первые два - координаты дощечки, которые мы определяем в ее классе, а остальные два – ширина и высота, которая выступает конечной точкой fillRect
. Далее, мы использовали метод draw
, который определили в объекте ball
.
Теперь вызываем draw()
и видим все на своих местах. Далее нам нужно создать цикл, используя requestAnimationFrame
, который мы определили ранее выше. Чтобы сделать это, создайте новую функцию animloop
и вызовите ее под ней следующим образом.
// Функция для запуска всей анимации
function animloop() {
requestAnimFrame(animloop);
draw();
}
animloop();
Здесь мы используем requestAnimFrame
, при этом функция выступает в роли ее же параметра, а потом, прямо под ней, вызываем функцию draw()
. Обязательно удалите предудыщий вызов этой функции Теперь у нас вроде как все работает, но нет никакого движения Это потому, что наш шарик не отлетает от дощечек. Мы будем анимировать их в следующем разделе.
Шаг 4 - Анимация шарика и игровых досок
Помните, мы определили с вами векторы скорости в нашем объекте ball
? Настало время их использовать. То есть, по сути, в каждом следующем фрейме мы добавляем эти векторы скорости в положеннях нашего шарика так, чтобы в каждом фрейме положение шарика обновлялось, и тем самым создавалась имитация движения. Чтобы сделать это, сначала создайте еще одну функцию и назовите ее update
. В этой части кода будут отображены все изменения, которые будет претерпевать наша игра в последовательных фреймах.
function update() {
// Перемещение шарика
ball.x += ball.vx;
ball.y += ball.vy;
}
Теперь вызовем эту функцию в конце нашей функции draw()
и вуаля! Шар движется! Итак, что мы реализовали обновление позиции шара в соответствии с его вектором скорости. Теперь нам нужно добавить элементы управления таким образом, чтобы игрок мог перемещать игровые дощечки с помощью мыши. Для этого мы должны добавить слушатель события на наш холст чтобы он мог обнаружить движения мыши. Это может быть сделано с помощью одной строки:
canvas.addEventListener("mousemove", trackPosition, true);
Теперь нам нужно создать функцию trackPosition()
, которая будет вызываться когда меняется положение курсора мыши. Но, сначала, давайте определим пустой объект mouse
, добавив var mouse = {}
туда где вы определили все ваши переменные. Опять же, это хорошая практика держать все переменные в одном месте в верхней части документа (в нашем случае ниже requestAnimFrame
).
// Отслеживать положение курсора мыши
function trackPosition(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
}
В этой функции, мы просто сохраняем текущее положение мыши в нашем объекта, как mouse.x
и mouse.y
для координат х и у соответственно. Теперь вернемся к нашей функции update()
и добавим следующие строки в нее:
// Перемещение дощечки при движении мыши
if(mouse.x && mouse.y) {
for(var i = 1; i < paddles.length; i++) {
p = paddles[i];
p.x = mouse.x - p.w/2;
}
}
Здесь мы меняем положение дощечки по оси Х в соответствии с координатой курсора мыши по оси Х. Мы также вычли их нее половину ширины дощечки чтобы центр доски совпадал с координатой миши. Теперь попробуйте переместить мышь и вы увидите, что дощечки движутся так, как мы и хотели. Классно!
Шаг 5 - обнаружение столкновения и другие прикольные вещи!
Теперь, когда все на своих местах, нам осталось сделать так, чтобы шарик отскакивал при столкновении с ракетками и стенами. Давайте вспомним еще раз логику игры, о которой мы говорили в самом начале.
- Мяч должен отскакивать назад (изменение направления) после удара о стену и дощечку.
- Когда мяч попадает в потолок или пол, игра должна закончиться.
То есть, теперь перед нами стоят две задачи. Первая – определить столкновения между шариком и ракетками, а вторая – определить столкновения между шариком и одной из стенок. Начнем с последнего, так как это легче.
// Солкновение со стенами, если шар попадает
// в верхнюю/нижнюю стены, запускаем функцию gameOver()
if(ball.y + ball.r > H) {
ball.y = H - ball.r;
gameOver();
}
else if(ball.y < 0) {
ball.y = ball.r;
gameOver();
}
// Если мяч ударяется вертикальные стены,
// инвертировать вектор х-скорости мяча
if(ball.x + ball.r > W) {
ball.vx = -ball.vx;
ball.x = W - ball.r;
}
else if(ball.x -ball.r < 0) {
ball.vx = -ball.vx;
ball.x = ball.r;
}
В этом коде, первые два условия используются для проверки столкновение шара с верхней и нижней стенками и, если это условие сбудется, запустить функцию gameOver
. Игнорировать эту функцию на данный момент, мы вернемся к нему позже. Следующие два условия для проверки столкновений между шаром и левой и правой стенок. Если это правда, то инвертировать вектор х-скорость шара. Это позволит сделать шар двигаться в противоположном направлении.
Поместите этот кусок кода в функцию update
. Теперь нам нужно обнаружить столкновение между шаром и дощечками. На первый взгляд это кажется сложным, но, если вы включите логику, вы поймете, что это не так. Мы сначала сравниваем горизонтальное положение шара и дощечек. Первая проверка: если х-положение шарика больше или равно х-положению дощечек и меньше или равно их конечной позиции. Если условие истинно, то перейти к следующему условию, которое будет сравнивать Y-координаты шарика и дощечки. Для этого нам необходимо создать функцию, которую мы будем вызывать в функции update
.
//Функция проверки столкновения между мячом и одной из дощечек
function collides(b, p) {
if(b.x + ball.r >= p.x && b.x - ball.r <=p.x + p.w) {
if(b.y >= (p.y - p.h) && p.y > 0){
return true;
}
else if(b.y <= p.h && p.y == 0) {
return true;
}
else return false;
}
}
Эта функция принимает два параметра. Первый отвечает за шарик, а второй за дощечку. Я сделал эту функцию так, что она будет работать для обоих дощечек: и верхней, и нижней. Вы можете видеть, что первое условие всегда будет возвращать true
когда шар столкнется с нижней доской, а второй возвращает true
только если шар попадет по верхней дощечке. Теперь мы можем использовать эту функцию в функции update()
, которая теперь будет выглядеть так:
function update() {
// Перемещение дощечки при перемещении мыши
if(mouse.x && mouse.y) {
for(var i = 1; i < paddles.length; i++) {
p = paddles[i];
p.x = mouse.x - p.w/2;
}
}
// Перемещение шарика
ball.x += ball.vx;
ball.y += ball.vy;
// Столкновение с дощечками
p1 = paddles[1];
p2 = paddles[2];
if(collides(ball, p1)) {
ball.vy = -ball.vy;
}
else if(collides(ball, p2)) {
ball.vy = -ball.vy;
}
else {
// Столкновение со стенами. Если мяч попадает в
// верхнюю/нижнюю стены, запустите функцию GameOver()
if(ball.y + ball.r > H) {
ball.y = H - ball.r;
gameOver();
}
else if(ball.y < 0) {
ball.y = ball.r;
gameOver();
}
// Если шарик ударяется вертикальные стены,
// инвертировать вектор х-скорости мяча
if(ball.x + ball.r > W) {
ball.vx = -ball.vx;
ball.x = W - ball.r;
}
else if(ball.x -ball.r < 0) {
ball.vx = -ball.vx;
ball.x = ball.r;
}
}
}
Теперь наша функция update проверяет столкновения шарика со стенами, полом, потолком и ракетками. Если он ударяется об одну из ракеток, преобразуйте y-скорость, чтобы он отскочил обратно ко второй ракетке.
Основная логика нашей игры завершена.
Шаг 6 - "Игра закончена" и другие плюшки
Теперь напишем функцию gameOver()
. В ней мы просто остановим анимацию, которую мы начали раньше, используя requestAnimFrame
. Вернитесь к нашей функции animloop()
и модифицируйте ее следующим образом.
// Функция для запуска всей анимации
function animloop() {
init = requestAnimFrame(animloop);
draw();
}
Сейчас, в верхней части документа, добавьте следующие строки. Они предназначены для отмены requestAnimationFrame
, который мы будем использовать в нашей функции gameOver()
.
window.cancelRequestAnimFrame = ( function() {
return window.cancelAnimationFrame ||
window.webkitCancelRequestAnimationFrame ||
window.mozCancelRequestAnimationFrame ||
window.oCancelRequestAnimationFrame ||
window.msCancelRequestAnimationFrame ||
clearTimeout
})();
Теперь создайте функцию gameOver()
и добавьте к ней следующие строки:
function gameOver() {
ctx.fillStyle = "white";
ctx.font = "20px Arial, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Игра окончена", W/2, H/2 + 25 );
// Остановить анимацию
cancelRequestAnimFrame(init);
}
В этой функции мы выводит текст "Игра окончена" на холсте с помощью функции fillText
, а затем отменяем анимацию при помощи cancelRequestAnimFrame(init)
. Теперь наша игра полностью завершена. Просто поиграйте в нее, чтобы проверить все ли работает.
Как вы, наверное, заметилили, в демо-версии игры ping pong на HTML5 Canvas, прилагаемой к этому уроку, есть много чего не было рассмотрено в этом уроке. А именно: звук и искры, когда шарик попадает в дощечки, система подсчета очков, кнопка запуска и перезапуска игры, увеличение скорости шара, чтобы сделать его более трудной. Вы можете посмотреть на код и самостоятельно разобраться как это делается. Дерзайте и все у вас получится!