Выразительный JavaScript - Марейн Хавербеке
Шрифт:
Интервал:
Закладка:
Чёрный прямоугольник представляет игрока, чья задача – собирать жёлтые квадраты (монеты), избегая красных участков (лава?). Уровень заканчивается, когда игрок собрал все монеты.
Игрок может ходить клавишами влево и вправо, и прыгать клавишей вверх. Прыжки – это специальность нашего персонажа. Он может прыгать в несколько раз выше своего роста и менять направление движения в воздухе. Это не очень-то реалистично, но помогает игроку почувствовать полный контроль над его экранным аватаром.
У игры фиксированный фон в виде решётки, где движущиеся элементы накладываются на фон. Каждая ячейка решётки либо пустая, либо является стеной, либо лавой. Движущиеся элементы – игрок, монеты и некоторые кусочки лавы. В отличие от симуляции жизни из главы 7, позиции этих элементов не привязаны к решётке. Их координаты могут быть дробными, обеспечивая плавное движение.
Технология
Мы используем DOM браузера для графики, и читаем ввод пользователя, обслуживая события клавиатуры.
Код, относящийся к экрану и клавиатуре – небольшая часть работы, которую нам над проделать для создания игры. Так как всё состоит из цветных квадратиков, рисовать это просто: мы создаём элементы DOM и используем стили, чтобы задать им цвет фона, размер и расположение.
Мы представляем фон как таблицу, поскольку это – неизменная решётка из квадратов. Свободно двигающиеся элементы можно накладывать сверху, используя абсолютное позиционирование.
В играх и других интерактивных программах с графической анимацией, которые должны реагировать на действия пользователя без задержки, очень важна эффективность. Хотя DOM не был задуман для вывода высокоскоростной графики, он справляется с этим лучше, чем можно ожидать. В главе 13 вы видели немножко анимации. На современном компьютере такая простая игра идёт неплохо, даже если не сильно мучиться с оптимизацией.
В следующей главе мы изучим другую технологию браузера, тег <canvas>, который предоставляет более традиционный способ для рисования, и работает с формами и пикселями вместо элементов DOM.
Уровни
В главе 7 мы использовали массивы строк для описания двумерной решётки. Мы можем сделать то же и здесь. Это позволит нам разрабатывать уровни без того, чтобы сначала писать редактор уровней.
Простой уровень может выглядеть так:
var simpleLevelPlan = [
" ",
" ",
" x = x ",
" x o o x ",
" x @ xxxxx x ",
" xxxxx x ",
" x!!!!!!!!!!!!x ",
" xxxxxxxxxxxxxx ",
" "
];
Фиксированная решётка и движущиеся элементы включены. Символы x обозначают стены, пробелы – пустое место, а восклицательные знаки – фиксированная лава.
@ отмечает место, где игрок начинает. o – монетки, знак равенства = означает блок движущейся лавы, который двигается по горизонтали туда и сюда. Заметьте, что решётка на этих позициях будет содержать пустое пространство, и для отслеживания позиции этих подвижных элементов используется ещё одна структура данных.
Мы будем поддерживать ещё два вида лавы: вертикальная черта | — для кусочков, двигающихся по вертикали, и v для капающей лавы. Она будет двигаться вниз, но не отскакивать обратно, а просто перепрыгивать на начальную позицию по достижению пола.
Игра состоит из нескольких уровней, которые надо закончить. Уровень закончен, когда собраны все монетки. Если игрок касается лавы, текущий уровень возвращается к исходному состоянию, и игрок начинает заново.
Чтение уровня
Следующий конструктор создаёт объект уровня. Аргументом должен быть массив строк, задающих уровень.
function Level(plan) {
this.width = plan[0].length;
this.height = plan.length;
this.grid = [];
this.actors = [];
for (var y = 0; y < this.height; y++) {
var line = plan[y], gridLine = [];
for (var x = 0; x < this.width; x++) {
var ch = line[x], fieldType = null;
var Actor = actorChars[ch];
if (Actor)
this.actors.push(new Actor(new Vector(x, y), ch));
else if (ch == "x")
fieldType = "wall";
else if (ch == "!")
fieldType = "lava";
gridLine.push(fieldType);
}
this.grid.push(gridLine);
}
this.player = this.actors.filter(function(actor) {
return actor.type == "player";
})[0];
this.status = this.finishDelay = null;
}
Для краткости код не проверяет входящие данные. Он предполагает, что план уровня допустимый, что там есть стартовая позиция игрока и другие необходимые вещи.
Уровень сохраняет свои ширину и высоту и ещё два массива – один для решётки, и один для движущихся частей. Решётку представляет массив массивов, где каждый вложенный массив представляет горизонтальную линию, а каждый квадрат содержит либо null для пустых квадратов, либо строку, отражающую тип квадрата – “wall” или “lava”.
Массив actors содержит объекты, отслеживающие положения и состояния динамических элементов. У каждого из них должно быть свойство pos, содержащее позицию (координаты верхнего левого угла), свойство size с размером, и свойство type со строчкой, описывающей его тип ("lava", "coin" или "player").
После построения решётки мы используем метод filter, чтобы найти объект игрока, хранящийся в свойстве уровня. Свойство status отслеживает, выиграл игрок или проиграл. Когда это случается, используется finishDelay, которое держит уровень активным некоторое время для показа простой анимации. (Просто сразу восстанавливать состояние уровня или начинать следующий – это выглядит некрасиво). Этот метод можно использовать, чтобы узнать, закончен ли уровень:
Level.prototype.isFinished = function() {
return this.status != null && this.finishDelay < 0;
};
Действующие лица (актёры)
Для хранения позиции и размера наших актёров мы вернёмся к нашему верному типу Vector, который группирует координаты x и y в объект.
function Vector(x, y) {
this.x = x; this.y = y;
}
Vector.prototype.plus = function(other) {
return new Vector(this.x + other.x, this.y + other.y);
};
Vector.prototype.times = function(factor) {
return new Vector(this.x * factor, this.y * factor);
};
Метод times масштабирует вектор, умножая на заданную величину. Это будет удобно, когда нам надо будет умножать вектор скорости на временной интервал, чтобы узнать пройденный путь за это время.
В предыдущей секции конструктором Level был использован объект actorChars, чтобы связать символы с функциями конструктора. Объект выглядит так:
var actorChars = {
"@": Player,
"o": Coin,
"=": Lava, "|": Lava, "v": Lava
};
Три символа ссылаются на Lava. Конструктор Level передаёт исходный символ актёра в качестве второго аргумента конструктора, и конструктор Lava использует его для корректировки своего поведения (прыгать по горизонтали, прыгать по вертикали, капать).
Тип player построен следующим конструктором. У него есть свойство speed, хранящее его текущую скорость, что поможет нам симулировать импульс и гравитацию.
function Player(pos) {
this.pos = pos.plus(new Vector(0, -0.5));
this.size = new Vector(0.8, 1.5);
this.speed = new Vector(0, 0);
}
Player.prototype.type = "player";
Поскольку высотой игрок в полтора квадратика, его начальная позиция задаётся на полквадрата выше позиции, где расположен символ “@”. Таким образом его низ совпадает с низом квадрата, в котором он появляется.
При создании динамического объекта Lava, нам надо проинициализировать объект в зависимости от символа. Динамическая лава двигается с заданной скоростью, пока не встретит препятствие. Затем, если у неё есть свойство repeatPos, она отпрыгнет назад на стартовую позицию (капающая). Если нет, она инвертирует скорость и продолжает двигаться в обратном направлении (отскакивает). Конструктор задаёт только необходимые свойства. Позже мы напишем метод, который занимается самим движением.
function Lava(pos, ch) {
this.pos = pos;
this.size = new Vector(1, 1);
if (ch == "=") {
this.speed = new Vector(2, 0);
} else if (ch == "|") {
this.speed = new Vector(0, 2);
} else if (ch == "v") {
this.speed = new Vector(0, 3);
this.repeatPos = pos;
}
}
Lava.prototype.type = "lava";