Dancing Crab mit P5.js Version 2: Jetzt mit HUD

P5.js
Spieleprogrammierung
Autor:in

Jörg Kantel

Veröffentlichungsdatum

14. Mai 2023

Ich habe meinem P5.js-Port der kleinen, roten, mit Luftblasen tanzenden Krabbe ein finales (aber dennoch vielleicht nur vorläufiges) Update verpaßt. Da ich in der Trinket-Version daran gescheitert war, eigene Fonts einzubinden, wollte ich nun wissen, wie einfach oder kompliziert sich dieses in P5.js gestaltet, und habe dem Spiel ein HUD (Head Up Display) mit einem lokal eingebundenen1 (Google-) Font verpaßt. Spoiler: Es war Pipi-einfach!

Dazu habe ich mir erst einmal auf den Seiten von Google einen furchtbar schrägen Font ausgesucht und heruntergeladen. Er heißt »Fredericka the Great« und sieht auch so aus, wie man sich eine weibliche, durch und durch preußische Friederike als Gegenstück zum »alten Fritz« vorstellt. Dieser Font von Tart Workshop steht – wie die meisten Fonts auf den Seiten von Google Fonts – unter der SIL Open Font License (OFL) und kann daher auch für kommerzielle Projekte frei genutzt werden.

Damit P5.js den Font auch findet, habe ich ihn für diese Webseite in ein Verzeichnis abgelegt. Da dieses Verzeichnis nun nicht mehr nur Bilder enthält, habe ich es – gemäß den Processing-Konventionen – von images nach data umbenannt2.

Dann habe ich in der preload()-Methode die Zeile

displayFont = loadFont("data/FrederickatheGreat-Regular.ttf")

eingefügt (die Variable displayFont hatte ich vorher als global definiert.)

Die setup()-Methode erhielt ebenfalls ein zusätzlich Zeile:

textFont(displayFont);

Da ich die draw()-Methode nicht überladen wollte, habe ich eine zusätzliche Funktion displayHUD() geschrieben,

function displayHUD() {
  let hud1 = ("Score: " + crab.score);
  let hud2 = ("Death: " + crab.death);
  push();
  stroke(0);
  strokeWeight(1);
  fill(200, 10, 10);
  textSize(36);
  text(hud1, 20, 40);
  text(hud2, 200, 40);
  pop();
}

die ich in draw() dann einfach aufgerufen habe. Diese Methode zeigt sehr schön, daß man den Fonts in P5.js nicht nur mit fill() eine Farbe, sondern mit stroke() und strokeWeight() auch eine Umrandung zuweisen kann. Und da man mit den Stati leicht durcheinanderkommt, habe ich diese mit push() und pop() geklammert. Das ist zwar nicht unbedingt notwendig, aber man ist so – speziell bei größeren Projekten – auf der sicheren Seite.

Und nun also den Sketch in seiner vollen Pracht, eingebunden in diese Seite:

Wie bisher wird die Krabbe mit den Pfeiltasten von rechts nach links oder von links nach rechts bewegt. Für jede eingefangene, weiße Luftblase bekommt der Spieler einen Punkt angezeigt. Die Kollision mit einer roten Luftblase beendet das Spiel (noch) nicht, stattdessen wird auch die Anzahl der Tode im Head up Dislplay angezeigt. (Um das Spiel neu zu starten, muß die komplette Seite neu geladen werden.)

Jetzt für diejenigen unter Euch, die gerne im Quellcode stöbern und/oder ihn nachvollziehen wollen, der komplette Sketch in all seiner Pracht:

// Dancing Crab v2
// Jörg Kantel 2023
// Inspiriert von Heiko Fehr: »Let's Code Python«, Bonn (Rheinwerk-Verlag) 2019, Seiten 247ff.
// Krabbe: Nitin Chowdary (CC0), https://opengameart.org/content/crab
// Luftblasen: HorrorPen (CC-BY 3.0), https://opengameart.org/content/bubbles8-colors
// Bildhintergrund: Kenney.nl Fish Pack (CC0), https://www.kenney.nl/assets/fish-pack
// Font: Fredericka the Great (OFL), https://fonts.google.com/specimen/Fredericka+the+Great

const windowWidth = 640;
const windowHeight = 416;
const fps = 60;
const numBubbles = 50;
const numEnemies = 5;

let bg;
let displayFont;
let crab;
let crabImages = [];
let bubbles = [];
let bubbleImages = [];
let enemyBubbles = [];
let enemyImage;

class Crab {

  constructor() {
    this.w = 64;
    this.h = 64;
    this.r = 32;
    this.x = width/2 - this.w/2;
    this.y = height/2 + 100;
    this.img = crabImages[0];
    this.dir = "none";
    this.speed = 5;
    this.animationCount = 0;
    this.score = 0;
    this.death = 0;
  }

  update() {
    // Bewegung
    if (this.dir == "none") {
      this.x += 0;
    } else if (this.dir == "right") {
      if (this.x <= width - this.w - 5) {
        this.x += this.speed;
      }
    } else if (this.dir == "left") {
      if (this.x >= 2) {
        this.x -= this.speed;
      }
    }
    // Animation
    this.animationCount += 1;
    if (this.dir == "none") {
      this.img = crabImages[0];
    } else {
    if (this.animationCount >= 10) {
      this.animationCount = 0;
    }
    if (this.animationCount <= 5) {
      this.img = crabImages[0];
    } else {
      this.img = crabImages[1];
    }
  }
  }

  display() {
    image(this.img, this.x, this.y);
  }
}

class Bubble {

  constructor() {
    this.reset();
    this.speed = 2;
  }

  reset() {
    let dia = int(random(0, 2))
    this.img = bubbleImages[dia];
    this.r = this.img.width/2;
    this.x = int(random(width));
    this.y = int(random(-2*height, -50));
  }

  isCircleCollision(other) {
    let distance = dist(this.x, this.y, other.x, other.y);
    if (distance < this.r + other.r) {
      return true;
    }
    return false;
  }

  update() {
    this.y += this.speed;
    if (this.y > height + 50) {
      this.reset();
    }
  }

  display() {
    image(this.img, this.x, this.y);
  }
}

class EnemyBubble extends Bubble {

  constructor() {
    super();
    this.reset();
    this.r = 15;
    this.speed = 3;
    this.img = enemyImage;
  }

    reset() {
      this.x = int(random(width));
      this.y = int(random(-2*height, -50));
    }
  }

function preload() {
  bg = loadImage("data/background.png");
  crabImages[0] = loadImage("data/crab1.png");
  crabImages[1] = loadImage("data/crab2.png");
  for (let i = 0; i < 3; i++) {
    bubbleImages[i] = loadImage("data/bubbleblue" + str(i) + ".png"); 
  }
  enemyImage = loadImage("data/bubblere1.png");
  displayFont = loadFont("data/FrederickatheGreat-Regular.ttf")
}

function setup() {
  myCanvas = createCanvas(windowWidth, windowHeight);
  myCanvas.parent("crab01");
  frameRate(fps);
  textFont(displayFont);
  for (let i = 0; i < numBubbles; i++) {
    bubbles[i] = new Bubble;
  }
  for (let j = 0; j < numEnemies; j++) {
    enemyBubbles[j] = new EnemyBubble;
  }
  crab = new Crab();
}

function draw() {
  background(49, 197, 244);    // Hellblau
  image(bg, 0, 0);
  for (let bubble of bubbles) {
    bubble.update();
    if (bubble.isCircleCollision(crab)) {
      bubble.reset();
      crab.score += 1;
      // console.log(crab.score);
    }
    bubble.display();
  }
  for (enemyBubble of enemyBubbles) {
    enemyBubble.update();
    if (enemyBubble.isCircleCollision(crab)) {
      console.log("GAME OVER");
      enemyBubble.reset();
      crab.death += 1;
      // crab.x = 2000;
      // crab.y = 2000;
    }
    enemyBubble.display();
  }
  crab.update();
  crab.display();
  displayHUD();
}

function displayHUD() {
  let hud1 = ("Score: " + crab.score);
  let hud2 = ("Death: " + crab.death);
  push();
  stroke(0);
  strokeWeight(1);
  fill(200, 10, 10);
  textSize(36);
  text(hud1, 20, 40);
  text(hud2, 200, 40);
  pop();
}

function keyPressed() {
  if (keyCode === LEFT_ARROW) {
    crab.dir = "left";
  } else if (keyCode === RIGHT_ARROW) {
    crab.dir = "right";
  }
  return false;
}

function keyReleased() {
  crab.dir = "none";
  return false;
}

Entwickelt habe ich wieder in Visual Studio Code mit dem Plugin p5.vscode. Speziell durch den darin enthaltenen Live Editor macht er eine lokale Entwicklung sogar noch einfacher, als im P5.js-Webeditor, und man ist nicht auf die Erreichbarkeit des Webeditors angewiesen (das heißt, man kann auch ohne Internetverbindung entwickeln und testen).

Neben dem oben schon erwähnten Font sind die übrigen Credits gleich geblieben: Die Impementierung wurde inspiriert von Heiko Fehrs »Let’s Code Python«, Bonn (Rheinwerk-Verlag) 2019, Seiten 247ff. Die Bilder der Krabbe sind von Nitin Chowdary, der sie unter der CC0 auf OpenGameArt.org veröffentlicht hatte. Ebenfalls von OpenGameArt.org sind die Luftblasen (CC-BY 3.0) des Users mit dem schönen Screen-Namen HorrorPen. Und der Bildhintergrund stammt wie so oft aus dem schier unerschöpflichen, freien (CC0) Fundus von Kenny.nl.

Den Quellcode und alle Assets habe ich wie gewohnt auf mein GitHub-Repositorium hochgeladen. Habt Spaß damit.

Fußnoten

  1. Ich möchte mir namlich keine bösen (und teuren!) Briefe einfangen. Denn Abmahnanwälte lauern überall.↩︎

  2. Weil ich es bei meiner Premiere vergessen hatte zu erwähnen: Im Gegensatz zu den vollmundigen Versprechungen der Dokumentation erkennt Quarto die Assets nicht, die in einer eingebundenen JavaScript-Datei angesprochen werden. Das Verzeichnis data muß daher »per Fuß« von /posts/<verzeichnis_des_blogposts>/data nach /docs/posts/<verzeichnis_des_blogposts>/dataherübergeschaufelt werden.↩︎