Давно уже не писал для хабра, сегодня хотелось бы порассуждать на тему того, как организовать простейшую механику уровней для игры 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 на гитхаб
→ Запустить пример вижвую
Дополнение игры новыми объектами:
Автор: Skaner