Tutorial: Erstellen von neuen Blöcken in Blockly

Dieser Artikel entstand im Rahmen meiner Bachelorarbeit und erscheint hier, weil er mir für die Aufnahme in eine wissenschaftliche Arbeit als zu lang und schwaffelig erschien. Dennoch bin ich der Überzeugung, dass er seinen Zweck als Tutorial durchaus erfüllt und so als Anleitung dienen kann, wie man für die graphische Programmiersprache Blockly neue Code-Statements einführt.

Noch ein Hinweis vorab, der für das Verständnis nötig ist: Das im Tutorial verwendete Beispiel geht von unserem Projekt aus. Darin geht es darum mittels eines Programms einen Käfer über ein Spielfeld mit weiteren Objekten, zum Beispiel Pilzen, Bäumen und Blättern, zu steuern und verschiedene Aufgaben zu erledigen; das zum besseren Verständnis.

Und los geht es

Im Folgenden möchte ich einen praktischen Aspekt bei der Arbeit mit Blockly im Detail vorstellen, das Erstellen von neuen Blöcken. Mit "Block" bezeichne ich dabei die graphische Repräsentation eines ausführbaren Code-Statements, das sich unter Umständen durch zusätzliche Statements oder Variablen erweitern lässt. Die Ausführungen können als Anleitung oder Tutorial herangezogen werden, falls das Erstellen eines neuen Blocks oder neuer Blöcke notwendig erscheint.

Bevor mit der Umsetzung begonnen wird, sollte gründlich überprüft werden, ob nicht bereits das Blockly-Hauptprojekt einen entsprechenden Block zur Verfügung stellt. Davon ist in der Regel nicht auszugehen, wenn man eine stark domänenspezifische Implementierung im Sinn hat. Im Projekt sind dagegen bereits diverse Kontrollstrukturen und Möglichkeiten zum Umgang mit Variablen (Strings, Integer und Listen) enthalten.

Diese Anleitung möchte ich an einem konkreten Beispiel abarbeiten: Unsere Spielfigur hat einen Sensor, um benachbarte Gegenstände, zum Beispiel Bäume, wahrzunehmen. In einem ersten Schritt hatten wir den Sensor derart implementiert, dass wir eine Reihe von Blöcken hatten die Bäume voraus, links und rechts erkennen konnten und entsprechend treeFront, treeLeft, und treeRight benannt waren.

Da verschiedene Objekte auf dem Spielfeld gegeben sind, die durch Sensoren der Spielfigur erfasst werden, ergibt dies eine verwirrende Menge an Sensor-Blöcken. Sinnvoller wäre es nur einen Block zu haben, bei dem sich die Richtung über ein Dropdown-Menü auswählen lässt. Eben die Erstellung dieses Blocks werde ich vorstellen.

Das Erstellen eines neuen Blocks erfordert es zunächst (Schritt 1) die Art und Weise festzulegen, wie der Block später im Editor dargestellt wird. Als nächstes muss die Funktionalität (Schritt 2) des Blocks definiert werden, d. h. was er tut, wenn er in einem Blockly-Programm verwendet wird.

Schritt 1: Graphische Ausgabe erstellen

Die Definition der graphischen Ausgabe ist nicht nur eine ästhetische Aufgabe – sie entscheidet auch darüber, wie ein einzelner Block mit anderen Blöcken interagieren kann. Blöcke in Blockly sind wie Puzzle-Teile aufgebaut: Sie passen nur an bestimmten Ecken aneinander. Diese Konnektoren erfüllen einen wichtigen, syntaktischen Zweck, lässt sich über sie doch beispielsweise festlegen, ob eine Funktion einen Rückgabewert hat, ob der Block eine Variable ist oder eine Funktion, die unter Umständen weitere Eingaben erwartet.

Daher sollte man sich vor der Definition von Blöcken zunächst Gedanken über die Gestaltung der Konnektoren machen.

Für unser Beispiel möchten wir einen Block erstellen, der eine Funktion repräsentiert, die als Rückgabe einen boolschen Wert liefert. Die aufgerufene Funktion und deren Rückgabe sind kontextabhängig, d. h. der Wert der Rückgabe hängt von der Position der Spielfigur auf dem Spielfeld ab. Diese Informationen werden nicht durch den Nutzer, sondern intern verwaltet, d. h. der Nutzer muss keine Argumente an die Funktion übergeben, abgesehen von der Richtung, die er betrachten möchte.

Die Rückgabe ist also ein boolescher Wert – diese Tatsache macht es erforderlich, dass ein Konnektor zur Verfügung gestellt wird, der es erlaubt unseren tree-Block an zum Beispiel if-then-else-Statements anzudocken. Die Auswahl der Richtung könnten wir ebenfalls über einen Konnektor ermöglichen, der die Verbindung mit Richtungen zur Verfügung stellt; wie oben bereits erwähnt, möchten wir diese Funktionalität jedoch über ein Dropdown-Menü abbilden.

Beginnen wir mit der Implementierung der Gestalt unseres Blocks. Dafür erweitern wir Blockly.Language um eine Funktion mit einem sprechenden, individuellen (alle Blöcke teilen sich denselben Namespace) Namen für unseren Block:

Blockly.Language.treeSomewhere = {
  // hier wird die Gestalt des Blocks im festgelegt
}

Blockly sieht nun drei weitere Felder für die Block-Definition vor: category, helpUrl und init. Das Feld category erwartet eine String über den später die Zuordnung des Blocks im Editor unter einen Reiter – eben die Kategorie – erfolgt. Das Feld helpUrl erwartet ebenfalls einen String, genauer eine Url. Durch Rechtsklick auf den Block und Anwahl von Help bzw. Hilfe wird man auf das Ziel der Url weitergeleitet – dies dient offenkundig dazu, dem Nutzer weitere Informationen zu einzelnen Blöcken zur Verfügung zu stellen. Das Feld init erwartet eine Funktion. Darin enthalten sind die Infos die die Gestalt des Blocks festlegen. Bevor ich auf die init-Funktion genauer eingehe, erweitere ich treeSomewhere um Kategorie und Hilfe-Url:

Blockly.Language.treeSomewhere = {
  category: 'Sensor',
  helpUrl: 'http://ti.informatik.uni-kiel.de/example/treeSensor-help'
  init: function() {
    // festlegen von Farbe, Konnektoren, Beschriftung, etc.   
  }
}

Die init-Funktion stellt eine Fülle von Möglichkeiten zur Verfügung, die gut dokumentiert sind. Für unseren Beispiel-Block legen wir zunächst die Farbe fest. Dafür stellt Blockly das Hue-Saturation-Value Farbschema zu Verfügung, wobei – für ein visuell einheitliches Erscheinungsbild – Saturation und Value bereits festgelegt sind und für den einzelnen Block allein der Hue-Wert zu wählen ist. Für unseren Block nehmen wir den Wert der als Variable SENSOR_COLOR für alle Sensoren festgelegt wurde. (i. e. der Wert 10, ein schmutziges Rot).

Als nächstes legen wir fest, dass der Block einen Rückgabewert besitzt und dass es sich bei dem Wert der Rückgabe um einen Boolean handelt. Dafür nutzen wir die Funktion setOutput. Neben dieser stehen auch die Funktionen setPreviousStatement und setNextStatement zur Verfügung, um Konnektoren zu schaffen; durch diese kann festgelegt werden, ob ein Statement vorausgehen oder folgen kann (ein Beispiel für einen Block, der diese Konnektoren nutzt, siehe die Implementierung unserer Instruktionen für die Spielfigur).

Außerdem möchten wir, dass der Nutzer einen Hinweis erhält, was der Block für eine Aufgabe hat, wenn er mit der Maus über ihn fährt. Die Funktion setTooltip erwartet als Eingabe einen String und stellt im Übrigen das gewünschte Verhalten zur Verfügung.

Der neue Zwischenstand sieht dann wie folgt aus:

var SENSOR_COLOR = 10;

Blockly.Language.treeSomewhere = {
  category: 'Sensor',
  helpUrl: 'http://ti.informatik.uni-kiel.de/example/treeSensor-help'
  init: function() {
    this.setColour(SENSOR_COLOR);
    this.setOutput(true, Boolean);
    this.setTooltip("Sensor für das Erfassen ob ein Baum benachbart
          ist; wähle die Richtung über das Dropdown");
  }
}

Abschließend fehlt nur noch die Bereitstellung des Dropdowns und die Beschriftung des Blocks. Für das Dropdown erstellen wir zunächst eine Liste, die wiederum 2-elementige Listen enthält. Dabei stellt das erste Element der inneren Liste die Beschriftung dar, die der Nutzer später im Dropdown sieht, das zweite Element dagegen ist für den internen Einsatz gedacht. Dies ist notwendig um für verschiedene Sprachen ohne hohen Anpassungsaufwand die gleiche Funktionalität abzubilden: Die Beschriftung kann in eine andere Sprache übertragen werden, sofern die Werte zum internen Gebrauch gleich bleiben.

Das Dropdown wird über den Aufruf des Konstruktors FieldDropdown und der Übergabe der geschachtelten Liste erstellt.

Für die Gestaltung des Blockinhalts gibt es drei unterschiedliche Möglichkeiten:

Für unser Beispiel ist appendDummyInput die richtige Wahl. In diesen Rahmen werden nun alle Elemente, d. h. das Dropdown, sowie die eigentliche Beschriftung (die über appendTitle angelegt wird) zusammengefügt:

var SENSOR_COLOR = 10;
var DIR = [["north", "NORTH"], ["west", "WEST"], 
           ["south", "SOUTH"], ["east", "EAST"]]
var dropdown = new Blockly.FieldDropdown(DIR);

Blockly.Language.treeSomewhere = {
  category: 'Sensor',
  helpUrl: 'http://ti.informatik.uni-kiel.de/example/treeSensor-help'
  init: function() {
    this.setColour(SENSOR_COLOR);
    this.appendDummyInput()
      .appendTitle("tree")
      .appendTitle(dropdown, 'ARG')
      .appendTitle("?");
    this.setOutput(true, Boolean);
    this.setTooltip("Sensor für das Erfassen ob ein Baum benachbart 
             ist; wähle die Richtung über das Dropdown");
  }
}

Damit ist die Gestalt unseres Blocks festgelegt: Der fertige Beispiel-Block

Wie man sehen kann ist der Vorgang des Block-Designs ein relativ schematischer Prozess bei dem man auf einen festen Satz an vorgegebenen Funktionen zurückgreift – was es ideal für eine Umsetzung in Blockly macht. Entsprechend gibt es eine Blockly-Demo, die es ermöglicht mit Hilfe von Blockly neue Blöcke zu erstellen. Diese ist auch ideal geeignet, um sich mit den verschiedenen Konfigurationsmöglichkeiten vertraut zu machen.

Schritt 2: Blockfunktion definieren

Der zweite Schritt besteht nun darin, dass festgelegt wird, was geschieht wenn aus dem Block Code generiert wird. Da Blockly keine eigene Programmiersprache ist heißt das, dass man festlegen muss zu welchem Statement ein Block ausgewertet wird, sobald man die Auswertung veranlasst.

Im Fall unseres Beispiels möchten wir, dass das Auswerten des Codes dafür sorgt, dass eine schon vorhandene Funktion des Spielfelds aufgerufen wird, die ermittelt, ob sich zu einem Feld benachbart, in einer bestimmten Richtung ein Baum befindet. Die Methode ist wie folgt gegeben (wobei "bug" die Spielfigur und "T" die interne Repräsentation eines Baumes meint):

/**
 @method treeSomewhere
 @return {Boolean} True if there is a tree infront of the bug, 
 false otherwise
 **/
 World.prototype.treeSomewhere = function(dir) {
   return "T" === this.nextField(this.bug, dir);
 }

Um das Aufrufen dieser Methode bei Auswertung des Blocks zu gewährleisten, erweitern wir Blockly.JavaScript (für eine andere Sprache muss entsprechend anderes gewählt werden) um die Funktion treeSomewhere. Die vollständige Methode ist nicht sehr kompliziert:

Blockly.JavaScript.tree = function() {
  var code = 'world.treeSomewhere(' 
             + dirMap[this.getTitleValue('ARG')] 
             + ')';
  return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
}

Zunächst wird ein String erstellt, der einen Aufruf der Methode treeSomewhere mit dem Parameter enthält, der aus der Dropdown-Auswahl ausgelesen wird. Dieser String wird sodann als erstes Element eines Arrays zurückgegeben, das als zweites Element die Information darüber enthält, welche Operatorenwertigkeit der zurückgegebene Code hat.

Weitere Möglichkeiten bei der Block-Erstellung

Es gibt eine Reihe weiterer Möglichkeiten zur Erstellung von Blöcken, die ich an dieser Stelle noch nicht vorgestellt habe; dies werde ich bei Gelegenheit in einem zweiten Teil nachholen.