Здравствуйте! Я предлагаю вам со мной создать небольшую казуальную игру на нескольких человек за одним компьютером на Javascript Canvas.
В статье я пошагово разобрала процесс создания такой игры при помощи MooTools и LibCanvas, останавливаясь на каждом мелком действии, объясняя причины и логику добавления нового и рефакторинга существующего кода.
p.s. К сожалению, Хабр обрезает большие раскрашенные статьи где-то на шестидесятитысячном символе, потому я была вынуждена вынести пару участков кода из статьи на pastebin. Если вы хотите прочитать статью, не бегая по ссылках в поисках кода — можно воспользоваться зеркалом.
Правила
Управляем игроком (Player), который должен поймать наживку (Bait) — и при этом увернуться от появляющихся «хищников» (Barrier).
Цель игры — поймать максимальное количество наживок, не соприкоснувшись с хищниками.
При соприкосновении с одним из хищников все они(хищники) пропадают, а очки — обнуляются, что, фактически, равносильно началу игры с нуля.
HTML файл
Создаем начальный файл, на котором будет наше канвас-приложение. Я воспользовалась файлами, размещенными на сайте libcanvas, но это — не обязательно. JavaScript-файлы добавлялись в процессе создания игры, но я не хочу больше возвращаться к этому файлу, потому объявлю их сразу.
[[ Код ./index.html на pastebin ]]
Создаем проект
Для начала создадим сам проект. Нам нужно сделать это только тогда, когда документ будет готов — воспользуемся предоставленным mootools событием "
domready
"Также создадим объект LibCanvas.Canvas2D, который поможет нам с анимацией.
./js/start.js
window.addEvent('domready', function () {
// с помощью Мутулз выберем первый елемент канвас на странице
var elem = $$('canvas')[0];
// На его основе создадим елемент LibCanvas
var libcanvas = new LibCanvas.Canvas2D(elem);
// Перерисовка будет осуществлятся каждый кадр, несмотря на наличие или отсутствие изменений
libcanvas.autoUpdate = true;
// Будем стремится к 60 fps
libcanvas.fps = 60;
// Стартуем наше приложение
libcanvas.start();
});
Добавляем пользователя
Добавим новый объект — Player, который будет управлятся мышью — его координаты будут равны координатам курсора мыши.
Этот объект будет выглядеть как кружочек определенного цвета и размера (указанные в свойстве)
[[ Код ./js/Player.js на pastebin ]]
Добавим в ./js/start.js перед
libcanvas.start();
:libcanvas.listenMouse();
var player = new Player().setZIndex(30);
libcanvas.addElement(player);
= Шаг 1 =
Можно заметить, что результат не совсем такой, как мы ожидали, потому-что после каждого из кадров холст не очищается автоматически.
Необходимо добавить очищающий и заливающий черным препроцессор в ./js/start.js
libcanvas.addProcessor('pre',
new LibCanvas.Processors.Clearer('#000')
);
= Шаг 2 =
Добавляем наживку
[[ Код ./js/Bait.js на pastebin ]]
Добавим в ./js/start.js:
// Возьмем индекс поменьше, чтобы наживка рисовалась под игроком
var bait = new Bait().setZIndex(20);
libcanvas.addElement(bait);
Рефакторинг — создаем родительский класс
У нас очень похожие классы Bait и Player. Давайте создадим класс GameObject, от которого у нас они будут наследоваться.
Для начала вынесем createPosition из конструктора класса Player:
./js/Player.js
var Player = new Class({
// ...
initialize : function () {
// ..
this.shape = new LibCanvas.Shapes.Circle({
center : this.createPosition()
// ...
},
createPosition : function () {
return this.libcanvas.mouse.point;
},
Теперь создадим класс GameObject
[[ Код ./js/GameObject.js на pastebin ]]
После этого можно облегчить другие классы:
./js/Bait.js
var Bait = new Class({
Extends : GameObject,
radius : 15,
color : '#f0f'
});
./js/Player.js
var Player = new Class({
Extends : GameObject,
radius : 15,
color : '#080',
createPosition : function () {
return this.libcanvas.mouse.point;
},
draw : function () {
if (this.libcanvas.mouse.inCanvas) {
this.parent();
}
}
});
Смотрим, ничего ли не сломалось:
= Шаг 3 =
Ура! Все везде работает, а код стал значительно легче.
Дружим игрока с наживкой
У нас на экране все двигается, но реально никакой реакции на наши действия нету.
Давайте начнем с того, что подружим наживку и игрока — при набегании на неё, наживка должна перемещаться в другое случайное место.
Для этого создадим отдельный от рендеринга таймаут, который будет проверять соприкасание.
Пишем в конец ./js/start.js:
(function(){
bait.isCatched(player);
}.periodical(30));
Теперь надо реализовать метод isCatched в ./js/Bait.js:
isCatched : function (player) {
if (player.shape.intersect(this.shape)) {
this.move();
return true;
}
return false;
},
move : function () {
// перемещаем в случайное место
this.shape.center = this.createPosition();
}
= Шаг 4 =
Почти отлично, но мы видим, что перемещение грубовато и раздражающе. Лучше было бы, если бы наживка плавно убегала.
Для этого можно воспользоваться одним из поведений ЛибКанвас. Достаточно добавить одну строчку и слегка поменять метод move:
Теперь надо реализовать метод isCatched в ./js/Bait.js:
var Bait = new Class({
Extends : GameObject,
Implements : [LibCanvas.Behaviors.Moveable],
// ...
move : function () {
// быстро (800), но плавно перемещаем в случайное место
this.moveTo(this.createPosition(), 800);
}
});
Очень просто, правда? А результат мне нравится намного больше:
= Шаг 5 =
Добавим хищников
./js/Barrier.js:
var Barrier = new Class({
Extends : GameObject,
full : null,
speed : null,
radius : 8,
color : '#0ff',
initialize : function () {
this.parent();
this.speed = new LibCanvas.Point(
$random(2,5), $random(2,5)
);
// Через раз летим влево, а не вправо
$random(0,1) && (this.speed.x *= -1);
// Через раз летим вверх, а не вниз
$random(0,1) && (this.speed.y *= -1);
},
move : function () {
this.shape.center.move(this.speed);
return this;
},
intersect : function (player) {
return (player.shape.intersect(this.shape));
}
});
Также чуть изменим ./js/start.js, чтобы при ловле наживки появлялись хищники:
bait.isCatched(player);
// меняем на
if (bait.isCatched(player)) {
player.createBarrier();
}
player.checkBarriers();
Реализуем добавление барьеров для игрока, ./js/Player.js и двигаем их все каждую проверку:
barriers : [],
createBarrier : function () {
var barrier = new Barrier().setZIndex(10);
this.barriers.push(barrier);
// Надо не забыть добавить его в наш объект libcanvas, чтобы хищник рендерился
this.libcanvas.addElement(barrier);
return barrier;
},
checkBarriers : function () {
for (var i = this.barriers.length; i--;) {
if (this.barriers[i].move().intersect(this)) {
this.die();
return true;
}
}
return false;
},
die : function () { },;
= Шаг 6 =
Отлично, появилось движение в игре. Но мы видим три проблемы:
1. Хищники улетают за игровое поле — надо сделать «отбивание от стен».
2. Иногда наживка успевает схватиться дважды, пока улетает — надо сделать небольшой таймаут «неуязвимости».
3. Не обработан случай смерти.
Хищники отбиваются от стен, наживка получает небольшое время «неуязвимости»
Реализовать отбивание от стен проще простого. Слегка меняем метод move класса Barrier, в файле ./js/Barrier.js:
[[ Код Barrier.move на pastebin ]]
Исправить проблему с наживкой тоже не очень сложно — вносим изменения в класс Bait, в файле ./js/Bait.js
[[ Код Bait.makeInvulnerable на pastebin ]]
= Шаг 7 =
Реализуем смерть и подсчёт очков
Т.к. очки — это сколько раз поймана наживка и она равная количеству хищников на экране — сделать подсчёт очков очень легко:
Чуть расширим метод draw в классе Player, файл ./js/Player.js:
draw : function () {
// ...
this.libcanvas.ctx.text({
text : 'Score : ' + this.barriers.length,
to : [20, 10, 200, 40],
color : this.color
});
},
// Т.к. очки - это всего-лишь количество хищников - при смерти достаточно удалить всех хищников
die : function () {
for (var i = this.barriers.length; i--;) {
this.libcanvas.rmElement(this.barriers[i]);
}
this.barriers = [];
}
Одиночная игра — закончена!
= Шаг 8 — одиночная игра =
Реализуем многопользовательскую игру за одним компьютером
Движение с клавиатуры
Для начала — перенесем управление с мышки на клавиатуру. В ./js/start.js меняем
libcanvas.listenMouse()
на libcanvas.listenKeyboard()
В нем же в таймаут добавляем
player.checkMovement();
.В ./js/Player.js удаляем переопределение
createPosition
, в методе draw
удаляем проверку мыши и реализуем движение с помощью стрелочек:speed : 8,
checkMovement : function () {
var pos = this.shape.center;
if (this.libcanvas.getKey('left')) pos.x -= this.speed;
if (this.libcanvas.getKey('right')) pos.x += this.speed;
if (this.libcanvas.getKey('up')) pos.y -= this.speed;
if (this.libcanvas.getKey('down')) pos.y += this.speed;
},
= Шаг 9 =
Неприятный нюанс — игрок заползает за экран и там может заблудиться.
Давайте ограничим его передвижение и немножко отрефакторим код, вынеся получение состояния клавиши в отдельный метод
isMoveTo : function (dir) {
return this.libcanvas.getKey(dir);
},
checkMovement : function () {
var pos = this.shape.center;
var full = this.getFull();
if (this.isMoveTo('left') && pos.x > 0 ) pos.x -= this.speed;
if (this.isMoveTo('right') && pos.x < full.width ) pos.x += this.speed;
if (this.isMoveTo('up') && pos.y > 0 ) pos.y -= this.speed;
if (this.isMoveTo('down') && pos.y < full.height) pos.y += this.speed;
},
Также слегка изменим метод isMoveTo — чтобы можно было с легкостью изменять клавиши для управления игроком:
control : {
up : 'up',
down : 'down',
left : 'left',
right : 'right'
},
isMoveTo : function (dir) {
return this.libcanvas.getKey(this.control[dir]);
},
= Шаг 10 =
Вводим второго игрока
Изменяем файл ./js/start.js:
var player = new Player().setZIndex(30);
libcanvas.addElement(player);
// =>
var players = [];
(2).times(function (i) {
var player = new Player().setZIndex(30 + i);
libcanvas.addElement(player);
players.push(player);
});
// Меняем стиль и управление второго игрока
players[1].color = '#ff0';
players[1].control = {
up : 'w',
down : 's',
left : 'a',
right : 'd'
};
Содержимое таймера оборачиваем в
players.each(function (player) { /* * */ });
= Шаг 11 =
Осталось сделать небольшие поправки:
1. Сдвинуть счёт второго игрока ниже счёта первого игрока.
2. Раскрасить хищников разных игроков в разные цвета.
3. Ради статистики ввести «Рекорд» — какой максимальный счёт каким игроком был достигнут.
Вносим соответствующие изменения в ./js/Player.js:
var Player = new Class({
// ...
// Красим хищников в соответствующий игроку цвет:
createBarrier : function () {
// ...
barrier.color = this.barrierColor || this.color;
// ...
},
// Реализуем подсчет максимального рекорда
maxScore : 0,
die : function () {
this.maxScore = Math.max(this.maxScore, this.barriers.length);
// ...
},
index : 0,
draw : function () {
this.parent();
this.libcanvas.ctx.text({
// Выводим максимальный рекорд:
text : 'Score : ' + this.barriers.length + ' (' + this.maxScore + ')',
// Смещаем очки игрока на 20 пикселей вниз зависимо от его индекса:
to : [20, 10 + 20*this.index, 200, 40],
color : this.color
});
}
});
Вносим коррективы в ./js/start.js:
(2).times(function (i) {
var player = new Player().setZIndex(30 + i);
player.index = i;
// ...
});
players[0].color = '#09f';
players[0].barrierColor = '#069';
// Меняем стиль и управление второго игрока
players[1].color = '#ff0';
players[1].barrierColor = '#960';
players[1].control = {
up : 'w',
down : 's',
left : 'a',
right : 'd'
};
Поздравляем, игра сделана!
= Шаг 12 — игра на двоих =
Добавляем третьего и четвертого игрока
При желании очень просто добавить третьего и четвертого игрока:
players[2].color = '#f30';
players[2].barrierColor = '#900';
players[2].control = {
up : 'i',
down : 'k',
left : 'j',
right : 'l'
};
// players[0] uses numpad
// players[3] uses home end delete & pagedown
players[3].color = '#3f0';
players[3].barrierColor = '#090';
players[3].control = {
up : '$',
down : '#',
left : 'delete',
right : '"'
};