
День добрый. В этом топике я расскажу, как сделать 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 и только потом орудовать с libcanvasPong.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); } });
Добавляем его вызов в контроллер. Нам необходимо каждый кадр обновлять положение шарика, потому мы подписываемся на обновление при помощи
addFuncPong.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, если вы не зарегистрированы на Хабре.
