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

Начнем по порядку. Я не буду расписывать все ненужное, а просто пробегусь по коду комментируя те или иные моменты.
Первое, это index.html — основной запускаемый файл:
→ Смотреть на гитхабе
Тут, думаю, все ясно: мы создали каркас страницы и подключили несколько JavaScript файлов.
Рассмотрим файл init.js:
→ Смотреть на гитхабе
Такой вот получился код, пока этого достаточно.
Теперь нам надо создать меню для игры, так как у нас стартует игра с цикла «menu», все необходимое для этого уже имеется в подключенном нами файле menu.js, надо лишь инициировать соответствующий игровой цикл, который будет являться нашим меню, делается это в том же файле menu.js, просто допишем там следующее:
→ Смотреть весь файл menu.js на гитхабе
Если запустим, то получим запущенное меню:

Теперь самое сложно: игровой процесс. Тут все достаточно просто, но не совсем. Код разделю на блоки.
Первое, что нам нужно сделать, это объявить игровой цикл. Так как она у нас будет самостоятельным объектом, со своей областью видимости, и нам нет необходимости делать его глобальным (данные игрового состояния не будут видны в других игровых циклах), воспользуемся простой конструкцией:
Тут мы можем задать игровому циклу наименование и конструктор.
Теперь переместимся внутрь конструктора, и напишем логику (механику) уровня внутри него. Нам на текущий момент понадобится только одно событие цикла: update. С ним работать и будем:
→ Смотреть файл на гитхабе
Теперь, если мы запустим и перейдем в игру, то увидим следующее:

Таким образом мы построили несложную механику игры, дополнить её новыми элементами не так сложно, вписать в обработку — аналогично.
Сам алгоритм физики можно дорабатывать, и менять коэффициенты, позволяя сужать или расширять поля столкновения. Так же их можно делать динамическими, беря за основу скорость движения персонажа.
→ Посмотреть исходник проекта Bounce на гитхаб
→ Запустить пример вижвую

Начнем по порядку. Я не буду расписывать все ненужное, а просто пробегусь по коду комментируя те или иные моменты.
Первое, это index.html — основной запускаемый файл:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="viewport" content="width=device-width,user-scalable=no"/> <title>Bounce</title> </head> <body> <script type="text/javascript" src="point.js"></script> <script type="text/javascript" src="init.js"></script> <script type="text/javascript" src="menu.js"></script> <script type="text/javascript" src="game.js"></script> <script type="text/javascript"> game.startLoop('menu'); </script> </body> </html>
→ Смотреть на гитхабе
Тут, думаю, все ясно: мы создали каркас страницы и подключили несколько JavaScript файлов.
- point.js — движок PointJS
- init.js — инициализация всего и вся + глобальные переменные
- menu.js — плагин к PointJS — Menu
- game.js — файл, описывающий механику игру
Рассмотрим файл init.js:
// Тут подключение движка, далее объект pjs будет глобальным var pjs = new PointJS('2D', 400, 400, { backgroundColor : '#C9D6FF' }); // Включаем полностраничный режим pjs.system.initFullPage(); // Объявляем ссылки на быстрый доступ к внутренностям движка var log = pjs.system.log; // логирование событий var game = pjs.game; // объект управления игровыми состояниями и объектами var point = pjs.vector.point; // конструктор точек var camera = pjs.camera; / доступ к камере var brush = pjs.brush; / доступ к методам простого рисования var OOP = pjs.OOP; / доступ к дополнительным обработчиком объектов var math = pjs.math; / модуль игровой математики // инициализируем мышь и клавиатуру var key = pjs.keyControl.initKeyControl(); var mouse = pjs.mouseControl.initMouseControl(); // тут объявим глобальные переменные счета и рекорда var score = 0; var record = 0;
→ Смотреть на гитхабе
Такой вот получился код, пока этого достаточно.
Теперь нам надо создать меню для игры, так как у нас стартует игра с цикла «menu», все необходимое для этого уже имеется в подключенном нами файле menu.js, надо лишь инициировать соответствующий игровой цикл, который будет являться нашим меню, делается это в том же файле menu.js, просто допишем там следующее:
game.newLoopFromClassObject('menu', new Menu(pjs, { name : 'Bounce', // Наименование игры (выводится вверху) author : 'SkanerSoft', // автор игры radius : 15, // радиус скругления пунктов меню items : { // сами пункты, формат: loopName : Видимая надпись game : 'В игру', // перейдет в игровой цикл game about : 'Об игре', // перейдет в игровой цикл about } }));
→ Смотреть весь файл menu.js на гитхабе
Если запустим, то получим запущенное меню:

Теперь самое сложно: игровой процесс. Тут все достаточно просто, но не совсем. Код разделю на блоки.
Первое, что нам нужно сделать, это объявить игровой цикл. Так как она у нас будет самостоятельным объектом, со своей областью видимости, и нам нет необходимости делать его глобальным (данные игрового состояния не будут видны в других игровых циклах), воспользуемся простой конструкцией:
game.newLoopFromConstructor('game', function () { /*your code*/ });
Тут мы можем задать игровому циклу наименование и конструктор.
Теперь переместимся внутрь конструктора, и напишем логику (механику) уровня внутри него. Нам на текущий момент понадобится только одно событие цикла: update. С ним работать и будем:
game.newLoopFromConstructor('game', function () { // объявим объект с данными карты var map = { width : 50, // ширина тайла height : 50, // высота тайла source : [ // исходные данные карты (массив строк) '', '', ' 0-', ' | P 0000', // P - позиция игрока ' 00000 000 00000', ' 0 0| |0 |', '0000000 000000W00 000000000000', ' 000 0W00 0 ', ' 0W0 0 ', ' 0W | 0 ', ' 000000 ', ] }; // стартовая позиция (переменная) var plStartPosition = false; var walls = []; // массив стен (блоки, по которым возможно передвигаться) var cells = []; // цели (колечки, которые можно собирать) var waters = []; // тут будет вода (блоки полупрозрачных объектов синего цвета), меняющая поведение объекта // OOP.forArr - проходит быстро по массиву OOP.forArr(map.source, function (string, Y) { // идем по массиву строк (Y - порядковый номер строки сверху вниз) OOP.forArr(string, function (symbol, X) { // идем уже по самой строке (X - порядковый номер символа в строке) if (!symbol || symbol == ' ') return; // если пробел или ошибка считывания - выходим из итерации // теперь проверяем символы if (symbol == 'P') { // позиционируем персонажа // Займемся игроком plStartPosition = point(map.width*X, map.height*Y); // если формула не ясна, напишите в комменты } else if (symbol == 'W') { // вода waters.push(game.newRectObject({ // создаем объект w : map.width, h : map.height, // ширина высота x : map.width*X, y : map.height*Y, // позиция fillColor : '#084379', // цвет alpha : 0.5 // прозрачность })); } else if (symbol == '|') { // цель (колечко) cells.push(game.newRectObject({ w : map.width/2, h : map.height, x : map.width*X, y : map.height*Y, fillColor : '#FFF953', userData : { active : true // флаг активности, пока не коснулся игрок - оно активно } })); } else if (symbol == '-') { // горизонтальное колечко cells.push(game.newRectObject({ w : map.width, h : map.height/2, x : map.width*X, y : map.height*Y, fillColor : '#FFF953', userData : { active : true } })); } else if (symbol == '0') { // блок стены walls.push(game.newRectObject({ w : map.width, h : map.height, x : map.width*X, y : map.height*Y, fillColor : '#B64141' })); } }); }); // При создании игрока мы смотрим // была ли задана позиция, и, если была // используем её, иначе устанавливаем в начало координат var player = game.newCircleObject({ radius : 20, fillColor : '#FF9191', position : plStartPosition ? plStartPosition : point(0, 0) }); player.gr = 0.5; // скорость падения player.speed = point(0, 0); // скорости по осям // а вот и тот самый обработчик на событие обновления this.update = function () { game.clear(); // очищаем прошлый кадр player.draw(); // отрисовываем игрока player.speed.y += player.gr; // используем гравитацию // управление с клавиатуры, думаю, ничего сложного if (key.isDown('RIGHT')) player.speed.x = 2; else if (key.isDown('LEFT')) player.speed.x = -2; else player.speed.x = 0; // теперь вызываем функцию отрисовки массива стен OOP.drawArr(walls, function (wall) { if (wall.isInCameraStatic()) { // если объект в пределах камеры (его видно) // wall.drawStaticBox(); if (wall.isStaticIntersect(player)) { // если объект столкнулся с игроком // теперь нам надо определить условия столкновения (подробное объяснение в видео ниже) // проверяем ось Y if (player.x+player.w > wall.x+wall.w/4 && player.x < wall.x+wall.w-wall.w/4) { if (player.speed.y > 0 && player.y+player.h < wall.y+wall.h/2) { // если объект НАД стеной if (key.isDown('UP')) // если при соприкосновении с полом нажать кнопку "вверх" player.speed.y = -10; // установим скорость движения вверх else { // иначе просто "гасим" скорость падения прыжками player.y = wall.y - player.h; player.speed.y *= -0.3; if (player.speed.y > -0.3) player.speed.y = 0; // и в итоге просто обнуляем } } else if (player.speed.y < 0 && player.y > wall.y+wall.h/2) { // если пбъект ПОД стеной player.y = wall.y+wall.h; // позиционируем (избегаем проваливания) player.speed.y *= -0.1; // начинаем падать } } // и тут то же самое, только уже для оси X if (player.y+player.h > wall.y+wall.h/4 && player.y < wall.y+wall.h-wall.h/4) { if (player.speed.x > 0 && player.x+player.w < wall.x+wall.w/2) { // если стена справа player.x = wall.x-player.w; // избегаем проваливания player.speed.x = 0; // убираем скорость движения } if (player.speed.x < 0 && player.x > wall.x+wall.w/2) { // если стена слева player.x = wall.w+wall.x; // избегаем проваливания player.speed.x = 0; // убираем скорость движения } } } } }); // теперь рисуем и орабатываем цели (колечки) OOP.drawArr(cells, function (cell) { if (cell.active) { // если колечко активно if (cell.isStaticIntersect(player)) { // проверяем столкновение cell.active = false; // снимаем активность cell.fillColor = '#9A9A9A'; // закрашиваем в другой цвет score++; // увеличиваем счет } } }); // зададим еще переменную флаг, определяющую находится ли // объект в воде var onWater = false; // Рисуем и обрабатываем воду OOP.drawArr(waters, function (water) { // Если наш игрок уже находится в воде, ничего не делаем if (onWater) return; // Тут нам надо определить стролкновение // и направить скорость вверх (выталкивание) // Надо хорошенько все продумать // Нам требуется учесть, что выталкивающая сила начинает // работать только тогда, когда шар опустится в воду // примерно на половину от его высоты if (water.isStaticIntersect(player) && player.y+player.h/2 > water.y) { player.speed.y -= 0.9; // определим оптимальную скорость onWater = true; } }); // тут само движение объектов if (player.speed.y) { player.y += player.speed.y; } if (player.speed.x) { player.x += player.speed.x; } // рисуем счет brush.drawTextS({ // команда рисования text : 'Score: '+score, // выводим саму надпись size : 30, // размер шрифта color : '#FFFFFF', // цвет текста strokeColor : '#002C5D', // цвет обводки текста strokeWidth : 1, // ширина обводки x : 10, y : 10, // позиция style : 'bold' // жирный шрифт }); camera.follow(player, 50); // следим камерой за объектом игрока }; });
→ Смотреть файл на гитхабе
Теперь, если мы запустим и перейдем в игру, то увидим следующее:

Таким образом мы построили несложную механику игры, дополнить её новыми элементами не так сложно, вписать в обработку — аналогично.
Сам алгоритм физики можно дорабатывать, и менять коэффициенты, позволяя сужать или расширять поля столкновения. Так же их можно делать динамическими, беря за основу скорость движения персонажа.
→ Посмотреть исходник проекта Bounce на гитхаб
→ Запустить пример вижвую
Видео, в котором я разрабатываю алгоритм физики для игры 20 минут
Физика игры
Дополнение игры новыми объектами:
Дополнение игры новыми объектами: