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

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

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

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

Оригинальная идея
По задумке автора оригинального примера, первоначально создается задний фон в виде длинной ленты путем копирования картинки на задний слой какое-то количество раз, которое ограничено длиной всего уровня. Такая же ситуация и с «землей».
И это работает, но делает уровень конечным по своей протяженности.
В классических же примерах «раннеров» (тип. прим.: «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» });
Итак, мы создали два объекта фона, два объекта земли и один объект «собачки», можно приступать к написанию алгоритма движения.
У нас есть несколько вариантов:
- Двигать собачку, и перемещать в сл��д за ней камеру
- Двигать фон, а собачку не двигать вообще
Я решил реализовать второй, и весь он помещается в одну функцию:
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» — это произвольное название игрового цикла, которое может быть любым.
Результат не заставил себя должно ждать:

Ну и, дабы вживую убедиться, запуск в браузере этого примера: Запустить и проверить
Ну и по традиции...
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');
