Проект: арканоид
Собираем игру
В этой теме из кусочков прошлых тем получается настоящая игра — арканоид: ракетка внизу, мяч и стена из кирпичей. Разобьёшь все — победа, мяч упадёт три раза — проигрыш. Здесь нет ничего нового по синтаксису: главное — разложить игру на части и собрать их в правильном порядке.
Состояние игры
Сначала придумываем, какие числа описывают игру целиком. Всё остальное — рисование — строится из них:
const game = {
ball: { x: 200, y: 200, vx: 150, vy: -150, r: 6 },
paddle: { x: 160, y: 280, w: 80, h: 10 },
bricks: makeBricks(4, 7, 50, 14, 6),
score: 0,
lives: 3,
over: false,
width: 400,
height: 300,
};
Хорошая проверка дизайна: если сохранить этот объект в JSON и потом загрузить, игра должна продолжиться с того же места. Значит, в состоянии есть всё нужное.
Уровень как данные
Стену из кирпичей не рисуют руками — её генерируют двумя циклами: строки и столбцы. Позиция
кирпича считается из номера строки и столбца: шаг по x — ширина кирпича плюс зазор.
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
bricks.push({ x: gap + col * (w + gap), y: gap + row * (h + gap), w, h, alive: true });
}
}
Уровни можно описывать и картой-строкой, где # — кирпич, а . — пусто, — и превращать её в
массив тем же двойным циклом. Так делают редакторы уровней.
Порядок шагов в кадре
Порядок важен: сначала мяч двигается, потом проверяются все столкновения в новом положении, потом — условия конца игры:
function update(dt) {
moveBall(dt); // 1. движение
bounceWalls(); // 2. стены (кроме нижней!)
bouncePaddle(); // 3. ракетка
hitBricks(); // 4. кирпичи и очки
checkFall(); // 5. мяч упал — минус жизнь
checkWin(); // 6. кирпичей не осталось — победа
}
Нижняя стена в арканоиде — не стена, а пропасть: мяч, улетевший вниз, отнимает жизнь.
Ракетка и «липкий» мяч
Если мяч глубоко вошёл в ракетку, на следующем кадре он всё ещё пересекается с ней. Простое
vy = -vy развернуло бы его обратно вниз — мяч застрял бы в ракетке. Поэтому:
- отбиваем только мяч, который летит вниз (
vy > 0); - направление ставим явно:
vy = -Math.abs(vy)— теперь он точно летит вверх.
В настоящих арканоидах угол отскока ещё зависит от того, в какую часть ракетки попал мяч: ближе к краю — круче в сторону. Это делает игру управляемой:
const hit = (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2); // от -1 до 1
ball.vx = hit * 250;
Управление мышью
Ракетка следует за мышью по горизонтали, центр ракетки — под курсором, и она не выезжает за поле:
canvas.addEventListener("mousemove", (e) => {
const x = e.clientX - canvas.getBoundingClientRect().left;
paddle.x = Math.max(0, Math.min(canvas.width - paddle.w, x - paddle.w / 2));
});
Конец игры
Флаг over останавливает update, но не draw: последний кадр с надписью «GAME OVER» или
«YOU WIN!» продолжает рисоваться. Перезапуск — просто создать состояние заново:
canvas.addEventListener("click", () => {
if (game.over) Object.assign(game, newGame());
});
Куда расти дальше
- бонусы, которые падают из кирпичей (расширить ракетку, второй мяч);
- кирпичи с несколькими «жизнями» (
hp) и разными цветами; - уровни из строк-карт и переход на следующий уровень;
- звук удара через
new Audio(...)и рекорд вlocalStorage.
Всё это — те же приёмы: числа в состоянии, update их меняет, draw показывает.
Итог
- Сначала придумываем состояние игры, потом пишем
updateиdrawвокруг него. - Уровень генерируем циклами из данных, а не рисуем руками.
- Порядок кадра: движение → стены → ракетка → кирпичи → жизни → победа.
- Отскок от ракетки только при
vy > 0и сMath.abs, чтобы мяч не залипал.