Новая Школа - онлайн-школа подготовки к ЕГЭ
Задача Задача №3779380
А
Аноним
3 дня назад · публичный вопрос
А
Аноним

Создай полноценную 2D-игру на HTML, CSS и JavaScript. Весь код должен находиться в одном HTML-файле, чтобы я мог запустить игру через OneCompiler на телефоне. Не используй внешние…

Запрос пользователя

Создай полноценную 2D-игру на HTML, CSS и JavaScript. Весь код должен находиться в одном HTML-файле, чтобы я мог запустить игру через OneCompiler на телефоне. Не используй внешние библиотеки

Ответ нейросети
3 дня назад

Звёздный страж — игра в одном HTML-файле

Короткий ответ: ниже один файл: <canvas> + игровой цикл на requestAnimationFrame, три состояния (start / play / over), спавн звёзд и астероидов по таймерам, столкновения проверяются по кругам, управление — пальцем (корабль держится чуть выше пальца). Скопируй всё в OneCompiler (вкладка HTML) → Run.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Звёздный страж</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    height: 100%; overflow: hidden; background: #05070f;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
    -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
  }
  #game { position: relative; width: 100%; height: 100vh; height: 100dvh; overflow: hidden; touch-action: none; }
  #board { display: block; width: 100%; height: 100%; }
  #hud {
    position: absolute; top: 0; left: 0; right: 0;
    display: flex; justify-content: space-between; padding: 14px 18px;
    font-size: 16px; font-weight: 800; letter-spacing: .5px;
    color: #e9f0ff; text-shadow: 0 2px 8px rgba(0,0,0,.6); pointer-events: none;
  }
  #hudLives { color: #ff8f9c; }
  #overlay {
    position: absolute; inset: 0; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 14px; padding: 26px;
    text-align: center; background: rgba(5, 7, 15, .78);
  }
  #overlay.hidden { display: none; }
  #ovTitle { font-size: clamp(22px, 7vw, 34px); color: #eaf3ff; }
  #ovText { max-width: 340px; font-size: clamp(13px, 3.6vw, 16px); line-height: 1.5; color: #9fb0d4; }
  #startBtn {
    border: 0; border-radius: 999px; padding: 14px 34px; font-size: 17px; font-weight: 800;
    color: #052014; background: linear-gradient(180deg, #8bf3c4, #2fc48d);
    box-shadow: 0 10px 26px rgba(47, 196, 141, .35); cursor: pointer;
  }
  #startBtn:active { transform: translateY(1px); }
</style>
</head>
<body>
<div id="game">
  <canvas id="board"></canvas>
  <div id="hud">
    <div id="hudScore">0</div>
    <div id="hudLives">♥♥♥</div>
  </div>
  <div id="overlay">
    <h1 id="ovTitle">Звёздный страж</h1>
    <p id="ovText">Веди корабль пальцем по экрану: он держится чуть выше пальца. Собирай звёзды и уклоняйся от астероидов. У тебя 3 жизни.</p>
    <button id="startBtn">Играть</button>
  </div>
</div>
<script>
(function () {
  "use strict";

  const canvas = document.getElementById("board");
  const ctx = canvas.getContext("2d");
  const hudScore = document.getElementById("hudScore");
  const hudLives = document.getElementById("hudLives");
  const overlay = document.getElementById("overlay");
  const ovTitle = document.getElementById("ovTitle");
  const ovText = document.getElementById("ovText");
  const startBtn = document.getElementById("startBtn");

  const rand = (a, b) => a + Math.random() * (b - a);
  const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);

  const keys = new Set();

  let W = 0, H = 0, DPR = 1, bgStars = [];

  const player = { x: 0, y: 0, r: 15, tx: 0, ty: 0 };

  const game = {
    state: "start", score: 0, lives: 3, time: 0, shake: 0, invuln: 0,
    stars: [], rocks: [], parts: [], starTimer: 0, rockTimer: 0, best: 0
  };

  try { game.best = parseInt(localStorage.getItem("zvezdnyBest") || "0", 10) || 0; }
  catch (e) { game.best = 0; }

  // ---------- размеры экрана ----------
  function resize() {
    DPR = Math.min(window.devicePixelRatio || 1, 2);
    W = canvas.clientWidth || window.innerWidth;
    H = canvas.clientHeight || window.innerHeight;
    canvas.width = Math.max(1, Math.round(W * DPR));
    canvas.height = Math.max(1, Math.round(H * DPR));
    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);

    bgStars = [];
    const count = Math.round((W * H) / 9000);
    for (let i = 0; i < count; i++) {
      bgStars.push({ x: rand(0, W), y: rand(0, H), r: rand(0.4, 1.7), a: rand(0.25, 0.85), sp: rand(4, 16) });
    }

    player.r = clamp(Math.min(W, H) * 0.033, 12, 20);

    if (game.state === "play") {
      player.x = clamp(player.x, player.r, W - player.r);
      player.y = clamp(player.y, player.r, H - player.r);
      player.tx = clamp(player.tx, player.r, W - player.r);
      player.ty = clamp(player.ty, player.r, H - player.r);
    } else {
      player.x = W / 2; player.y = H * 0.72;
      player.tx = player.x; player.ty = player.y;
    }
  }

  // ---------- интерфейс и состояния ----------
  function updateHud() {
    hudScore.textContent = String(game.score);
    hudLives.textContent = "♥".repeat(Math.max(0, game.lives));
  }

  function startGame() {
    game.state = "play";
    game.score = 0; game.lives = 3; game.time = 0; game.shake = 0; game.invuln = 0;
    game.stars.length = 0; game.rocks.length = 0; game.parts.length = 0;
    game.starTimer = 0.4; game.rockTimer = 1.1;
    player.x = W / 2; player.y = H * 0.72;
    player.tx = player.x; player.ty = player.y;
    overlay.classList.add("hidden");
    updateHud();
  }

  function gameOver() {
    game.state = "over";
    if (game.score > game.best) {
      game.best = game.score;
      try { localStorage.setItem("zvezdnyBest", String(game.best)); } catch (e) {}
    }
    ovTitle.textContent = "Игра окончена";
    ovText.innerHTML = "Звёзд собрано: <b>" + game.score + "</b><br>Рекорд: <b>" + game.best + "</b>";
    startBtn.textContent = "Ещё раз";
    overlay.classList.remove("hidden");
  }

  // ---------- создание объектов ----------
  function fallSpeed() {
    return H * rand(0.26, 0.34) * (1 + Math.min(game.time / 90, 0.5));
  }

  function spawnStar() {
    const r = rand(9, 13);
    game.stars.push({ x: rand(r + 8, Math.max(r + 9, W - r - 8)), y: -r - 12, r: r, vy: fallSpeed(), rot: rand(0, Math.PI) });
  }

  function spawnRock() {
    const r = rand(12, 22);
    const shape = [];
    for (let i = 0; i < 9; i++) shape.push(rand(0.72, 1.1));
    game.rocks.push({
      x: rand(r + 8, Math.max(r + 9, W - r - 8)), y: -r - 12, r: r,
      vy: fallSpeed() * 0.95, rot: rand(0, Math.PI * 2), spin: rand(-1.4, 1.4), shape: shape
    });
  }

  function burst(x, y, color, count) {
    for (let i = 0; i < count; i++) {
      const a = rand(0, Math.PI * 2);
      const sp = rand(40, 220);
      game.parts.push({
        x: x, y: y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 40,
        life: rand(0.35, 0.75), max: 0.75, r: rand(1.5, 3.6), color: color
      });
    }
  }

  function updateParts(dt) {
    for (let i = game.parts.length - 1; i >= 0; i--) {
      const p = game.parts[i];
      p.life -= dt;
      if (p.life <= 0) { game.parts.splice(i, 1); continue; }
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vy += 180 * dt;
      p.vx *= 1 - Math.min(0.5, 1.2 * dt);
    }
  }

  // ---------- логика кадра ----------
  function update(dt) {
    for (let i = 0; i < bgStars.length; i++) {
      const s = bgStars[i];
      s.y += s.sp * dt;
      if (s.y > H + 2) { s.y = -2; s.x = rand(0, W); }
    }

    updateParts(dt);
    if (game.shake > 0) game.shake = Math.max(0, game.shake - dt * 2.5);
    if (game.state !== "play") return;

    game.time += dt;
    game.invuln = Math.max(0, game.invuln - dt);

    const diff = 1 + Math.min(game.time / 26, 1.7);

    // клавиатура — для проверки на компьютере
    let kx = 0, ky = 0;
    if (keys.has("arrowleft") || keys.has("a") || keys.has("ф")) kx -= 1;
    if (keys.has("arrowright") || keys.has("d") || keys.has("в")) kx += 1;
    if (keys.has("arrowup") || keys.has("w") || keys.has("ц")) ky -= 1;
    if (keys.has("arrowdown") || keys.has("s") || keys.has("ы")) ky += 1;
    if (kx !== 0 || ky !== 0) {
      const sp = Math.min(W, H) * 0.95;
      player.tx += kx * sp * dt;
      player.ty += ky * sp * dt;
    }

    player.tx = clamp(player.tx, player.r, W - player.r);
    player.ty = clamp(player.ty, player.r, H - player.r);

    const k = Math.min(1, dt * 13);
    player.x += (player.tx - player.x) * k;
    player.y += (player.ty - player.y) * k;
    player.x = clamp(player.x, player.r, W - player.r);
    player.y = clamp(player.y, player.r, H - player.r);

    // спавн: чем дальше, тем чаще
    game.starTimer -= dt;
    if (game.starTimer <= 0) {
      spawnStar();
      game.starTimer = rand(0.5, 0.9) / Math.min(diff, 1.5);
    }
    game.rockTimer -= dt;
    if (game.rockTimer <= 0) {
      spawnRock();
      game.rockTimer = rand(1.0, 1.6) / Math.min(diff, 2.1);
    }

    // падение
    for (let i = game.stars.length - 1; i >= 0; i--) {
      const s = game.stars[i];
      s.y += s.vy * dt;
      s.rot += dt * 2.2;
      if (s.y - s.r > H + 30) game.stars.splice(i, 1);
    }
    for (let i = game.rocks.length - 1; i >= 0; i--) {
      const rk = game.rocks[i];
      rk.y += rk.vy * dt;
      rk.rot += rk.spin * dt;
      if (rk.y - rk.r > H + 30) game.rocks.splice(i, 1);
    }

    // сбор звёзд
    for (let i = game.stars.length - 1; i >= 0; i--) {
      const s = game.stars[i];
      const dx = s.x - player.x, dy = s.y - player.y, rr = s.r + player.r;
      if (dx * dx + dy * dy < rr * rr) {
        game.score += 1;
        game.shake = Math.min(0.35, game.shake + 0.15);
        burst(s.x, s.y, "#ffe27a", 10);
        game.stars.splice(i, 1);
        updateHud();
      }
    }

    // столкновение с астероидами
    if (game.invuln <= 0) {
      for (let i = game.rocks.length - 1; i >= 0; i--) {
        const rk = game.rocks[i];
        const dx = rk.x - player.x, dy = rk.y - player.y, rr = rk.r * 0.85 + player.r;
        if (dx * dx + dy * dy < rr * rr) {
          game.lives -= 1;
          game.invuln = 1.4;
          game.shake = 1;
          burst(player.x, player.y, "#ff6b8a", 18);
          game.rocks.splice(i, 1);
          updateHud();
          if (game.lives <= 0) gameOver();
          break;
        }
      }
    }
  }

  // ---------- рисование ----------
  function starPath(outer, inner) {
    ctx.beginPath();
    for (let i = 0; i < 10; i++) {
      const r = i % 2 === 0 ? outer : inner;
      const a = (i * Math.PI) / 5 - Math.PI / 2;
      const x = Math.cos(a) * r, y = Math.sin(a) * r;
      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.closePath();
  }

  function drawStar(s) {
    ctx.save();
    ctx.translate(s.x, s.y);
    ctx.rotate(s.rot);
    ctx.shadowColor = "#ffd24a";
    ctx.shadowBlur = 14;
    ctx.fillStyle = "#ffe27a";
    starPath(s.r, s.r * 0.45);
    ctx.fill();
    ctx.restore();
  }

  function drawRock(rk) {
    ctx.save();
    ctx.translate(rk.x, rk.y);
    ctx.rotate(rk.rot);
    ctx.beginPath();
    const n = rk.shape.length;
    for (let i = 0; i < n; i++) {
      const a = (i / n) * Math.PI * 2;
      const r = rk.r * rk.shape[i];
      const x = Math.cos(a) * r, y = Math.sin(a) * r;
      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.closePath();
    ctx.fillStyle = "#7a3550";
    ctx.fill();
    ctx.strokeStyle = "#ff6b8a";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.restore();
  }

  function drawShip() {
    const r = player.r;
    ctx.save();
    ctx.translate(player.x, player.y);
    if (game.invuln > 0) ctx.globalAlpha = 0.35 + 0.65 * Math.abs(Math.sin(game.invuln * 12));
    ctx.shadowColor = "#5fd4ff";
    ctx.shadowBlur = 16;
    ctx.fillStyle = "#e6f7ff";
    ctx.beginPath();
    ctx.moveTo(0, -r * 1.25);
    ctx.lineTo(r * 0.9, r * 0.95);
    ctx.lineTo(0, r * 0.45);
    ctx.lineTo(-r * 0.9, r * 0.95);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }

  function render() {
    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);

    const g = ctx.createLinearGradient(0, 0, 0, H);
    g.addColorStop(0, "#080d1c");
    g.addColorStop(0.55, "#0b1430");
    g.addColorStop(1, "#05070f");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);

    for (let i = 0; i < bgStars.length; i++) {
      const s = bgStars[i];
      ctx.globalAlpha = s.a;
      ctx.fillStyle = "#cfe3ff";
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.save();
    if (game.shake > 0) {
      const m = game.shake * 9;
      ctx.translate(rand(-m, m), rand(-m, m));
    }

    if (game.state !== "start") {
      for (let i = 0; i < game.rocks.length; i++) drawRock(game.rocks[i]);
      for (let i = 0; i < game.stars.length; i++) drawStar(game.stars[i]);

      for (let i = 0; i < game.parts.length; i++) {
        const p = game.parts[i];
        ctx.globalAlpha = Math.max(0, p.life / p.max);
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;

      if (game.state === "play") drawShip();
    }

    ctx.restore();
  }

  // ---------- управление ----------
  function steerTo(clientX, clientY) {
    const rect = canvas.getBoundingClientRect();
    const x = clientX - rect.left;
    const y = clientY - rect.top - Math.min(H * 0.1, 70); // корабль выше пальца
    player.tx = clamp(x, player.r, W - player.r);
    player.ty = clamp(y, player.r, H - player.r);
  }

  canvas.addEventListener("pointerdown", function (e) {
    if (game.state !== "play") return;
    steerTo(e.clientX, e.clientY);
    if (canvas.setPointerCapture) {
      try { canvas.setPointerCapture(e.pointerId); } catch (err) {}
    }
    e.preventDefault();
  }, { passive: false });

  canvas.addEventListener("pointermove", function (e) {
    if (game.state !== "play") return;
    steerTo(e.clientX, e.clientY);
    e.preventDefault();
  }, { passive: false });

  window.addEventListener("keydown", function (e) {
    const k = e.key.toLowerCase();
    if (k === " " || k === "arrowup" || k === "arrowdown" || k === "arrowleft" || k === "arrowright") e.preventDefault();
    keys.add(k);
    if ((k === " " || k === "enter") && game.state !== "play") startGame();
  });

  window.addEventListener("keyup", function (e) {
    keys.delete(e.key.toLowerCase());
  });

  startBtn.addEventListener("click", function () { startGame(); });

  // ---------- игровой цикл ----------
  let last = performance.now();

  function frame(now) {
    let dt = (now - last) / 1000;
    last = now;
    if (dt < 0) dt = 0;
    if (dt > 0.05) dt = 0.05; // защита от «прыжка» после сворачивания
    update(dt);
    render();
    requestAnimationFrame(frame);
  }

  resize();
  updateHud();
  window.addEventListener("resize", resize);
  window.addEventListener("orientationchange", function () { setTimeout(resize, 150); });
  document.addEventListener("visibilitychange", function () { last = performance.now(); });

  requestAnimationFrame(frame);
})();
</script>
</body>
</html>

Как это устроено (принцип, который переносится на любую 2D-игру)

  1. Сцена. <canvas> растянут на весь экран. Чтобы картинка не была мыльной на телефоне, внутренний размер умножается на devicePixelRatio, а масштаб задаётся через ctx.setTransform(DPR,0,0,DPR,0,0) — дальше везде работаем в обычных CSS-пикселях.
  2. Три состояния вместо флагов. game.state принимает "start" | "play" | "over". update и render спрашивают состояние и рисуют/считают только нужное. Это спасает от половины багов «после проигрыша всё ещё летит».
  3. Игровой цикл с dt. requestAnimationFrame даёт время кадра; dt = (now - last)/1000 — секунды с прошлого кадра. Все скорости умножаются на dt (y += vy * dt), поэтому игра идёт одинаково на 60 и на 120 Гц. Ограничение dt ≤ 0.05 не даёт объектам «телепортироваться» через экран после переключения вкладки.
  4. Спавн по таймерам, а не по кадрам. starTimer -= dt, при ≤ 0 создаём объект и заново ставим интервал. Сложность растёт коэффициентом diff = 1 + min(time/26, 1.7), который делит интервал: чем дольше играешь, тем чаще падают камни.
  5. Столкновения — круги и теорема Пифагора. Расстояние между центрами сравниваем с суммой радиусов: \(\sqrt{dx^2+dy^2} < r_1+r_2\). Корень не извлекаем, сравниваем квадраты — быстрее и без потери точности: \[ dx^{2}+dy^{2} < (r_1+r_2)^{2} \]
  6. Управление пальцем. Корабль не рисуется под пальцем (иначе его не видно), поэтому цель смещена вверх на ~10 % высоты экрана, а сам корабль плавно «догоняет» цель: x += (tx - x) * min(1, dt*13). Это тот же приём сглаживания, что используют почти все аркады.
  7. Обратная связь. Неуязвимость 1.4 с с мерцанием, тряска экрана (ctx.translate случайным сдвигом) и частицы — чтобы игрок мгновенно понимал, что произошло.

Проверка результата

  • Собрал звезду → счёт +1, звёздочка исчезает, летят жёлтые частицы.
  • Попал в астероид → минус одно сердце, экран трясётся, корабль мигает ~1.4 с и не может потерять второе сердце в этот момент.
  • Сердца кончились → экран «Игра окончена» со счётом и рекордом; кнопка «Ещё раз» сбрасывает всё (объекты, таймеры, позицию).
  • Повернул телефон → resize пересчитывает всё, корабль остаётся в пределах экрана.

Что подкрутить (для похожих заданий)

Хочу Где менять
Больше жизней game.lives = 3 в startGame
Быстрее/медленнее объекты fallSpeed(), множители 0.26…0.34
Сложнее делители Math.min(diff, 1.5) и Math.min(diff, 2.1)
Другой персонаж функция drawShip() — рисуй любую фигуру
Бонусы (щит, замедление) добавь массив powerups по образцу game.stars и флаг-таймер

Если захочешь развить игру (стрельба, бонусы, уровни) — скажи, какой элемент добавить, и я покажу, как встроить его в эту же структуру.

Обсуждение

1 сообщение
А
Аноним

ОшЩя