KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » Марейн Хавербеке - Выразительный JavaScript

Марейн Хавербеке - Выразительный JavaScript

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Марейн Хавербеке, "Выразительный jаvascript" бесплатно, без регистрации.
Перейти на страницу:

    this.speed = new Vector(0, 3);

    this.repeatPos = pos;

  }

}

Lava.prototype.type = "lava";

Монеты просты в реализации. Они просто сидят на месте. Но для оживления игры они будут подрагивать, слегка двигаясь по вертикали туда-сюда. Для отслеживания этого, объект coin хранит основную позицию вместе со свойством wobble, которое отслеживает фазу движения. Вместе они определяют положение монеты (хранящееся в свойстве pos).

function Coin(pos) {

  this.basePos = this.pos = pos.plus(new Vector(0.2, 0.1));

  this.size = new Vector(0.6, 0.6);

  this.wobble = Math.random() * Math.PI * 2;

}

Coin.prototype.type = "coin";

В главе 13 мы видели, что Math.sin даёт координату y точки на круге. Она движется туда и обратно в виде плавной волны, пока мы движемся по кругу, что делает функцию синуса пригодной для моделирования волнового движения.

Чтобы избежать случая, когда все монетки двигаются синхронно, начальная фаза каждой будет случайной. Фаза волны Math.sin и ширина волны — 2π. Мы умножаем значение, возвращаемое Math.random, на этот номер, чтобы задать монете случайное начальное положение в волне.

Теперь мы написали всё, что необходимо для представления состояния уровня.

var simpleLevel = new Level(simpleLevelPlan);

console.log(simpleLevel.width, "by", simpleLevel.height);

// → 22 by 9

Нам предстоит выводить эти уровни на экран и моделировать время и движение внутри них.

Бремя инкапсуляции

В большинстве случаев код данной главы не заботится об инкапсуляции. Во-первых, инкапсуляция требует дополнительных усилий. Программы становятся больше, требуют больше концепций и интерфейсов. А так как от слишком большого объёма кода глаза читателя стекленеют, я постарался сохранить программу небольшой.

Во-вторых, различные элементы игры так связаны вместе, что если бы менялось поведение одного из них, вряд ли оставшийся код оставался бы неизменным. Создание интерфейсов между элементами привело бы к использованию слишком большого количества предположений по поводу того, как работает игра. И тогда они были бы неэффективными – меняя одну часть системы, вам приходилось бы думать, как это влияет на другие части, потому что их интерфейсы не охватывали бы новую ситуацию.

Некоторые части системы хорошо поддаются разделению на кусочки со строго прописанными интерфейсами, а другие – нет. В попытках инкапсулировать нечто, не имеющее чётких границ, вы гарантированно потратите много сил. Совершив такую ошибку, вы увидите, что интерфейсы становятся чересчур большими и детальными, и что их надо часто менять в процессе эволюции программы.

Одну вещь мы всё-таки инкапсулируем – подсистему рисования. Это сделано специально для того, чтобы в следующей главе мы могли выводить на экран ту же игру другим способом. Спрятав рисование за интерфейс, мы можем просто загрузить ту же программу и подключить к ней новый модуль вывода на экран.

Рисование

Инкапсулировать код для рисования мы будем, введя объект display, который выводит уровень на экран. Тип экрана, который мы определяем, зовётся DOMDisplay, потому что он использует элементы DOM для показа уровня.

Мы используем таблицу стилей для задания цветов и других фиксированных свойств элементов, составляющих игру. Было бы возможно непосредственно назначать стиль элементу через свойство style при его создании, но программа в этом случае стала бы излишне многословной.

Следующая вспомогательная функция даёт простой способ создания элемента с назначением класса.

function elt(name, className) {

  var elt = document.createElement(name);

  if (className) elt.className = className;

  return elt;

}

Экран создаём, передавая ему родительский элемент, к которому необходимо подсоединиться, и объект уровня.

function DOMDisplay(parent, level) {

  this.wrap = parent.appendChild(elt("div", "game"));

  this.level = level;


  this.wrap.appendChild(this.drawBackground());

  this.actorLayer = null;

  this.drawFrame();

}

Используя тот факт, что appendChild возвращает добавленный элемент, мы создаём окружающий элемент wrapper и сохраняем его в свойстве wrap.

Неизменный фон уровня рисуется единожды. Актёры перерисовываются каждый раз при обновлении экрана. Свойство actorLayer используется в drawFrame для отслеживания элемента, содержащего актёра – чтобы их было легко удалять и заменять.

Координаты и размеры измеряются в единицах, относительных к размеру решётки так, что дистанция в единицу означает один элемент решётки. Когда мы задаём размеры в пикселях, нам нужно будет масштабировать координаты – игра была бы очень мелкой, если б один квадратик задавался одним пикселем. Переменная scale даёт количество пикселей, которое занимает один элемент решётки.

var scale = 20;


DOMDisplay.prototype.drawBackground = function() {

  var table = elt("table", "background");

  table.style.width = this.level.width * scale + "px";

  this.level.grid.forEach(function(row) {

    var rowElt = table.appendChild(elt("tr"));

    rowElt.style.height = scale + "px";

    row.forEach(function(type) {

      rowElt.appendChild(elt("td", type));

    });

  });

  return table;

};

Как мы уже упоминали, фон рисуется через элемент <table>. Это удобно соответствует тому факту, что уровень задан в виде решётки – каждый ряд решётки превращается в ряд таблицы (элемент <tr>). Строки решётки используются как имена классов ячеек таблицы (<td>). Следующий CSS приводит фон к необходимому нам внешнему виду:

.background    { background: rgb(52, 166, 251);

                 table-layout: fixed;

                 border-spacing: 0;              }

.background td { padding: 0;                     }

.lava          { background: rgb(255, 100, 100); }

.wall          { background: white;              }

Некоторые из настроек (table-layout, border-spacing и padding) используются для подавления нежелательного поведения по умолчанию. Не нужно, чтобы вид таблицы зависел от содержимого ячеек, и не нужны пробелы между ячейками или отступы внутри них.

Правило background задаёт цвет фона. CSS разрешает задавать цвета словами (white) и в формате rgb(R, G, B), где красная, зелёная и синяя компоненты разделены на три числа от 0 до 255. То есть, в записи rgb(52, 166, 251) красный компонент равен 52, зелёный 166 и синий 251. Поскольку синий компонент самый большой, результирующий цвет будет синеватым. Вы можете видеть, что самый большой компонент в правиле .lava – красный.

Каждый актёр рисуется созданием элемента DOM и заданием позиции и размера, основываясь на свойства актёра. Значения надо умножать на масштаб scale, чтобы переходить от единиц игры к пикселям.

DOMDisplay.prototype.drawActors = function() {

  var wrap = elt("div");

  this.level.actors.forEach(function(actor) {

    var rect = wrap.appendChild(elt("div",

                                    "actor " + actor.type));

    rect.style.width = actor.size.x * scale + "px";

    rect.style.height = actor.size.y * scale + "px";

    rect.style.left = actor.pos.x * scale + "px";

    rect.style.top = actor.pos.y * scale + "px";

  });

  return wrap;

};

Чтобы задать элементу больше одного класса, мы разделяем их имена пробелами. В коде CSS класс actor задаёт позицию absolute. Имя типа используется в дополнительном классе для задания цвета. Нам не надо заново определять класс lava, потому что мы повторно используем класс для лавы из решётки, который мы определили ранее.

.actor  { position: absolute;            }

.coin   { background: rgb(241, 229, 89); }

.player { background: rgb(64, 64, 64);   }

При обновлении экрана метод drawFrame удаляет старое изображение актёра, если оно было, и затем перерисовывает его на новой позиции. Напрашивается использование элементов DOM в качестве актёров, но для этого нам потребовалось бы передавать слишком много дополнительной информации между кодом дисплея и кодом симуляции. Надо было бы связать актёров с элементами DOM, и код рисования должен был бы удалять элементы при исчезновении актёров. Так как обычно в игре актёров совсем немного, их перерисовка отнимает немного ресурсов.

DOMDisplay.prototype.drawFrame = function() {

  if (this.actorLayer)

    this.wrap.removeChild(this.actorLayer);

  this.actorLayer = this.wrap.appendChild(this.drawActors());

  this.wrap.className = "game " + (this.level.status || "");

  this.scrollPlayerIntoView();

};

Добавив в обёртку wrapper текущий статус уровня в виде класса, мы можем стилизовать персонажа по-разному в зависимости от того, выиграна игра или проиграна. Мы добавим правило CSS, которое работает, только когда у игрока есть потомок с заданным классом.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*