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!