
Одно из самых приятных проявлений программирования — это создание игр. Почему бы и нам не запилить свою?
Тем более не так давно у меня появилась довольно странная мысль, которая уже который день не отпускает меня — а что если сделать многопользовательский игровой сервер на базе ESP32? О_о
Можно и на базе обычного вебсервера и хостинга, но это как-то уныло, как у всех, «а душа хочет гусарства» :-))) Понятно, что это наложит довольно жёсткие ограничения на максимальное количество игроков и объём передаваемых между ними данных из-за слабости аппаратной платформы, но мысль всё же занятная.
Проще говоря: игра хостится на ESP32, клиенты подключаются к ней, и ESP32 отдаёт им страницу с игрой. После этого начинается игровой процесс: клиенты обмениваются данными через ESP32, которая выступает в роли сервера, а все ресурсоёмкие задачи по максимуму обрабатываются на стороне клиентов. Теоретически, при таком подходе может что-то получиться. Попробуем..
Сразу спойлер: полный код игры есть в самом конце статьи.
В качестве идеи для концепции возьмём игру с видом сверху, классический шутер, где будет ездить некое транспортное средство, которое может стрелять и выстрелами разрушать стены (что-то это всё мне напоминает, а впрочем — показалось :-) ). В дальнейшем попробуем прикрутить мультиплеер (то бишь, многопользовательский режим).
Просто ездить по игровому полю неинтересно, поэтому в качестве ограничивающих стен у нас будут выступать стены лабиринта, а чтобы было гораздо интересней, пускай лабиринт не будет статичным, и его стены будут генерироваться каждый раз, при новом запуске игры.
Чем хорош лабиринт, так как это тем, что он даёт стратегию — появляется необходимость не просто стрелять, но и думать, как двигаться в лабиринте, чтобы не застрять.
Чтобы придать лабиринту визуальную привлекательность, добавим на его стенки текстуру, в качестве которой будут выступать маленькие «шумовые» квадратики, несколько отличающихся по цвету.
Мы сможем взаимодействовать с лабиринтом следующим образом: сможем стрелять, и пули будут попадать в стены; при этом будет происходить интересное: от каждого попадания пули в стену — стена будет медленно разрушаться, и от неё будут летать маленькие квадратики-осколки, как будто стена крошится от удара.
Помимо визуальной привлекательности, это будет давать обратную связь игроку, который будет видеть, что его выстрелы достигают цели.
Пока на этом ограничимся, так как реализация даже этих моментов потребует условно объёмного кода.
В дальнейшем, возможно, можно будет подумать и над добавлением в игру противников, а также других «фишек».
▍ Почему JavaScript
Один из самых простых способов создать игру (хотя, понятно, это кому как — «каждый кулик своё болото хвалит» :-), это сделать браузерную игру, которая запускается в интернет-браузере, и именно поэтому мы будем использовать JavaScript.
Кроме того, JavaScript хорошо интегрируется с HTML и CSS, что пригодится для создания визуальной части игры.
Итак, давайте прикинем, какие необходимые элементы игры у нас должны присутствовать на экране:
Игровое поле: пускай это будет квадрат 400 на 400 пикселей.
Машинка: основной действующий объект игры. У неё будет ствол, из которого она сможет стрелять и сможет двигаться в четырёх направлениях.
Лабиринт: стены с текстурой, которые могут разрушаться от попаданий. Для этого зададим им параметр «здоровье».
Пули/снаряды: это то, чем стреляет машинка. Будут лететь по прямой и повреждать стены при попадании.
Визуальные эффекты: при попадании пуль в стены, стены будут «повреждаться», что будет отражаться во внешнем виде их текстуры. Кроме того, от стен будут отлетать квадратики, символизирующие осколки.
Теперь, когда мы определились с основными элементами игры, рассмотрим все существенные игровые моменты.
▍ Оформление
Один из самых простых и логичных способов задавать оформление игровых элементов в нашем случае, это использовать CSS, который представляет собой описание стилей, с помощью которых можно задавать игровым элементам на странице: цвет, размеры, позицию и другие параметры.
Описание будет выполнено в виде так называемых «классов», которые выглядят как некое слово с точкой спереди, после которого идут фигурные скобки, где и описываются определённые параметры.
Например: .battle-machine { ( параметры) }.
Однако, почему мы используем CSS, а не задаём напрямую оформление в коде JavaScript?
Мы так делаем потому, что CSS позволяет централизовано управлять всем оформлением, и нам не нужно будет переписывать код при потребности внесения изменений/дополнений, — для этого достаточно будет всего лишь изменить одно значение в CSS. Такой подход делает код гораздо более чистым и удобным для поддержки.
Например, мы могли бы написать что-то вроде machine.style.backgroundColor = 'green'; — но это весьма громоздко будет сильно перегружать код, особенно если стилей много.
▍ Захват нажатий клавиш
Игра рассчитана на управление с компьютерной полноразмерной клавиатуры (у меня именно такая, поэтому я не могу проверить, как это работает на ноутбуках) и захват нажатий клавиш реализован с помощью JavaScript-события keydown, которое срабатывает, когда нажата любая клавиша на клавиатуре.
Почему мы используем именно keydown? Потому что это событие срабатывает сразу после нажатия клавиши и отлично подходит для игр, где нужна быстрая реакция.
Кроме него, существуют ещё такие события как keyup (отжатие клавиши), keypress (устаревшее событие, не рекомендовано к использованию).
Заинтересовавшиеся работой с клавишами, могут найти документацию по событиям клавиатуры для JavaScript вот здесь.
После чего мы «ловим» это событие, и проверяем, какая клавиша была нажата, а далее выполняем определённые действия — передвигаем машинку или стреляем (здесь и далее будут рассматриваться только фрагменты кода, в то время как полный код вы сможете найти в конце статьи):
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') {
navigateBattleMachine('west');
} else if (event.key === 'ArrowRight') {
navigateBattleMachine('east');
} else if (event.key === 'ArrowUp') {
navigateBattleMachine('north');
} else if (event.key === 'ArrowDown') {
navigateBattleMachine('south');
} else if (event.key === ' ') {
fireProjectile(); // Выстрел
}
});
Как можно видеть в коде выше, за движение и стрельбу отвечают следующие клавиши на клавиатуре:
ArrowLeft — Стрелка влево;
ArrowRight — Стрелка вправо;
ArrowUp — Стрелка вверх;
ArrowDown — Стрелка вниз;
Пробел — Выстрел.
(Привет, поклонникам WASD, но я, к сожалению, не поклонник этой раскладки, извините :-) ).
Кроме того, как можно видеть, в зависимости от нажатия клавиш, вызывается две функции: navigateBattleMachine (для перемещений) и fireProjectile (для стрельбы).
Рассмотрим функцию для перемещений подробнее:
function navigateBattleMachine(direction) {
let newX = machineX;
let newY = machineY;
if (direction === 'north') {
newY -= machineVelocity;
} else if (direction === 'south') {
newY += machineVelocity;
} else if (direction === 'west') {
newX -= machineVelocity;
} else if (direction === 'east') {
newX += machineVelocity;
}
if (!checkObstacleCollision(newX, newY)) {
machineX = newX;
machineY = newY;
machineOrientation = direction;
refreshBattleMachinePosition();
}
}
Как можно видеть, она принимает в качестве аргумента направление движения (north, south, west, east), далее, в зависимости от направления, вычисляет новые координаты (newX, newY), а также проверяет, не сталкивается ли машинка со стенами; если столкновения нет, то машинка может переместиться на новую позицию.
Переменные newX, newY изначально равны текущим координатам машинки (machineX, machineY).
В зависимости от направления, мы либо производим увеличение, либо уменьшение одной из этих переменных на значение machineVelocity (скорость машинки).
▍ Движение машинки
Движение машинки осуществляется с помощью изменения координат, где для этого у нас есть две переменные machineX и machineY, в которых будет храниться текущее положение машинки по осям X и Y.
Когда игрок нажмёт на управляющие клавиши клавиатуры, эти координаты будут изменяться, и код будет обновлять на экране положение машинки.
И тут есть один нюанс: машинка не может ездить где угодно, так как у нас ещё есть стенки лабиринта, то есть нам нужно проверять, не столкнулась ли машинка со стенками.
Для этих целей у нас есть функция checkObstacleCollision, которая будет осуществлять проверку, находится ли машинка на том же месте, что и стена, и если да, то движение будет заблокировано и машинка останется на месте (об этом подробнее ниже).
▍ Проверка коллизий
Итак, после вычисления новых координат, мы вызываем функцию checkObstacleCollision, которая производит проверку, не сталкивается ли машинка со стенами. Если столкновения нет, то происходит вызов функции refreshBattleMachinePosition, которая перемещает машинку на новое место.
В качестве альтернативного варианта можно было бы использовать изменение CSS-свойств напрямую, однако этот подход менее гибкий, и если, например, мы захотим добавить некую анимацию движения или изменить логику коллизий, то нам придётся переписывать код:
machine.style.left = `${newX}px`;
machine.style.top = `${newY}px`;
Таким образом, если подвести промежуточный итог, то каждый раз, когда игрок нажимает клавишу для движения, вызывается функция navigateBattleMachine, которая вычисляет новые координаты машинки, и, если нет коллизий, обновляет переменные machineX и machineY, после чего происходит вызов функции refreshBattleMachinePosition, которая перемещает машинку на новое место на экране:
function refreshBattleMachinePosition() {
battleMachine.style.left = `${machineX}px`;
battleMachine.style.top = `${machineY}px`;
battleMachine.style.transform = `rotate(${
machineOrientation === 'north' ? 0 :
machineOrientation === 'east' ? 90 :
machineOrientation === 'south' ? 180 :
machineOrientation === 'west' ? 270 : 0
}deg)`;
}
Как можно видеть, мы здесь используем CSS-свойства left и top для задания новых координат машинки. Кроме того, используется свойство transform, чтобы машинка смотрела в нужную сторону.
Если взглянуть на всё это концептуально, сверху, то такое построение кода, где всё обновление положений машинки происходит через переменные machineX и machineY даёт нам гибкость и централизованное управление.
Например, если в будущем мы захотим добавить какую-то анимацию движения, то нам не нужно будет переписывать всю логику, достаточно будет внести изменения в функцию refreshBattleMachinePosition.
Также, если мы решим изменить размер машинки или добавить новые объекты на поле, то нам не нужно будет переписывать всю логику коллизий, а достаточно будет обновить проверку в функции checkObstacleCollision.
Кроме того, изменение описанных CSS-свойств (left, top, transform) работает весьма быстро, что даст нам быструю реакцию на действия игрока.
В качестве альтернативного подхода можно было бы использовать Canvas, который некоторые разработчики и предлагают использовать для создания игр, однако в нашем случае, мы используем обычные html-элементы div (здесь и далее — без фигурных скобок, т.к. иначе Хабр не отображает), что гораздо проще для понимания и реализации, кроме того, мы можем использовать CSS (а если бы использовали Canvas, то пришлось бы всё, что делается у нас с помощью CSS — делать средствами чистого JavaScript, что существенно увеличило бы количество кода).
Таким образом, Canvas лучше подходит для создания больших игр, в то время как наша маленькая игра вполне может обойтись и более скромными средствами.
▍ Стрельба
Когда игрок нажимает на клавишу Пробел, то вызывается функция fireProjectile, которая создаёт новый элемент — пулю (или, если хотите, снаряд), который представляет собой новый блок div, определяемый CSS-классом .projectile.
Начальные координаты пули зависят от того, куда повёрнута машинка:
- Если машинка смотрит вверх, то пуля появляется чуть выше машинки.
- Если машинка смотрит вправо — она появляется правее машинки.
- и т.д.
После создания пули, запускается отсчёт интервала, где через каждые 50 миллисекунд обновляются её координаты, и один шаг пули составляет 5 пикселей.
Таким образом, пуля вылетает в том же направлении, куда «смотрит» машинка и продолжает своё движение до тех пор, пока не выйдет за пределы игрового поля или не столкнётся со стеной:
function fireProjectile()
function fireProjectile() {
let projectileStartX, projectileStartY;
let projectileDirection = machineOrientation;
// Определяем начальные координаты пули в зависимости от направления танка
if (projectileDirection === 'north') {
projectileStartX = machineX + 10; // Центр танка по X
projectileStartY = machineY - 3; // Над танком
} else if (projectileDirection === 'south') {
projectileStartX = machineX + 10; // Центр танка по X
projectileStartY = machineY + 23; // Под танком
} else if (projectileDirection === 'west') {
projectileStartX = machineX - 3; // Слева от танка
projectileStartY = machineY + 10; // Центр танка по Y
} else if (projectileDirection === 'east') {
projectileStartX = machineX + 23; // Справа от танка
projectileStartY = machineY + 10; // Центр танка по Y
}
// Создаём элемент пули
const projectile = document.createElement('div');
projectile.classList.add('projectile');
projectile.style.left = `${projectileStartX}px`;
projectile.style.top = `${projectileStartY}px`;
battleArena.appendChild(projectile);
let projectileX = projectileStartX;
let projectileY = projectileStartY;
// Запускаем интервал для движения пули
const projectileInterval = setInterval(() => {
// Двигаем пулю в зависимости от направления
if (projectileDirection === 'north') {
projectileY -= 5;
} else if (projectileDirection === 'south') {
projectileY += 5;
} else if (projectileDirection === 'west') {
projectileX -= 5;
} else if (projectileDirection === 'east') {
projectileX += 5;
}
// Обновляем положение пули
projectile.style.left = `${projectileX}px`;
projectile.style.top = `${projectileY}px`;
// Если пуля вышла за пределы поля, удаляем её
if (projectileX < 0 || projectileX > 400 || projectileY < 0 || projectileY > 400) {
clearInterval(projectileInterval);
projectile.remove();
}
// Проверяем столкновение пули со стенами
barriers.forEach(barrier => {
const barrierX = barrier.x * 20;
const barrierY = barrier.y * 20;
if (projectileX >= barrierX && projectileX <= barrierX + 20 && projectileY >= barrierY && projectileY <= barrierY + 20) {
const barrierElement = barrierElements.get(`${barrier.x}-${barrier.y}`);
if (barrierElement) {
let durability = parseInt(barrierElement.dataset.durability);
durability -= 1;
barrierElement.dataset.durability = durability;
// Удаляем квадратики текстуры
const textureColors = Array.from(barrierElement.querySelectorAll('.barrier-texture'))
.map(pixel => pixel.style.backgroundColor);
const numFragments = Math.min(10, Math.floor(Math.random() * 10) + 1);
removeTexturePixels(barrierElement, projectileX - barrierX, projectileY - barrierY, numFragments);
// Создаём отлетающие квадратики-брызги
for (let i = 0; i < numFragments; i++) {
const color = textureColors[Math.floor(Math.random() * textureColors.length)];
createFragments(projectileX, projectileY, color);
}
// Если здоровье стены закончилось, удаляем её
if (durability <= 0) {
barrierElement.remove();
barriers.splice(barriers.indexOf(barrier), 1);
}
}
clearInterval(projectileInterval);
projectile.remove();
}
});
}, 50);
}
▍ Взаимодействие пули и стен
Как можно видеть в коде выше, там же обрабатывается и взаимодействие пули со стенами.
На каждом шаге движения пули происходит проверка, не столкнулась ли она со стеной, при этом используется та же самая логика, что и для проверки коллизий танка:
- Если пуля попадает в стену, то уменьшается «здоровье» стены (durability);
- Как только здоровье достигает нуля, то сегмент* стены удаляется:
if (durability <= 0) {
barrierElement.remove(); // Удаляем сегмент стены
barriers.splice(barriers.indexOf(barrier), 1); // Удаляем его из массива стен
}
- При попадании в стену также удаляются квадратики текстуры и создаются отлетающие осколки.
*Здесь под «сегментом» стены понимается квадрат 20х20 пикселей, который создаётся как div, стилизуемый CSS-классом .barrier.
const barrierElement = document.createElement('div');
barrierElement.classList.add('barrier');
barrierElement.style.left = `${barrier.x * 20}px`;
barrierElement.style.top = `${barrier.y * 20}px`;
barrierElement.dataset.durability = 3; // Здоровье сегмента
А уже внутри каждого сегмента имеется множество маленьких div, с классом .barrier-texture, которые и создают эффект «шума» текстуры.
При попадании пули в стену вызывается функция createFragments, которая создаёт несколько маленьких квадратиков, разлетающихся в случайных направлениях и движущихся по своим траекториям, постепенно замедляясь и пропадая в самом конце.
Кстати говоря, вот мы выше постоянно упоминаем блоки div, а почему вообще мы их используем? Потому что их просто стилизовать с помощью CSS, а их положением управлять с помощью JavaScript и кроме того, div-ы корректно распознаются во всех браузерах без дополнительных «танцев с бубном».
В итоге, как можно заметить, все визуальные элементы на игровом поле — выполнены в виде таких блоков…
▍ Генерация лабиринта
Лабиринт генерируется с помощью функции createChaoticTerrain, которая случайным образом размещает стены на игровом поле, в то же время оставляя свободное место для машинки в центре:
function createChaoticTerrain(width, height) {
const terrain = [];
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
if (Math.random() < 0.3 && !(i === Math.floor(width / 2) && j === Math.floor(height / 2))) {
terrain.push({ x: i, y: j });
}
}
}
return terrain;
}
Работает это следующим образом: мы проходим по всем возможным координатам игрового поля (20х20 блоков) и с вероятностью в 30% (Math.random() < 0.3) на текущей позиции создаём стену — этим достигается случайное размещение сегментов стен.
Кстати говоря, если бы мы захотели увеличить плотность лабиринта, то нам нужно было бы увеличить параметр random, например, так: (Math.random() < 0.5.
Как можно видеть в коде выше, мы не размещаем стены в центре поля i === Math.floor(width / 2) && j === Math.floor(height / 2).
Такая случайная генерация лабиринта делает лабиринт уникальным каждый раз, когда игрок запускает игру, что привносит элемент неожиданности и делает игру более интересной.
Кроме того, стены становятся красивыми за счёт украшения их множеством различающихся по цвету «шумовых» блоков, о чём уже было написано выше.
Однако рассмотрим их несколько подробнее. Вот как выглядит такая генерация в коде:
for (let i = 0; i < 20; i += 3) {
for (let j = 0; j < 20; j += 3) {
const texturePixel = document.createElement('div');
texturePixel.classList.add('barrier-texture');
texturePixel.style.left = `${i}px`;
texturePixel.style.top = `${j}px`;
texturePixel.style.backgroundColor = `rgba(0, ${100 + Math.floor(Math.random() * 155)}, 0, 0.8)`;
barrierElement.appendChild(texturePixel);
}
}
Как можно видеть, здесь мы создаём маленькие квадратики div, размером 3х3 пикселя, стилизованные CSS-классом .barrier-texture, где цвет каждого квадратика немножко отличается.
▍ Разрушение стен
Как выше уже было мельком сказано, каждый сегмент стены (20х20 пикселей) имеет параметр «здоровье» (durability).
Каждое попадание пули в стену уменьшает здоровье сегмента стены:
let durability = parseInt(barrierElement.dataset.durability);
durability -= 1;
barrierElement.dataset.durability = durability;
Если здоровье сегмента стены ещё не уменьшилось до предела, то мы удаляем часть текстуры с помощью функции div removeTexturePixels:
removeTexturePixels(barrierElement, projectileX - barrierX, projectileY - barrierY, numFragments);
Если здоровье достигает нуля — сегмент удаляется:
if (durability <= 0) {
barrierElement.remove();
barriers.splice(barriers.indexOf(barrier), 1);
}
Как уже говорилось, любое попадание пули в стену — вызывает возникновение отлетающих от неё осколков, которые генерируются с помощью вызова функции createFragments:
for (let i = 0; i < numFragments; i++) {
const color = textureColors[Math.floor(Math.random() * textureColors.length)];
createFragments(projectileX, projectileY, color);
}
Разрушение стен привносит в игру элемент интерактива, своего рода «прогресса» игры. Ну и опять же «ломать — не строить», в этом есть какое-то своё особое удовольствие :-)
Как уже было выше сказано, отлетающие осколки генерируются с помощью функции createFragments, которая создаёт маленькие div-ы, с классом .fragments, где каждый квадратик получает случайное направление движения, постепенно замедляется и потом вовсе исчезает. Рассмотрим её подробнее:
function createFragments(x, y, color) {
const fragment = document.createElement('div');
fragment.classList.add('fragments');
fragment.style.left = `${x}px`;
fragment.style.top = `${y}px`;
fragment.style.backgroundColor = color;
battleArena.appendChild(fragment);
let fragmentX = x;
let fragmentY = y;
let dx = (Math.random() - 0.5) * 4; // Случайное направление по X
let dy = (Math.random() - 0.5) * 4; // Случайное направление по Y
let bounceCount = 0;
const fragmentInterval = setInterval(() => {
fragmentX += dx;
fragmentY += dy;
// Если квадратик улетел слишком далеко, удаляем его
if (Math.abs(fragmentX - x) > 30 || Math.abs(fragmentY - y) > 30) {
clearInterval(fragmentInterval);
fragment.remove();
return;
}
// Если квадратик ударился о край поля, он отскакивает
if (fragmentX < 0 || fragmentX > 400 || fragmentY < 0 || fragmentY > 400) {
dx *= -0.8;
dy *= -0.8;
bounceCount++;
}
// Если квадратик отскочил 5 раз, удаляем его
if (bounceCount >= 5) {
clearInterval(fragmentInterval);
fragment.remove();
return;
}
// Обновляем положение квадратика
fragment.style.left = `${fragmentX}px`;
fragment.style.top = `${fragmentY}px`;
}, 50);
}
И напоследок, по поводу движения машинки можно сказать ещё вот что: движение реализовано в пошаговом варианте, так как это гораздо проще, чем плавное, и может быть важно для браузерной игры, где производительность может быть ограничена.
В дальнейшем есть мысли усовершенствовать эту игру: добавить компьютерных «врагов», а также режим многопользовательской игры — использующейся для обмена данными между игроками. Думаю, штук 10 одновременных websocket-соединений esp32 потянет (т.е. 10 игроков одновременно). Надо будет ещё оптимизировать количество передаваемых данных между игроками, чтобы esp32 это всё вытянула:
- Передача только ключевых данных (координаты, направление, выстрелы).
- Анимация и физика обрабатываются на стороне клиента, что уменьшает нагрузку на сервер.
Думаю, что в целом это было бы занятно, особенно если рядом с игровым полем запустить ещё и чат, чтобы игрокам было удобнее общаться друг с другом.
Кстати, на данный момент игровое поле выглядит вот так:

Так что, как говорится — «To be continued! Stay tuned…»
В качестве затравки скажу только — что на данный момент (момент выхода статьи) вторая часть игры с мультиплеером уже почти готова — так что ждём :-)
На этом я на сегодня закругляюсь, а желающие могут взять полный код ниже, скопировать его к себе, вставить в блокнот, сохранить с расширением .html и запустить этот файл.
Специально не выкладываю в виде готового файла, чтобы вы не стали сомневаться — «а вдруг там страшный вирус»? Поэтому предлагаю сделать файл самостоятельно, просмотрев код.
Для написания кода использовался «модный» способ с применением DeepSeek (было любопытно глянуть, как он в этом деле), правда это оказалось довольно тяжко и заняло существенное время — довольно трудно было объяснить многие моменты. По времени заняло 2 вечера (8-9 часов). Причём, что интересно — эти вечера были разнесены во времени на неделю примерно. И во второй вечер он смог сделать то, чего не смог в первый, причём с теми же запросами! Умнеет на глазах, однако… :-)
Тем не менее, проблем тоже хватало: к примеру, надо сделать 3 итерации: a, b, c. На итерации b мы забываем, что делали на a; соответственно, на итерации c — забываем про a, b. И приходится делать итерацию d (как минимум), чтобы восстановить как было. Кроме того, несмотря на массу попыток, он так и не смог сделать так, чтобы пули летели из центра ствола — только ниже или выше. Мой педагогический талант оказался бессилен ему это объяснить :-)
В целом, для себя я понял, что в теории — можно, но довольно-таки с оглядкой и нужно быть программистом, чтобы пояснять многие моменты на профессиональном языке. Но общая тенденция становится понятна — всё идёт «в ту сторону»…
Так что для себя на будущее решил, что буду по максимуму пользоваться этим инструментом для написания кода, так как позволяет ускорить процессы и быстрее прийти к конечному результату, к тому же (что для меня показалось любопытным) — интересно изучать код, который он пишет, потому что, как многие отмечают, он довольно хорошо оптимизирован и можно научиться новым подходам. Тем более, насколько мы знаем, есть такой подход к программированию, когда происходит обучение, с помощью изучения чужого кода. Но просто так изучать довольно нудно, а вот когда при этом решается именно твоя задача — так гораздо интереснее! Получается довольно интересная концепция обучения: обычно мы учимся, изучая кучу всего, что потенциально может помочь в решении твоей задачи. А тут всё переворачивается с ног на голову: мы уже имеем решение и изучаем, как надо было. Кстати, по опыту, довольно быстрый путь обучения. Век живи — век учись…
Полный код
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chaos Machines</title>
<!-- Разработано с применением DeepSeek -->
<style>
body {
margin: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #000;
}
#battleArena {
position: relative;
width: 400px;
height: 400px;
background-color: #333;
border: 2px solid #fff;
}
.battle-machine {
position: absolute;
width: 20px;
height: 20px;
background-color: green;
display: flex;
justify-content: center;
align-items: center;
}
.battle-machine::after {
content: '';
position: absolute;
width: 6px;
height: 6px;
background-color: black;
top: -3px;
left: 50%;
transform: translateX(-50%);
}
.projectile {
position: absolute;
width: 5px;
height: 5px;
background-color: yellow;
}
.barrier {
position: absolute;
width: 20px;
height: 20px;
background-color: green;
}
.barrier-texture {
position: absolute;
width: 3px;
height: 3px;
background-color: rgba(0, 255, 0, 0.5);
}
.fragments {
position: absolute;
width: 3px;
height: 3px;
background-color: red;
}
</style>
</head>
<body>
<div id="battleArena"></div>
<script>
const battleArena = document.getElementById('battleArena');
const battleMachine = document.createElement('div');
battleMachine.classList.add('battle-machine');
battleArena.appendChild(battleMachine);
let machineX = 200;
let machineY = 200;
let machineOrientation = 'north';
const machineVelocity = 5;
const barriers = createChaoticTerrain(20, 20);
const barrierElements = new Map();
barriers.forEach(barrier => {
const barrierElement = document.createElement('div');
barrierElement.classList.add('barrier');
barrierElement.style.left = `${barrier.x * 20}px`;
barrierElement.style.top = `${barrier.y * 20}px`;
barrierElement.dataset.durability = 3;
for (let i = 0; i < 20; i += 3) {
for (let j = 0; j < 20; j += 3) {
const texturePixel = document.createElement('div');
texturePixel.classList.add('barrier-texture');
texturePixel.style.left = `${i}px`;
texturePixel.style.top = `${j}px`;
texturePixel.style.backgroundColor = `rgba(0, ${100 + Math.floor(Math.random() * 155)}, 0, 0.8)`;
barrierElement.appendChild(texturePixel);
}
}
battleArena.appendChild(barrierElement);
barrierElements.set(`${barrier.x}-${barrier.y}`, barrierElement);
});
function refreshBattleMachinePosition() {
battleMachine.style.left = `${machineX}px`;
battleMachine.style.top = `${machineY}px`;
battleMachine.style.transform = `rotate(${
machineOrientation === 'north' ? 0 :
machineOrientation === 'east' ? 90 :
machineOrientation === 'south' ? 180 :
machineOrientation === 'west' ? 270 : 0
}deg)`;
}
function navigateBattleMachine(direction) {
let newX = machineX;
let newY = machineY;
if (direction === 'north') {
newY -= machineVelocity;
} else if (direction === 'south') {
newY += machineVelocity;
} else if (direction === 'west') {
newX -= machineVelocity;
} else if (direction === 'east') {
newX += machineVelocity;
}
if (!checkObstacleCollision(newX, newY)) {
machineX = newX;
machineY = newY;
machineOrientation = direction;
refreshBattleMachinePosition();
}
}
function checkObstacleCollision(x, y) {
return barriers.some(barrier => {
const barrierX = barrier.x * 20;
const barrierY = barrier.y * 20;
return x < barrierX + 20 && x + 20 > barrierX && // По X
y < barrierY + 20 && y + 20 > barrierY; // По Y
});
}
function removeTexturePixels(barrierElement, x, y, numPixels) {
const texturePixels = Array.from(barrierElement.querySelectorAll('.barrier-texture'));
const pixelsToRemove = texturePixels.filter(pixel => {
const pixelX = parseFloat(pixel.style.left);
const pixelY = parseFloat(pixel.style.top);
const distance = Math.sqrt((pixelX - x) ** 2 + (pixelY - y) ** 2);
return distance <= 30; // Радиус 30 пикселей
});
for (let i = 0; i < Math.min(numPixels, pixelsToRemove.length); i++) {
const pixel = pixelsToRemove[Math.floor(Math.random() * pixelsToRemove.length)];
pixel.remove();
}
}
function createFragments(x, y, color) {
const fragment = document.createElement('div');
fragment.classList.add('fragments');
fragment.style.left = `${x}px`;
fragment.style.top = `${y}px`;
fragment.style.backgroundColor = color;
battleArena.appendChild(fragment);
let fragmentX = x;
let fragmentY = y;
let dx = (Math.random() - 0.5) * 4; // Случайное направление по X
let dy = (Math.random() - 0.5) * 4; // Случайное направление по Y
let bounceCount = 0;
const fragmentInterval = setInterval(() => {
fragmentX += dx;
fragmentY += dy;
if (Math.abs(fragmentX - x) > 30 || Math.abs(fragmentY - y) > 30) {
clearInterval(fragmentInterval);
fragment.remove();
return;
}
if (fragmentX < 0 || fragmentX > 400 || fragmentY < 0 || fragmentY > 400) {
dx *= -0.8;
dy *= -0.8;
bounceCount++;
}
if (bounceCount >= 5) {
clearInterval(fragmentInterval);
fragment.remove();
return;
}
fragment.style.left = `${fragmentX}px`;
fragment.style.top = `${fragmentY}px`;
}, 50);
}
function fireProjectile() {
let projectileStartX, projectileStartY;
let projectileDirection = machineOrientation;
if (projectileDirection === 'north') {
projectileStartX = machineX + 10;
projectileStartY = machineY - 3;
} else if (projectileDirection === 'south') {
projectileStartX = machineX + 10;
projectileStartY = machineY + 23;
} else if (projectileDirection === 'west') {
projectileStartX = machineX - 3;
projectileStartY = machineY + 10;
} else if (projectileDirection === 'east') {
projectileStartX = machineX + 23;
projectileStartY = machineY + 10;
}
const projectile = document.createElement('div');
projectile.classList.add('projectile');
projectile.style.left = `${projectileStartX}px`;
projectile.style.top = `${projectileStartY}px`;
battleArena.appendChild(projectile);
let projectileX = projectileStartX;
let projectileY = projectileStartY;
const projectileInterval = setInterval(() => {
if (projectileDirection === 'north') {
projectileY -= 5;
} else if (projectileDirection === 'south') {
projectileY += 5;
} else if (projectileDirection === 'west') {
projectileX -= 5;
} else if (projectileDirection === 'east') {
projectileX += 5;
}
projectile.style.left = `${projectileX}px`;
projectile.style.top = `${projectileY}px`;
if (projectileX < 0 || projectileX > 400 || projectileY < 0 || projectileY > 400) {
clearInterval(projectileInterval);
projectile.remove();
}
barriers.forEach(barrier => {
const barrierX = barrier.x * 20;
const barrierY = barrier.y * 20;
if (projectileX >= barrierX && projectileX <= barrierX + 20 && projectileY >= barrierY && projectileY <= barrierY + 20) {
const barrierElement = barrierElements.get(`${barrier.x}-${barrier.y}`);
if (barrierElement) {
let durability = parseInt(barrierElement.dataset.durability);
durability -= 1;
barrierElement.dataset.durability = durability;
const textureColors = Array.from(barrierElement.querySelectorAll('.barrier-texture'))
.map(pixel => pixel.style.backgroundColor);
const numFragments = Math.min(10, Math.floor(Math.random() * 10) + 1);
removeTexturePixels(barrierElement, projectileX - barrierX, projectileY - barrierY, numFragments);
for (let i = 0; i < numFragments; i++) {
const color = textureColors[Math.floor(Math.random() * textureColors.length)];
createFragments(projectileX, projectileY, color);
}
if (durability <= 0) {
barrierElement.remove();
barriers.splice(barriers.indexOf(barrier), 1);
}
}
clearInterval(projectileInterval);
projectile.remove();
}
});
}, 50);
}
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') {
navigateBattleMachine('west');
} else if (event.key === 'ArrowRight') {
navigateBattleMachine('east');
} else if (event.key === 'ArrowUp') {
navigateBattleMachine('north');
} else if (event.key === 'ArrowDown') {
navigateBattleMachine('south');
} else if (event.key === ' ') {
fireProjectile();
}
});
function createChaoticTerrain(width, height) {
const terrain = [];
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
if (Math.random() < 0.3 && !(i === Math.floor(width / 2) && j === Math.floor(height / 2))) {
terrain.push({ x: i, y: j });
}
}
}
return terrain;
}
refreshBattleMachinePosition();
</script>
</body>
</html>
Продолжение статьи можно найти здесь: Пилим игровой мультиплеерный сервер на базе esp32: завершение. Портируем игру на esp32.
© 2025 ООО «МТ ФИНАНС»
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
