Просматривая примеры разных игровых приложений и интересных решений я наткнулся на пример механики "типичного" раннера. Рассматривался там только принцип движения заднего фона с применением эффекта «параллакс», но эта идея натолкнула меня на некоторые мысли, о которых я и хотел бы рассказать ниже.


image


В качестве инструмента, я, как и ранее, буду использовать PointJS, потому что наглядно и просто.


Подготовка графики
Для заднего фона я буду использовать ту же картинку из примера (которая, судя по всему, взята из другого примера другого движка):


image


Для «земли», аналогично:


image


В качестве «персонажа» выступает милый пёсик:


image


Оригинальная идея
По задумке автора оригинального примера, первоначально создается задний фон в виде длинной ленты путем копирования картинки на задний слой какое-то количество раз, которое ограничено длиной всего уровня. Такая же ситуация и с «землей».


И это работает, но делает уровень конечным по своей протяженности.


В классических же примерах «раннеров» (тип. прим.: «FlappyBird») уровни, как правило, бесконечны, и проигрываются до тех пор, пока игрок не допустит фатальную ошибку, которая бы привела к завершению уровня.


Принцип работы
Моя идея заключается в том, чтобы сделать уровень бесконечным, но при этом не создавать бесконечной длины ленту для фона и «земли».


Задумка в целом очень и очень проста: создать несколько объектов, которые заполнят собой заднее пространство, и немного «вылезут» за пределы экрана, чтобы имитировать эффект движения.


Для «земли» все в точности так же.


Программирование
Так как я выбрал для работы PointJS, то и язык будет — JavaScript.


Подготовим полигон для действий:


// создание экземпляра движка
var pjs = new PointJS('2d', 800, 400); // размер экрана - 800x400
pjs.system.initFullPage(); // растянем на весь экран

// Объявим нужные нам для работы ссылки
var game = pjs.game; // менеджер игры
var point = pjs.vector.point; // конструктор точки

Размеры, которые мы задали сцене (800x400) конечно хорошо подойдут для удобства расчетов, но в реальности экраны все совсем разных размеров, и больше, и меньше.


После выполнения команды initFullPage() размеры сцены изменятся, и работать мы будем именно с ними, но сперва нам надо их получить:


// получим новые размеры сцены
var height = game.getWH().h; // высота
var width = game.getWH().w; // ширина

Отлично, мы имеем рабочую область, в которой можем работать.


Первым делом я думал воспользоваться массивом, но новичкам, скорее всего, пример с массивами будет ненаглядным, поэтому я воспользуюсь обычными переменными:


// создаем изображения для фона
var fon1 = game.newImageObject({ // создание картинки
 x : 0, y : 0, // начальная позиция в нулях (это левый верхний угол)
 file : 'imgs/fon.jpg', // путь к самой картинке
 h : height, // высоту заднего фона равна высоте сцены
 onload : function () { // эта функция выполнится, когда изображение загрузится
    fon2.x = fon1.x+fon1.w; // и станет доступна новая ширина
 }
});

Зачем нам «onload»? Тут все в целом ясно для тех, кто использует JavaScript в качестве основного языка, или хотя бы знаком с асинхронным подходом.


После создания картинки, мы явно указали ей высоту, и, новая ширина картинки после масштабирования станет доступна только после того, как картинка полностью загрузится. После загрузки в объект запишется переменная «w», которую мы и используем в формуле: «fon2.x = fon1.x+fon1.w», где fon2 — это вторая картинка.
Этой строкой мы с вами установили позицию второй картинки сразу за первой.


После этого создадим сам объект:


var fon2 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/fon.jpg',
 h : height
});

Тут все так же, но только без «onload».


Теперь создадим объект земли:


var gr1 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/ground.png',
 w : width,
 onload : function () {
    gr2.y = gr1.y = height — gr1.h; // установим позицию по Y в низ сцены
    gr2.x = gr1.x+gr1.w; // тот же принцип позиционирования, что и для фона
 }
});

var gr2 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/ground.png',
 w : width
});

Теперь создадим объект собачки, который у нас будет «бежать» по движущейся земле:


var dog = game.newAnimationObject({ // создаем анимационный объект
 x : width / 4, y : 0, // позиция по X будет одна четвертая ширины сцены
 h : 120, w : 150, // размеры «собачки» указываем явные
 delay : 4, // задержка (в FPS) при воспроизведении анимации
 animation : pjs.tiles.newAnimation('imgs/run_dog.png', 150, 120, 5) // тут получаем из файла спрайта анимацию и возвращаем её как свойство объекту «dog»
});

Итак, мы создали два объекта фона, два объекта земли и один объект «собачки», можно приступать к написанию алгоритма движения.


У нас есть несколько вариантов:


  1. Двигать собачку, и перемещать в след за ней камеру
  2. Двигать фон, а собачку не двигать вообще

Я решил реализовать второй, и весь он помещается в одну функцию:


var moveBackGround = function (s) { // аргумент s — это скорость движения фона

 // движение с эффектом «параллакс»
 fon1.move(point(-s / 2, 0)); // двигаем первую картинку с половиной скорости
 fon2.move(point(-s / 2, 0)); // двигаем вторую

 gr1.move(point(-s, 0)); // «землю» же двигаем на полной скорости
 gr2.move(point(-s, 0)); // и эту тоже

 // теперь проверим, не ушел ли объект фона «за кадр»
 if (fon1.x + fon1.w < 0) { // если ушел
  fon1.x = fon2.x+fon2.w; // перемещаем его сразу за вторым
 }

 // аналогично для второго
 if (fon2.x + fon2.w < 0) {
  fon2.x = fon1.x+fon1.w; // позиционируем за первым
 }

 // для земли все в точности так же
 if (gr1.x + gr1.w < 0) {
  gr1.x = gr2.x+gr2.w;
 }

 if (gr2.x + gr2.w < 0) {
  gr2.x = gr1.x+gr1.w;
 }
};

Вот и весь алгоритм, осталось только «запустить» всё это дело, для этого объявим игровой цикл:


// создание нового игрового цикла
game.newLoop('dog_game', function () {
 game.clear(); // очистим все, что было отрисовано в предыдущем кадре

 fon1.draw(); // рисуем первый фон
 fon2.draw(); // рисуем второй фон
 gr1.draw(); // рисуем первую землю
 gr2.draw(); // рисуем вторую землю

 // расположим нашего «пёсика» по высоте следующей формулой:
 dog.y = -dog.h + gr1.y + gr1.h /2.7;
 // тут все просто: нижнюю точку объекта (ноги) устанавливаем в позицию
 // объекта земли, и сдвигаем еще ниже, на расстояние равное 2.7 части от всей высоты объекта земли

 // ну и отрисуем его
 dog.draw();

 // и начинаем двигать!
 moveBackGround(4);

});

После объявления игрового цикла, просто призовем его к исполнению задуманного:


// запуск игрового цикла
game.startLoop('dog_game');

Тут надо понимать, что «dog_game» — это произвольное название игрового цикла, которое может быть любым.


Результат не заставил себя должно ждать:


image

Ну и, дабы вживую убедиться, запуск в браузере этого примера: Запустить и проверить


Ну и по традиции...


Видео разработки

Полный код примера
var pjs = new PointJS('2d', 400, 400);
pjs.system.initFullPage();

var game = pjs.game;
var point = pjs.vector.point;

var height = game.getWH().h;
var width = game.getWH().w;

var fon1 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/fon.jpg',
 h : height,
 onload : function () {
    fon2.x = fon1.x+fon1.w;
 }
});

var fon2 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/fon.jpg',
 h : height
});

var gr1 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/ground.png',
 w : width,
 onload : function () {
    gr2.y = gr1.y = height - gr1.h;
    gr2.x = gr1.x+gr1.w;
 }
});

var gr2 = game.newImageObject({
 x : 0, y : 0,
 file : 'imgs/ground.png',
 w : width
});

var dog = game.newAnimationObject({
    x : width / 4, y : 0,
    h : 120, w : 150,
    delay : 4,
    animation : pjs.tiles.newAnimation('imgs/run_dog.png', 150, 120, 5)
});

var moveBackGround = function (s) {
    fon1.move(point(-s / 2, 0));
    fon2.move(point(-s / 2, 0));

    gr1.move(point(-s, 0));
    gr2.move(point(-s, 0));

    if (fon1.x + fon1.w < 0) {
        fon1.x = fon2.x+fon2.w;
    }

    if (fon2.x + fon2.w < 0) {
        fon2.x = fon1.x+fon1.w;
    }

    if (gr1.x + gr1.w < 0) {
        gr1.x = gr2.x+gr2.w;
    }

    if (gr2.x + gr2.w < 0) {
        gr2.x = gr1.x+gr1.w;
    }

};

game.newLoop('game', function () {
    game.fill('#D9D9D9');

    fon1.draw();
    fon2.draw();
    gr1.draw();
    gr2.draw();

    dog.y = -dog.h + gr1.y + gr1.h /2.7;
    dog.draw();

    moveBackGround(4);

});

game.startLoop('game');
Поделиться с друзьями
-->

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


  1. Sirion
    12.08.2016 17:35
    -15

    Почему у пёсика вместо глаза картошка с пальцем?


    1. Mr_Rm
      12.08.2016 23:21
      +2

      Это ж Рекс (только зеркальный).


    1. ange007
      12.08.2016 23:22
      +2

      Отличный пёс.
      Это вообще кажется пёс из польского мультика (смотрел в детстве): ВИКИ

      UPD: Опоздал с комментом :)


    1. bjc
      15.08.2016 11:36

      Вот это у вас фантазия!..


  1. frustum
    12.08.2016 18:22

    В chromium на 1366x768 (debian gnome) видимо при окончании фона справа — появляется кратковременно белая полоса…


    1. lekzd
      12.08.2016 20:17

      Движок автора ради простоты перерисовывает весь экран, игнорируя даже невидимые участки, не округляет координаты до целого, чем еще добавляет расчетов цвета для соседних пикселей. Ну и код примера в любом случае рисует по 2 дубля фонов. Skaner, прочитай про createPattern().


      1. Skaner
        13.08.2016 16:09

        Это все опционально настраивается через настройки, и округление, и сглаживание, и отрисовки (включая обрезку объектов) ну или включение WebGL режима отрисовки. Последний пока в разработке.


        1. lekzd
          13.08.2016 17:59

          Но ведь это ложь)
          Во всем движке нашлась только одна функция для «округления» — toInt и она не используется ни при задании позиции, ни при отрисовке (я про типы объектов с собачкой и фоном, конечно), не говоря уже о других способах округления.
          Сглаживание это про то, что получается из-за разницы между canvas.width и canvas.style.width? Других вариантов как это «настраивается» я пока не нашел.
          Про обрезку это кажется про параметры context,drawImage(), но там везде по нулям по-умолчанию.

          Ждем WebGL, полагаю, не скоро.

          Это был бы хороший ответ для сейлза или маркетолога но не для разработчика движка.


  1. botaniQQQ
    12.08.2016 18:24

    Как на счет уроков по созданию мультиплеерных игр на JS (agar, slither, etc.)? В одиночных играх люди долго не задерживаются.


  1. dmitryvashkevich
    12.08.2016 19:20
    +1

    image

    Песик взлетел :)


    1. Skaner
      12.08.2016 19:21

      Да, потому что кто-то (я) случайно не в ту папку залил эксперимент)) Все уже поправил, соответствует статье)


  1. Darthman
    12.08.2016 20:20

    Результат, мягко говоря, никакой. Фон картонный сзади. Для параллакса двух слоёв мало. Висюльки желтые должны были бы быть ближе, а дальние деревья дальше, тогда бы смотрелось хорошо. Псевдотрехмерность дорожки тоже не добавляет красивости.
    Кстати, на месте стыков фона у меня полоска вертикальная видна. Уж не знаю фаерфокс так рендерит или что.

    В чем инновационность метода? Я из статьи не понял о чем она вообще. Параллакс в играх используется испокон веков и никто не делает никогда бесконечную простыню, это не разумно и не логично, ну и не очень экономично по отношению к ресурсам ПК.


  1. dolphin4ik
    12.08.2016 22:16
    +1

    Я честно говоря не понял зачем point? Обьясните для тех кто в танке


  1. Jmann
    12.08.2016 23:21

    Спасибо за статью. Мне, как новичку, статья оказалась очень полезной и познавательной!


  1. QtRoS
    12.08.2016 23:30

    32% загружено на Core i5, визуально подлагивает…


    1. GeMir
      13.08.2016 17:11

      i5, Windows 10, Chrome 52 — загрузка ~3%, «лагов» не замечено.


      1. QtRoS
        13.08.2016 17:51

        Ubuntu 16.04 :)


        1. Fearz
          14.08.2016 11:02

          48% загружено, тоже Ubuntu 16.04 на i5. Но лагов не видно.


  1. stepanp
    13.08.2016 01:11
    +3

    Урок уровня hello world от человека который даже не знает слова background.


    1. shushu
      13.08.2016 08:04
      +1

      Покажите как надо ;) Вы ведь слова на ветер не бросаете? Так ведь?


      1. shushu
        13.08.2016 12:31
        +2

        Нет, правда. Минусовать любой сможет. Вы статью напишите.


        1. stepanp
          13.08.2016 16:28
          +2

          Не правда, минусовать тут может далеко не каждый ;)

          Вы лучше вместо того чтобы высказываться в стиле «сперва добейся», аргументируйте в чем конкретно я неправ в моем комментарии


          1. shushu
            15.08.2016 10:09

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


            1. stepanp
              15.08.2016 13:48
              +3

              Он как раз и написан чтобы демотивировать писать мусорные статьи.
              Нейтральный комментарий скорее всего был бы проигнорирован. Ну и согласитесь, на недостатки я все-таки указал.


        1. ModoStudio
          14.08.2016 05:00

          +shushu Не любой. Не у всех статус позволяет.


          1. shushu
            15.08.2016 10:09
            -1

            В песочницу любой может написать.


      1. romy4
        13.08.2016 13:09

        Ваш пример аналогичен бесконечной «карусели».


  1. rogallic
    13.08.2016 08:12

    Если открыть эту игру на относительно слабых мобильных устройствах, то fps просидает до 5!!! (nexus 10). Эксперементировал с аналогичными параллаксами в браузере и вот результаты: Плавного хода фона на абсолютном большинстве устройств можно добиться используя CSS-анимации:

    @keyframes NAME-YOUR-ANIMATION {
      0%   { transform: translate(0px, 0); }
      100% { transform: translate(YOUR-px, 0); }
    }
    

    Но в случае с CSS-анимациями на многих устройствах невозможно добиться отзывчивости (например при следовании за пальцем).
    Для отзывчивого параллакса лучше просто из javascript 60 раз в секунду задавать
    background-position: YOUR-CALCULATED-px
    
    для каждого слоя фона. В таком случае на многих устройствах так же бывают просидания но скорость в несколько раз выше чем в примере статьи.


    1. profesor08
      13.08.2016 16:06
      +1

      И получите дерганный и жрущий ужас. Для подобной анимации надо использовать requestAnimationFrame, тут рисовать нужное кол-во «кадров» раз в секунду. Но этого может оказаться недостаточно, придется воспользоваться will-change.


  1. blackstrip
    13.08.2016 11:42
    -1

    «Моя идея заключается в том, чтобы сделать уровень бесконечным, но при этом не создавать бесконечной длины ленту для фона и «земли». Задумка в целом очень и очень проста: создать несколько объектов, которые заполнят собой заднее пространство, и немного «вылезут» за пределы экрана, чтобы имитировать эффект движения. Для «земли» все в точности так же.»

    Такие игры делали описанным образом уже лет 30 назад. Возьмем фон, землю, 5 кадров перса и наложим все это. Урок по бейсику в 5 классе школы.

    Да еще и на очередном «движке» потому что мозга не хватает написать чего нибудь самому даже на гнусном ява-скрипте.

    Хабр скатился куда-то…


    1. genew
      13.08.2016 16:06

      Движок автор сам написал, насколько я понимаю


      1. blackstrip
        13.08.2016 16:09

        «Так как я выбрал для работы PointJS, то и язык будет — JavaScript.» — а я так понял он взял готовый ибо, видимо, не умеет три картинки в JS вывести на экран без движков =)


  1. petr97
    13.08.2016 13:19

    В качестве «персонажа» выступает милый пёсик:

    А разве этот «милый песик» не Рекс из польского мультсериала?


  1. sim31r
    13.08.2016 16:53

    Сцена и интересная, релаксирующий скринсейвер. Только спрайт вырезан не идеально, видны остатки белого фона по краям. И полном экране (по F11), появляется белая полоса внизу, скрипт не отследил увеличение по вертикали.


  1. ModoStudio
    14.08.2016 04:55

    Автору спасибо за статью, скрипт и урок! ) Жаль скрипт чуть тормозит, идёт рывками: Chrome 52.0.2743.116 m, Mozilla 48, Opera 28.0.1750.48.
    Но всё равно прикольно.


    1. GeMir
      14.08.2016 10:17

      1. ModoStudio
        14.08.2016 10:44

        У меня 7-ка Максималка.


  1. AllegroMod
    14.08.2016 15:37
    +2

    «Первым делом я думал воспользоваться массивом, но новичкам, скорее всего, пример с массивами будет ненаглядным, поэтому я воспользуюсь обычными переменными»
    «Зачем нам onload? Тут в целом всё ясно для тех, кто использует JavaScript»

    Кто целевая аудитория?


  1. Taradast
    15.08.2016 13:15
    +1

    Статья отличная! Надеюсь будет продолжение в виде дресировки прикручивания управления песиком. Пока смотрел на пример хотелось нажать на пробел — чтоб песик исполнил какой-нибудь финт в прыжке. Интересно увидеть практическую реализацию смены анимации персонажа.


  1. VDemot
    17.08.2016 12:23
    +2

    Накидал все то же самое на Phaser.js, как мне кажется немножко плавнее анимация. Но все равно есть рывки.
    пример


    1. profesor08
      17.08.2016 20:04
      +1

      Сделал анимации на CSS3 и ужаснулся. Видны рывки. Похоже суть проблемы в том, что каждый раз перерисовывается элемент с нужной позицией фона.