Статьи

Совет: как сделать игровой цикл в JavaScript

Эта статья была рецензирована Эндрю Рэем и Себастьяном Зейтцем . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

В этой короткой статье вы узнаете, как работает этот фундаментальный метод, и вы сможете начать создавать свои собственные браузерные игры и анимации.

Вот как выглядит игровой цикл в JavaScript:

function update(progress) { // Update the state of the world for the elapsed time since last render } function draw() { // Draw the state of the world } function loop(timestamp) { var progress = timestamp - lastRender update(progress) draw() lastRender = timestamp window.requestAnimationFrame(loop) } var lastRender = 0 window.requestAnimationFrame(loop) 

Метод requestAnimationFrame запрашивает, чтобы браузер вызвал указанную функцию как можно скорее, прежде чем произойдет следующая перерисовка. Это API специально для рендеринга анимации, но вы также можете использовать setTimeout с коротким таймаутом для получения аналогичного результата. requestAnimationFrame передается метка времени, когда обратный вызов начал срабатывать, он содержит количество миллисекунд с момента загрузки окна и равен performance.now () .

Значение progress или время между рендерами имеет решающее значение для создания плавной анимации. Используя его для регулировки положения x и y в нашей функции update , мы гарантируем, что наша анимация движется с постоянной скоростью.

Обновление позиции

Наша первая анимация будет очень простой. Красный квадрат, который перемещается вправо, пока не достигнет края холста и не вернется к началу.

Нам нужно сохранить позицию квадрата и увеличить позицию x в нашей функции update . Когда мы достигаем границы, мы можем вычесть ширину холста, чтобы вернуться назад.

 var width = 800 var height = 200 var state = { x: width / 2, y: height / 2 } function update(progress) { state.x += progress if (state.x > width) { state.x -= width } } 

Рисование нового кадра

В этом примере для отображения графики используется элемент <canvas> но игровой цикл можно использовать и с другими выходными данными, такими как документы HTML или SVG.

Функция draw просто отображает текущее состояние мира. На каждом кадре мы очистим холст, а затем нарисуем красный квадрат размером 10 пикселей с центром в позиции, сохраненной в нашем объекте state .

 var canvas = document.getElementById("canvas") var width = canvas.width var height = canvas.height var ctx = canvas.getContext("2d") ctx.fillStyle = "red" function draw() { ctx.clearRect(0, 0, width, height) ctx.fillRect(state.x - 5, state.y - 5, 10, 10) } 

И у нас есть движение!

Примечание. В демонстрации вы можете заметить, что размер холста был установлен как в CSS, так и в атрибутах width и height в HTML-элементе. Стили CSS задают фактический размер элемента холста, который будет отображаться на странице, атрибуты HTML задают размер системы координат или «сетки», которую будет использовать API холста. См. Этот вопрос переполнения стека для получения дополнительной информации.

Отвечая на ввод пользователя

Далее мы получим ввод с клавиатуры для управления положением нашего объекта, state.pressedKeys будет отслеживать, какие клавиши были нажаты.

 var state = { x: (width / 2), y: (height / 2), pressedKeys: { left: false, right: false, up: false, down: false } } 

Давайте послушаем все события state.pressedKeys и state.pressedKeys и обновим state.pressedKeys соответственно. Клавиши, которые я буду использовать, — D для правой, A для левой, W для восходящей и S для нижней Вы можете найти список кодов ключей здесь .

 var keyMap = { 68: 'right', 65: 'left', 87: 'up', 83: 'down' } function keydown(event) { var key = keyMap[event.keyCode] state.pressedKeys[key] = true } function keyup(event) { var key = keyMap[event.keyCode] state.pressedKeys[key] = false } window.addEventListener("keydown", keydown, false) window.addEventListener("keyup", keyup, false) 

Затем нам просто нужно обновить значения x и y на основе нажатых клавиш и убедиться, что мы удерживаем наш объект в границах.

 function update(progress) { if (state.pressedKeys.left) { state.x -= progress } if (state.pressedKeys.right) { state.x += progress } if (state.pressedKeys.up) { state.y -= progress } if (state.pressedKeys.down) { state.y += progress } // Flip position at boundaries if (state.x > width) { state.x -= width } else if (state.x < 0) { state.x += width } if (state.y > height) { state.y -= height } else if (state.y < 0) { state.y += height } } 

И у нас есть пользовательский ввод!

Астероиды

Теперь, когда у нас есть основы, мы можем сделать что-то более интересное.

Сделать корабль не так сложно, как это было в классической игре Asteroids .

Наше state должно хранить дополнительный вектор (пара x, y) для движения, а также вращение для направления кораблей.

 var state = { position: { x: (width / 2), y: (height / 2) }, movement: { x: 0, y: 0 }, rotation: 0, pressedKeys: { left: false, right: false, up: false, down: false } } 

Наша функция update должна обновлять три вещи:

  • вращение на основе нажатых влево / вправо
  • движение на основе клавиш вверх / вниз и вращения
  • положение на основе вектора движения и границ холста
 function update(progress) { // Make a smaller time value that's easier to work with var p = progress / 16 updateRotation(p) updateMovement(p) updatePosition(p) } function updateRotation(p) { if (state.pressedKeys.left) { state.rotation -= p * 5 } else if (state.pressedKeys.right) { state.rotation += p * 5 } } function updateMovement(p) { // Behold! Mathematics for mapping a rotation to it's x, y components var accelerationVector = { x: p * .3 * Math.cos((state.rotation-90) * (Math.PI/180)), y: p * .3 * Math.sin((state.rotation-90) * (Math.PI/180)) } if (state.pressedKeys.up) { state.movement.x += accelerationVector.x state.movement.y += accelerationVector.y } else if (state.pressedKeys.down) { state.movement.x -= accelerationVector.x state.movement.y -= accelerationVector.y } // Limit movement speed if (state.movement.x > 40) { state.movement.x = 40 } else if (state.movement.x < -40) { state.movement.x = -40 } if (state.movement.y > 40) { state.movement.y = 40 } else if (state.movement.y < -40) { state.movement.y = -40 } } function updatePosition(p) { state.position.x += state.movement.x state.position.y += state.movement.y // Detect boundaries if (state.position.x > width) { state.position.x -= width } else if (state.position.x < 0) { state.position.x += width } if (state.position.y > height) { state.position.y -= height } else if (state.position.y < 0) { state.position.y += height } } 

Функция draw переводит и поворачивает начало холста перед рисованием формы стрелки.

 function draw() { ctx.clearRect(0, 0, width, height) ctx.save() ctx.translate(state.position.x, state.position.y) ctx.rotate((Math.PI/180) * state.rotation) ctx.strokeStyle = 'white' ctx.lineWidth = 2 ctx.beginPath () ctx.moveTo(0, 0) ctx.lineTo(10, 10) ctx.lineTo(0, -20) ctx.lineTo(-10, 10) ctx.lineTo(0, 0) ctx.closePath() ctx.stroke() ctx.restore() } 

Вот и весь код, который нам нужен для воссоздания корабля, как в Астероидах. Клавиши для этой демонстрации такие же, как и в предыдущей ( D для правой, A для левой, W для верхней и S для нижней).

Я оставлю это вам, чтобы добавить астероиды, пули и обнаружение столкновений ?

Уровень повышен

Если вам показалась эта статья интересной, вам понравится наблюдать за живым кодом Space Invaders Мэри Рук с нуля для более сложного примера, которому уже несколько лет, но он отлично знакомит с созданием игр в браузере. Наслаждайтесь!