Linienbewegung mit Arduino
Kennst du noch das Spiel Snake? Wusstest du, dass man dieses Spiel ganz einfach selbst programmieren kann – und statt der Pfeiltasten sogar einen Arduino Uno zur Steuerung nutzen kann?
In diesem Projekt zeigen wir dir eine einfachere Version, bei der du mit vier analogen Tastern eine Linie steuerst.
Hardware Aufbau

Für unseren Aufbau brauchen wir:
einen Arduino Uno
4 Taster
5 Widerstände
5 Verbindungskabel
Arduino-Code
// Wird einmal beim Starten des Mikrocontrollers ausgeführt
void setup() {
Serial.begin(9600); // Startet die serielle Kommunikation mit 9600 Baud
}
// Wird in einer Endlosschleife wiederholt
void loop() {
int value = analogRead(A0); // Liest den analogen Wert vom Pin A0
// Überprüft den Bereich des gelesenen Wertes und gibt entsprechende Richtung aus
if (value > 135 && value < 200) {
Serial.println("U"); // Wenn der Wert zwischen 135 und 200 liegt → "Up"
delay(200); // Kurze Pause, um wiederholte Ausgaben zu vermeiden
} else if (value > 200 && value < 400) {
Serial.println("D"); // Wenn der Wert zwischen 200 und 400 liegt → "Down"
delay(200);
} else if (value > 400 && value < 650) {
Serial.println("L"); // Wenn der Wert zwischen 400 und 650 liegt → "Left"
delay(200);
} else if (value > 650 && value < 1030) {
Serial.println("R"); // Wenn der Wert zwischen 650 und 1030 liegt → "Right"
delay(200);
}
}
Aufbau
Die sind vier Taster auf dem Arduino, arbeiten jeweils mit einem individuellen Widerstand in einem Spannungsteiler. Wenn ein Taster gedrückt wird, ergibt sich je nach verwendetem Widerstand eine bestimmte Spannung am analogen Pin A0. Diese Spannung wird mit analogRead(A0)
gemessen und ergibt einen Wert zwischen 0 und 1023.
Jeder Taster erzeugt dabei einen anderen Messwertbereich:
Taster 1 (Up) → ca. 135–200
Taster 2 (Down) → ca. 200–400
Taster 3 (Left) → ca. 400–650
Taster 4 (Right) → ca. 650–1030
Verbindung zu p5.js
let serial; // Serialport-Objekt für die Kommunikation mit dem Arduino
let latestData = ""; // Variable zum Speichern der letzten seriellen Eingabe
function setup() {
createCanvas(800, 500); // Zeichenfläche erstellen
colorMode(HSB, 360, 100, 100); // Farbmodus auf HSB setzen
frameRate(60); // 60 Frames pro Sekunde
// Serielle Kommunikation vorbereiten
serial = new p5.SerialPort();
serial.open("COM3"); // Den richtigen Port auswählen (bei Mac z. B. /dev/tty.usbmodemXXXX)
serial.on("data", serialEvent); // Funktion aufrufen, wenn serielle Daten empfangen werden
// Snake mittig platzieren
let startX = width / 2;
let startY = height / 2;
// Richtung auf Mitte ausrichten (am Anfang neutral)
let toCenter = createVector(width / 2 - startX, height / 2 - startY);
direction = toCenter.normalize();
// Snake initial mit 20 Segmenten füllen
for (let i = 0; i < 20; i++) {
snake.push(createVector(
startX - i * segmentSpacing * direction.x,
startY - i * segmentSpacing * direction.y
));
}
}
1. Initialisierung und Setup
Zu Beginn wird mit createCanvas(800, 500)
eine Zeichenfläche definiert, auf der die Snake grafisch dargestellt wird. Der Farbmodus wird auf HSB (Farbton, Sättigung, Helligkeit) gesetzt, um spätere Farbverläufe intuitiver steuern zu können. Die Bildwiederholrate wird auf 60 Bilder pro Sekunde (frameRate(60)
) festgelegt, um eine flüssige Animation zu ermöglichen.
2. Serielle Kommunikation
Das Objekt serial
vom Typ p5.SerialPort
dient als Schnittstelle zur seriellen Kommunikation mit dem Arduino. Mit serial.open("COM3")
wird der entsprechende Port geöffnet (unter Windows typischerweise „COM3“, unter macOS z. B. /dev/tty.usbmodemXXXX
). Durch serial.on("data", serialEvent)
wird sichergestellt, dass bei jedem Eintreffen neuer Daten die Funktion serialEvent()
ausgelöst wird – diese verarbeitet die Richtungseingaben des Arduino.
3. Snake-Initialisierung
Zur Positionierung der Snake wird ein Startpunkt in der Mitte der Zeichenfläche definiert. Ein Vektor (toCenter
) wird berechnet, der theoretisch die Ausrichtung zur Mitte anzeigen würde – in der Praxis ergibt sich hier ein Nullvektor, da der Startpunkt bereits zentriert ist. Dennoch wird dieser Vektor normalisiert (normalize()
), um ihn als initiale Bewegungsrichtung zu speichern.
Die Schlange selbst besteht aus einem Array von Vektoren (snake
), die jeweils die Position eines Segments repräsentieren. Die Schleife erzeugt 20 Segmente mit konstantem Abstand zueinander (segmentSpacing
) entlang der aktuellen Bewegungsrichtung.
Das Linien-Spiel in p5.js
let snake = []; // Array, das die Positionen aller Snake-Segmente speichert
let direction; // Richtungsvektor für die Bewegung der Snake
let segmentSpacing = 10; // Abstand zwischen den Segmenten der Snake
let moveSpeed = 4; // Bewegungsgeschwindigkeit (Pixel pro Schritt)
let moveInterval = 100; // Zeitintervall in Millisekunden zwischen Bewegungen
let lastMoveTime = 0; // Zeitpunkt der letzten Bewegung
let stopped = false; // Zustand: true, wenn die Snake gestoppt ist (z. B. bei Kollision)
let gameStarted = false; // Status, ob das Spiel bereits gestartet wurde
function setup() {
// Setup erfolgt wie im vorherigen Abschnitt (Canvas, Serial, Snake-Initialisierung etc.)
}
function draw() {
if (!gameStarted) {
showStartScreen(); // Startbildschirm anzeigen, wenn Spiel noch nicht gestartet ist
} else {
background(0); // Spielfeld-Hintergrund (schwarz)
// Snake nur bewegen, wenn sie nicht gestoppt ist und genug Zeit seit dem letzten Schritt vergangen ist
if (!stopped && millis() - lastMoveTime > moveInterval) {
moveSnake(); // Bewegung ausführen
lastMoveTime = millis(); // Zeitstempel aktualisieren
}
drawSnake(); // Snake zeichnen
}
}
function moveSnake() {
// Neues Segment (Kopie des letzten Elements) in Bewegungsrichtung hinzufügen
let head = snake[snake.length - 1].copy();
head.add(p5.Vector.mult(direction, moveSpeed));
// Spielfeldbegrenzung prüfen – wenn Snake den Rand berührt, Spiel stoppen
if (head.x < 0 || head.x > width || head.y < 0 || head.y > height) {
stopped = true;
return;
}
snake.push(head); // Neues Segment hinzufügen
// Maximale Länge der Snake festlegen – alte Segmente werden entfernt
while (snake.length > 20) {
snake.shift(); // Erstes Segment löschen (Schwanz kürzen)
}
}
function drawSnake() {
noStroke(); // Keine Kontur um die Kreise
// Jedes Segment zeichnen
for (let i = 0; i < snake.length; i++) {
let pos = snake[i]; // Position des aktuellen Segments
let hueVal = map(i, 0, snake.length, 240, 0); // Farbe abhängig von der Position im Körper (Farbverlauf)
fill(hueVal, 100, 100); // HSB-Farbfüllung
ellipse(pos.x, pos.y, 14, 14); // Segment als Kreis zeichnen
}
}
function showStartScreen() {
background(255); // Weißer Hintergrund für Startbildschirm
fill(0); // Schwarzer Text
textSize(32); // Textgröße setzen
textAlign(CENTER, CENTER); // Text zentrieren
text('Pfeiltaste drücken', width / 2, height / 2); // Aufforderung zum Start
}
1. Spielstruktur und Zustandsverwaltung
Das Spiel unterscheidet zwei Hauptzustände: den Startbildschirm (!gameStarted
) und den aktiven Spielmodus. Erst nach einer Benutzereingabe wird das Spiel gestartet. Die Variable stopped
dient der Zustandsverwaltung für das Spielende, z. B. wenn die Spielfigur den Rand der Zeichenfläche erreicht.
2. Bewegungslogik und Timing
Die Schlange besteht aus einem Array von Positionsvektoren (snake
), die die einzelnen Segmente repräsentieren. Die Bewegung erfolgt in Intervallen, die durch moveInterval
(in Millisekunden) und lastMoveTime
gesteuert werden. So entsteht eine zeitlich getaktete Bewegung, unabhängig von der Bildwiederholrate.
Durch das Kopieren der aktuellen Kopfposition und das Fortschreiten in Richtung eines normierten Richtungsvektors (direction
) wird ein neuer Punkt an das Ende der Snake angehängt. Um die Länge der Schlange konstant zu halten, wird bei Überschreitung einer maximalen Segmentanzahl das älteste Segment entfernt.
3. Grafische Darstellung mit HSB-Farben
Die visuelle Darstellung der Snake erfolgt segmentweise in Form farbiger Kreise. Dabei wird der Farbton dynamisch über map()
berechnet, was einen fließenden Verlauf von Blau zu Rot entlang der Schlange erzeugt. Der Farbmodus HSB
(Hue, Saturation, Brightness) ermöglicht eine intuitive Kontrolle über diesen Farbverlauf.
4. Startbildschirm
Vor dem Spielstart wird ein zentrierter Text angezeigt, der den Nutzer zur Interaktion auffordert. Dies wird durch eine einfache Bedingung (if (!gameStarted)
) und die Funktion showStartScreen()
realisiert.
Verarbeitung der seriellen Daten
function serialEvent() {
// Funktion wird aufgerufen, sobald neue serielle Daten vom Arduino empfangen werden
let inString = serial.readLine(); // Liest eine Zeile als String von der seriellen Schnittstelle
if (inString.length > 0) {
latestData = inString.trim(); // Entfernt eventuell vorhandene Leerzeichen/Zeilenumbrüche
handleDirectionInput(latestData); // Übergibt die Eingabe zur Weiterverarbeitung an die Steuerlogik
}
}
function handleDirectionInput(input) {
// Wenn das Spiel noch nicht gestartet wurde, initialisiere die Snake und Spielvariablen
if (!gameStarted) {
gameStarted = true;
stopped = false;
snake = [];
// Startpunkt in der Mitte des Canvas
let startX = width / 2;
let startY = height / 2;
// Richtung zur Mitte (hier 0,0 da bereits zentriert)
let toCenter = createVector(width / 2 - startX, height / 2 - startY);
direction = toCenter.normalize(); // Normalisierter Richtungsvektor (initial 0)
// Erzeugt eine Snake mit 20 Segmenten entlang der Start-Richtung
for (let i = 0; i < 20; i++) {
snake.push(createVector(
startX - i * segmentSpacing * direction.x,
startY - i * segmentSpacing * direction.y
));
}
} else {
// Spiel läuft bereits – Eingabe wird als Richtungsänderung interpretiert
let newDir;
// Eingabe "L" oder linke Pfeiltaste → nach links, sofern aktuelle Richtung nicht rechts ist
if (input === LEFT_ARROW || input === "L") {
if (direction.x !== 1) newDir = createVector(-1, 0);
}
// Eingabe "R" oder rechte Pfeiltaste → nach rechts, sofern aktuelle Richtung nicht links ist
else if (input === RIGHT_ARROW || input === "R") {
if (direction.x !== -1) newDir = createVector(1, 0);
}
// Eingabe "U" oder obere Pfeiltaste → nach oben, sofern aktuelle Richtung nicht unten ist
else if (input === UP_ARROW || input === "U") {
if (direction.y !== 1) newDir = createVector(0, -1);
}
// Eingabe "D" oder untere Pfeiltaste → nach unten, sofern aktuelle Richtung nicht oben ist
else if (input === DOWN_ARROW || input === "D") {
if (direction.y !== -1) newDir = createVector(0, 1);
}
// Wenn eine gültige neue Richtung erkannt wurde, wird diese übernommen
if (newDir) {
direction = newDir;
stopped = false; // Falls das Spiel gestoppt war, z. B. nach Kollision, wird es fortgesetzt
}
}
}
1. Empfang und Bereinigung der seriellen Eingabe
Die Funktion serialEvent()
wird automatisch aufgerufen, sobald eine neue Zeile Daten vom Arduino empfangen wird. Die Zeile wird mittels serial.readLine()
ausgelesen und durch trim()
von überflüssigen Leerzeichen und Steuerzeichen bereinigt. Dies stellt sicher, dass nur gültige Zeichen wie "L"
oder "R"
weiterverarbeitet werden.
2. Initialisierung bei Spielstart
Falls das Spiel noch nicht gestartet wurde (!gameStarted
), wird in handleDirectionInput()
das Spielfeld vorbereitet:
Die Snake wird zentriert im Canvas erzeugt.
Die Bewegungsrichtung wird normiert gesetzt.
Die Snake besteht zu Beginn aus 20 Segmenten, die in der Start-Richtung linear angeordnet werden.
Dieser Initialisierungsschritt sorgt für einen definierten Ausgangszustand und verhindert unvorhersehbares Verhalten beim ersten Tastendruck.
3. Interpretation der Eingaben als Richtungsbefehle
Ist das Spiel aktiv, interpretiert die Funktion handleDirectionInput()
die Eingabezeichen als Bewegungsrichtungen:
"L"
bzw.LEFT_ARROW
→ Bewegung nach links (sofern nicht bereits nach rechts)"R"
bzw.RIGHT_ARROW
→ Bewegung nach rechts (sofern nicht bereits nach links)"U"
bzw.UP_ARROW
→ Bewegung nach oben (sofern nicht bereits nach unten)"D"
bzw.DOWN_ARROW
→ Bewegung nach unten (sofern nicht bereits nach oben)
Diese Prüfungen verhindern, dass sich die Snake direkt in die entgegengesetzte Richtung bewegt – ein zentrales Prinzip des klassischen Snake-Spiels.
4. Dynamische Richtungsänderung
Wenn eine zulässige Richtungsänderung erkannt wird, wird der globale Richtungsvektor aktualisiert. Außerdem wird stopped
auf false
gesetzt, sodass das Spiel nach einem eventuellen Stopp (z. B. nach Kollision) fortgeführt werden kann.
Ergebnis
Als Ergebnis haben wir eine Snake-ähnliche-Linie erstellt, die sich anhand des Arduinos steuern lässt. Die Bewegungsrichtung wird dabei direkt über die analogen Buttons gesteuert, was eine interessante Kombination zwischen Hardware und Software darstellt.

Quellenangaben
Hier kommst du direkt zum Code, im Webeditor p5.js.
Unsere Inspiration haben wir von DIY Life und p5.js.
Wir haben mit den Programmen Arduino IDE, p5.serialcontrol und p5.js gearbeitet.
Wir wünschen euch viel Spaß beim experimentieren und testen von dieser Anwendung:)