Как стать автором
Обновить

Создание игры на Javascript Canvas

Время на прочтение13 мин
Количество просмотров70K

Здравствуйте! Я предлагаю вам со мной создать небольшую казуальную игру на нескольких человек за одним компьютером на 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.*= -1);
        
// Через раз летим вверх, а не вниз
        
$random(0,1) && (this.speed.*= -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 
this.barriers.lengthi--;) {
        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 : [201020040],
        
color this.color
    
});
},

// Т.к. очки - это всего-лишь количество хищников - при смерти достаточно удалить всех хищников
die : function () {
    for (var 
this.barriers.lengthi--;) {
        
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.-= this.speed;
    if (
this.libcanvas.getKey('right')) pos.+= this.speed;
    if (
this.libcanvas.getKey('up'))    pos.-= this.speed;
    if (
this.libcanvas.getKey('down'))  pos.+= 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.0          pos.-= this.speed;
    if (
this.isMoveTo('right') && pos.full.width pos.+= this.speed;
    if (
this.isMoveTo('up')    && pos.0          pos.-= this.speed;
    if (
this.isMoveTo('down')  && pos.full.heightpos.+= 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.maxScorethis.barriers.length);
        
// ...
    
},

    
index 0,
    
draw : function () {
        
this.parent();
        
this.libcanvas.ctx.text({
            
// Выводим максимальный рекорд:
            
text 'Score : ' this.barriers.length ' (' this.maxScore ')',
            
// Смещаем очки игрока на 20 пикселей вниз зависимо от его индекса:
            
to : [2010 20*this.index20040],
            
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 '"'
};

= Шаг 13 — игра на четверых =

Теги:
Хабы:
Всего голосов 122: ↑111 и ↓11+100
Комментарии76

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань