Eine schwebende, goldene 3 als Skulptur

3 Dinge in Processing, die ich gern eher gewusst hätte

| Processing   Programmierung   Tipps  

Ich arbeite schon seit einiger Zeit mit Processing. Speziell am Anfang hätte ich mir allerdings gewünscht, diese 3 (oder vielleicht 4?) Kniffe zu kennen, die jeden Sketch gleich viel mehr Spaß machen lassen.

1. Pixeldichte einstellen

Wenn Du Dich schon immer gefragt hast, warum Deine Sketches auf Deinem ultrahochauflösenden Monitor (oder auf einem Mac mit Retina-Display) so verpixelt aussehen, möchte ich Dir pixelDensity() vorstellen. In Kombination mit displayDensity() erkennt Processing Deine Bildschirmauflösung automatisch und passt die Ausgabe der einzelnen Pixel der Pixeldichte Deines Monitors an. Somit werden Kanten, Kurven und vor allem Texte in voller Schärfe gerendert und nicht (mehr) verpixelt dargestellt. Wenn Du den Befehl in Deiner Setup-Funktion einmalig wie folgt angibst, übernimmt Processing ab da den Rest:

pixelDensity(displayDensity());

Das Ergebnis kann sich sehen lassen:

Die 2D Rakete ist verpixelt
Vorher
Die 2D-Rakete hat scharfe Kanten, nachdem die Pixeldichte eingestellt wurde
Nachher
Optional kannst Du übrigens auch eine Zahl (meistens „2“ für Bildschirme mit doppelter Pixeldichte) statt `displayDensity()` hardcoden, um keine Kompromisse einzugehen.

Ich muss zu meiner Schande gestehen, dass ich diesen Befehl erst kürzlich entdeckt habe. Auf meinem MacBook haben mich vorher besonders die verpixelten Schriften wahnsinnig gemacht. Tatsächlich sind in Vergangenheit auch einige meiner bezahlten Arbeiten ausgerollt worden, ohne dass die Pixeldichte berücksichtigt worden wäre. Rückblickend wurden all diese Programme aber auf Geräten verwendet, die eine Pixeldichte von 1x hatten (Messe-Touchscreens und Infotainment-Monitore zum Beispiel). Noch mal Glück gehabt… 😅 Aber in Zukunft nicht mehr ohne!

Gut zu wissen: es werden auch Pixel-basierte Befehle, wie z.B. get(), set(), blend() usw. von pixelDensity() beeinflusst. Etwaige Filter und Bildoperationen können ab jetzt also in herrlicher Klarheit erstrahlen!

2. Formen passgenau um Texte platzieren

Es gibt Momente, in denen möchte man einfach nur eine kleine Box um einen Text ziehen. Dann probiert man rum, bis Text und Box halbwegs ansehnlich auf der Bühne angeordnet sind und dann… kommt eine Textänderung rein. Für genau solche Fälle gibt es textWidth(). Dieser Befehl rechnet die Breite eines Strings oder Characters aus und stellt sie zur Verfügung. Mit anderen Worten:

    size(200,200);
  pixelDensity(displayDensity());
  int textHeight = 32;
  textSize(textHeight);
  String helloWorld = "Hello World";
  fill(255);
  rect(10, height/2, textWidth(helloWorld), textHeight);
  fill(0);
  text(helloWorld, 10, height/2 + textHeight);

Die Box passt sich dem Text an. In Kombination mit einem Rastersystem lässt sich so ganz einfach ein User-Interface bauen. Wenn ich darüber nachdenke, wie viel Zeit ich – vor allem als ich angefangen habe, Processing zu benutzen – damit verbracht habe, Texte passgenau mit Hintergründen zu versehen, gruselt es mich.

3. Threads und parallele Abläufe für Splashscreens, Ladeanimationen und Menüs

Wenn Du in Processing schon einmal Schriften eingebunden hast, kennst Du das Problem vielleicht: Bevor ein Text angezeigt wird, muss zunächst die Schrift geladen und von Processing verarbeitet werden. Das führt entweder zu Lag im Programm oder, wenn Du createFont()bereits im Setup aufgerufen hast, zu einem extrem langen Ladevorgang, bei dem lediglich ein graues Fenster gezeigt wird, bis die Schrift endlich einsatzbereit ist. Zugegeben: Eine .vlw-Schrift über Tools -> Make Font zu bereitzustellen kann den Prozess beschleunigen, aber je nach Schriftart kann es trotzdem einige Sekunden dauern. Besonders bei Spielen ist das sehr ärgerlich, da scheinbar nicht mal ein Splashscreen angezeigt werden kann, während das Programm lädt – dachte ich zumindest, bis ich die thread()-Funktion entdeckt habe.

thread() macht, wie der Name vermuten lässt, einen neuen, parallelen Thread auf, der abseits vom Haupt-Loop (der void draw()) läuft. Um nun einen Splashscreen einzurichten und nebenher die Schriften und andere Assets zu laden, kann der folgende Code verwendet werden:

PFont mainFont;
boolean contentLoaded = false;
boolean contentLoading = false; 

void setup(){
  size(500,500);
}
void draw(){
  if(contentLoaded){
    //Whatever happens after the Splashscreen
  }else if(contentLoading){
    //Your Splashscreen
  }else{
   thread("loadContent");
   contentLoading = true;
  }
}

void loadContent(){
  mainFont = createFont("Audiowide-Regular.ttf", 32);
  textFont(mainFont);
    contentLoading = false;
  contentLoaded = true;

Wie Du siehst, nimmt thread() den Namen einer beliebigen Funktion als String als Argument, um diese dann – losgelöst vom Main-Loop – auszuführen. Kleiner Tipp am Rande: Achte speziell beim asynchronen Laden und Kreieren von Schriften darauf, dass Du die Ladefunktion nur einmal rufst (oben durch contentLoading sichergestellt). Ansonsten macht der Rechner mit jedem Frame einen neuen Thread auf und erstellt die Schrift immer und immer wieder neu, was ziemlich sicher zu Performance-Problemen führt.

4. Bonus Tipp: FrameCount und Modulo gemeinsam benutzen, um alle paar Frames eine Kollisionsabfrage durchzuführen

Ich habe mittlerweile schon einige Auftraggeber mit Arcade-Style-Games beglücken dürfen, die in Processing entstehen sollten. Also klassische Jump’n’Runs und vor allem Auto-Runner. Dabei ist – neben den Bewegungsfunktionen – die Kollisionsabfrage ein regelmäßiges Ärgernis. Und wer in Processing schon mal Kollisionserkennung für mehr als 100 Objekte gleichzeitig umgesetzt hat weiß, dass das dem Rechner einiges abverlangen kann. Häufig reicht es allerdings, die Kollision nur wenige Male pro Sekunde und nicht mit jedem Frame abzufragen, um die Rechenlast beträchtlich zu mindern, ohne dass den Spielenden auffallen würde, dass es manchmal zu minimalen Verzögerungen bei den Zusammenstößen gibt. Was ich in einigen meiner Tutorien in diesem Zusammenhang schon häufiger erlebt habe ist, dass dafür extra eine Variable verwendet wird, die mit jedem Frame hochgezählt und nach n Wiederholungen, wenn der gewünschte Wert erreicht ist, auf 0 zurückgesetzt wird. Um z.B. alle 5 Frames eine Zahl um 5 hochzuzählen und auszugeben, würde dies hier programmiert werden:

int counter = 0;
int collector = 0;
void draw(){
  counter++;
  if(counter == 5){
      collector += counter;
    println(collector);
    counter = 0;
      // An dieser Stelle könnte Ihre Werbung stehen.
      // Oder eine Kollisionsabfrage.
  }
}

Das geht allerdings auch deutlich kompakter, denn Processing hat die hochzählende Variable bereits von Haus aus eingebaut. Das Zauberwort lautet frameCountund gibt die Nummer des aktuellen Frames an. Wir können uns das zu Nutze machen, indem wir darauf den %-Operator (Modulo) anwenden. Falls Du noch nie mit Modulo gearbeitet hast, hier die Kurzform:

Der Modulo gibt den „Rest“ einer Division aus, wie man es häufig in der Grundschule lernt. Wenn z.B. 5/3 = 1 Rest 2 sind, dann wird beim %-Operator nur der Rest als Ergebnis ausgegeben: 5%3 = 2

Wenn wir dieses Konzept nun weiterdenken, ist jeder n-te Frame genau dann gegeben, wenn frameCount % n == 0, (frameCount also „glatt“ teilbar) ist. Am Beispiel n = 3:

  • 0 % 3 = 0
  • 1 % 3 = 1
  • 2 % 3 = 2
  • 3 % 3 = 0
  • 4 % 3 = 1
  • 5 % 3 = 2
  • 6 % 3 = 0
  • usw…

Unser Beispiel von oben lässt sich auf diese Weise wie folgt verkürzen:

void draw(){
  if(frameCount % 5 == 0){
    println(frameCount);
  }
}

Natürlich ist das Beispiel mit dem println()-Befehl nicht besonders eindrucksvoll. Aber angenommen, in der if-Abfrage stünde die Kollisionsabfrage: die eingesparten Ressourcen wären enorm!

Ich hoffe, diese Tipps haben Dir geholfen. Lass gern einen Kommentar da, falls Du Fragen hast oder Feedback geben möchtest. Ich würde mich außerdem riesig freuen, wenn Du meinen Newsletter abonnierst. Ich schreibe nicht häufig, aber wenn, dann lohnt es sich – versprochen!

Kommentare

Füge einen Kommentar hinzu:

Noch keine Kommentare vorhanden 😢