Dynamisches Sandkörnerspiel
Ein dynamisches Sandkörnerspiel wird mit Hilfe einer JavaScript Bibliothek in p5.js erstellt werden. Dabei soll durch die Leertaste die Zeichenfarbe der Sandkörner geändert werden können und über die Pfeiltasten die Richtung, in die der Sand fällt.
Editor starten
Als erstes starten wir den p5-Editor, welchen du hier öffnen kannst: https://editor.p5js.org/. Dort wird dir folgender Code angezeigt:
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
}
In der Funktion setup()
wird deine Zeichenfläche erzeugt, auf welcher wir später unsere Sandkörner zeichnen und bewegen können. Hier werden Umgebungseigenschaften, wie Bildschirmgröße und Hintergrundfarbe definiert. Diese Funktion wird beim Programmstart einmalig aufgerufen.
Die Funktion draw()
hingegen wird wiederholt ausgeführt bis das Programm gestoppt wird. In unserem Fall soll über diese Funktion kontinuierlich die Positionierung der Sandkörner in der Welt und die Position des Mauszeigers geprüft werden. Dazu aber später mehr.
Globale Variablen
let n = 64; //number of the collumns and rows
let s; //size of the cells in px
let world; //two-dimensional array that contains the color information of each individual cell
let palette; //color palette
let col = 0; //index of the current color
let backCol; //background color of the cells
let xDirection = 0; //direction of motion (horizontal)
let yDirection = 0; //direction of motion (vertical)
Die in den Kommentaren beschriebenen Funktionenen/ Werte werden teilweise erst im späteren Verlauf des Programmes zugewiesen.
initParams()-Funktion
//creates the color palette and the world
function initParams() {
let palettes = [
["#751212", "#ba821e", "#a55200", "#a33d3d", "#ffd385", "#fcf4ae", "#ffec00"],
["#1a1a1a", "#4d4d4d", "#808080", "#b3b3b3", "#cccccc", "#f2f2f2", "#ffffff"],
["#0b1a1a", "#560719", "#a4161b", "#e5473b", "#b1a6a5", "#f5f3f4", "#ffffff"],
["#89b574", "#d4e56e", "#c5ffe6", "#a0d1f9", "#bdc5ff", "#ffff8d", "#c8faa5"],
["#ffc5fa", "#bdcfff", "#a0dfff", "#b636c1", "#667ded", "#aa16df", "#ea75d4"],
["#ffb3b3", "#ffc5fa", "#c5ffe6", "#bdc5ff", "#ffd386", "#d4e56e", "#fcf4ae"],
["#04162d", "#6b140e", "#8c0217", "#d00000", "#e76d03", "#fba418", "#feba17"],
["#132924", "#21461c", "#4e884d", "#90a955", "#ecf28e", "#edf0d5", "#f7f7fc"]
];
let c = round(random(0,7));
palette = palettes[c]; //randomly picks a color palette from the array palettes
shuffle(palette, true); //randomizes the order of the colors in the chosen palette
backCol = palette.pop();
world = [];
for (let i = 0; i < n; i++) {
let line = [];
for (let j = 0; j < n; j++) {
line.push(backCol);
}
world.push(line);
}
}
Hier wird die Farbpalette, die Hintergrundfabe und die Zeichenfläche initialisiert. Im Feld palettes
sind die verschiedenen Farbpaletten enthalten. Die Variable c
, welche zufällig generiert wird gibt die aus palettes
ausgewählte Farbpalette in die Variable palette
. Die shuffle()
-Funktion ordnet die Elemente im Feld palette
in einer zufälligen Reihenfolge an. Daraufhin wird in der Variable blackCol
die Farbinformation der Hintergrundfarbe gespeichert. Durch die Funktion pop()
wird dafür die letzte Farbe in der sortierten Palette verwendet. Zuletzt wird die Welt definiert. Bei der Welt handelt es sich um ein zweidimensionales Feld, welches durch die for
-Schleife mit der Hintergrundfarbe befüllt wird. Die Größe des Feldes world
wird dabei durch n
beschrieben.
setup()-Funktion
//creates canvas
function setup() {
createCanvas(500, 500);
noStroke();
s = width / n;
initParams();
}
Die Zeichenfläche soll 500 x 500 px groß sein. noStroke()
wird hinzugefügt, da sonst jede Zelle eine Umrandung hätte. s
beschreibt die Größe jeder Zelle in Pixel und kann erst definiert werden, wenn die Größe des Canvas definiert wurde, deshalb wird dies hier definiert und nicht bei den globale Variablen. Die initPrams()
-Funktion wird aufgerufen. Somit ist unsere Zeichenfläche vorbereitet, nun geht es ans Zeichnen.
oneStep()-Funktion
function oneStep() {
//if-loop goes through the world from the top right to bottom left and shifts the grains of sand down/left
if (yDirection === 1 || xDirection === -1) {
for (let i = n - 1; i > -1; i--) {
for (let j = 0; j < n; j++) {
let v = world[i][j];
if (v != backCol) {
let i_new = i + yDirection;
let j_new = j + xDirection;
if (
i_new < n &&
i_new > -1 &&
j_new < n &&
j_new > -1 &&
world[i_new][j_new] == backCol
) {
world[i][j] = backCol;
world[i_new][j_new] = v;
}
}
}
}
//else-loop goes through the world from the bottom left to top right and shifts the grains of sand up/right
} else {
// Richtung nach oben oder rechts
for (let i = 0; i < n; i++) {
for (let j = n - 1; j > -1; j--) {
let v = world[i][j];
if (v != backCol) {
let i_new = i + yDirection;
let j_new = j + xDirection;
if (
i_new < n &&
i_new > -1 &&
j_new < n &&
j_new > -1 &&
world[i_new][j_new] == backCol
) {
world[i][j] = backCol;
world[i_new][j_new] = v;
}
}
}
}
}
}
Zur einfacheren Erklärung dieser Funktion gehen wir davon aus, dass die Sandkörner von oben nach unten fallen sollen (yDirection === 1
). Allgemein gesagt geht diese Funktion nun die Welt von oben nach unten durch und verschiebt die gezeichneten Sandkörner, wenn möglich, nach unten. Sie überprüft jede einzelne Zelle auf ihre Farbinformation v
und gleicht diese mit der Hintergrundfarbe der Zeichenfläche backCol
ab. Stimmt die Farbe überein, so wird die Zählschleife weitergezählt und die nächste Zelle überprüft. Entspricht die Farbe allerdings nicht der Hintergrundfarbe, so wird die darunterliegende Zelle auf ihre Farbinformation geprüft. Entspricht diese ebenfalls nicht der Hintergrundfarbe kann das Sandkorn nicht weiter herunterfallen, weil es schon am „Boden“ angekommen ist. Ist die darunterliegende Zelle jedoch noch in der Hintergrundfarbe eingefärbt, so soll das Sandkorn nach unten fallen, bis es entweder auf die Begrenzung der Zeichenfläche oder ein anderes gezeichnetes Sandkorn fällt.
Im nächsten Schritt wird also die Farbinformation der oberen Zelle wieder auf die Hintergrundfarbe backCol
gesetzt. Die untere Zelle bekommt zu Farbinformation v
der oberen Zelle zugewiesen. Die if
-Schleife geht die Welt von oben rechts nach unten links durch und verschiebt die Sandkörner nach unten/ links, wenn möglich. Die else
-Schleife hingegen geht die Welt von unten links nach oben rechts durch und verschiebt die Sandkörner nach oben/ rechts, wenn möglich.
drawWorld()-Funktion
//draws the individual cells in the world
function drawWorld() {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
fill(world[i][j]);
square(j * s, i * s, s);
}
}
}
Bisher weisen wir durch die oneStep()
-Funktion jeder Zelle wiederholt ihre Farbinformationen zu. Diese ist jedoch noch nicht auf der Zeichenfläche sichtbar. Hier kommt die drawWorld()
-Funktion ins Spiel. Diese zeichnet die einzelnen Zellen in der Welt und geht die Welt von oben links nach unten rechts durch und zeichnet n^2 viele Quadrate auf der Zeichenfläche. Dabei betrachtet das Programm jede Zelle einzeln, überprüft sie auf ihre Farbinformation und setzt in dieser Farbe ein Quadrat.
draw()-Funktion
function draw() {
oneStep();
drawWorld();
if ( //checks if the mouse pointer is on the drawing area
mouseX > -1 &&
mouseY > -1 &&
mouseX < width &&
mouseY < height &&
!keyIsPressed
) {
let i = floor((n * mouseY) / height);
let j = floor((n * mouseX) / width);
//by a mouse click the color of the cell, that was clicked on changed
if (mouseIsPressed) world[i][j] = palette[col % palette.length];
//visual indicator of the mouse position on the canvas
fill(palette[col % palette.length]);
square(j * s, i * s, s);
}
}
Diese Funktion ruft die oneStep()
-Funktion auf und gibt jeder Zelle ihre Farbinformation. Sie ruft die draw-World()
-Funktion auf und zeichnet jede Zelle mit der in oneStep()
zugewiesenen Farbfunktion. Anschließend wird überprüft, ob sich die Maus auf dem Canvas befindet. Die Bedingung !keyIsPressed
verhindert, dass während Farb- oder Richtungsänderungen gezeichnet werden kann. Die Zeichenfunktion ist somit erst nach Abschluss dieser Änderungen wieder verfügbar. Somit ist es zu jeder Zeit eindeutig, mit welcher Farbe und in welche Richtung gezeichnet werden soll. Anschließend wird die genaue Position der Maus auf der Zeichenfläche ermittelt. i
bestimmt den Index der Zeile, und j
den Index der Spalte in der sich die Maus befindet. Durch einen Mausklick wird die Farbe der Zelle auf die geklickt wurde geändert. Um einen visuellen Indikator für die Mausposition auf dem Canvas zu erhalten wird mit square()
ein temporärers Quadrat an der aktuellen Mausposition mit der aktuellen Füllfarbe, welche durch fill()
gesetzt wird, gezeichnet. Dieser Indikator ist wichtig, um zum einen die Orientierung auf dem Canvas zu gewährleisten und zum anderen die Funktionsweise der Anwendung verständlich zu visualisieren.
keyPressed()-Funktion
function keyPressed() {
if (key == " ") { //changes the drawing color
col++;
}
if (keyCode == DELETE) {
n = round(random(10,60)); //randomizes the number of the collumns and rows
setup();
}
if (keyCode == UP_ARROW) { //grains move upwards
yDirection = -1;
xDirection = 0;
}
if (keyCode == DOWN_ARROW) { //grains move downwards
yDirection = 1;
xDirection = 0;
}
if (keyCode == LEFT_ARROW) { //grains move left
yDirection = 0;
xDirection = -1;
}
if (keyCode == RIGHT_ARROW) { //grains move right
yDirection = 0;
xDirection = 1;
}
if(key === "s"){ //saves the canvas as a jpg
save("myCanvas.jpg");
}
}
Diese Funktion macht die Anwendung interaktiv und flexibel. Die Leertaste zählt col
, also den Index der aktuellen Farbpalette hoch und wechselt somit die Zeichenfarbe. DELETE
gibt n
einen zufälligen Wert und ruft die setup()
-Funktion auf. Dadurch wird ein neuer Canvas erstellt mit einem neuen Wert für die Variable n
. Da in der setup()
-Funktion die initParams()
-Funktion enthalten ist wird auch die Farbpalette neu ausgewählt. Die letzten vier Schleifen ändern jeweils die Bewegungsrichtung der Sandkörner. Je nachdem welche Werte xDirection
und yDirection
besitzen fallen die Sandkörner durch die oneStep()
-Funktion in die gwünschte Richtung. Anschließend kann man die Zeichenfläche als jpg-Datei speichern.
Zusatztipp
Setzt du bei den globalen Variablen sowohl xDirection
als auch yDirection
auf 0 kannst du zu Beginn die Sandkörner frei auf der Zeichenfläche platzieren und anschließend mit Drücken der Pfeiltasten in sich zusammenfallen lassen. So kannst du noch interessantere Bilder kreieren und es ist auch sehr spannend anzusehen.
Quellcode
Hier findest du unseren Quellcode und unsere Inspiration:
https://editor.p5js.org/schindler/sketches/fcFB9p2Nu
https://openprocessing.org/sketch/1442701
Viel Spaß beim Zeichnen!