Ein Feuerwerk besteht aus tausenden Funken, und jeder einzelne folgt denselben einfachen Regeln: Er fliegt, die Schwerkraft zieht ihn nach unten, und irgendwann verglimmt er. In diesem Projekt programmierst du diese Regeln in C mit PixelRAM, vom ersten Funken bis zu Raketen, die du mit der Maus in den Himmel schießt.

Dieses Projekt baut auf Echtzeitgrafik mit C auf. Von dort kennst du die Vorlage, make PROGRAM=…, die Vorschau und die Schleife mit present(). Arbeite in einer eigenen Datei feuerwerk.c neben main.c und übersetze sie mit:
make PROGRAM=feuerwerk
Fehlt dir die Vorlage noch, klone sie wie dort beschrieben:
Stelle zuerst sicher, dass du keinen Ordner geöffnet hast. Um sicherzugehen, drücke einfach den Shortcut für »Ordner schließen«: StrgK und dann F. Dein Workspace sollte jetzt ungefähr so aussehen:

Klicke auf »Clone Repository« und füge die Adresse der Vorlage ein:
https://github.com/specht/pixelram-starter.git

Bestätige mit Enter. Danach wählst du das Verzeichnis, in dem das Repository gespeichert werden soll: den Ordner /workspace.

Das Repository wird jetzt in einen Unterordner von /workspace geklont, der so heißt wie das Repository. Beantworte die Frage, ob es jetzt geöffnet werden soll, mit »Open«:

Ein Funke hat eine Position x, y und eine Geschwindigkeit vx, vy: wie viele Pixel er pro Sekunde nach rechts und nach unten fliegt. Ein negatives vy heißt: nach oben. Beides fasst ein struct zusammen.
In jedem Bild fragt das Programm, wie viel Zeit seit dem letzten Bild vergangen ist, dt, meistens etwa eine Sechzigstelsekunde. Dann gelten zwei Regeln:
vy += GRAVITY * dt. Jede Sekunde fällt der Funke 40 Pixel pro Sekunde schneller.x += vx * dt und y += vy * dt.Mehr Physik braucht ein Feuerwerk nicht. Hier ein Funke, der immer wieder von links unten schräg nach oben geworfen wird:
#include "pixelram.h" #include <stdlib.h> #define W 320 #define H 180 #define GRAVITY 40.0 typedef struct { double x, y; // Position in Pixeln double vx, vy; // Geschwindigkeit in Pixeln pro Sekunde } Particle; double random_between(double low, double high) { return low + (high - low) * rand() / (double)RAND_MAX; } void throw_spark(Particle *p) { p->x = 20; p->y = H - 1; p->vx = random_between(20, 60); p->vy = random_between(-110, -60); } int main(void) { screen_open(W, H, pixel_indexed8, "Ein Funke"); use_palette("sweetie_16"); Particle spark; throw_spark(&spark); double last = seconds(); while (!should_close()) { double now = seconds(); double dt = now - last; // Sekunden seit dem letzten Bild last = now; spark.vy += GRAVITY * dt; // die Schwerkraft zieht nach unten spark.x += spark.vx * dt; spark.y += spark.vy * dt; if (spark.x >= W || spark.y >= H) throw_spark(&spark); set_pixel((int)spark.x, (int)spark.y, 4); present(); } screen_close(); return 0; }
random_between liefert eine zufällige Kommazahl zwischen zwei Grenzen: rand() gibt eine ganze Zahl von 0 bis RAND_MAX zurück, geteilt durch RAND_MAX liegt sie zwischen 0 und 1. Weil das Programm den Bildschirm nie löscht, bleibt jede Flugbahn stehen.

Übersetze das Programm mit make PROGRAM=feuerwerk und öffne feuerwerk.html in der Vorschau:

Jede Bahn ist eine Parabel, wie beim Wurf eines Balls. Das Programm rechnet sie nirgends aus: Sie entsteht von selbst aus den zwei Regeln.
Schreib das Programm mit dem Funken in feuerwerk.c und übersetze es mit
make PROGRAM=feuerwerk.
Ein Funke startet senkrecht nach oben mit vy = -100, also 100 Pixel pro
Sekunde, und GRAVITY ist 40. Wie viele Pixel steigt er, bevor er
umkehrt? Rechne erst, dann probier es aus: Setze im Programm vx = 0 und
vy = -100.
Er wird jede Sekunde um 40 Pixel pro Sekunde langsamer. Wie lange dauert es bis zum Stillstand, und wie schnell war er im Mittel?
dt, statt den Funken in jedem Bild um einen festen Wert zu bewegen? Was würde sonst auf einem Computer passieren, der nur 30 Bilder pro Sekunde schafft?
Für ein Feuerwerk braucht es nicht einen Funken, sondern viele. Sie stehen in einem Array particles, und count zählt, wie viele davon gerade leben. Eine Explosion wirft 100 Funken auf einmal von derselben Stelle aus, jeden in eine zufällige Richtung. Die Richtung ist ein Winkel zwischen 0 und 2π, und mit cos und sin wird daraus der Anteil nach rechts und nach unten:
double angle = random_between(0, 2 * PI); double speed = random_between(30, 50); add(x, y, cos(angle) * speed, sin(angle) * speed, random_between(1.5, 2.5));
Alle Funken sind etwa gleich schnell. Deshalb liegen sie nach einem Moment auf einem Kreis um die Explosion, wie die Sterne eines echten Feuerwerkskörpers, und nicht in einer vollen Scheibe.
Jeder Funke bekommt eine Lebenszeit life in Sekunden, die in jedem Bild um dt kleiner wird. Ist sie abgelaufen, muss der Funke aus dem Array verschwinden. Statt alle folgenden Funken eine Stelle nach vorne zu schieben, rückt einfach der letzte Funke in die Lücke. Die Reihenfolge ist egal, und es geht schnell, egal wie viele Funken es gibt:
particles[i] = particles[count - 1]; count--; i--; // den nachgerückten Funken auch noch bewegen
In particles leben 10 Funken, an den Stellen 0 bis 9. Der Funke an
Stelle 3 verlischt. Was steht danach an Stelle 3?
Jetzt löscht das Programm den Bildschirm in jedem Bild mit clear(0) und zeichnet alle Funken neu, sonst sähe man nur noch Streifen. Hier das ganze Programm:
#include "pixelram.h" #include <math.h> #include <stdlib.h> #define W 320 #define H 180 #define GRAVITY 40.0 #define PI 3.14159265358979 #define MAX_PARTICLES 2000 typedef struct { double x, y; // Position in Pixeln double vx, vy; // Geschwindigkeit in Pixeln pro Sekunde double life; // verbleibende Lebenszeit in Sekunden } Particle; Particle particles[MAX_PARTICLES]; int count = 0; double random_between(double low, double high) { return low + (high - low) * rand() / (double)RAND_MAX; } void add(double x, double y, double vx, double vy, double life) { if (count == MAX_PARTICLES) return; particles[count] = (Particle){ x, y, vx, vy, life }; count++; } void explode(double x, double y) { for (int i = 0; i < 100; i++) { double angle = random_between(0, 2 * PI); double speed = random_between(30, 50); add(x, y, cos(angle) * speed, sin(angle) * speed, random_between(1.5, 2.5)); } } int main(void) { screen_open(W, H, pixel_indexed8, "Explosion"); use_palette("sweetie_16"); double last = seconds(); double next_explosion = 0; while (!should_close()) { double now = seconds(); double dt = now - last; last = now; if (now > next_explosion) { explode(random_between(60, W - 60), random_between(30, 90)); next_explosion = now + 1.0; } clear(0); for (int i = 0; i < count; i++) { Particle *p = &particles[i]; p->vy += GRAVITY * dt; p->x += p->vx * dt; p->y += p->vy * dt; p->life -= dt; if (p->life <= 0) { // verloschen: der letzte Funke rückt an seinen Platz particles[i] = particles[count - 1]; count--; i--; continue; } set_pixel((int)p->x, (int)p->y, 12); } present(); } screen_close(); return 0; }
(Particle){ x, y, vx, vy, life } baut einen ganzen Funken auf einmal, die Werte in der Reihenfolge der Felder im struct. M_PI gibt es in strengem C nicht, deshalb steht π als PI oben im Programm.

Jede Sekunde explodiert es an einer neuen Stelle. Die Funken fliegen als Kreis auseinander und sinken dabei nach unten.
Ein Funke soll hell aufleuchten und dann langsam dunkler werden. Dafür baut sich das Programm eine eigene Palette aus vier Farbverläufen zu je 16 Farben: Rot auf den Nummern 0 bis 15, Grün auf 16 bis 31, Blau auf 32 bis 47 und Gold auf 48 bis 63. Innerhalb eines Verlaufs ist 0 das Dunkelblau des Nachthimmels und 15 fast weiß. Die Farbe eines Funkens ist dann 16 * ramp + brightness, und brightness sinkt mit der Lebenszeit:
// Vier Farbverläufe zu je 16 Farben: 16 * ramp + Helligkeit void make_palette(void) { const int sky[3] = {10, 12, 35}; const int colors[4][3] = { {255, 80, 60}, {80, 255, 120}, {90, 150, 255}, {255, 210, 80} }; for (int ramp = 0; ramp < 4; ramp++) for (int b = 0; b < 16; b++) { double f = b / 15.0; // vom Nachthimmel zur Farbe, ganz hell fast weiß double white = f > 0.8 ? (f - 0.8) * 5 : 0; int rgb[3]; for (int i = 0; i < 3; i++) rgb[i] = (sky[i] + (colors[ramp][i] - sky[i]) * f) * (1 - white) + 255 * white; set_palette(16 * ramp + b, rgb[0], rgb[1], rgb[2]); } }
Nummer 0, der Himmel, ist damit Dunkelblau, und der ganze Bildschirm beginnt in dieser Farbe: PixelRAM füllt ihn am Anfang mit 0.
Echte Funken ziehen einen Schweif hinter sich her, weil das Auge ein Leuchten noch kurz sieht, wenn es schon weg ist. Das lässt sich mit dem Bildschirm selbst nachbauen: Statt ihn mit clear(0) zu löschen, wird jedes Pixel nur ein wenig dunkler, eine Stufe in seinem Farbverlauf. Ein Funke, der weiterfliegt, lässt so eine Spur zurück, die von selbst verglüht:
// Nachleuchten: jedes Pixel wird ein wenig dunkler void fade(void) { for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { int c = get_pixel(x, y); if (c % 16 > 0) set_pixel(x, y, c - 1); } }
c % 16 ist die Helligkeit innerhalb des Verlaufs. Ist sie schon 0, bleibt das Pixel schwarz, sonst rutscht es eine Farbe tiefer, im selben Verlauf.
Bis jetzt explodieren die Funken einfach irgendwo in der Luft. Ein Feuerwerk schießt erst eine Rakete hoch, und sie explodiert am höchsten Punkt, wenn sie umkehrt, also sobald vy nicht mehr negativ ist. Eine Rakete ist auch nur ein Partikel, mit einem Merkmal rocket mehr.
Wie schnell muss die Rakete starten, damit sie in einer bestimmten Höhe umkehrt? Aus der Physik: Wer mit der Geschwindigkeit v nach oben startet, steigt bei der Schwerkraft g genau h = v² / 2g hoch. Umgekehrt ist v = √(2gh):
void launch(double x, double target_y) { // so schnell, dass die Rakete genau bei target_y umkehrt double vy = -sqrt(2 * GRAVITY * (H - target_y)); add(x, H - 1, random_between(-8, 8), vy, 10, rand() % 4, true); }
Eine Rakete soll 160 Pixel hoch steigen, bei GRAVITY 40. Mit wie
vielen Pixeln pro Sekunde muss sie starten? Gib die Zahl ohne
Vorzeichen an.
Die Formel steht in launch. Was ist dort H - target_y?
Explodiert eine Rakete, bekommen ihre Funken ihren Farbverlauf und ihre Geschwindigkeit mit, sie fliegen also mit der Rakete weiter. Und weil Luft die Funken bremst, verlieren sie in jeder Sekunde einen Teil ihres Tempos:
p->vx -= p->vx * 1.5 * dt; p->vy -= p->vy * 1.5 * dt;
Unten im Bild steht eine Stadt, vor der das Feuerwerk aufsteigt. Sie bekommt vier eigene Farben hinter den Farbverläufen: 64 für die Häuser, 65 bis 67 für Fenster in drei Lichtfarben. Die Stadt wird einmal am Anfang ausgedacht: Häuser mit zufälliger Breite und Höhe, nebeneinander von links nach rechts, jedes mit eigenem Fensterabstand und einem eigenen Anteil heller Fenster. Sie steht dann im zweidimensionalen Array city, eine Zahl für jedes Pixel, 0 für Himmel. In jedem Bild malt draw_skyline sie nach den Funken. So steigen die Raketen hinter den Häusern auf.
// Häuser mit zufälliger Breite und Höhe, von links nach rechts. Jedes Haus // hat seinen eigenen Fensterabstand, und in jedem leuchtet ein anderer Teil // der Fenster. Die Stadt wird einmal ausgedacht und in city gemalt. void make_skyline(void) { set_palette(HOUSE, 0, 0, 0); set_palette(WINDOW, 255, 210, 120); // Glühbirne set_palette(WINDOW + 1, 255, 150, 70); // warmes Licht set_palette(WINDOW + 2, 140, 180, 255); // Fernseher int x = 0; while (x < W) { int width = 10 + rand() % 20; int top = H - 12 - rand() % 30; int step = 2 + rand() % 2; // Fensterabstand double lit = random_between(0.05, 0.4); // Anteil der hellen Fenster for (int i = x; i < x + width && i < W; i++) for (int y = top; y < H; y++) city[y][i] = HOUSE; for (int y = top + 2; y < H - 1; y += 3) for (int i = x + 2; i < x + width - 1 && i < W; i += step) if (random_between(0, 1) < lit) city[y][i] = WINDOW + rand() % 3; x += width; } } // die Stadt vor den Himmel malen void draw_skyline(void) { for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) if (city[y][x] != 0) set_pixel(x, y, city[y][x]); }
Eine Kleinigkeit fehlt noch: Kommen zwei Raketen nacheinander auf derselben Seite hoch, drängt sich das Feuerwerk an einer Stelle. Deshalb startet die nächste Rakete abwechselnd in der linken und der rechten Hälfte.
Mit der Maus kannst du selbst Raketen starten: mouse_button_pressed meldet einen Klick, mouse_position sagt, wohin. Die Rakete fliegt genau bis zu dieser Höhe. Hier das ganze Programm:
#include "pixelram.h" #include <math.h> #include <stdlib.h> #define W 320 #define H 180 #define MAX_PARTICLES 3000 #define GRAVITY 40.0 #define PI 3.14159265358979 #define HOUSE 64 // Farbe der Häuser #define WINDOW 65 // 65 bis 67: drei Farben für Fenster typedef struct { double x, y; // Position in Pixeln double vx, vy; // Geschwindigkeit in Pixeln pro Sekunde double life; // verbleibende Lebenszeit in Sekunden int ramp; // Farbverlauf 0 bis 3 bool rocket; // eine Rakete explodiert, wenn sie umkehrt } Particle; Particle particles[MAX_PARTICLES]; int count = 0; uint8_t city[H][W]; // die Stadt: 0 ist Himmel, sonst eine Farbe double random_between(double low, double high) { return low + (high - low) * rand() / (double)RAND_MAX; } void add(double x, double y, double vx, double vy, double life, int ramp, bool rocket) { if (count == MAX_PARTICLES) return; particles[count] = (Particle){ x, y, vx, vy, life, ramp, rocket }; count++; } void launch(double x, double target_y) { // so schnell, dass die Rakete genau bei target_y umkehrt double vy = -sqrt(2 * GRAVITY * (H - target_y)); add(x, H - 1, random_between(-8, 8), vy, 10, rand() % 4, true); } void explode(Particle *p) { // etwa gleich schnell: die Funken bilden eine Kugelschale, keine volle Scheibe for (int i = 0; i < 100; i++) { double angle = random_between(0, 2 * PI); double speed = random_between(30, 50); add(p->x, p->y, p->vx + cos(angle) * speed, p->vy + sin(angle) * speed, random_between(1.5, 2.5), p->ramp, false); } } // Vier Farbverläufe zu je 16 Farben: 16 * ramp + Helligkeit void make_palette(void) { const int sky[3] = {10, 12, 35}; const int colors[4][3] = { {255, 80, 60}, {80, 255, 120}, {90, 150, 255}, {255, 210, 80} }; for (int ramp = 0; ramp < 4; ramp++) for (int b = 0; b < 16; b++) { double f = b / 15.0; // vom Nachthimmel zur Farbe, ganz hell fast weiß double white = f > 0.8 ? (f - 0.8) * 5 : 0; int rgb[3]; for (int i = 0; i < 3; i++) rgb[i] = (sky[i] + (colors[ramp][i] - sky[i]) * f) * (1 - white) + 255 * white; set_palette(16 * ramp + b, rgb[0], rgb[1], rgb[2]); } } // Häuser mit zufälliger Breite und Höhe, von links nach rechts. Jedes Haus // hat seinen eigenen Fensterabstand, und in jedem leuchtet ein anderer Teil // der Fenster. Die Stadt wird einmal ausgedacht und in city gemalt. void make_skyline(void) { set_palette(HOUSE, 0, 0, 0); set_palette(WINDOW, 255, 210, 120); // Glühbirne set_palette(WINDOW + 1, 255, 150, 70); // warmes Licht set_palette(WINDOW + 2, 140, 180, 255); // Fernseher int x = 0; while (x < W) { int width = 10 + rand() % 20; int top = H - 12 - rand() % 30; int step = 2 + rand() % 2; // Fensterabstand double lit = random_between(0.05, 0.4); // Anteil der hellen Fenster for (int i = x; i < x + width && i < W; i++) for (int y = top; y < H; y++) city[y][i] = HOUSE; for (int y = top + 2; y < H - 1; y += 3) for (int i = x + 2; i < x + width - 1 && i < W; i += step) if (random_between(0, 1) < lit) city[y][i] = WINDOW + rand() % 3; x += width; } } // die Stadt vor den Himmel malen void draw_skyline(void) { for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) if (city[y][x] != 0) set_pixel(x, y, city[y][x]); } // Nachleuchten: jedes Pixel wird ein wenig dunkler void fade(void) { for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { int c = get_pixel(x, y); if (c % 16 > 0) set_pixel(x, y, c - 1); } } int main(void) { screen_open(W, H, pixel_indexed8, "Feuerwerk"); make_palette(); make_skyline(); double last = seconds(); double next_rocket = 0; bool left = false; int frame = 0; while (!should_close()) { double now = seconds(); double dt = now - last; last = now; if (now > next_rocket) { // abwechselnd links und rechts, damit sich die Raketen verteilen left = !left; double x = left ? random_between(30, W / 2) : random_between(W / 2, W - 30); launch(x, random_between(20, 70)); next_rocket = now + random_between(0.3, 1.2); } if (mouse_button_pressed(pixel_mouse_left)) { int mx, my; mouse_position(&mx, &my); launch(mx, my); } // jedes zweite Bild: die Spuren verblassen langsamer frame++; if (frame % 2 == 0) fade(); for (int i = 0; i < count; i++) { Particle *p = &particles[i]; p->vy += GRAVITY * dt; if (!p->rocket) { // Luftwiderstand bremst die Funken p->vx -= p->vx * 1.5 * dt; p->vy -= p->vy * 1.5 * dt; } p->x += p->vx * dt; p->y += p->vy * dt; p->life -= dt; if (p->rocket && p->vy >= 0) { explode(p); p->life = 0; } if (p->life <= 0 || p->y >= H) { // Lücke mit dem letzten Partikel füllen particles[i] = particles[count - 1]; count--; i--; continue; } int brightness = p->rocket ? 15 : (int)(15 * fmin(1, p->life)); set_pixel((int)p->x, (int)p->y, 16 * p->ramp + brightness); } // die Stadt vor dem Himmel: nach den Funken gezeichnet draw_skyline(); present(); } screen_close(); return 0; }

Die Funken einer Explosion haben unterschiedliche Lebenszeiten zwischen 1,5 und 2,5 Sekunden. Deshalb hört eine Explosion nicht auf einen Schlag auf, sondern franst aus. Ein Funke ist hell, solange er mehr als eine Sekunde vor sich hat, und wird in der letzten Sekunde dunkler.
wind, der vx in jedem Bild ein wenig verändert und sich mit den Pfeiltasten einstellen lässt.fade() läuft in jedem zweiten Bild. Auf einem Bildschirm mit 120 Bildern pro Sekunde verglühen die Spuren also doppelt so schnell. Wie lässt sich das mit dt lösen?Bau mindestens eine der Ideen am Ende der Seite in dein Feuerwerk ein, oder eine eigene, und gib die Datei ab. Deine Lehrkraft sieht genau diesen Stand; du kannst jederzeit neu abgeben.