Этот учебник является отличным учебным пособием, которое укрепит ваши навыки в разработке HTML5 Canvas и SVG путем создания реальной веб-игры. Стартовый пакет и полный пакет решений можно найти здесь .
Резюме
Вступление
Целью данного руководства является изучение разработки графики с использованием SVG и Canvas (которые являются двумя основными технологиями HTML5).
Для этого мы вместе напишем игру «Разрушитель кирпичей» («Арканоид» или «Блокировка»). Он будет состоять из анимированного фона (с использованием Canvas) и будет использовать SVG для кубиков, пэдов и мячей.
Вы можете попробовать финальную версию здесь: http://www.catuhe.com/ms/en/index.htm
Предпосылки
- Internet Explorer 9/10 или другой современный браузер HTML5 с аппаратным ускорением
- Visual Studio 2010 SP1
- Обновление веб-стандартов: http://visualstudiogallery.msdn.microsoft.com/a15c3ce9-f58f-42b7-8668-53f6cdc2cd83
Настройка фона
Фон — только алиби для использования холста. Это позволит нам рисовать пиксели в заданной области. Поэтому мы будем использовать его, чтобы нарисовать космическую червоточину (да, я люблю Stargate !). Пользователи будут иметь возможность отображать его или не использовать кнопку режима:
Вы можете заметить, что мы добавим счетчик производительности в правом верхнем углу (просто чтобы увидеть мощь ускоренной графики )
Настройка страницы HTML5
Начиная с файла index.htm , мы добавим наш холст в качестве дочернего элемента div » gameZone «:
<canvas id="backgroundCanvas"> Your browser doesn't support HTML5. Please install Internet Explorer 9 : <br /> <a href="http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing"> http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing</a> </canvas>
Добавление кода JavaScript
Фон обрабатывается файлом background.js (какой сюрприз!). Поэтому мы должны зарегистрировать его внутри index.htm. Так что перед закрывающим тегом тела мы добавим следующий код:
<script type="text/javascript" src="background.js"></script>
Настройка констант
Прежде всего, нам нужны константы для управления рендерингом:
var circlesCount = 100; // Circles count used by the wormhole var offsetX = 70; // Wormhole center offset (X) var offsetY = 40; // Wormhole center offset (Y) var maxDepth = 1.5; // Maximal distance for a circle var circleDiameter = 10.0; // Circle diameter var depthSpeed = 0.001; // Circle speed var angleSpeed = 0.05; // Circle angular rotation speed
Вы, конечно, можете изменить эти константы, если хотите, чтобы ваш червоточина имела разные эффекты
Получение элементов
Нам также необходимо сохранить ссылку на основные элементы HTML-страницы:
var canvas = document.getElementById("backgroundCanvas"); var context = canvas.getContext("2d"); var stats = document.getElementById("stats");
Как отобразить круг?
Червоточина — это только последовательность кругов с разными позициями и размерами. Таким образом, чтобы нарисовать его, мы будем использовать функцию круга, которая строится вокруг глубины, угла и интенсивности (основного цвета).
function Circle(initialDepth, initialAngle, intensity) { }
Угол и интенсивность являются частными, но глубина является общедоступной, чтобы червоточина могла ее изменить
function Circle(initialDepth, initialAngle, intensity) { var angle = initialAngle; this.depth = initialDepth; var color = intensity; }
Нам также нужна публичная функция рисования, чтобы нарисовать круг и обновить глубину, угол. Таким образом, мы должны определить, где отображать круг. Для этого определены две переменные (x и y):
var x = offsetX * Math.cos(angle); var y = offsetY * Math.sin(angle);
Поскольку x и y находятся в пространственных координатах, нам нужно спроецировать их на экран:
function perspective(fov, aspectRatio, x, y) { var yScale = Math.pow(Math.tan(fov / 2.0), -1); var xScale = yScale / aspectRatio; var M11 = xScale; var M22 = yScale; var outx = x * M11 + canvas.width / 2.0; var outy = y * M22 + canvas.height / 2.0; return { x: outx, y: outy }; }
Таким образом, окончательная позиция круга вычисляется с помощью следующего кода:
var x = offsetX * Math.cos(angle); var y = offsetY * Math.sin(angle); var project = perspective(0.9, canvas.width / canvas.height, x, y); var diameter = circleDiameter / this.depth; var ploX = project.x - diameter / 2.0; var ploY = project.y - diameter / 2.0;
И используя эту позицию, мы можем просто нарисовать наш круг:
context.beginPath(); context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false); context.closePath(); var opacity = 1.0 - this.depth / maxDepth; context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")"; context.lineWidth = 4; context.stroke();
Вы можете заметить, что круг становится более непрозрачным, когда он ближе.
Итак, наконец:
function Circle(initialDepth, initialAngle, intensity) { var angle = initialAngle; this.depth = initialDepth; var color = intensity; this.draw = function () { var x = offsetX * Math.cos(angle); var y = offsetY * Math.sin(angle); var project = perspective(0.9, canvas.width / canvas.height, x, y); var diameter = circleDiameter / this.depth; var ploX = project.x - diameter / 2.0; var ploY = project.y - diameter / 2.0; context.beginPath(); context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false); context.closePath(); var opacity = 1.0 - this.depth / maxDepth; context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")"; context.lineWidth = 4; context.stroke(); this.depth -= depthSpeed; angle += angleSpeed; if (this.depth < 0) { this.depth = maxDepth + this.depth; } }; };
инициализация
С нашей функцией окружности мы можем иметь массив окружностей, которые мы будем инициировать все ближе и ближе к нам с небольшим смещением угла каждый раз:
// Initialization var circles = []; var angle = Math.random() * Math.PI * 2.0; var depth = maxDepth; var depthStep = maxDepth / circlesCount; var angleStep = (Math.PI * 2.0) / circlesCount; for (var index = 0; index < circlesCount; index++) { circles[index] = new Circle(depth, angle, index % 5 == 0 ? 200 : 255); depth -= depthStep; angle -= angleStep; }
Вычислительный FPS
Мы можем вычислить FPS, измерив количество времени между двумя вызовами данной функции. В нашем случае функция будет computeFPS . Это сохранит последние 60 мер и вычислит среднее значение для получения желаемого результата:
// FPS var previous = []; function computeFPS() { if (previous.length > 60) { previous.splice(0, 1); } var start = (new Date).getTime(); previous.push(start); var sum = 0; for (var id = 0; id < previous.length - 1; id++) { sum += previous[id + 1] - previous[id]; } var diff = 1000.0 / (sum / previous.length); stats.innerHTML = diff.toFixed() + " fps"; }
Рисование и анимация
Холст является инструментом прямого режима . Это означает, что мы должны воспроизводить все содержимое холста каждый раз, когда нам нужно что-то изменить.
И прежде всего нам нужно очистить содержимое перед каждым кадром. Лучшее решение для этого — использовать clearRect :
// Drawing & Animation function clearCanvas() { context.clearRect(0, 0, canvas.width, canvas.height); }
Таким образом, полный код рисования червоточины будет выглядеть так:
function wormHole() { computeFPS(); canvas.width = window.innerWidth; canvas.height = window.innerHeight - 130 - 40; clearCanvas(); for (var index = 0; index < circlesCount; index++) { circles[index].draw(); } circles.sort(function (a, b) { if (a.depth > b.depth) return -1; if (a.depth < b.depth) return 1; return 0; }); }
Код сортировки используется для предотвращения наложения кругов.
Кнопка настройки режима
Чтобы завершить наш фон, нам просто нужно подключить кнопку режима, чтобы отобразить или скрыть фон:
var wormHoleIntervalID = -1; function startWormHole() { if (wormHoleIntervalID > -1) clearInterval(wormHoleIntervalID); wormHoleIntervalID = setInterval(wormHole, 16); document.getElementById("wormHole").onclick = stopWormHole; document.getElementById("wormHole").innerHTML = "Standard Mode"; } function stopWormHole() { if (wormHoleIntervalID > -1) clearInterval(wormHoleIntervalID); clearCanvas(); document.getElementById("wormHole").onclick = startWormHole; document.getElementById("wormHole").innerHTML = "Wormhole Mode"; } stopWormHole();
Настройка игры
Чтобы немного упростить учебник, код обработки мыши уже сделан. Вы можете найти все, что вам нужно, в файле mouse.js .
Добавление игрового файла JavaScript
Фон обрабатывается файлом game.js. Поэтому мы должны зарегистрировать его внутри index.htm . Поэтому прямо перед закрывающим тегом тела мы добавим следующий код:
<script type="text/javascript" src="game.js"></script>
Обновление страницы HTML5
В игре будет использоваться SVG (масштабируемая векторная графика) для отображения кубиков, пэда и мяча. SVG — это инструмент с сохраненным режимом. Таким образом, вам не нужно перерисовывать все каждый раз, когда вы хотите переместить или изменить элемент.
Чтобы добавить тег SVG на нашу страницу, нам просто нужно вставить следующий код (сразу после холста):
<svg id="svgRoot"> <circle cx="100" cy="100" r="10" id="ball" /> <rect id="pad" height="15px" width="150px" x="200" y="200" rx="10" ry="20"/> </svg>
Как вы можете заметить, SVG начинается с двух уже определенных объектов: круга для шара и прямоугольника для пэда.
Определение констант и переменных
В файле game.js мы начнем с добавления некоторых переменных:
// Getting elements var pad = document.getElementById("pad"); var ball = document.getElementById("ball"); var svg = document.getElementById("svgRoot"); var message = document.getElementById("message");
Мяч будет определяться:
- Позиция
- Радиус
- Скорость
- Направление
- Его предыдущая позиция
// Ball var ballRadius = ball.r.baseVal.value; var ballX; var ballY; var previousBallPosition = { x: 0, y: 0 }; var ballDirectionX; var ballDirectionY; var ballSpeed = 10;
Пэд будет определяться:
- ширина
- Высота
- Позиция
- скорость
- Значение инерции (просто чтобы сделать вещи более плавными )
// Pad var padWidth = pad.width.baseVal.value; var padHeight = pad.height.baseVal.value; var padX; var padY; var padSpeed = 0; var inertia = 0.80;
Кирпичи будут сохранены в массиве и будут определены следующим образом:
- ширина
- Высота
- Маржа между ними
- Количество строк
- Количество столбцов
Нам также нужно смещение и переменная для подсчета разрушенных кирпичей.
// Bricks var bricks = []; var destroyedBricksCount; var brickWidth = 50; var brickHeight = 20; var bricksRows = 5; var bricksCols = 20; var bricksMargin = 15; var bricksTop = 20;
И, наконец, нам также нужны пределы игровой площадки и дата начала для расчета продолжительности сеанса.
// Misc. var minX = ballRadius; var minY = ballRadius; var maxX; var maxY; var startDate;
Обработка кирпича
Чтобы создать кирпич, нам понадобится функция, которая добавит новый элемент в корень svg. Это также настроит каждый кирпич с необходимой информацией:
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); svg.appendChild(rect); rect.setAttribute("width", brickWidth); rect.setAttribute("height", brickHeight); // Random green color var chars = "456789abcdef"; var color = ""; for (var i = 0; i < 2; i++) { var rnd = Math.floor(chars.length * Math.random()); color += chars.charAt(rnd); } rect.setAttribute("fill", "#00" + color + "00");
Функция brick также предоставляет функцию drawAndCollide для отображения кирпича и проверки на наличие столкновения с мячом:
this.drawAndCollide = function () { if (isDead) return; // Drawing rect.setAttribute("x", position.x); rect.setAttribute("y", position.y); // Collision if (ballX + ballRadius < position.x || ballX - ballRadius > position.x + brickWidth) return; if (ballY + ballRadius < position.y || ballY - ballRadius > position.y + brickHeight) return; // Dead this.remove(); isDead = true; destroyedBricksCount++; // Updating ball ballX = previousBallPosition.x; ballY = previousBallPosition.y; ballDirectionY *= -1.0; };
Наконец, полная функция кирпича будет выглядеть так:
// Brick function function Brick(x, y) { var isDead = false; var position = { x: x, y: y }; var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); svg.appendChild(rect); rect.setAttribute("width", brickWidth); rect.setAttribute("height", brickHeight); // Random green color var chars = "456789abcdef"; var color = ""; for (var i = 0; i < 2; i++) { var rnd = Math.floor(chars.length * Math.random()); color += chars.charAt(rnd); } rect.setAttribute("fill", "#00" + color + "00"); this.drawAndCollide = function () { if (isDead) return; // Drawing rect.setAttribute("x", position.x); rect.setAttribute("y", position.y); // Collision if (ballX + ballRadius < position.x || ballX - ballRadius > position.x + brickWidth) return; if (ballY + ballRadius < position.y || ballY - ballRadius > position.y + brickHeight) return; // Dead this.remove(); isDead = true; destroyedBricksCount++; // Updating ball ballX = previousBallPosition.x; ballY = previousBallPosition.y; ballDirectionY *= -1.0; }; // Killing a brick this.remove = function () { if (isDead) return; svg.removeChild(rect); }; }
Столкновения с площадкой и детской площадкой
Мяч также будет иметь функции столкновения, которые будут обрабатывать столкновения с площадкой и игровой площадкой. Эти функции должны будут обновлять направление мяча при обнаружении столкновения.
// Collisions function collideWithWindow() { if (ballX < minX) { ballX = minX; ballDirectionX *= -1.0; } else if (ballX > maxX) { ballX = maxX; ballDirectionX *= -1.0; } if (ballY < minY) { ballY = minY; ballDirectionY *= -1.0; } else if (ballY > maxY) { ballY = maxY; ballDirectionY *= -1.0; lost(); } } function collideWithPad() { if (ballX + ballRadius < padX || ballX - ballRadius > padX + padWidth) return; if (ballY + ballRadius < padY) return; ballX = previousBallPosition.x; ballY = previousBallPosition.y; ballDirectionY *= -1.0; var dist = ballX - (padX + padWidth / 2); ballDirectionX = 2.0 * dist / padWidth; var square = Math.sqrt(ballDirectionX * ballDirectionX + ballDirectionY * ballDirectionY); ballDirectionX /= square; ballDirectionY /= square; }
collideWithWindow проверяет пределы игровой площадки, а collideWithPad проверяет пределы площадки (здесь мы добавим небольшое изменение: горизонтальная скорость мяча будет вычислена с использованием расстояния с центром площадки).
Перемещение площадки
Вы можете управлять пэдом мышью или стрелками влево и вправо. Функция movePad отвечает за обработку движения площадки. Это также справится с инерцией :
// Pad movement function movePad() { padX += padSpeed; padSpeed *= inertia; if (padX < minX) padX = minX; if (padX + padWidth > maxX) padX = maxX - padWidth; }
Код, отвечающий за обработку входных данных, довольно прост :
registerMouseMove(document.getElementById("gameZone"), function (posx, posy, previousX, previousY) { padSpeed += (posx - previousX) * 0.2; }); window.addEventListener('keydown', function (evt) { switch (evt.keyCode) { // Left arrow case 37: padSpeed -= 10; break; // Right arrow case 39: padSpeed += 10; break; } }, true);
Игровой цикл
Перед настройкой игрового цикла нам нужна функция для определения размера игровой площадки. Эта функция будет вызываться при изменении размера окна.
function checkWindow() { maxX = window.innerWidth - minX; maxY = window.innerHeight - 130 - 40 - minY; padY = maxY - 30; }
Кстати, игровой цикл здесь является оркестратором :
function gameLoop() { movePad(); // Movements previousBallPosition.x = ballX; previousBallPosition.y = ballY; ballX += ballDirectionX * ballSpeed; ballY += ballDirectionY * ballSpeed; // Collisions collideWithWindow(); collideWithPad(); // Bricks for (var index = 0; index < bricks.length; index++) { bricks[index].drawAndCollide(); } // Ball ball.setAttribute("cx", ballX); ball.setAttribute("cy", ballY); // Pad pad.setAttribute("x", padX); pad.setAttribute("y", padY); // Victory ? if (destroyedBricksCount == bricks.length) { win(); } }
Инициализация и победа
Первым шагом инициализации является создание кирпичей:
function generateBricks() { // Removing previous ones for (var index = 0; index < bricks.length; index++) { bricks[index].remove(); } // Creating new ones var brickID = 0; var offset = (window.innerWidth - bricksCols * (brickWidth + bricksMargin)) / 2.0; for (var x = 0; x < bricksCols; x++) { for (var y = 0; y < bricksRows; y++) { bricks[brickID++] = new Brick(offset + x * (brickWidth + bricksMargin), y * (brickHeight + bricksMargin) + bricksTop); } } }
Следующий шаг о настройке переменных, используемых в игре:
function initGame() { message.style.visibility = "hidden"; checkWindow(); padX = (window.innerWidth - padWidth) / 2.0; ballX = window.innerWidth / 2.0; ballY = maxY - 60; previousBallPosition.x = ballX; previousBallPosition.y = ballY; padSpeed = 0; ballDirectionX = Math.random(); ballDirectionY = -1.0; generateBricks(); gameLoop(); }
Каждый раз, когда пользователь будет менять размер окна, нам придется переустанавливать игру:
window.onresize = initGame;
Затем мы должны прикрепить обработчик событий к новой кнопке игры:
var gameIntervalID = -1; function startGame() { initGame(); destroyedBricksCount = 0; if (gameIntervalID > -1) clearInterval(gameIntervalID); startDate = (new Date()).getTime(); ; gameIntervalID = setInterval(gameLoop, 16); } document.getElementById("newGame").onclick = startGame;
Наконец, мы добавим две функции для обработки начала и конца игры:
var gameIntervalID = -1; function lost() { clearInterval(gameIntervalID); gameIntervalID = -1; message.innerHTML = "Game over !"; message.style.visibility = "visible"; } function win() { clearInterval(gameIntervalID); gameIntervalID = -1; var end = (new Date).getTime(); message.innerHTML = "Victory ! (" + Math.round((end - startDate) / 1000) + "s)"; message.style.visibility = "visible"; }
Вывод
Теперь вы разработчик игр ! Используя мощь ускоренной графики, мы разработали небольшую игру, но с действительно интересными спецэффектами!
Теперь вам нужно обновить игру, чтобы сделать ее следующим блокбастером !
Идти дальше
- Узнайте о Internet Explorer 9/10 и почему важно аппаратное ускорение
- Мои другие игровые блоги HTML5
- W3C HTML5
- W3C Холст
- W3C SVG
об авторе
Он определяет себя как выродка и любит кодировать все, что относится к графике. До работы в Microsoft он основал компанию, которая разработала 3D-движок в реальном времени, написанный на DirectX ( www.vertice.fr ).