Schere-Stein-Papier mit p5.js und Handtracking

In unserem Projekt erstellen wir ein interaktives „Schere-Stein-Papier“-Spiel, das per Webcam gesteuert wird. Dafür nutzen wir JavaScript mit p5.js für die Spielmechanik und Darstellung, sowie HTML zur Strukturierung der Webseite.

Ein Einblick ins Spiel


Das Projekt

Für die Erkennung der Handgesten verwenden wir die Bibliothek ml5.js, die auf dem MediaPipe Handpose-Modell basiert. Dieses Modell erkennt in Echtzeit über die Webcam die Positionen verschiedener Punkte auf der Hand (sogenannte „Landmarks“). Damit können wir analysieren, ob der Spieler „Schere“, „Stein“ oder „Papier“ zeigt – oder auch einen „Daumen hoch“, mit dem das Spiel gestartet oder erneut begonnen wird.

Der Computer wählt ebenfalls zufällig eine Geste, und es wird automatisch ausgewertet, wer die Runde gewinnt.

Zur visuellen Darstellung haben wir eigene Bilder gezeichnet, die im Spiel anzeigen:

  • Ein Daumen hoch zum Starten der Spielrunde

  • Welche Geste der Computer gewählt hat


    Code

    Basic HTML

    <!DOCTYPE html>
    <html lang="de">
      <head>
        <meta charset="UTF-8" />
        <title>Schere Stein Papier – mit Handpose</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
    
        <!-- Einbindung des Handposen Modells: ml5.js -->
        <script src="https://unpkg.com/ml5@0.12.2/dist/ml5.min.js"></script>
    
        <!-- Externes CSS -->
        <link rel="stylesheet" href="style.css" />
      </head>
      <body>
        <h1>✌️ Schere, Stein, Papier ✊</h1>
        <p>Zeige 👍 um das Spiel zu starten</p>
    
        <script src="sketch.js"></script>
      </body>
    </html>

    JAVA Script

    Globale Variablen werden definiert & Spielstatus vor Rundenstart wird festgelegt:

    // Globale Variablen
    let video;             // Webcam-Videoquelle
    let handpose;          // Handpose-Modell von ml5.js
    let predictions = [];  // Aktuelle Handerkennungs-Ergebnisse
    
    let thumbImage;        // Bild für "Daumen hoch"
    let compImages = {};   // Bilder für Computer-Gesten ("Stein", "Papier", "Schere")
    
    let playerGesture = "❓";   // Aktuelle Geste des Spielers
    let computerGesture = "❓"; // Aktuelle Geste des Computers
    let resultText = "";        // Text zum Ergebnis einer Runde
    
    let gameState = "waiting";  // Spielstatus: "waiting", "playing", "showing_result", "game_over"
    let round = 0;              // Rundenzähler
    let playerScore = 0;        // Punktestand Spieler
    let computerScore = 0;      // Punktestand Computer
    
    let lastGestureTime = 0;        // Zeitstempel der letzten erkannten Geste
    let gestureCooldown = 3000;     // Wartezeit (ms) zwischen Gestenerkennung
    let showResultStart = 0;        // Zeitstempel wann Ergebnis angezeigt wird
    let showResultDuration = 1500;  // Wie lange das Ergebnis angezeigt wird (ms)
    
    // Emojis für Gesten
    const gestureEmojis = {
      "Stein": "✊",
      "Papier": "✋",
      "Schere": "✌️",
      "Unklar": "❓"
    };

    Vorladen der Bilder dass Sie später direkt angezeigt werden können. Wenn die Bilder nicht angezeigt werden wird eine Fehlermeldung angezeigt.

    // Bilder laden bevor das Spiel startet 
    function preload() {
      // Einzelne Gesten-Bilder laden
      compImages["Stein"] = loadImage("stein.png", 
        () => console.log("Stein geladen"),
        () => console.error("Stein konnte nicht geladen werden"));
    
      compImages["Papier"] = loadImage("papier.png", 
        () => console.log("Papier geladen"),
        () => console.error("Papier konnte nicht geladen werden"));
    
      compImages["Schere"] = loadImage("schere.png", 
        () => console.log("Schere geladen"),
        () => console.error("Schere konnte nicht geladen werden"));
    
      // Daumen-hoch-Bild laden
      thumbImage = loadImage("daumen.png", 
        () => console.log("Daumen geladen"),
        () => console.error("Daumen konnte nicht geladen werden"));
    }

    Initialisierung des Spiels:

    // Aufrufen des Set Ups
    function setup() {
      createCanvas(640, 480);        // Leinwand erstellen
      video = createCapture(VIDEO);  // Webcam-Stream holen
      video.hide();                  // Webcam-Video verstecken (wird später manuell gezeichnet)
    
      // Handpose-Modell starten
      handpose = ml5.handpose(video, () => {
        console.log("Handpose bereit");
      });
    
      // Wenn Handpose neue Vorhersagen liefert
      handpose.on("predict", results => {
        predictions = results;
      });
    
      // Textausrichtung und -größe setzen
      textAlign(CENTER, CENTER);
      textSize(32);
    }

    Spiel Logik pro Frame:

    • Es wird der Status "waiting" zu beginn definiert welcher bei dem Ereignis "Daumen hoch" zu "playing" wechselt

    // === Hauptzeichenschleife: wird jedes Frame aufgerufen ===
    function draw() {
      background(220); // Hellgrauer Hintergrund
    
      let timePassed = millis() - lastGestureTime; // Zeit seit letzter Geste
    
      // Wenn eine Hand erkannt wurde
      if (predictions.length > 0) {
        let landmarks = predictions[0].landmarks; // Fingerpunkte
    
        if (gameState === "waiting") {
          // Spiel wartet auf Daumen hoch
          if (detectThumbsUp(landmarks)) {
            resetGame();
            gameState = "playing";
            lastGestureTime = millis();
          }
    
        } else if (gameState === "playing") {
          // Countdown (3, 2, 1, Los!)
          let countdown = 3 - floor((millis() - lastGestureTime) / 1000);
          if (countdown > 0) {
            drawUI(countdown);
            return; // Frühzeitiger Abbruch, während Countdown läuft
          }
    
          // Geste erkennen nach Ablauf des Countdowns
          if (timePassed > gestureCooldown) {
            let gesture = detectGesture(landmarks);
            if (gesture !== "Unklar") {
              playerGesture = gestureEmojis[gesture];
              let comp = getRandomGesture();  // Computer wählt zufällig
              computerGesture = comp;
              resultText = compareGestures(gesture, comp);
    
              // Punkte vergeben
              if (resultText === "Du gewinnst!") playerScore++;
              else if (resultText === "Computer gewinnt!") computerScore++;
    
              round++;
              gameState = "showing_result";
              showResultStart = millis();
            }
          }
    
        } else if (gameState === "showing_result" && millis() - showResultStart > showResultDuration) {
          // Ergebnis zeigen, dann nächste Runde oder Spielende
          if (round >= 5) {
            gameState = "game_over";
          } else {
            gameState = "playing";
            lastGestureTime = millis();
          }
    
        } else if (gameState === "game_over") {
          // Nach Spielende: Warten auf Daumen hoch für Neustart
          if (detectThumbsUp(landmarks)) {
            resetGame();
            gameState = "playing";
            lastGestureTime = millis();
          }
        }
      }
    
      // Benutzeroberfläche zeichnen
      drawUI();
    }

    Gestenerkennung:

    // Erkenne, welche Geste gemacht wird (Stein, Papier, Schere)
    function detectGesture(landmarks) {
      let fingerTips = [8, 12, 16, 20]; // Indizes der Fingerkuppen
      let baseJoints = [6, 10, 14, 18]; // Indizes der Fingermittelgelenke
    
      let upFingers = 0; // Zählung der gestreckten Finger
      for (let i = 0; i < fingerTips.length; i++) {
        if (landmarks[fingerTips[i]][1] < landmarks[baseJoints[i]][1]) {
          upFingers++;
        }
      }
    
      if (upFingers === 0) return "Stein";   // Faust
      if (upFingers === 2) return "Schere";  // Zwei Finger
      if (upFingers >= 4) return "Papier";   // Hand offen
      return "Unklar";
    }

    Daumen Hoch zum Spielstart erkennen:

    // Erkennen, ob der Spieler einen "Daumen hoch" zeigt
    function detectThumbsUp(landmarks) {
      let thumbTip = landmarks[4];
      let thumbBase = landmarks[2];
    
      let fingersFolded = landmarks[8][1] > landmarks[6][1] &&
                          landmarks[12][1] > landmarks[10][1] &&
                          landmarks[16][1] > landmarks[14][1] &&
                          landmarks[20][1] > landmarks[18][1];
    
      let thumbUp = thumbTip[1] < thumbBase[1];
      return thumbUp && fingersFolded;
    }

    Zufällige Auswahl vom Computer:

    function getRandomGesture() {
      let options = ["Stein", "Papier", "Schere"];
      return random(options);
    }

    // Wähle zufällig eine Geste für den Computer function getRandomGesture() { let options = ["Stein", "Papier", "Schere"]; return random(options); }

    Vergleich der Gesten:

    // Vergleiche Spieler- und Computer-Gesten
    function compareGestures(player, computer) {
      if (player === computer) return "Unentschieden!";
      if (
        (player === "Stein" && computer === "Schere") ||
        (player === "Schere" && computer === "Papier") ||
        (player === "Papier" && computer === "Stein")
      ) {
        return "Du gewinnst!";
      }
      return "Computer gewinnt!";
    }

    Neustart vom Spiel:

    // Spielstände zurücksetzen
    function resetGame() {
      playerScore = 0;
      computerScore = 0;
      round = 0;
      playerGesture = "❓";
      computerGesture = "❓";
      resultText = "";
    }

    UI Darstellung:

    // Benutzeroberfläche zeichnen
    function drawUI(countdown = null) {
      fill(0);
      textSize(24);
    
      if (gameState === "waiting") {
        // Daumen-hoch Bild zeigen
        if (thumbImage) {
          imageMode(CENTER);
          image(thumbImage, width / 2, height / 2, 300, 300);
        }
    
      } else if (gameState === "playing") {
        text("Runde " + (round + 1) + " / 5", width / 2, 30);
        text("Punkte - Du: " + playerScore + " | Computer: " + computerScore, width / 2, 60);
    
        if (countdown !== null && countdown >= 0) {
          textSize(64);
          fill(50);
          text(countdown > 0 ? countdown : "Los!", width / 2, height / 2);
        }
    
      } else if (gameState === "showing_result") {
        // Computers Wahl als Bild zeigen
        if (compImages[computerGesture]) {
          image(compImages[computerGesture], width / 2 - 50, height / 2 - 50, 300, 300);
        }
        textSize(32);
        text(resultText, width / 2, height / 2 + 100);
    
      } else if (gameState === "game_over") {
        let finalText = playerScore > computerScore ? "Du gewinnst das Spiel!" :
                         playerScore < computerScore ? "Computer gewinnt!" :
                         "Unentschieden!";
        textSize(32);
        text(finalText, width / 2, height / 2 - 30);
        textSize(24);
        text("👍 um nochmal zu spielen", width / 2, height / 2 + 30);
      }

    Anzeige des Kamera Feeds unten:

    // Kleine Live-Kamera anzeigen
      drawSmallCamera();
    }
    
    // Webcam klein und gespiegelt anzeigen
    function drawSmallCamera() {
      push();
      translate(width, 0);
      scale(-1, 1); // Spiegeln
      image(video, width - 90, height - 70, 160, 120);
      pop();
    }

    Fazit

    Die Funktion des Posentrackings kann in einem größeren Projekt zum Beispiel auch Gebärdensprache erkennen und somit könnte man ebenfalls eine Lernanwendung programmieren die dem User Gebärden live beibringen kann.

    Das Schere-Stein-Papier-Spiel ist also nur ein kleiner Schritt in die Richtung und zeigt, wie einfach man mit Webtechnologien wie p5.js, ml5.js und ein wenig HTML spannende, interaktive Anwendungen umsetzen kann. Durch Handtracking mit der Webcam wird das klassische Spiel neu interpretiert – ganz ohne Mausklicks oder Tastatureingaben.

    Wir hoffen, dieses Projekt inspiriert euch dazu, selbst kreativ zu werden und eigene Ideen mit Kamera-Interaktion und JavaScript umzusetzen!


    Zum p5.js Projekt:

    https://editor.p5js.org/hollmi01/sketches/uEZzzuLgm

    Schlagworte


    © 0t1

    Cookies

    0t1 mag keine Kekse (und kein Tracking). Wir verwenden lediglich notwendige Cookies für essentielle Funktionen.

    Wir verwenden Schriftarten von Adobe Fonts. Dafür stellt dein Browser eine Verbindung zu den Servern von Adobe in den USA her. Wenn du unsere Seite nutzen möchtest, musst du dich damit einverstanden erklären.

    Weitere Informationen in unserer Datenschutzerklärung.