День добрый. В этом топике я расскажу, как сделать ping-pong при помощи LibCanvas. Я значительно упростил её, оставив только самую важную часть, так как цель топика не создать игру ping-pong, а объяснить основы LibCanvas.
Итак, в топике пошаговая инструкция, как создать ping-pong при помощи LibCanvas (без оптимизаций).
Итак, пинг понг — две доски, от которых отбивается мячик. В общем, вы все знаете. Первое, что нам необходимо — это создать начальный html-файл. Он достаточно прост — одинокий элемент canvas, ссылки на AtomJS и LibCanvas и ссылки на файлы приложения:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>LibCanvas :: ping-pong</title>
<link href="/styles.css" rel="stylesheet" />
<script src="/lib/atom.js"></script>
<script src="/lib/libcanvas.js"></script>
</head>
<body>
<canvas></canvas>
<!-- Game engine -->
<script src="js/init.js"></script>
<script src="js/controller.js"></script>
<script src="js/field.js"></script>
<script src="js/unit.js"></script>
<script src="js/ball.js"></script>
</body>
</html>
Инициализация
Всё начинается с файла инициализации.
В нём я вызываю
LibCanvas.extract
для того, чтобы была возможность использовать глобальные имена. По-умолчанию, все классы хранятся в своих пространствах имен: LibCanvas.Shapes.Circle
. После extract
их можно использовать сокращённо: Circle
Вторым шагом я объявляю пространство имен для своей игрушки. Все класы будут хранится в нём.
Последний шаг — это создание контроллера при старте dom.
LibCanvas.extract();
window.Pong = {};
atom.dom(function () {
new Pong.Controller('canvas');
});
Инициализация может отличаться зависимо от приложений, но, в целом, она подобна среди них. Один из моих товарищей любит интересный подход — минимальный html-файл со всей логикой (даже создание элемента и подключение скриптов) в JavaScript. И да, этот код валиден!
<!DOCTYPE html>
<title>LibCanvas :: ping-pong</title>
<script src="js/load.ls"></script>
Контроллер
Следующий шаг — создание контроллера. В нём мы создадим объект
LibCanvas
и игровые элементы. Все игровые классы я буду создавать при помощи atom.Class
, где initialize
— это конструктор.Pong.Controller = atom.Class({
initialize: function (canvas) {
this.libcanvas = new LibCanvas(canvas, {
preloadImages: { elems : 'im/elems.png' }
})
.listenKeyboard([ 'aup', 'adown', 'w', 's' ])
.addEvent('ready', this.start.bind(this))
.start();
},
start: function () {
var libcanvas = this.libcanvas;
[...]
}
});
В конструктор мы передаём объект для предзагрузки картинки. Приложение не запуститься, пока картинка не будет загружена. Это два спрайта — палки и мячика.
Мы сообщаем
LibCanvas
, что мы будем использовать клавиатуру и необходимо избежать действий по-умолчанию для клавиш 'aup', 'adown', 'w' и 's'. Это позволит реализовать удобное управление и, при этом не будет, например, сдвигаться стрелочками окно браузера.Когда LibCanvas будет готов начать отрисовку — мы запустим метод start контроллера. К нему мы вернемся позже.
Игровое поле
Игровое поле у нас будет отвечать за отрисовку бэкграунда, очков, кое-какие вычисления, создание юнитов.
Pong.Field = atom.Class({
Implements: [ Drawable ],
width : 800,
height: 500,
[...]
});
Создаем сущность и добавляем её в libcanvas для отрисовки. Обратите внимание, как мы ловко меняем размер холста. Это потому что наш объект имеет свойства width и height.
Pong.Controller = atom.Class({
[...]
start: function () {
var libcanvas = this.libcanvas;
var field = new Pond.Field();
libcanvas.size( field, true );
libcanvas.addElement( field );
[...]
}
});
Мяч
Логика мяча предельно проста — у него есть «импульс» — направление и скорость передвижения.
Скорость задаётся в пикселях/секунду. Каждый раз во время обновления мы получаем время, которое прошло с предыдущего обновления, на которое умножем скорость. За счёт этого мы имеем постоянную скорость передвижения, независимо от fps
Когда шарик достигает верхней или нижней границы — он ударяется и летит в другую сторону.
appendTo позволяет легко присовить шарик полю. Нам важно знать размеры поля для начальной позиции и учёта стен.
Отрисовка очень проста — мы просто отрисовываем нужную часть спрайта в текущий прямоугольник.
Обратите внимание, что во время конструирования объекта свойства
libcanvas
ещё нету, потому необходимо дождаться события libcanvasSet
и только потом орудовать с libcanvas
Pong.Ball = atom.Class({
Implements: [ Drawable ],
impulse: null,
initialize: function (controller) {
this.impulse = new Point(
Number.random(325, 375),
Number.random(325, 375)
);
this.addEvent('libcanvasSet', function () {
this.image = this.libcanvas.getImage('elems').sprite( 23, 0, 26, 26 );
});
},
move: function (time) {
this.shape.move(
this.impulse.clone().mul(time / 1000)
);
},
update: function (time) {
this.move(time);
var from = this.shape.from, to = this.shape.to;
// Обрабатываем верхнюю и нижнюю границы
if (
(this.impulse.y < 0 && from.y < 0) ||
(this.impulse.y > 0 && to.y > this.field.height)
) this.impulse.y *= -1;
},
appendTo: function (field) {
this.shape = new Rectangle( 40, field.height / 2, 24, 24 );
this.field = field;
return this;
},
draw: function () {
this.libcanvas.ctx.drawImage(this.image, this.shape);
}
});
Добавляем его вызов в контроллер. Нам необходимо каждый кадр обновлять положение шарика, потому мы подписываемся на обновление при помощи
addFunc
Pong.Controller = atom.Class({
[...]
start: function () {
[...]
ball = new Pong.Ball();
libcanvas
[...]
.addElement( ball.appendTo( field ) )
.addFunc(function (time) {
ball.update( time );
libcanvas.update();
});
}
});
Создаём юнитов
Следующее, что нам требуется — это ракетки. Они будут управляться при помощи клавиатуры (w-s для левой и вверх-вниз для правой).
Этот класс будет отвечать за контролы, передвижение, соприкосновение с шариком.
Обратите внимание, что свойство «speed» — статическое, то есть добавлено в прототип. Мы его не будем изменять, а только использовать.
В controls мы привязываемся к обновлению холста и проверяем состояние необходимых клавиш. При необходимости — сдвигаем объект.
Интересный способ передвинуть фигуру на нужную скорость — мы просто используем метод move нашего прямоугольника для этого.
fitToField убеждается, что элемент находится в допустимых пределах и, если это не так, то возвращает его на место.
В методе draw, по аналогии с Ball, отрисовывается нужная часть картинки в текущую shape.
Pong.Unit = atom.Class({
Implements: [ Drawable ],
size: { width: 20, height: 100, padding: 20 },
speed: new Point( 0, 300 ),
score: 0,
controls: function (up, down) {
this.addEvent('libcanvasSet', function () {
var lc = this.libcanvas.addFunc(function (time) {
if (lc.getKey(up)) {
this.move( -time );
} else if (lc.getKey(down)) {
this.move( time );
}
}.bind(this));
});
return this;
},
appendTo: function (field, number) {
var s = this.size;
this.field = field;
this.number = number;
this.shape = new Rectangle({ // field.width, field.height
from: [
(number == 2 ? field.width - s.width - s.padding : s.padding),
(field.height - s.height) / 2
],
size: s
});
return this;
},
fitToField: function () {
var shape = this.shape;
var top = shape.from.y, bottom = shape.to.y - this.field.height;
if (top < 0) shape.move(new Point(0, -top));
if (bottom > 0) shape.move(new Point(0, -bottom));
},
move: function (time) {
this.shape.move( this.speed.clone().mul( time / 1000 ) );
this.fitToField();
},
draw: function() {
this.libcanvas.ctx.drawImage(
this.libcanvas.getImage('elems').sprite(0,0,20,100),
this.shape
);
}
});
Юнитов будем создавать на поле, где будем задавать им управление и положение:
Pong.Field = atom.Class({
[...]
createUnits: function (libcanvas) {
this.unit = new Pong.Unit()
.controls('w', 's')
.appendTo( this, 1 );
this.enemy = new Pong.Unit()
.controls('aup', 'adown')
.appendTo( this, 2 );
libcanvas
.addElement( this.unit )
.addElement( this.enemy );
},
[...]
Естественно, необходимо добавить вызов метода в Контроллер:
Pong.Controller = atom.Class({
[...],
start: function () {
[...],
field.createUnits( libcanvas );
}
});
Взаимодействие объектов
Теперь необходимо заставить шарик взаимодействовать с крайними границами и игроками. Добавляем простой метод в Ball.
Обратите внимание, что необходимо проверять направление движения шарика, иначе он может «застревать» в игроках и стенах.
Pong.Ball = atom.Class({
[...]
checkCollisions: function () {
var coll = this.field.collidesUnits( this ),
isOut = this.field.isOut( this.shape );
if (
(( coll < 0 || isOut < 0 ) && this.impulse.x < 0) ||
(( coll > 0 || isOut > 0 ) && this.impulse.x > 0)
) this.impulse.x *= -1;
},
update: function (time) {
[...]
this.checkCollisions();
},
[...]
});
Проверка внутри Field очень простая.
Проверку на столкновение с юнитом мы перекладываем на плечи юнита, и только возвращаем направление.
Проверка на выход за правую или левую границу — тоже очень простая. В случае соприкосновения с границей мы увеличиваем счёт противоположного игрока.
Pong.Field = atom.Class({
[...]
collidesUnits: function (ball) {
return this.unit .collides(ball) ? -1 :
this.enemy.collides(ball) ? 1 : 0;
},
isOut: function (shape) {
if (shape.from.x < 0) {
this.enemy.score++;
return -1;
} else if (shape.to.x > this.width) {
this.unit.score++;
return 1;
}
return 0;
},
[...]
Внутри Unit мы воспользуемся встроенным методом
Rectangle().intersect
, для проверки пересечения двух прямоугольников.Pong.Unit = atom.Class({
[...]
collides: function (ball) {
return ball.shape.intersect(this.shape);
},
});
Вывод счета
Последний шаг — отобразить счёт игроков. Это можно легко сделать при помощи
ctx.text
— он позволяет вывоодить текст более прибилиженно к css, указывать отступы, прямоугольник, в который необходимо вывести текст и некоторые дополнительные возможности.Pong.Field = atom.Class({
[...]
drawScore: function (unit, align) {
this.libcanvas.ctx.text({
text: unit.score,
size: 32,
padding: [0, 70],
color: 'white',
align: align
});
return this;
},
draw: function () {
this
.drawScore( this.unit , 'left' )
.drawScore( this.enemy, 'right' );
}
});
Заключение
Вот и всё. Полный код и игру вы можете найти по адресу
libcanvas.github.com/games/pingpong/
Игру из топика можно развивать дальше. Например, добавить сетевую игру с сервером на node.js и WebSocket.
Или добавить красивый внешний вид, анимации. Также можно усовершенстовать геймплей — добавить преграды, иной угол отражения шарика.
Это всё делается очень легко при помощи LibCanvas. Какие темы вас интересуют? Если будут желающие — я их опишу.
Ещё вопрос — стоит ли описывать более базовые вещи, делать топики про LibCanvas не так загруженные информацией, а более узкие и описывающие отдельные мелкие аспекты или подобные полноформатные статьи воспринимаются достаточно легко?
Мне кажется, что иногда мысли были довольно сумбурны, потому не стесняйтесь задавать вопросы в комментах или на емейл shocksilien@gmail.com, если вы не зарегистрированы на Хабре.