«В гуще врагов» — это уже не статичный пример, а классический мини-выживач на JavaScript и Canvas. Игрок свободно двигается по полю во все четыре стороны, целится мышью, стреляет в направление курсора и отбивается от врагов, которые атакуют со всех сторон с разной скоростью. На телефоне работают экранный джойстик и кнопка стрельбы.
Полноценный survival-прототип: свободное перемещение, стрельба по курсору, волны врагов, рост сложности, счёт, рекорд и рестарт после поражения.
Сначала попробуйте готовую игру
Запустите демо в статье. На компьютере зажмите левую кнопку мыши для стрельбы и подвигайтесь WASD или стрелками. На телефоне используйте джойстик слева и кнопку «Огонь» справа.
Структура проекта
Вся игра собрана в трёх файлах, без библиотек и сборщиков.
v-gusche-vragov/
├── source.html
├── style.css
└── game.js
HTML: интерфейс и игровое поле
Разметка содержит HUD со счётом, рекордом и текущей волной, а также Canvas с фокусом и aria-подписью. Через data-swarm-* JavaScript получает ссылки на элементы и обновляет интерфейс без лишних селекторов.
<main class="swarm" data-swarm-game>
<section class="swarm__stats" aria-label="Показатели игры">
<p data-swarm-score>Счёт: 0</p>
<p data-swarm-best>Рекорд: 0</p>
<p data-swarm-wave>Волна: 1</p>
</section>
<canvas data-swarm-canvas width="820" height="460" tabindex="0"></canvas>
<p class="swarm__status" data-swarm-status aria-live="polite">
Нажмите на поле и удерживайте левую кнопку мыши для стрельбы.
</p>
</main>
CSS: стиль в духе сайта и других игр
Оформление выполнено в общей тёмной палитре: карточный контейнер, мягкие бордеры, чипы для подсказок управления и контрастный Canvas. Визуально игра теперь ближе к остальным игровым страницам проекта.
JavaScript: состояние выживача
Основная логика хранится в едином объекте состояния: позиция игрока, массив пуль, массив врагов, счёт, волна, таймер спавна и режим игры.
let state = {
player: { x: canvas.width * 0.5, y: canvas.height * 0.5, r: 14, speed: 250 },
bullets: [],
enemies: [],
score: 0,
wave: 1,
spawnTimer: 0,
mode: "playing",
};
Движение во все четыре стороны
Игрок управляется через WASD и стрелки по двум осям сразу. Вектор нормализуется, поэтому диагональ не даёт лишнего ускорения. Позиция жёстко ограничивается границами поля.
if (keys.has("ArrowLeft") || keys.has("KeyA")) axisX -= 1;
if (keys.has("ArrowRight") || keys.has("KeyD")) axisX += 1;
if (keys.has("ArrowUp") || keys.has("KeyW")) axisY -= 1;
if (keys.has("ArrowDown") || keys.has("KeyS")) axisY += 1;
Прицел мышью и стрельба в курсор
Курсор переводится в координаты Canvas, затем из позиции игрока считается вектор прицеливания. Пуля получает скорость по этому вектору, поэтому летит ровно туда, куда смотрит мышь. Стрельба идёт при удержании левой кнопки мыши с небольшим кулдауном.
let direction = normalize(pointer.x - state.player.x, pointer.y - state.player.y);
aim.x = direction.x;
aim.y = direction.y;
if (isFiring && fireCooldown <= 0) {
shoot();
fireCooldown = 0.11;
}
Враги со всех сторон и разная скорость
Каждый враг появляется с одной из четырёх сторон экрана и движется в сторону игрока. Скорость берётся из диапазона случайных значений и дополнительно растёт вместе с волной.
let side = Math.floor(Math.random() * 4);
let enemy = {
x: 0,
y: 0,
r: randomBetween(10, 16),
speed: randomBetween(78, 132) + state.wave * randomBetween(7, 11),
};
Визуал пуль и попаданий
Пули получили светящийся след, а при выстреле и уничтожении врага генерируются частицы. Это не просто декор: игрок лучше считывает траекторию и результат попадания в плотной сцене.
Поражение, рестарт и рост сложности
При столкновении игрока с врагом игра переходит в режим over. Повторный запуск выполняется клавишей R. Волна растёт по сумме счёта и времени выживания, поэтому давление увеличивается даже при аккуратной игре.
Как проверить сборку
| Проверка | Ожидаемый результат |
|---|---|
| Движение A/D/W/S и стрелками | Игрок свободно перемещается во все 4 стороны и не выходит за края |
| Удержание ЛКМ | Пули летят в сторону курсора с контролируемой частотой |
| Телефон: джойстик и «Огонь» | Игрок двигается, пули летят в сторону джойстика, рестарт работает без клавиатуры |
| Игра 30+ секунд | Враги продолжают идти с разных сторон, волна увеличивается |
| Серия попаданий | Счёт и рекорд растут, заметны эффекты попадания |
| Столкновение с врагом и нажатие R | Игра корректно завершается и запускается заново |
FAQ
Почему стрельба на мыши, а не на пробеле?
Для выживача с движением по всей арене важнее быстрое прицеливание в любую сторону. Мышь даёт более точный контроль направления выстрела.
Можно ли добавить автоприцел?
Да. Можно искать ближайшего врага и автоматически вычислять вектор выстрела на него. Но сначала лучше освоить базовый ручной режим.
Как сделать разные типы врагов?
Добавьте поле типа и меняйте параметры: размер, скорость, здоровье, урон или рисунок. Текущая архитектура состояния это позволяет без переписывания цикла.
Подойдёт ли этот каркас для мобильной версии?
Да. В готовой игре уже есть экранный джойстик и кнопка стрельбы. Как их собрать — в бонусном блоке ниже.
Бонус: управление на телефоне
Клавиатура и мышь на телефоне недоступны. Поэтому под игровым полем появляются два сенсорных элемента: джойстик слева и кнопка стрельбы справа. Мышиный прицел оставляем компьютеру, а на сенсоре стреляем туда, куда направлен джойстик. Автоприцел в ближайшего врага ломает баланс: можно стоять на месте и зажать «Огонь».
HTML: джойстик и кнопка огня
Джойстик — круглая зона с ручкой внутри. Кнопка «Огонь» держит стрельбу, пока палец на ней. «Заново» ставим рядом со счётом, чтобы не задеть её вместо огня.
<section class="swarm__stats">
<p data-swarm-score>Счёт: 0</p>
<button class="swarm__restart" type="button" data-swarm-restart>Заново</button>
</section>
<div class="swarm__touch" data-swarm-touch>
<div class="swarm__stick" data-swarm-stick aria-label="Джойстик движения">
<div class="swarm__stick-knob" data-swarm-knob></div>
</div>
<button class="swarm__fire" type="button" data-swarm-fire aria-label="Стрелять">Огонь</button>
</div>
CSS: показываем только на сенсоре
На компьютере блок скрыт. На узком экране и на устройствах с грубым указателем (pointer: coarse) включаем flex-раскладку. touch-action: none не даёт странице уехать, пока палец едет по джойстику.
.swarm__touch {
display: none;
touch-action: none;
}
@media (max-width: 720px), (pointer: coarse) {
.swarm__touch {
display: flex;
}
}
JavaScript: два пальца сразу
Каждому пальцу браузер даёт свой pointerId. Его нужно запомнить: левый палец крутит джойстик, правый в это же время может жать «Огонь». Смещение ручки делим на радиус базы — получается тот же вектор, что дают WASD.
stickRoot.addEventListener("pointerdown", function (event) {
event.preventDefault();
stickPointerId = event.pointerId;
stickRoot.setPointerCapture(event.pointerId);
applyStickPoint(event.clientX, event.clientY);
});
fireBtn.addEventListener("pointerdown", function (event) {
event.preventDefault();
firePointerId = event.pointerId;
isFiring = true;
});
Пока джойстик сдвинут, прицел совпадает с вектором движения. Отпустил стик — направление выстрела остаётся последним. Мышиный прицел при этом не трогаем.
if (stick.x !== 0 || stick.y !== 0) {
let stickDir = normalize(stick.x, stick.y);
aim.x = stickDir.x;
aim.y = stickDir.y;
return;
}
if (stickPointerId !== null || firePointerId !== null) return;
Готовый вариант уже стоит в демо наверху: откройте статью с телефона и попробуйте удержать джойстик и «Огонь» двумя руками.
Откройте финальные файлы этой версии. Это тот же код, который работает в демо наверху статьи и на отдельной странице раздела «Поиграть».
Книга по JavaScript для начинающих
«JavaScript с нуля: учимся программировать, создавая игры»Пошаговый формат с маленькими практическими задачами: от базового синтаксиса до первых игровых механик без перегруза теорией.
Читать книгу на ЛитРес →