Выразительный JavaScript.
Назад к игре.
Теперь мы знаем о холсте достаточно, чтобы начать разработку графической системы для игры из предыдущей главы. Новая система не будет показывать только цветные квадратики. Мы будем использовать drawImage для рисования картинок, представляющих элементы игры.
Мы определим тип объекта CanvasDisplay, который будет поддерживать тот же интерфейс, что и DOMDisplay из главы 15, а именно, методы drawFrame и clear.
Объект хранит больше информации, чем DOMDisplay. Вместо использования позиции прокрутки элемента DOM, он отслеживает окно просмотра, которое сообщает, какую часть уровня мы сейчас видим. Также он отслеживает время и использует это, чтобы решить, какой кадр анимации показывать. И ещё он хранит свойство flipPlayer, чтобы даже когда игрок стоял на месте, он был повёрнут в ту сторону, в которую шёл в последний раз.
Function CanvasDisplay(parent, level) {
This.canvas = document.createElement("canvas");
This.canvas.width = Math.min(600, level.width * scale);
This.canvas.height = Math.min(450, level.height * scale);
Parent.appendChild(this.canvas);
This.cx = this.canvas.getContext("2d");
This.level = level;
This.animationTime = 0;
This.flipPlayer = false;
This.viewport = {
Left: 0,
Top: 0,
Width: this.canvas.width / scale,
Height: this.canvas.height / scale.
};
This.drawFrame(0);
}
CanvasDisplay.prototype.clear = function() {
This.canvas.parentNode.removeChild(this.canvas);
};
В 15 главе мы передавали размер шага в drawFrame из-за счётчика animationTime, несмотря на то, что DOMDisplay его не использовал. Наша новая функция drawFrame использует его для отсчёта времени, чтобы переключаться между кадрами анимации в зависимости от текущего времени.
CanvasDisplay.prototype.drawFrame = function(step) {
This.animationTime += step;
This.updateViewport();
This.clearDisplay();
This.drawBackground();
This.drawActors();
};
Кроме отслеживания времени, метод обновляет окно просмотра текущей позиции игрока, заполняет холст цветом фона, и рисует фон и актёров. Заметьте, что всё происходит не так, как в главе 15, где мы рисовали фон один раз, а затем прокручивали оборачивающий элемент DOM для перемещения по нему.
Так как формы на холсте – всего лишь пиксели, после их отрисовки их нельзя сдвинуть (или убрать). Единственным способом обновить холст будет очистить его и перерисовать сцену.
Метод updateViewport похож на метод scrollPlayerIntoView из DOMDisplay. Он проверяет, не находится ли игрок слишком близко к краю экрана и двигает окно просмотра, если это случается.
CanvasDisplay.prototype.updateViewport = function() {
Var view = this.viewport, margin = view.width / 3;
Var player = this.level.player;
Var center = player.pos.plus(player.size.times(0.5));
If (center.x < view.left + margin).
View.left = Math.max(center.x - margin, 0);
Else if (center.x > view.left + view.width - margin).
View.left = Math.min(center.x + margin - view.width,
This.level.width - view.width);
If (center.y < view.top + margin).
View.top = Math.max(center.y - margin, 0);
Else if (center.y > view.top + view.height - margin).
View.top = Math.min(center.y + margin - view.height,
This.level.height - view.height);
};
Вызовы Math.max и Math.min гарантируют, что окно просмотра не будет показывать пространство за пределами уровня. Math.max(x, 0) гарантирует, что итоговое число не меньше нуля. Сходным образом Math.min гарантирует, что значение не превысит заданную границу.
При очистке дисплея мы используем другой цвет, в зависимости от того, выиграна игра или проиграна.
CanvasDisplay.prototype.clearDisplay = function() {
If (this.level.status == "won").
This.cx.fillStyle = "rgb(68, 191, 255)";
Else if (this.level.status == "lost").
This.cx.fillStyle = "rgb(44, 136, 214)";
Else.
This.cx.fillStyle = "rgb(52, 166, 251)";
This.cx.fillRect(0, 0,
This.canvas.width, this.canvas.height);
};
Для рисования фона мы пробегаемся по клеткам, видимым в текущем окне просмотра, используя тот же фокус, что и в obstacleAt в предыдущей главе.
Var otherSprites = document.createElement("img");
OtherSprites.src = "img/sprites.png";
CanvasDisplay.prototype.drawBackground = function() {
Var view = this.viewport;
Var xStart = Math.floor(view.left);
Var xEnd = Math.ceil(view.left + view.width);
Var yStart = Math.floor(view.top);
Var yEnd = Math.ceil(view.top + view.height);
For (var y = yStart; y < yEnd; y++) {
For (var x = xStart; x < xEnd; x++) {
Var tile = this.level.grid[y][x];
If (tile == null) continue;
Var screenX = (x - view.left) * scale;
Var screenY = (y - view.top) * scale;
Var tileX = tile == "lava" ? scale : 0;
This.cx.drawImage(otherSprites,
TileX, 0, scale, scale,
ScreenX, screenY, scale, scale);
}
}
};
Непустые клетки ( null) рисуются через drawImage. Изображение otherSprites содержит картинки для элементов, не относящихся к игроку. Слева направо — это стена, лава и монетка.
Спрайты для нашей игры.
Клетки фона 20×20 пикселей, так как мы используем ту же шкалу, что была в DOMDisplay. Значит, сдвиг клеток лавы 20 (значение переменной scale), а сдвиг стен 0.
Мы не ждём загрузки спрайта. Вызов drawImage с незагруженной пока картинкой ничего не сделает. Поэтому, на нескольких первых кадрах игра может быть отрисована неверно, но это не так уж критично. Так как мы обновляем экран, правильная сцена появится сразу после окончания загрузки.
Наш персонаж будет использован в качестве игрока. Код его отрисовки должен выбирать правильный спрайт и направление, зависящее от текущего движения игрока. Первые восемь спрайтов содержат анимацию ходьбы. Когда игрок передвигается по полу, мы перебираем их в зависимости от свойства animationTime объекта display. Оно измеряется в секундах, а нам надо менять кадры 12 раз в секунду, поэтому мы умножаем время на 12. Когда игрок стоит, мы рисуем девятый спрайт. В прыжках, которые мы распознаём по тому, что вертикальная скорость отлична от нуля, мы рисуем десятый, самый правый спрайт.
Поскольку спрайты чуть шире ширины объекта игрока – 24 пикселя вместо 16, чтобы было место для рук и ног, метод должен подправлять координату x и ширину на заданное число ( playerXOverlap).
Var playerSprites = document.createElement("img");
PlayerSprites.src = "img/player.png";
Var playerXOverlap = 4;
CanvasDisplay.prototype.drawPlayer = function(x, y, width,
Height) {
Var sprite = 8, player = this.level.player;
Width += playerXOverlap * 2;
X -= playerXOverlap;
If (player.speed.x != 0).
This.flipPlayer = player.speed.x < 0;
If (player.speed.y != 0).
Sprite = 9;
Else if (player.speed.x != 0).
Sprite = Math.floor(this.animationTime * 12) % 8;
This.cx.save();
If (this.flipPlayer).
FlipHorizontally(this.cx, x + width / 2);
This.cx.drawImage(playerSprites,
Sprite * width, 0, width, height,
X, y, width, height);
This.cx.restore();
};
Метод drawPlayer вызывается через drawActors, который рисует всех актёров в игре.
CanvasDisplay.prototype.drawActors = function() {
This.level.actors.forEach(function(actor) {
Var width = actor.size.x * scale;
Var height = actor.size.y * scale;
Var x = (actor.pos.x - this.viewport.left) * scale;
Var y = (actor.pos.y - this.viewport.top) * scale;
If (actor.type == "player") {
This.drawPlayer(x, y, width, height);
} else {
Var tileX = (actor.type == "coin" ? 2 : 1) * scale;
This.cx.drawImage(otherSprites,
TileX, 0, width, height,
X, y, width, height);
}
}, this);
};
При отрисовке чего-либо кроме игрока мы смотрим на его тип, чтобы найти смещение для нужного спрайта. Лава находится по смещению 20, монета – 40.
Нужно вычитать позицию окна просмотра при подсчёте позиции актёра, так как точка (0, 0) нашего холста соответствует левой верхней точке окна просмотра, а не левой верхней точке уровня. Ещё мы могли бы использовать для этой цели translate.
Следующий маленький документ подключает новый display в runGame:
<body>
<script>
RunGame(GAME_LEVELS, CanvasDisplay);
</script>
</body>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.