Beispiel:Breakout.html

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Breakout</title>
<style>
  body { 
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh; 
    margin: 0;
    background: #111;
    color: #fff;
    font-family: monospace;
  }
  canvas { 
    background: #000;
    border: medium solid #555;
    display: block;
    width: 100%;
    max-width: 800px;
    margin: 0 0.5rem;
    cursor: none;
  }
  p { 
    margin: 0.5rem;
    font-size: clamp(0.7rem, 2vw, 1.2rem);
  }
  #tonBtn {
    background: none;
    border: none;
    font-size: 2em;
    font-family: inherit;
    cursor: pointer;
    margin-top: -1rem;
  }
</style>
</head>

<body>

  <p id="info"><span id="infoText">PUNKTE: 0 | LEBEN: 3 | TON: </span><button id="tonBtn">🔊</button></p>
  
  <canvas id="c"></canvas>
  
  <p>← → Pfeiltasten / Maus · Leertaste / Klick für Start</p>

<script>
document.addEventListener('DOMContentLoaded', function () {

  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d');

  // --- Ton ---
  let audioCtx = null;
  let soundOn = true;
  document.getElementById('tonBtn').addEventListener('click', () => {
    soundOn = !soundOn;
    document.getElementById('tonBtn').textContent = soundOn ? '🔊' : '🔇';
  });
  function getAudioCtx() {
    if (!audioCtx) audioCtx = new AudioContext();
    return audioCtx;
  }
  function beep(freq = 440, dur = 0.05) {
    if (!soundOn) return;
    const ac = getAudioCtx();
    const osc = ac.createOscillator();
    const gain = ac.createGain();
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.frequency.value = freq;
    gain.gain.setValueAtTime(0.2, ac.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
    osc.start();
    osc.stop(ac.currentTime + dur);
  }

  // --- Canvas-Grösse responsiv bestimmen, nur beim Laden ---
  const SEITENVERHAELTNIS = 4 / 3;
  const dpr = window.devicePixelRatio || 1;

  function canvasGroesseSetzen() {
    const cssBreite = Math.min(canvas.parentElement.clientWidth - 16, 800);
    const cssHoehe  = Math.round(cssBreite / SEITENVERHAELTNIS);
    canvas.style.width  = cssBreite + 'px';
    canvas.style.height = cssHoehe  + 'px';
    canvas.width  = Math.round(cssBreite * dpr);
    canvas.height = Math.round(cssHoehe  * dpr);
    ctx.scale(dpr, dpr);
  }

  canvasGroesseSetzen();

  // Logische Spielgrösse (CSS-Pixel, unabhängig von dpr)
  const W = canvas.width  / dpr;
  const H = canvas.height / dpr;

  // --- Konfiguration ---
  const KONFIG = {
    paddle: {
      breite:       Math.round(W * 0.15),
      hoehe:        Math.round(H * 0.018),
      y:            H - H * 0.08,
      geschwindigkeit: W * 0.012,
    },
    ball: {
      radius:       Math.round(W * 0.012),
      geschwindigkeit: Math.hypot(W * 0.006, H * 0.011),
    },
    steine: {
      spalten:      10,
      reihen:       6,
      abstand:      Math.round(W * 0.007),
      get breite()  { return Math.round((W - this.abstand) / this.spalten - this.abstand); },
      hoehe:        Math.round(H * 0.038),
      get offsetX() { return (W - this.spalten * (this.breite + this.abstand) + this.abstand) / 2; },
      offsetY:      Math.round(H * 0.1),
      farben:       ['#e74c3c','#e67e22','#f1c40f','#2ecc71','#3498db','#9b59b6'],
    },
    schrift: {
      gross: Math.min(Math.round(W * 0.15), 80),
      klein: Math.round(W * 0.03),
    },
  };

  // --- Zustand ---
  const state = { score: 0, lives: 3, paused: false, pauseMsg: '' };
  let bricks = [], ball = {}, paddle = {};
  let rafId = null;
  const keys = {};

  function pause(msg) {
    state.paused  = true;
    state.pauseMsg = msg;
  }

  // --- Init ---
  function init() {
    if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
    Object.assign(state, { score: 0, lives: 3, paused: false, pauseMsg: '' });
    const { paddle: P, steine: S } = KONFIG;
    paddle = { x: W/2 - P.breite/2, y: P.y };
    bricks = [];
    for (let r = 0; r < S.reihen; r++)
      for (let c = 0; c < S.spalten; c++)
        bricks.push({ x: S.offsetX + c*(S.breite+S.abstand), y: S.offsetY + r*(S.hoehe+S.abstand), alive: true, color: S.farben[r] });
    resetBall();
    updateInfo();
    rafId = requestAnimationFrame(loop);
  }

  function resetBall() {
    const { paddle: P, ball: B } = KONFIG;
    ball = { x: W/2, y: P.y - B.radius - 2, vx: B.geschwindigkeit * 0.5, vy: -B.geschwindigkeit, launched: false };
  }

  // --- Game Loop ---
  function loop() {
    update();
    draw();
    if (state.paused) {
      showMessage(state.pauseMsg, 'Punkte: ' + state.score);
      rafId = null;
    } else {
      rafId = requestAnimationFrame(loop);
    }
  }

  // --- Steuerung ---
  window.addEventListener('keydown', e => {
    keys[e.key] = true;
    if (e.key === ' ') {
      getAudioCtx();
      if (state.paused) init();
      else ball.launched = true;
    }
  });
  window.addEventListener('keyup', e => { keys[e.key] = false; });

  canvas.addEventListener('mousemove', e => {
    const r = canvas.getBoundingClientRect();
    const scale = W / r.width;
    paddle.x = (e.clientX - r.left) * scale - KONFIG.paddle.breite / 2;
  }, { passive: true });

  canvas.addEventListener('touchmove', e => {
    e.preventDefault();
    const r = canvas.getBoundingClientRect();
    const scale = W / r.width;
    paddle.x = (e.touches[0].clientX - r.left) * scale - KONFIG.paddle.breite / 2;
  }, { passive: false });

  canvas.addEventListener('click', () => {
    getAudioCtx();
    if (state.paused) init();
    else ball.launched = true;
  });

  // --- Update ---
  function update() {
    const { paddle: P, ball: B, steine: S } = KONFIG;

    if (keys['ArrowLeft'])  paddle.x -= P.geschwindigkeit;
    if (keys['ArrowRight']) paddle.x += P.geschwindigkeit;
    paddle.x = Math.max(0, Math.min(W - P.breite, paddle.x));

    if (!ball.launched) { ball.x = paddle.x + P.breite/2; return; }

    ball.x += ball.vx;
    ball.y += ball.vy;

    // Wände (Richtung mit Math.abs garantieren, dann Position korrigieren)
    if (ball.x - B.radius < 0) {
      ball.vx = Math.abs(ball.vx);
      ball.x  = B.radius;
      beep(300);
    } else if (ball.x + B.radius > W) {
      ball.vx = -Math.abs(ball.vx);
      ball.x  = W - B.radius;
      beep(300);
    }
    if (ball.y - B.radius < 0) {
      ball.vy = Math.abs(ball.vy);
      ball.y  = B.radius;
      beep(300);
    }

    // Paddle-Kollision
    if (ball.vy > 0
      && ball.y + B.radius >= paddle.y
      && ball.y + B.radius <= paddle.y + P.hoehe
      && ball.x >= paddle.x
      && ball.x <= paddle.x + P.breite) {
      const angle = (ball.x - (paddle.x + P.breite/2)) / (P.breite/2) * (Math.PI / 3);
      ball.vx = B.geschwindigkeit * Math.sin(angle);
      ball.vy = -Math.abs(B.geschwindigkeit * Math.cos(angle));
      ball.y  = paddle.y - B.radius;                           // rauskorrigieren
      beep(480);
    }

    // Ball verloren
    if (ball.y > H) {
      state.lives--;
      updateInfo();
      if (state.lives <= 0) { pause('GAME OVER'); return; }
      resetBall();
    }

    // Block-Kollision
    for (const b of bricks) {
      if (!b.alive) continue;
      if (ball.x + B.radius > b.x && ball.x - B.radius < b.x + S.breite &&
          ball.y + B.radius > b.y && ball.y - B.radius < b.y + S.hoehe) {
        b.alive = false;
        state.score++;
        updateInfo();
        ball.vy *= -1;
        beep(660);
        break;
      }
    }

    if (bricks.every(b => !b.alive)) pause('GEWONNEN!');
  }

  function updateInfo() {
    document.getElementById('infoText').textContent = `PUNKTE: ${state.score} | LEBEN: ${state.lives} | TON: `;
  }

  // --- Zeichnen ---
  function draw() {
    const { paddle: P, ball: B, steine: S, schrift: F } = KONFIG;
    ctx.clearRect(0, 0, W, H);

    ctx.save();
    ctx.globalAlpha  = 0.15;
    ctx.fillStyle    = '#fff';
    ctx.font         = `bold ${Math.round(W * 0.18)}px monospace`;
    ctx.textAlign    = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('BREAKOUT', W/2, H * 0.83);
    ctx.restore();

    for (const b of bricks) {
      if (!b.alive) continue;
      ctx.fillStyle = b.color;
      ctx.fillRect(b.x, b.y, S.breite, S.hoehe);
    }

    ctx.fillStyle = '#fff';
    ctx.fillRect(paddle.x, paddle.y, P.breite, P.hoehe);

    ctx.beginPath();
    ctx.arc(ball.x, ball.y, B.radius, 0, Math.PI * 2);
    ctx.fillStyle = '#fff';
    ctx.fill();
  }

  // --- Meldung im Canvas ---
  function showMessage(msg, sub) {
    const { schrift: F } = KONFIG;
    ctx.save();
    ctx.textBaseline = 'alphabetic';
    ctx.fillStyle    = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign    = 'center';
    ctx.fillStyle    = '#fff';
    ctx.font         = `bold ${F.gross}px monospace`;
    ctx.fillText(msg, W/2, H/2 - F.gross * 0.5);
    ctx.font         = `${F.klein}px monospace`;
    ctx.fillStyle    = '#aaa';
    ctx.fillText(sub, W/2, H/2 + F.klein * 1.2);
    ctx.restore();
  }

  init();

});
</script>

</body>
</html>