5 апреля 2011 в 10:57

Canvas шаг за шагом: ПОНГ

HTML*
Сегодня попробуем написать небольшую игру Понг используя html5 тег canvas. Те кто не хочет читать пост тот может сразу ИГРАТЬ.
Если верить Википедии, то можно узнать что Pong является простейшим симулятором настольного тенниса. Небольшой квадратик, заменяющий пинг-понговый мячик, двигается по экрану по линейной траектории. Если он ударяется о периметр игрового поля или об одну из нарисованных ракеток, то его траектория изменяется в соответствии с углом столкновения.
Геймплей состоит в том, что игроки передвигают свои ракетки вертикально, чтобы защищать свои ворота. Игрок получает одно очко, если ему удаётся отправить мячик за ракетку оппонента…

Мы попытаемся реализовать игру так что бы в неё можно было играть при помощи мыши, а оппонентом управлял компьютер. Итак приступим. Для начала определимся с тем что, для полного понимания происходящего в этой записи желательно ознакомиться с постом описывающим основы.

Наш мегапроект будет состоять из двух файлов, соответственно pong.htm и pong.js которые собственно нужно создать и сохранить в одной папке.
Содержимое html файла:
<html>
    <head>    
        <meta charset = "utf-8">
        <title>html5Pong</title>
        <script src="pong.js"></script>
    </head>
    <body>
        <canvas id="pong">wtf?!</canvas>
        <script>init()</script>
    </body>
</html>

Собственно вся механика игры будет вынесена в файл pong.js и вносим в него первые строки нашей будущей игры:
// Инициализация переменных
function init() {
    canvas = document.getElementById("pong");
    canvas.width = 480; // задаём ширину холста
    canvas.height = 320; // задаём высоту холста
    context = canvas.getContext('2d');
    draw();
}
// Отрисовка игры
function draw() {
    context.fillStyle = "#000";
    context.fillRect(0, 0, 480, 320);
}
init();

Если открыть html фал браузером то можно будет увидеть работу этого скрипта который собственно закрашивает наш холст в чёрный цвет.

Игровые объекты


Все игровые объекты в Понге представляют собой прямоугольники и это существенно облегчит нашу задачу. Зададим небольшой класс Rect который будет содержать все нужные поля для отрисовки прямоугольника, а так же метод draw:
function rect(color, x, y, width, height) {
    this.color = color; // цвет прямоугольника
    this.x = x; // координата х
    this.y = y; // координата у
    this.width = width; // ширина
    this.height = height; // высота
    this.draw = function() // Метод рисующий прямоугольник
    {
        context.fillStyle = this.color;
        context.fillRect(this.x, this.y, this.width, this.height);
    }
}

Теперь слегка изменим содержимое функций инициализации и отрисовки добавив объекты игрового поля, игроков, «шарика»
// Инициализация переменных
function init() {
    // объект который задаёт игровое поле
    game = new rect("#000", 0, 0, 480, 320);
    // Ракетки-игроки
    ai = new rect("#fff", 10, game.height / 2 - 40, 20, 80);
    player = new rect("#fff", game.width - 30, game.height / 2 - 40, 20, 80);
    // количество очков
    ai.scores = 0;
    player.scores = 0;
    // наш квадратный игровой "шарик"
    ball = new rect("#fff", 40, game.height / 2 - 10, 20, 20);
    canvas = document.getElementById("pong");
    canvas.width = game.width;
    canvas.height = game.height;
    context = canvas.getContext("2d");
    draw();
}
// Отрисовка игры
function draw() {
    game.draw(); // рисуем игровое поле
    // рисуем на поле счёт
    context.font = 'bold 128px courier';
    context.textAlign = 'center';
    context.textBaseline = 'top';
    context.fillStyle = '#ccc';
    context.fillText(ai.scores, 100, 0);
    context.fillText(player.scores, game.width-100, 0);
    for (var i = 10; i < game.height; i += 45) // линия разделяющая игровое поле на две части
   {
        context.fillStyle = "#ccc";
        context.fillRect(game.width/2 - 10, i, 20, 30);
    }
    ai.draw(); // рисуем левого игрока
    player.draw(); // правого игрока
    ball.draw(); // шарик
}

Если теперь открыть файл, то можно увидеть все элементы нашей игры.

Да будет жизнь


Конечно всё на данном этапе выглядит очень даже неплохо, но статичная картинка не то что нам нужно. Поэтому сейчас мы займёмся «оживлением» шарика. Для этого создадим новую функцию play () в которую вынесем вызов функции draw (), а саму play будем вызывать из init () посредством таймера, а именно setInterval (play, 1000 / 50). Координату y игрока мы привяжем к координате мыши передвигающейся по холсту. В функции update содержатся изменения которые следут произвести, такие как координаты мыши и прочие радости. Что бы не запутаться в произведенных действиях ниже код всего того файла pong.js который у нас должен выйти:
// класс определяющий параметры игрового прямоугольника и метод для его отрисовки
function rect(color, x, y, width, height) {
    this.color = color;
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.draw = function() {
        context.fillStyle = this.color;
        context.fillRect(this.x, this.y, this.width, this.height);
    };
}
// движение игрока
function playerMove(e) {
    var y = e.pageY;
    if (player.height / 2 + 10 < y && y < game.height - player.height / 2 - 10) {
        player.y = y - player.height / 2;
    }
}
// отрисовка игры
function draw() {
    game.draw(); // рисуем игровое поле
    // рисуем на поле счёт
    context.font = 'bold 128px courier';
    context.textAlign = 'center';
    context.textBaseline = 'top';
    context.fillStyle = '#ccc';
    context.fillText(ai.scores, 100, 0);
    context.fillText(player.scores, game.width - 100, 0);
    for (var i = 10; i < game.height; i += 45)
    // линия разделяющая игровое поле на две части
    {
        context.fillStyle = "#ccc";
        context.fillRect(game.width / 2 - 10, i, 20, 30);
    }
    ai.draw(); // рисуем левого игрока
    player.draw(); // правого игрока
    ball.draw(); // шарик
}
// Изменения которые нужно произвести
function update() {
    // меняем координаты шарика
    ball.x += ball.vX;
    ball.y += ball.vY;

}
function play() {
    draw(); // отрисовываем всё на холсте
    update(); // обновляем координаты
}
// Инициализация переменных
function init() {
    // объект который задаёт игровое поле
    game = new rect("#000", 0, 0, 480, 320);
    // Ракетки-игроки
    ai = new rect("#fff", 10, game.height / 2 - 40, 20, 80);
    player = new rect("#fff", game.width - 30, game.height / 2 - 40, 20, 80);
    // количество очков
    ai.scores = 0;
    player.scores = 0;
    // наш квадратный игровой "шарик"
    ball = new rect("#fff", 40, game.height / 2 - 10, 20, 20);
    // скорость шарика
    ball.vX = 2; // скорость по оси х
    ball.vY = 2; // скорость по оси у
    canvas = document.getElementById("pong");
    canvas.width = game.width;
    canvas.height = game.height;
    context = canvas.getContext("2d");
    canvas.onmousemove = playerMove;
    setInterval(play, 1000 / 50);
}


Игровые столкновения


Самое интересное начнётся сейчас, нам нужно научить шарик не вылетать за пределы игрового поля, а так же соприкасаться с ракетками, для этого я написал небольшую функцию которая возвращает истинное значение если два игровых объекта соприкасаться. Ниже её код
function collision(objA, objB) {
    if (objA.x+objA.width  > objB.x &&
        objA.x             < objB.x+objB.width &&
        objA.y+objA.height > objB.y &&
        objA.y             < objB.y+objB.height) {
            return true;
        }
        else {
            return false;
            }
    }

Теперь что бы шарик не просто улетал за пределы игрового поля нам надо немного скорректировать функцию update, а именно 
function update() {
    // меняем координаты шарика
    // Движение по оси У
    if (ball.y<0 || ball.y+ball.height>game.height) {
        // соприкосновение с полом и потолком игрового поля
        ball.vY = -ball.vY;
    }
    // Движение по оси Х
    if (ball.x<0) {
        // столкновение с левой стеной
        ball.vX = -ball.vX;
        player.scores ++;
    }
    if (ball.x+ball.width>game.width) {
        // столкновение с правой
        ball.vX = -ball.vX;
        ai.scores ++;
    }
    // Соприкосновение с ракетками
    if ((collision(ai, ball) && ball.vX<0) || (collision(player, ball) && ball.vX>0)){
        ball.vX = -ball.vX;
    }
    // приращение координат
    ball.x += ball.vX;
    ball.y += ball.vY;
}

Теперь в нашу игру практически можно играть, мячик летает правильно, ракетки его могут отбивать, оппонент правда у нас слегка труп, но это мы поправим, небольшой функцией

function aiMove() {
    var y;
    // делаем скорость оппонента на две единицы меньше чем скорость шарика
    var vY = Math.abs(ball.vY) - 2;
    if (ball.y < ai.y + ai.height/2) {
        y = ai.y - vY;
    }
    else {
        y = ai.y + vY;
    }
    if (10 < y && y < game.height - ai.height - 10) {
        ai.y = y;
    }
}

Вызов функции следует раместить в update.

Итог


Собственно вот и написали, осталось лишь немного подлатать и вот он полноценный понг. Статистику сыгранных игр мы будем так же как и счёт выводить на экран, а изменения скорости привяжем к клику мыши. По ссылке можно найти ПОНГ с простой статистикой, полный комментариев.
Alexander Shpak @shpaker
карма
27,5
рейтинг 0,0
Дворник
Похожие публикации
Самое читаемое Разработка

Комментарии (22)

  • –5
    Спасибо большое!
    • –7
      Ябывдул уже не модно писать? «Спасибо большое» теперь модный первый коммент!
  • +5
    А Вы пробовали theshock.github.com/libcanvas от theshock?
    • 0
      github.com/theshock/theshock.github.com — это для тех, кто хочет участвовать в развитии проекта или же просто ознакомиться в исходниками.
      • +1
        *с исходниками
      • +3
        Спасибо) Тут лежит сайт с исходниками примеров, а сам LibCanvas — theshock.github.com/libcanvas
      • 0
        Ссылки эти у меня в закладках пока хоронятся, пока руки не доходят. Но в планах )
  • +1
    Затягивает… зараза
    • +1
      обновил чуток )) сам игрался сегодня с час ))
  • 0
    В данном примере, конечно, в этом смысла нет, но гораздо интересней просчитывать перемещение шарика через углы. В конечном счёте, открывает больший простор для разнообразных действий. Правда реализовать это у меня получилось только в прямоугольной области. С отражением от окружности пока загвоздки.
    • 0
      Ну через углы это конечно лучше, но просто несколько усложнило бы код, да и в игре где все прямоугольное я думаю это было бы неоправданной роскошью ))
      • 0
        Ну да, я то просто добавлял управление с клавиатуры, шариком можно было «летать» :) А здесь это может пригодится в случае добавления гравитационных полей и всяческих, связанных с этим павер-апов.

        Что-то я замечтался :)
        • 0
          Я когда начал писать понг на всё дело у меня ушло часа два, и неделя на фарширование его всякими не нужными фичами. В итоге получился Понг в который играть было не интересно. Так что я думаю минимализм в написании понга просто необходим)
  • 0
    вы принципиально не используете ключевое слово «var»?
    • 0
      В данном случае var не используется, т. к. это не переменные, а классы. С ключевым словом «var» методы класса работать не будут.
      • 0
        хм, немного пристальней вгляделся в код. Что вы имеете ввиду под «классами»? В любом случае, внутри функции init() у Вас объявляются переменные start, game, ai, player, ball без ключевого слова var. JavaScript, когда встречает такие объявления, делает такие переменные глобальными (если точнее — свойствами объекта window), поэтому все и работает. Но это очень плохой стиль, т.к. мало того, что засоряется глобальное пространство имен, так еще и понять, где что было объявлено потом очень трудно.

        Да, да, я понимаю, что это простенький пример и тут и так все понятно, просто советую Вам в более серъезных задачах так не делать.

        И, кстати, даже в Вашем примере, уж если без глобальных переменных было не обойтись, то правильно бы было вверху скрипта сделать объявление:

        var start, game, ai, player, ball;
        • 0
          Я, конечно, не силён в яваскрипте. Вполне вероятно, что сморозил глупость.

          Я так понял, что start, game, ai, player и ball это не переменные, а объекты класса rect, который содержит поля width, height, color и метод draw. Обычные переменные не могут их содержать.

          В любом случае, в Опере вызываемые методы этих объектов не работают, если объекты объявлять с ключевым словом var.
          • 0
            Ах да, этот код совсем не мой :)
            • +2
              код мой )) И он писался сразу по ходу написания поста и поэтому есть некоторые огрешности. Класс всего один, а переменные без var потому что они нужны были глобальные в силу того что их надо было таскать по всем функциям, и как написал safron, правильней их было объявить в начале скрипта
  • +1
    Извините, у вас в листинге об aiMove() в коде допущена ошибка. Вы обращаетесь к свойству vY неизвестного объекта:
    y = ai.y + vY;
    

    Должно быть вы обращаетесь к свойству vY объекта ball, тогда должно быть:
    y = ai.y + ball.vY;
    

    • +1
      Прошу прощения, это я ошибся! Не обратил внимание на строку:

      var vY = Math.abs(ball.vY)-2
      

      Но не могу понять почему мы отнимаем 2 от ball.vY, если ball.vY по-умолчанию 2. Тогда значение vY всегда будет ровно 0 и ракетка всегда будет стоять на месте.
      • 0
        Так скорость шара после каждого столкновения со стенкой немного увеличивается и потому vY равен 0 только в самом начале.
        ball.x += ball.vX; ball.y += ball.vY;

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.