Створення нескінченного раннери на JavaScript, механіка руху фону

Переглядаючи приклади різних ігрових додатків і цікавих рішень я натрапив на приклад механіки "типового" раннери. Розглядався там тільки принцип руху заднього фону з застосуванням ефекту «паралакс», але ця ідея наштовхнула мене на деякі думки, про які я й хотів би розповісти нижче.
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 : висота, // висоту заднього фону дорівнює висоті сцени
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 : висота
});

Тут все так само, але тільки без «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 : висота,
onload : function () {
fon2.x = fon1.x+fon1.w;
}
});

var fon2 = game.newImageObject({
x : 0, y : 0,
file : 'imgs/fon.jpg',
h : висота
});

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');

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.