144 81
German Pages [411] Year 2022
Inhaltsverzeichnis Impressum Einleitung Kapitel 1: Minecraft-Server 1.1 Java installieren 1.2 Installation 1.2.1 CraftBukkit 1.2.2 Spigot 1.3 Konfiguration 1.4 Befehle 1.5 Verbinden 1.6 Updates Kapitel 2: Python 2.1 Programmiersprachen 2.2 Besonderheiten von Python 2.3 Einrichtung 2.3.1 Jython 2.3.2 PPLoader 2.4 Editor Kapitel 3: Das erste Plugin 3.1 Ordner anlegen 3.2 plugin.py 3.3 plugin.yml
3.4 Testen 3.5 Fehler finden 3.6 Entdecken Kapitel 4: Chat-Kommandos 4.1 Eigene Befehle definieren 4.2 Chat-Nachrichten versenden Kapitel 5: Variablen 5.1 Namen 5.2 Werte 5.2.1 Operatoren 5.2.2 Umwandlung 5.2.3 Runden 5.3 +1-Plugin 5.4 Listen und Arrays 5.5 Konstanten Kapitel 6: Schleifen 6.1 Kürbis-Plugin 6.1.1 Positionierung 6.1.2 Blöcke platzieren 6.2 Die verschiedenen Schleifen 6.2.1 for-Schleife 6.2.2 while-Schleife 6.2.3 Verschachtelte Schleifen Kapitel 7: Verzweigungen 7.1 if
7.2 else 7.3 elif Kapitel 8: Funktionen 8.1 Deklaration von Funktionen 8.2 Rückgabewerte 8.3 Parameter 8.4 Anwendungsbeispiel Kapitel 9: Bauen 9.1 Notunterkunft 9.1.1 Decke und Wände 9.1.2 Tür 9.1.3 Bett 9.1.4 Fackel 9.2 Runde Objekte 9.2.1 Kreise 9.2.2 Kugeln Kapitel 10: Schilder 10.1 Hängende Schilder 10.2 Stehende Schilder 10.3 Text festlegen 10.3.1 Farbe 10.3.2 Formatierung 10.4 Schilder-Plugin 10.4.1 Wiederholung: Listen 10.4.2 Das Plugin Kapitel 11: Listener
11.1 Grundgerüst 11.2 Spieler-Events 11.3 Kreaturen-Events 11.4 Block-Events 11.5 Inventar-Events 11.6 Server-Events 11.7 Fahrzeug-Events 11.8 Wetter-Events 11.9 Welt-Events 11.10 Mehrere Listener in einem Plugin Kapitel 12: Klassen und Objekte 12.1 Die ganze Welt ist ein Objekt 12.2 Funktionen in Klassen 12.3 Zugriffskontrolle 12.4 Vererbung 12.5 Mehrfachvererbung und mehrstufige Vererbung 12.6 Bau-Plugin Kapitel 13: Crafting-Rezepte 13.1 Rezepte festlegen 13.2 Eigene Rezepte entwerfen 13.3 Feuerschwert 13.4 Enderbogen Kapitel 14: Informationen dauerhaft speichern 14.1 Konfigurationsdateien 14.1.1 Lesen
14.1.2 Schreiben 14.2 Objekte in Dateien speichern Kapitel 15: Eigene Spielmodi entwickeln 15.1 Schneeballschlacht 15.1.1 Schneebälle verteilen 15.1.2 Schneebälle auffüllen 15.1.3 Punkte zählen 15.1.4 Punkte dauerhaft speichern 15.1.5 Highscore-Liste anzeigen 15.1.6 Vollständiger Quellcode 15.2 Sammelspiel 15.2.1 Aufbau des Plugins 15.2.2 Plugin starten 15.2.3 Spieler betritt den Server 15.2.4 Gegenstände zählen 15.2.5 Auftrag anzeigen 15.2.6 Vollständiger Quellcode Kapitel 16: Eigenständige Python-Programme 16.1 Python einrichten 16.2 Grundgerüst 16.3 Ein- und Ausgabe 16.4 Quiz programmieren Anhang A: Befehlsreferenz Anhang B: Materialien
Daniel Braun
Let’s Play: Programmieren lernen mit Python und Minecraft Plugins erstellen ohne Vorkenntnisse
Impressum Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. ISBN 978-3-7475-0507-6
3. Auflage 2022 www.mitp.de
E-Mail: [email protected]
Telefon: +49 7953 / 7189 - 079
Telefax: +49 7953 / 7189 - 082 © 2022 mitp Verlags GmbH & Co. KG KEIN OFFIZIELLES MINECRAFT-PRODUKT.
NICHT VON MOJANG GENEHMIGT ODER MIT MOJANG VERBUNDEN. Minecraft and its graphics are a trademark of Mojang Synergies AB. Dieses Werk, einschließlich aller seiner Teile, ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Dies gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-
Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Lektorat: Sabine Schulz, Janina Bahlmann
Sprachkorrektorat: Petra Kleinwegen
Coverbild: Daniel Braun
electronic publication: III-satz, Husby, www.drei-satz.de Dieses Ebook verwendet das ePub-Format und ist optimiert für die Nutzung mit dem iBooks-reader auf dem iPad von Apple. Bei der Verwendung anderer Reader kann es zu Darstellungsproblemen kommen. Der Verlag räumt Ihnen mit dem Kauf des ebooks das Recht ein, die Inhalte im Rahmen des geltenden Urheberrechts zu nutzen. Dieses Werk, einschließlich aller seiner Teile, ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheherrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Dies gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und Einspeicherung und Verarbeitung in elektronischen Systemen. Der Verlag schützt seine ebooks vor Missbrauch des Urheberrechts durch ein digitales Rechtemanagement. Bei Kauf im Webshop des Verlages werden die ebooks mit einem nicht sichtbaren digitalen Wasserzeichen individuell pro Nutzer signiert. Bei Kauf in anderen ebook-Webshops erfolgt die Signatur durch die Shopbetreiber. Angaben zu diesem DRM finden Sie auf den Seiten der jeweiligen Anbieter.
Einleitung Liebe Leserin, lieber Leser, die Welt von Minecraft steckt voller Dinge, die es zu entdecken gilt. Verschiedene Landschaften, Hunderte verschiedene Gegenstände und allerlei Tiere und Monster sind nur einige der Dinge, die dich erwarten. Irgendwann ist aber selbst diese Vielzahl an Möglichkeiten erschöpft und man hat das Gefühl, alles schon einmal gesehen oder gemacht zu haben. Wenn es dir so geht, dann ist dieses Buch genau richtig für dich. Denn im Verlaufe dieses Buches lernst du, wie man mithilfe von Python und dem Bukkit- oder Spigot-Server eigene Erweiterungen für die Java-Edition von Minecraft programmiert, sogenannte Plugins, die du dann zusammen mit deinen Freunden auf deinem eigenen Minecraft-Server ausprobieren kannst. Egal ob du neue Crafting-Rezepte entwerfen, ganze Häuser mit einem einfachen Chat-Befehl bauen oder sogar einen eigenen Spielmodus programmieren möchtest, mit eigenen Plugins steckt die Welt von Minecraft wieder voller Herausforderungen und Dinge, die entdeckt werden wollen. Und ganz nebenbei lernst du auch noch zu programmieren – und wer weiß, vielleicht kommt das nächste Minecraft eines Tages von dir! Bevor es soweit ist, liegt allerdings noch ein ordentliches Stück Weg vor dir. Die ersten beiden Kapitel dieses Buches beschäftigen sich deshalb zunächst einmal damit, wie du deinen Computer für das Programmieren und Testen eigener Plugins vorbereitest. Dazu wird dir erklärt, wie du den Bukkit- oder Spigot-Server installierst, der in diesem Buch verwendet wird, ihn nach deinen Wünschen konfigurierst und wie du deinen Computer so einrichtest, dass du Python-Plugins schreiben kannst.
Direkt im Anschluss geht es im dritten Kapitel ohne Umschweife direkt los mit dem Programmieren deines ersten eigenen Plugins. Die ersten Schritte werden dir vielleicht noch etwas unspektakulär vorkommen, aber mit jedem der folgenden Kapitel wirst du immer mehr Möglichkeiten besitzen, um immer ausgeklügeltere Plugins zu programmieren. Schon im vierten Kapitel wirst du zum Beispiel lernen, wie du eigene Chat-Befehle programmieren und verwenden kannst. Die Kapitel 5 bis 8 beschäftigen sich mit grundlegenden Konzepten des Programmierens im Allgemeinen und der Programmiersprache Python im Besonderen. Was du hier liest, wird dir nicht nur beim Programmieren von Minecraft-Plugins helfen, sondern beim Programmieren jedes Programms in jeder Programmiersprache. Trotzdem entstehen dabei natürlich auch einige praktische kleine Plugins, wie zum Beispiel das Mauer-Plugin, das es dir erlaubt, mit einem einfachen Chat-Befehl auf die Schnelle eine Mauer zu bauen, wenn du möchtest, sogar aus purem Gold. Das neunte Kapitel widmet sich dann ganz der Baukunst. Häuser, Kreise und Kugeln – hier wird kein Block auf dem anderen gelassen. Und wenn du schon einmal versucht hast, eine Kugel in Minecraft von Hand zu bauen, dann wirst du ganz besonders die Dienste des Kugel-Plugins zu schätzen wissen, das dir auf Knopfdruck eine nahezu perfekte Kugel zaubern kann. Weiter geht es danach mit dem Bau von Schildern, denen das gesamte zehnte Kapitel gewidmet ist. Wenn dir selbst ein Knopfdruck zum Bauen noch zu viel ist, dann wird dir das elfte Kapitel besonders gefallen. Dort geht es nämlich um Plugins, die vollautomatisch auf Geschehnisse in der Spielwelt reagieren. Egal ob ein Creeper über die Karte schleicht, ein Spieler etwas isst oder ein Baum wächst, hier lernst du wie deinem Plugin nichts mehr von dem entgeht, was auf deinem Server passiert und natürlich auch, wie du darauf reagieren kannst. Ein sehr grundlegendes und wichtiges Konzept moderner Programmiersprachen, die objektorientierte Programmierung, lernst
du in Kapitel 12 kennen, hier dreht sich alles um Objekte und Klassen. Falls du dich um die umherschleichenden Creeper aber doch lieber ganz manuell kümmern möchtest, kannst du die Informationen aus Kapitel 13 nutzen, um ganz eigene Waffen zu kreieren. In diesem Kapitel geht es nämlich um das Erstellen eigener Crafting-Rezepte und ein Beispiel, das dir dort begegnen wird, ist ein Rezept für ein Flammenschwert, das alles in Brand setzt, worauf es trifft. Das vierzehnte Kapitel ist dann wieder etwas technischer, aber nicht weniger nützlich. Hier lernst du nämlich, wie du Informationen dauerhaft speichern kannst, die auch dann erhalten bleiben, wenn der Server zwischenzeitlich ausgeschaltet wird. Das ist zum Beispiel praktisch, wenn du wie in Kapitel 15 eigene Spielmodi kreieren willst, also sozusagen ein Spiel im Spiel. Wir wäre es zum Beispiel mit einem Schneeballschlacht-Mod mit eigener Highscore-Liste, die die Treffer zählt? Oder lieber ein lustiges Suchspiel, bei dem der Gewinner mit Erfahrungspunkten oder wertvollen Gegenständen belohnt wird? Ganz wie du möchtest: Deiner Kreativität sind keine Grenzen gesetzt! Im letzten Kapitel bekommst du dann noch einen kurzen Ausblick darauf, was du mit deinen neu gewonnenen Programmierfähigkeiten noch anstellen kannst, außer Minecraft-Plugins zu programmieren. Denn wenn du am Ende des Buches angelangt bist, hört der Spaß noch lange nicht auf: Nun hast du alle Werkzeuge und alles Wissen, das du benötigst, um ganz eigene Plugins ganz nach deinen Vorstellungen zu entwerfen. Dabei helfen dir einige Listen im Anhang des Buches, in denen du Befehle und besonders häufig benötigte Dinge schnell nachschlagen kannst. Denn egal wie erfahren man als Programmierer ist, alles kann und muss man nicht auswendig können, man muss nur wissen, wo man es nachschlagen kann – und genau dazu dient der Anhang dieses Buches. Falls du Fragen, Kritik oder Anregungen zum Buch oder generell zu Minecraft-Plugins hast, kannst du mich gerne jederzeit kontaktieren.
Du erreichst mich per Mail an [email protected] oder über meine Website www.daniel-braun.com.
Downloads zum Buch Unter der Webadresse buch.daniel-braun.com findest du: Links zu allen Downloads, die du benötigst Alle Plugins, die du im Rahmen des Buches programmieren wirst, falls du den Code nicht aus dem Buch abtippen möchtest
Mein besonderer Dank gilt Karl-Heinz Barzen, der den Entstehungsprozess dieses Buches unermüdlich mit zahlreichen hilfreichen Kommentaren und Anmerkungen begleitet und damit einen wichtigen Beitrag dazu geleistet hat, dass dieses Buch möglichst verständlich und einsteigerfreundlich wird. Nun wünsche ich dir aber vor allem viel Spaß beim Lesen, Programmieren und Entdecken! Daniel Braun
Minecraft-Server Kapitel 1
Alleine Minecraft zu spielen, kann schon jede Menge Spaß machen, noch lustiger wird es aber, wenn du dich mit anderen Spielern zusammentust, um mit ihnen oder auch gegen sie zu spielen. Dazu kannst du dir entweder einen der hunderten öffentlichen Server aussuchen, die du überall im Internet findest, oder du kannst deinen eigenen Server nutzen – dann hast du die volle Kontrolle über alle Einstellungen. Noch mehr Spaß wird dir dein eigener Server machen, wenn du im Laufe des Buches lernst, immer ausgefeiltere Plugins für ihn zu programmieren, mit denen du Minecraft nach deinen Vorstellungen erweitern kannst. Um deinen eigenen Server zu betreiben, benötigst du neben dem normalen Minecraft-Spiel, das auch Clientgenannt wird, noch ein weiteres Programm, nämlich den Minecraft-Server. Den »normalen« Minecraft-Server, manchmal auch »Vanilla-Server« genannt, kannst du auf der offiziellen Minecraft Webseite www.minecraft.net herunterladen. Neben dieser Version gibt es aber auch noch zahlreiche sogenannte Mods, also Modifikationen des OriginalServers. Als Mods oder Modifikationen bezeichnet man im Zusammenhang mit Spielen Versionen eines Spiels, die in irgendeiner Form verändert, also modifiziert wurden. Diese meist von Fans entwickelten Mods bieten häufig viele zusätzliche Funktionen und Annehmlichkeiten, über die der Vanilla-Server nicht verfügt, wie zum Beispiel auch die Möglichkeit eigene Plugins zu programmieren.
Merke
Das normale Minecraft-Spiel, das du auch startest, wenn du alleine spielst, wird Client genannt. Das Programm, das wir in diesem Kapitel installieren werden, das du benötigst, um mit Freunden zusammen spielen zu können, heißt hingegen Server.
Dieses Buch ist für gleich zwei der beliebtesten Server ausgelegt. Du kannst dich entscheiden zwischen dem CraftBukkit-Server, häufig auch einfach nur Bukkitgenannt, und dem Spigot-Server. Da der Spigot- auf dem Bukkit-Server aufbaut, funktionieren alle Plugins, die wir im Rahmen dieses Buches programmieren werden, auf beiden Servern. Der einzige Unterschied liegt in der Administration der Server, hier bietet Spigot mehr Möglichkeiten, ist dafür in der Bedienung aber auch etwas komplexer. Außerdem ist der Spigot-Server etwas effizienter, was bedeutet, dass er insbesondere etwas weniger Arbeitsspeicher (RAM) benötigt. Für Anfänger, die zum ersten Mal einen eigenen Server betreiben, ist es daher ratsam zunächst auf Bukkit zu setzen; wer schon Erfahrung mit der Verwaltung eines Minecraft-Servers hat, kann sich auch an Spigot herantrauen. Ein Wechsel ist ohnehin jederzeit möglich.
1.1 Java installieren Egal für welchen der beiden Server du dich entscheidest: Um sie später starten zu können, muss auf deinem Computer eine aktuelle Java-Versioninstalliert sein, denn die Server sind, wie auch Minecraft selbst, in Java programmiert. Um das zu testen, kannst du unter Windows die Eingabeaufforderungöffnen, indem du den Namen einfach in das Suchfeld im Startmenü eingibst, beziehungsweise unter GNU/Linux und macOS ein Terminal öffnest. Dort gibst du dann den Befehl java -version ein und bestätigst deine Eingabe mit der Enter -Taste. Sieht die darauf folgende Ausgabe aus wie in Abbildung 1.1 gezeigt, so ist Java bereits korrekt
auf deinem Computer installiert und du kannst zum nächsten Abschnitt springen.
Abb. 1.1: Ausgabe nach java -version
Bekommst du stattdessen eine Meldung angezeigt wie »Der Befehl "java" ist entweder falsch geschrieben oder konnte nicht gefunden werden.«, so ist Java noch nicht auf deinem Computer installiert. In diesem Fall kannst du unter buch.daniel-braun.com einen Link zum Download der aktuellsten Java-Version finden. Unter GNU/Linux kannst du Java direkt über den Paketmanager deiner Wahl installieren. Unter macOS und Windows lädst du zunächst ein gepacktes Verzeichnis herunter, das nach dem Download entpackt werden muss. Dieses Verzeichnis, das je nach Version zum Beispiel den Namen jdk-17.0.1 trägt, kopierst du unter macOS in den Ordner /library/Java/JavaVirtualMachines/ und unter Windows in ein beliebiges Verzeichnis deiner Wahl, zum Beispiel direkt in C:\. Unter Windows musst du dieses Verzeichnis dann noch zur sogenannten PATH-Variablehinzufügen. Dazu öffnest du zunächst die erweiterten Systemeinstellungen deines Computers. Windows XP, Vista und 7: Unter Windows XP, Vista und 7 öffnest du dafür zunächst das Startmenü und dann die Systemsteuerung. Dort wählst du aus der Kategorie System und Sicherheit den Eintrag System aus und im sich danach öffnenden Fenster den Eintrag Erweiterte Systemeinstellungen. Windows 8, 10 und 11: Unter Windows 8, 10 und 11 kannst du die erweiterten Systemeinstellungen öffnen, indem du den Begriff einfach direkt in die Suche eingibst. Alternativ kannst du auch zunächst mit der rechten Maustaste auf das Windows-Logo in der
unteren linken Ecke klicken und dort dann auf System und in dem sich öffnenden Fenster wieder auf Erweiterte Systemeinstellungen. Nun solltest du, unabhängig von deiner verwendeten WindowsVersion, das in Abbildung 1.2 gezeigte Fenster sehen.
Abb. 1.2: Erweiterte Systemeinstellungen
Dort findest du in der rechten unteren Ecke einen Button mit der Beschriftung Umgebungsvariablen. Bei einem Klick darauf öffnet sich das in Abbildung 1.3 gezeigte Fenster.
Abb. 1.3: Umgebungsvariablen
Dann wählst du, wie in Abbildung 1.3 gezeigt, den Eintrag PATH aus und klickst anschließend auf Bearbeiten. Sollte der Eintrag nicht vorhanden sein, so kannst du direkt zum nächsten Absatz springen. Danach öffnet sich ein langes Textfeld, in dem es schon zahlreiche Einträge gibt, die auf keinen Fall geändert werden dürfen. Stattdessen solltest du am Ende, abgetrennt durch ein Semikolon, den Pfad angeben, an den du zuvor das entpackte Verzeichnis kopiert hast, gefolgt von \bin\. Also zum Beispiel: C:\jdk-17.0.1\bin\;
Je nachdem, welche Java-Version du installiert hast, kann der Pfad aber, insbesondere bei der Versionsnummer, leicht abweichen.
Daher solltest du unbedingt darauf achten, den tatsächlichen Installationspfad zu nutzen. Danach musst du die Änderungen nur noch mit OK und Übernehmen bestätigen. Sollte es bei dir noch keinen Eintrag mit dem Namen PATH geben, so kannst du diesen ganz einfach selbst anlegen. Dazu klickst du statt auf Bearbeiten einfach auf Neu. Im Fenster, das sich daraufhin öffnet, gibst du als Name der Variablen das Wort PATH ein und als Wert der Variablen den Pfad zur Installation, beendet durch ein Semikolon, und bestätigst deine Eingabe mit OK. Anschließend sollte der Befehl java -version dann in der Eingabeaufforderung funktionieren.
Hinweis Sollte beim späteren starten des Servers die in Abbildung 1.4 gezeigte Fehlermeldung erscheinen, so ist die auf deinem Computer installiere Java-Version veraltet. Bitte führe die oben beschriebenen Schritte dann mit der neusten Java-Version durch.
Abb. 1.4: Fehlermeldung nach dem Starten des Servers bei veralteter JavaVersion
1.2 Installation An dieser Stelle musst du dich nun entscheiden, welchen Server du zum Testen deiner Plugins verwenden möchtest. Wenn du dich für den Bukkit-Server entscheidest, kannst du in Abschnitt 1.2.1 weiterlesen, möchtest du lieber den Spigot-Server verwenden, dann kannst du direkt zu Abschnitt 1.2.2 springen.
1.2.1 CraftBukkit Einen Link zum Download der neusten Version des Bukkit-Servers findest auf der Website zum Buch unter buch.daniel-braun.com. Dabei handelt es sich um eine einzelne sogenannte Jar-Datei, die, je nach Version, zum Beispiel den Namen craftbukkit-1.18.jar trägt. Zunächst solltest du einen leeren Ordner anlegen, in den du diese Datei kopierst. Prinzipiell kannst du diesen Ordner nennen, wie du möchtest, im Verlaufe des Buches werden wir davon ausgehen, dass der Ordner den Namen server trägt und in C:\server unter Windows, /home/Benutzername/server unter GNU/Linux beziehungsweise /Users/Benutzername/server unter OS X, abgelegt ist. Um den Server nun zum ersten Mal zu starten, musst du zunächst wieder die Eingabeaufforderung beziehungsweise ein Terminal öffnen, und in den Server-Ordner wechseln. Das kannst du mithilfe des Befehls cd. Die englische Abkürzung steht für »change directory«, also »Ordner wechseln«, und genau das, also zwischen verschiedenen Ordnern hin- und herwechseln, kann man mit diesem Befehl auch tun. Unter Windows gibst du also zum Beispiel cd C:\server ein und unter GNU/Linux cd /home/Benutername/server. Bist du erst einmal im richtigen Ordner, so kannst du den Server mit dem Befehl java -jar craftbukkit-1.18.jar starten. Beim ersten Starten wirst du aber zunächst einmal nur die in Abbildung 1.5 gezeigten Warnhinweise sehen.
Abb. 1.5: Ausgabe nach dem ersten Starten des Servers
Merke Der Server wird mit dem Befehl java -jar craftbukkit-1.18.jar gestartet. Achte darauf, die Versionsnummer im Befehl an die von dir verwendete Server-Version anzupassen.
Dort steht im Wesentlichen, dass du zunächst den Nutzungsbedingungen zustimmen musst, bevor du den Server verwenden kannst. Wenn du jetzt einen Blick in deinen ServerOrdner wirfst, dann wird dir auffallen, dass es dort, wie in Abbildung 1.6, nun zwei weitere Dateien und einen Ordner gibt.
Abb. 1.6: Inhalt des Server-Ordners nach dem ersten Start
Tipp Sollte beim Starten des Servers folgender Hinweis angezeigt werden *** Error, this build is outdated ***
*** Please download a new build as per instructions from https://www.spigotmc.org/go/outdated-spigot ***
*** Server will start in 20 seconds ***
dann bedeutet das, dass du nicht die aktuellste Version des Servers benutzt. Du kannst den Server trotzdem weiterhin wie gewohnt nutzen oder eine neuere Version aus dem Internet herunterladen.
Um den Nutzungsbedingungen zuzustimmen, musst du die dort nun vorhandene Datei eula.txt öffnen. In dieser Datei findest du auch einen Link, unter dem du die Bedingungen lesen kannst. Wenn du diesen Link öffnest, wirst du auf die offizielle Seite des MinecraftHerstellers Mojang geleitet, wo du die Nutzungsbedingungen glücklicherweise auch auf Deutsch vorfindest. Dort wird geregelt, was du mit dem Spiel und dem Server machen darfst – und was nicht. Außerdem steht dort auch explizit, dass du, solltest du unter 18 sein, die Zustimmung eines gesetzlichen Vertreters einholen musst, also zum Beispiel eines Elternteils. Auf jeden Fall solltest du die Bedingungen sorgfältig lesen. Den Inhalt der eula.txt findest du auch in Listing 1.1. Bist du mit den Bedingungen einverstanden, so kannst du dies kenntlich machen, indem du die letzte Zeile der Datei von eula=false zu eula=true änderst. Nur wenn du das tust, kannst du den Server benutzen. Genau das wird in der ersten Zeile der Datei auf Englisch erklärt. #By changing the setting below to TRUE you are indicating your agreement to our EULA
(https://account.mojang.com/documents/minecraft_eula).
#Mon Apr 01 13:37:00 BST 2020
eula=false
Listing 1.1: Inhalt der Datei eula.txt
Wenn du die Änderungen gespeichert hast, kannst du wieder versuchen, den Server mit dem Befehl java -jar craftbukkit1.18.jar zu starten. Der Startvorgang wird dieses Mal wahrscheinlich eine Weile dauern und es werden sehr viele Zeilen relativ schnell über den Bildschirm laufen. Wichtig ist besonders die letzte Zeile. Steht dort so etwas wie Done (13,370s)! For help, type "help" or "?", dann bedeutet das, dass dein Server nun problemlos läuft. Ein erneuter Blick in den Server-Ordner wird dir zeigen, dass es dort nun, wie in Abbildung 1.7 zu sehen, noch einmal deutlich mehr Dateien gibt.
Abb. 1.7: Inhalt des Server-Ordners nach erfolgreichem Starten des Servers
Hinweis
Der Server läuft nur, solange das entsprechende Fenster der Eingabeaufforderung beziehungsweise des Terminals geöffnet bleibt. Schließt du das Fenster, so wird auch der Server geschlossen.
1.2.2 Spigot Einen Link zum Download der neusten Version des Spigot-Servers findest auf der Website zum Buch unter buch.daniel-braun.com. Dabei handelt es sich um eine einzelne sogenannte Jar-Datei, die, je nach Version, zum Beispiel den Namen spigot-1.18.jar trägt. Zunächst solltest du einen leeren Ordner anlegen, in den du diese Datei kopierst. Prinzipiell kannst du diesen Ordner nennen, wie du möchtest, im Verlaufe des Buches werden wir davon ausgehen, dass der Ordner den Namen server trägt und unter Windows in C:\server, unter GNU/Linux in /home/Benutzername/server beziehungsweise unter OS X in /Users/Benutzername/server abgelegt ist. Um den Server zum ersten Mal zu starten, musst du zunächst wieder die Eingabeaufforderung beziehungsweise ein Terminal öffnen, und in den Server-Ordner wechseln. Das kannst du mithilfe des Befehls cd. Unter Windows gibst du also zum Beispiel cd C:\server ein und unter GNU/Linux cd /home/Benutername/server. Bist du erst einmal im richtigen Ordner, so kannst du den Server mit dem Befehl java -jar spigot-1.18.jar starten. Beim ersten Starten wirst du aber zunächst einmal nur die in Abbildung 1.8 gezeigten Warnhinweise sehen.
Abb. 1.8: Ausgabe nach dem ersten Starten des Servers
Merke Der Server wird mit dem Befehl java -jar spigot-1.18.jar gestartet. Achte darauf, die Versionsnummer im Befehl an die von dir verwendete Server-Version anzupassen.
Dort steht im Wesentlichen, dass du zunächst den Nutzungsbedingungen zustimmen musst, bevor du den Server verwenden kannst. Wenn du jetzt einen Blick in deinen ServerOrdner wirfst, dann wird dir auffallen, dass es dort, wie in Abbildung 1.9, nun zwei weitere Dateien und einen Ordner gibt.
Abb. 1.9: Inhalt des Server-Ordners nach dem ersten Start
Um den Nutzungsbedingungen zuzustimmen, musst du die dort nun vorhandene Datei eula.txt öffnen. In dieser Datei findest du auch
einen Link, unter dem du die Bedingungen lesen kannst. Wenn du diesen Link öffnest, wirst du auf die offizielle Seite des MinecraftHerstellers Mojang geleitet, wo du die Nutzungsbedingungen glücklicherweise auch auf Deutsch vorfindest. Dort wird geregelt, was du mit dem Spiel und dem Server machen darfst – und was nicht. Außerdem steht dort auch explizit, dass du, solltest du unter 18 sein, die Zustimmung eines gesetzlichen Vertreters einholen musst, also zum Beispiel eines Elternteils. Auf jeden Fall solltest du die Bedingungen sorgfältig lesen. Den Inhalt der eula.txt findest du auch in Listing 1.1. Bist du mit den Bedingungen einverstanden, so kannst du dies kenntlich machen, indem du die letzte Zeile der Datei von eula=false zu eula=true änderst. Nur wenn du das tust, kannst du den Server benutzen. Genau das wird in der ersten Zeile der Datei auf Englisch erklärt. Wenn du die Änderungen gespeichert hast kannst du wieder versuchen, den Server mit dem Befehl java -jar spigot-1.18.jar zu starten. Der Startvorgang wird dieses Mal wahrscheinlich eine Weile dauern und es werden sehr viele Zeilen relativ schnell über den Bildschirm laufen. Wichtig ist besonders die letzte Zeile. Steht dort so etwas wie Done (13,370s)! For help, type "help" or "?", dann bedeutet das, dass dein Server nun problemlos läuft. Ein erneuter Blick in den Server-Ordner wird dir zeigen, dass es dort nun, wie in Abbildung 1.10 zu sehen, noch einmal deutlich mehr Dateien gibt.
Abb. 1.10: Inhalt des Server-Ordners nach erfolgreichem Starten des Servers
1.3 Konfiguration Die Konfiguration der Server funktioniert für beide Versionen sehr ähnlich. Größter und offensichtlicher Unterschied ist es hier, dass der Spigot-Server über eine zusätzliche Datei, die spigot.yml, verfügt. In den Ordnern world, world_nether und world_the_end werden, unabhängig vom verwendeten Server, Informationen über die Spielwelt gespeichert. Im Ordner logs werden sogenannte LogDateiengespeichert, diese Dateien enthalten im Wesentlichen alle Informationen, die dir auch in der Eingabeaufforderung beziehungsweise dem Terminal angezeigt werden. Das ist besonders später beim Programmieren von Plugins praktisch, denn sollte es einmal zu einem Fehler kommen, so kannst du die genaue Fehlermeldung hier in Ruhe nachlesen. Der Ordner plugins ist zu Beginn noch leer, hier werden wir später unsere selbstgeschriebenen Plugins speichern.
Zunächst einmal interessieren uns aber vor allem die zahlreichen .properties-, .json- und .yml-Dateien, die erzeugt wurden. Mit diesen kannst du deinen Server nämlich konfigurieren und ihn nach deinen Wünschen anpassen. server.properties Die wichtigsten Grundeinstellungen findest du in der Datei server.properties. 35 verschiedene Einstellungen kannst du hier insgesamt vornehmen. Welche das sind, kannst du in Tabelle 1.1 sehen. Am Anfang kannst du aber ruhig auch alle Einstellungen unverändert lassen, dann wird dein Server auf jeden Fall problemlos funktionieren.
Einstellung
Erklärung
spawnprotection=16
Legt fest, in welchem Radius um den SpawnPunkt Blöcke unzerstörbar sind.
generatorsettings=
Ist der Welttyp FLAT oder CUSTOMIZED (s. level-type), können hier Optionen für die Generierung festgelegt werden. Für den Welttyp FLAT erzeugt zum Beispiel 3;minecraft:bedrock, 2*minecraft: dirt,minecraft:grass;1;village eine
mit Dörfern
Ebene
Einstellung
Erklärung
op-permissionlevel=4
Bestimmt welche Rechte Nutzer mit dem Status Operator haben (1 = können geschützten Spawnbereich verändern, 2 = können Befehlsblöcke editieren und ChatBefehle ausführen, 3 = können Spieler verbannen, kicken und zum Operator ernennen, 4 = können den Server stoppen)
allow-nether=true
Aktiviert (true) oder deaktiviert (false) Neather-Portale
level-name=world
Der Name des Ordners, in dem sich die WeltDatei befindet
enablequery=false
Aktiviert (true) oder deaktiviert (false) die Schnittstelle zum Abfragen von ServerInformationen
allowflight=false
Erlaubt (true) oder verbietet (false) Spielern im Überlebensmodus zu fliegen
announce-playerachievements=true
Aktiviert (true) oder deaktiviert (false) Nachrichten an alle Spieler, wenn ein Spieler ein Achievement erzielt
server-port=25565
Legt den Port des Servers fest
Einstellung
Erklärung
max-worldsize=29999984
Legt die Größe der Welt fest (maximal 30.000.000, größere Werte werden ignoriert)
leveltype=DEFAULT
Legt den Welttyp fest (DEFAULT = Standardwelt, FLAT = komplett flache Welt, LARGEBIOMES = große Biome, AMPLIFIED = Welt mit extremen Höhenunterschieden, CUSTOMIZED = individuelle Welt nach den Einstellungen in generator-settings)
enable-rcon=false
Aktiviert (true) oder deaktiviert (false) den Fernzugriff auf die Server-Konsole
level-seed=
Erlaubt die manuelle Eingabe eines Startwerts (Seed) für die Generierung der Welt
forcegamemode=false
Legt fest, ob Spieler beim Betreten in den Spielmodus zurückkehren, in dem sie den Server verlassen haben (false) oder immer im Standardmodus (true) starten
server-ip=
Soll der Server nur unter einer bestimmten IP erreichbar sein, so kann diese hier eingetragen werden
Einstellung
Erklärung
networkcompressionthreshold=256
Legt die Kompressionsstärke der Datenübertragung fest
max-buildheight=256
Legt die maximale Bauhöhe fest
spawn-npcs=true
Aktiviert (true) oder deaktiviert (false) die Generierung von Dorfbewohnern
white-list=false
Legt fest, ob nur Spieler, die sich auf der Whitelist befinden, den Server betreten dürfen (true) oder alle Spieler, die nicht verbannt sind (false)
spawnanimals=true
Aktiviert (true) oder deaktiviert (false) die Generierung von Tieren
hardcore=false
Aktiviert (true) oder deaktiviert (false) den Hardcore-Modus (Spieler werden dauerhaft gebannt, sobald sie sterben)
snooperenabled=true
Aktiviert (true) oder deaktiviert (false) das Senden von anonymisierten Server-Daten an Mojang
Einstellung
Erklärung
resource-packsha1=
Prüfsumme des Ressourcenpakets, kann genutzt werden, um zu überprüfen, dass das Paket nicht verändert wurde
online-mode=true
Gleicht verbundene Spieler mit der Datenbank von Mojang ab, falls aktiviert (true). Verhindert Fake-Accounts
resource-pack=
Legt das empfohlene Ressourcenpaket des Servers fest
pvp=true
Legt fest, ob sich Spieler gegenseitig angreifen können (true) oder nicht (false)
difficulty=1
Legt den Schwierigkeitsgrad fest, von 0 (friedlich) bis 3 (schwer)
enable-commandblock=false
Aktiviert (true) oder deaktiviert (false) Befehlsblöcke
gamemode=0
Legt den Spielmodus fest (0 = Überlebensmodus, 1 = Kreativmodus, 2 = Abenteuermodus, 3 = Zuschauermodus)
Einstellung
Erklärung
player-idletimeout=0
Legt fest, nach wie vielen Minuten inaktive Spieler vom Server gekickt werden (0 = überhaupt nicht)
max-players=20
Legt die Zahl der maximal auf dem Server erlaubten Spieler fest
max-ticktime=60000
Schaltet den Server automatisch ab, wenn zwischen zwei Aktualisierungen (ticks) mehr als die angegebene Zahl von Millisekunden vergeht (-1 = deaktiviert)
spawnmonsters=true
Aktiviert (true) oder deaktiviert (false) die Generierung von Monstern
generatestructures=true
Aktiviert (true) oder deaktiviert (false) die Generierung von Dörfern, Tempeln und anderen Gebäuden
view-distance=10
Legt die Sichtweite fest
motd=A Minecraft Server
Text, der in der Serverliste als Beschreibung angezeigt wird
Tabelle 1.1: Einstellungsmöglichkeiten der server.properties
bukkit.yml Die zweite wichtige Datei mit Einstellungen, die, trotz des Namens, sowohl bei Bukkit als auch bei Spigot vorhanden ist, ist die bukkit.yml. Sie bietet noch einmal 24 weitere Einstellmöglichkeiten, die du in Tabelle 1.2 finden kannst.
Einstellung
Erklärung
allow-end: true
Aktiviert (true) oder deaktiviert (false) Endportale
warn-onoverload: true
Aktiviert (true) oder deaktiviert (false) Warnhinweis bei Überlastung des Servers
permissionsfile: permissions.yml
Dateiname der Datei, die die Berechtigungen festlegt
update-folder: update
Legt den Ordner (im Plugin-Ordner) fest, in dem Updates für Plugins gespeichert werden
pluginprofiling: false
Aktiviert (true) oder deaktiviert (false) den Befehl /timings
connectionthrottle: 4000
Zeit in Millisekunden, bevor ein Client sich nach einer Trennung wieder verbinden darf
Einstellung
Erklärung
query-plugins: true
Aktiviert (true) oder deaktiviert (false) den Remote-Zugriff auf die Plugin-Liste
deprecatedverbose: default
Aktiviert (true) oder deaktiviert (false) Warnhinweis bei Plugins, die veraltete Methoden verwenden.
shutdownmessage: Server closed
Legt die Nachricht fest, die beim Schließen des Servers an die Spieler gesendet wird
monsters: 70
Legt die Zahl der Monster fest, die in der Welt spawnen können
animals: 15
Legt die Zahl der Tiere fest, die in der Welt spawnen können
water-animals: 5
Legt die Zahl der Wassertiere fest, die in der Welt spawnen können
ambient: 15
Legt die Zahl der »Ambient«-Kreaturen fest, die in der Welt spawnen können (zurzeit nur Fledermäuse)
Einstellung
Erklärung
period-inticks: 600
Legt fest, in welchen Abständen (in Ticks) geprüft wird, ob Chunks aus dem Speicher entfernt werden können
load-threshold: 0
Zahl der Chunks, die geladen sein müssen, bevor versucht wird, ältere Chunks aus dem Speicher zu entfernen.
animal-spawns: 400
Legt fest, in welchen Abständen (in Ticks) der Server versucht Tiere zu spawnen.
monster-spawns: 1
Legt fest, in welchen Abständen (in Ticks) der Server versucht Monster zu spawnen
autosave: 6000
Legt die Zahl von Ticks fest, nach denen die Inhalte des Servers gespeichert werden (6000 entspricht ca. alle 5 Minuten)
aliases: nowin-commands.yml
Gibt an, in welcher Datei alternative Namen für Befehle festgelegt werden
username: bukkit
Legt den Nutzernamen für Datenbankzugriff fest
isolation: SERIALIZABLE
Datenbankeinstellung, die nicht verändert werden sollte
Einstellung
Erklärung
driver: org.sqlite.JDBC
Verwendeter Treiber für die Verbindung zur Datenbank
password: walrus
Leg das Passwort für Datenbankzugriff fest
url: jdbc:sqlite: {DIR}{NAME}.db
Adresse der Datenbank
Tabelle 1.2: Einstellungsmöglichkeiten bukkit.yml
spigot.yml Wem diese fast 60 Einstellungsmöglichkeiten noch nicht kompliziert genug sind, der kann in der spigot.ymlnoch fast 100 weitere Einstellungen vornehmen, vorausgesetzt, man verwendet den Spigot-Server, denn nur der verfügt über diese Datei. Das sind so viele, dass an dieser Stelle nicht einzeln auf alle eingegangen werden kann. Zudem handelt es sich bei den meisten Optionen um Detaileinstellungen, die du vermutlich niemals benötigen wirst. Folgende sechs Einstellmöglichkeiten könnten aber durchaus interessant für dich sein: whitelist, unknown-command, server-full, outdated-client, outdated-server und restart. Mit diesen sechs Befehlen kannst du die Nachrichten festlegen, die an einen Spieler geschickt werden, wenn er sich nicht auf der Whitelist befindet, einen unbekannten Befehl eingibt, der Server voll ist, der Client des Spielers veraltet ist, der Server veraltet ist oder der Server neu gestartet wird. Mit eigenen Nachrichten kannst du deinem Server schnell und unkompliziert eine persönliche Note verleihen.
banned-ips.json, banned-players.json, ops.json, whitelist.json Die Dateien banned-ips.json, banned-players.json, ops.json, whitelist.json gibt es wieder unabhängig davon, welchen Server du verwendest. In ihnen wird gespeichert, welche IPs und Spieler vom Server verbannt wurden, welche Spieler Administratoren oder genauer gesagt Operatoren sind und welche Spieler sich auf der Whitelist befinden. Wie so eine Datei zum Beispiel aussehen kann, zeigt Listing 1.2. Die dort dargestellte whitelist.json würde es nur einem Spieler, dem mit dem Namen »notch«, erlauben auf dem Server zu spielen. 1 2 3 4 5 6
[
]
{
}
"uuid": "8d15678-a7f3-1234-8d11-c2ab1234dc9",
"name": "notch"
Listing 1.2: Beispielinhalt whitelist.json
Alle vier Dateien zum Beispiel sind nach diesem Prinzip aufgebaut und können theoretisch auch von Hand verwaltet werden, vorausgesetzt, du kennst die uuid des Spielers, also seine eindeutige Benutzeridentifizierung, den du zu einer Liste hinzufügen möchtest. Allerdings ist das überhaupt nicht notwendig, denn viel bequemer lassen sich all diese Listen durch die Eingabe von Befehlen im Server verwalten. Wie genau das funktioniert, darum soll es im nächsten Abschnitt gehen.
1.4 Befehle Einige Befehlekennst du vermutlich schon aus dem Einzelspielermodus von Minecraft. Wenn du im Spiel mit der T -Taste den Chat öffnest, stehen dir verschiedene Befehle oder Cheats, zur Verfügung, mit denen du die Welt beeinflussen kannst. Mit /weather
kannst du es zum Beispiel regnen lassen, mit /time kannst du die Nacht zum Tag machen. rain
set day
Alle Befehle, die du bereits aus dem Einzelspielermodus kennst, funktionieren auch auf deinem Server. Du kannst sie sogar direkt in dein geöffnetes Server-Fenster eingeben, dann allerdings ohne den Schrägstrich am Anfang, also zum Beispiel weather rain statt /weather rain. Wie das aussieht, kannst du in Abbildung 1.11 sehen.
Abb. 1.11: Befehlseingabe im Server-Fenster
Darüber hinaus stehen dir aber noch weitere Befehle zur Verfügung, die dir bei der Verwaltung deines Servers helfen. Mit dem Befehl help bekommst du eine Liste aller verfügbaren Befehle angezeigt, die wichtigsten von ihnen findest du in alphabetischer Reihenfolge in Tabelle 1.3.
Befehl
Beschreibung
/ban
Verbannt einen Spieler dauerhaft vom Server
/ban-ip
Verbannt eine IP-Adresse dauerhaft vom Server
/kick
Wirft einen Spieler temporär vom Server
Befehl
Beschreibung
/op
Gibt einem Spieler Administrationsrechte
/pardon
Hebt die Verbannung eines Spielers auf
/pardon-ip
Hebt die Verbannung einer IP-Adresse auf
/restart
Startet den Server neu
/say
Sendet eine Nachricht an alle Spieler
/spawnpoint
Setzt den Spawnpunkt an die angegebene Stelle
/stop
Schaltet den Server ab
/tell
Sendet eine private Nachricht an einen Spieler
/version
Zeigt die Versionsnummer des Servers an
/whitelist on
Erlaubt nur Spieler auf dem Server, die auf der Warteliste stehen
Befehl
Beschreibung
/whitelist off
Erlaubt alle Spieler auf dem Server, die nicht verbannt sind
/whitelist add
Fügt der Whitelist einen Spieler hinzu
/whitelist remove
Entfernt einen Spieler von der Whitelist
Tabelle 1.3: Liste der Server-Befehle
Merke Wenn du Befehle direkt ins Server-Fenster eingibst, muss der Schrägstrich am Anfang des Befehls weggelassen werden.
Spieler, die Operatoren sind, also mit op zur Liste der Operatoren hinzugefügt wurden, können diese Befehle auch direkt im Spiel, wie gewohnt über den Chat, verwenden.
1.5 Verbinden Inzwischen ist dein Server perfekt eingerichtet und konfiguriert, deshalb wird es jetzt langsam Zeit, ihn endlich einmal zu testen, indem du dich mit deinem Minecraft-Client darauf verbindest. Bevor
du das machst, solltest du noch einmal überprüfen, dass du alle vorherigen Schritte ausgeführt hast und dein Server auch läuft.
Merke Bevor du weiterliest, solltest du noch einmal überprüfen, ob du alle nötigen Installationsschritte ausgeführt hast: 1. Installation von Java 2. Herunterladen der Server-Datei von buch.daniel-braun.com 3. Neuen Ordner server anlegen und die Datei dorthin kopieren 4. Datei mit java
-jar
starten
5. Nutzungsbedingungen lesen und akzeptieren 6. Server erneut starten 7. Server-Fenster geöffnet lassen
Nachdem du das erledigt hast, kannst du Minecraft wie gewohnt starten. Im Hauptmenü wählst du dort dann den Eintrag Mehrspieler aus. Daraufhin öffnet sich das in Abbildung 1.12 gezeigte Menü.
Abb. 1.12: Mehrspieler-Menü
Dort klickst du nun auf den Button mit der Beschriftung Direkt verbinden, worauf sich die in Abbildung 1.13 gezeigte Maske öffnet. Wenn du den Server auf dem selben Computer laufen hast, auf dem auch der Client läuft, so muss als Serveradresse immer 127.0.0.1 angegeben werden. Damit sagst du Minecraft, dass sich Server und Client auf demselben Computer befinden. Nun musst du nur noch auf Server beitreten klicken – und schon solltest du in einer ganz neuen Welt auf deinem eigenen Server stehen.
Abb. 1.13: Direkt Verbinden-Ansicht
Herzlichen Glückwunsch, dein Server funktioniert! So richtig Spaß macht ein Server natürlich erst, wenn sich mehrere Spieler darauf befinden. Damit sich deine Freunde in einem lokalen Netzwerk, also zum Beispiel bei dir zu Hause im WLAN, auf deinen Server verbinden können, musst du ihnen die IP-Adressedeines Computers geben. Unter Windows kannst du diese herausfinden, indem du ipconfig in der Eingabeaufforderung eingibst, unter Linux und OS X lautet der entsprechende Befehl ifconfig. In Abbildung 1.14 ist die gesuchte Server-Adresse zum Beispiel 192.168.2.84.
Abb. 1.14: Ergebnis des Befehls ipconfig
Wenn du nun an einem anderen Computer ebenfalls den MinecraftClient öffnest und diese IP-Adresse in der Direkt VerbindenAnsicht als Serveradresse eingibst, kannst du mit deinen Freunden zusammen auf deinem Server spielen. Mit einem kleinen Trick kannst du es dir übrigens sparen, die IPAdresse bei jedem Verbinden neu eingeben zu müssen. Dazu wählst du im Mehrspieler-Menü, zu sehen in Abbildung 1.12, den Button Server hinzufügen aus. Daraufhin öffnet sich der in Abbildung 1.15 gezeigte Dialog.
Abb. 1.15: Server hinzufügen
Hier kannst du nun wie gewohnt die Serveradresse angeben, zusätzlich aber auch einen Servernamen. Sobald du deine Eingabe mit Fertig bestätigst, gelangst du automatisch zurück in das Mehrspieler-Menü. Hier findest du, wie in Abbildung 1.16 gezeigt, einen eigenen Eintrag für den eben hinzugefügten Server, zusammen mit Informationen über die Spielerzahl und die Verbindungsqualität. In Zukunft kannst du dich dann immer direkt mit einem Klick auf den entsprechenden Eintrag mit deinem eigenen Server verbinden, ganz ohne Adresseingabe.
Tipp Sollte der Server sehr langsam sein oder die Fehlermeldung Exception: java.lang.OutOfMemoryError: Java heap space
angezeigt werden, so ist der Arbeitsspeicher, der dem Server zur Verfügung steht, zu gering. Um den verfügbaren Arbeitsspeicher zu vergrößern, kann der Server mit java -Xms1024M -Xmx1024M jar craftbukkit-1.18.jar, beziehungsweise java -Xms1024M Xmx1024M -jar spigot-1.18.jar, gestartet werden. Achte auch hier darauf, die Versionsnummer im Befehl an die von dir verwendete Server-Version anzupassen.
Abb. 1.16: Mehrspieler-Menü mit gespeichertem Server
1.6 Updates Damit hast du den Server, den du später benötigst, um deine Plugins zu testen, fertig aufgesetzt und erfolgreich erprobt. Zum Abschluss dieses Kapitels noch ein Hinweis zum Thema Updates: Wie du sicher weißt, ist Minecraft ein lebendiges Spiel, das sich ständig weiterentwickelt. Diese Änderungen betreffen nicht nur das Spiel selbst, also den Client, sondern auch den Server. Jedes Mal wenn
es ein größeres Minecraft-Update gibt, das neue Funktionen mitbringt, muss deshalb auch der Server aktualisiert werden, egal ob du einen Mod oder den originalen Server verwendest. Da es sich bei Bukkit und Spigot, wie bei fast allen Mods, um reine Fanprojekte handelt, können schon einmal einige Tage vergehen, bis ein neues Update auch Eingang in den Mod findet. Um in der Zwischenzeit trotzdem auf deinem Server spielen zu können, musst du dich eines kleinen Tricks bedienen. Wenn du im Minecraft-Launcher, also dem Fenster, das sich vor dem Start des eigentlichen Spiels nach einem Doppelklick auf das Minecraft-Icon öffnet, auf Installationen klickst, und dort dann auf Neu..., so öffnet sich das in Abbildung 1.17 gezeigte Fenster, das unter anderem eine Versionsauswahl anbietet.
Abb. 1.17: Versionsauswahl im Minecraft-Launcher
Sollte dein Server einmal nicht auf dem neusten Stand sein, so kannst du aus dieser Liste einfach die Version auswählen, mit der dein Server läuft, und problemlos weiterhin auf ihm spielen. Sobald dein Server aktualisiert wurde, kannst du diese Einstellung selbstverständlich wieder zurücksetzen.
Kapitel 2
Python Ob bewusst oder unbewusst, eine der wichtigsten Entscheidungen, die man auf dem Weg zum Programmierer zu treffen hat, hast du bereits getroffen: welche Programmiersprachedu lernen möchtest. Mit diesem Buch hast du dich nämlich für die Programmiersprache Pythonentschieden. Bevor wir aber einen Blick auf die Besonderheiten von Python werfen, soll es zunächst um die Frage gehen, was Programmiersprachen eigentlich sind und warum sie benötigt werden.
2.1 Programmiersprachen Beim Programmieren geht es im Wesentlichen darum, dass der Programmierer dem Computer eine bestimmte Aufgabe gibt, die dieser erledigen soll. Damit er das kann, braucht der Computer eine genaue Handlungsvorschrift, die auch Algorithmusgenannt wird. Auch im Alltag begegnen uns oft Handlungsvorschriften, zum Beispiel in Form eines Rezepts: 1. 250 Gramm Mehl in eine Schüssel geben 2. 500 Milliliter Milch dazugeben 3. 2 Eier hinzugeben 4. Mit einer Prise Salz würzen 5. Umrühren Fertig ist der Crêpes-Teig! Damit eine Handlungsvorschrift korrekt ausgeführt werden kann, müssen sich beide Seiten auf eine
gemeinsame Sprache einigen. Wenn dir jemand ein chinesisches Rezept gibt, kannst du vermutlich nicht viel damit anfangen. Computer »sprechen« in Einsen und Nullen, also in einer Sprache, mit der Menschen nicht besonders gut umgehen können. Unsere Sprache wiederum, egal ob es sich um Deutsch, Englisch oder Chinesisch handelt, ist für den Computer viel zu ungenau. Nehmen wir zum Beispiel den Satz »Da vorne ist eine Bank.« Obwohl es sich dabei um einen vollkommen korrekten deutschen Satz handelt, ist doch nicht eindeutig klar, was mit dem Satz eigentlich gemeint ist. Steht da vorne eine Parkbank, auf die man sich setzen kann, oder ist dort die Filiale einer Bank, auf der man Geld einzahlen und abheben kann? Es wäre ein recht kostspieliger Fehler, wenn dein Computer beim Online-Shoppen aus Versehen die Deutsche Bank statt einer Bank für den Garten kauft. Algorithmen müssen deshalb nicht nur Handlungsvorschriften sein, sondern eindeutige Handlungsvorschriften. Auch mit Begriffen wie »eine Prise« kann ein Computer wenig anfangen, weil sie einfach zu ungenau sind. Aus diesem Grund nutzen wir Programmiersprachen, denn sie ermöglichen es uns, eindeutige Handlungsvorschriften festzulegen. Und obwohl sie auf den ersten Blick recht kompliziert scheinen, können wir sie doch leichter lernen als eine Sprache aus Nullen und Einsen. Viele Programmiersprachen, wie zum Beispiel C, verwenden sogenannte Compiler, die die Programmiersprache in Maschinensprache übersetzen. Andere, wie auch Python, verwenden einen sogenannten Interpreter. Wir werden in diesem Buch einen besonderen Interpreter mit dem Namen Jython verwenden. Ein Vorteil des Interpreters ist, dass er den Programmcode bei jedem Ausführen neu »interpretiert«, also für den Computer übersetzt, Deshalb werden Änderungen am Programmcode bei einem Interpreter sofort wirksam, während man bei einer Compilersprache den Quellcode zuerst neu kompilieren muss, bevor die Änderungen auch im Programm ankommen. Außerdem sind Interpretersprachen unabhängiger vom
Betriebssystem, deine fertigen Plugins kannst du daher später auch problemlos an Freunde weitergeben, egal ob sie Windows, GNU/Linux oder OS X nutzen.
Merke Ein Algorithmus ist eine eindeutige Handlungsvorschrift. Ein Interpreter übersetzt Programmiersprache bei Ausführung in Maschinensprache.
2.2 Besonderheiten von Python Python wurde Anfang der 1990er-Jahre vom niederländischen Informatiker Guido van Rossum entwickelt. Die Programmiersprache ist heute weit verbreitet und dafür bekannt, dass sie besonders leicht zu erlernen ist, genau die richtige Sprache also für den Einstieg in die Programmierung. Python gilt aber auch als besonders übersichtlich und minimalistisch. Gut sichtbar wird das am sogenannten »Hallo Welt!«-Programm. Programmierer nutzen »Hallo Welt!«-Programme gerne, um auf besonders einfache Weise den Aufbau einer Programmiersprache zu verdeutlichen, denn dieses kleine Programm tut nichts anderes, als den Text »Hallo Welt!« auf dem Bildschirm auszugeben. In Listing 2.1 kannst du den Code des »Hallo Welt!«-Programms in Java sehen, also der Programmiersprache, in der auch Minecraft geschrieben wurde. Das sieht schon ziemlich kompliziert aus, es gibt viele Wörter und auffällig viele sogenannte geschweifte Klammern, also { und }. Insgesamt hat das Programm eine Länge von fünf Zeilen.
1 class Hallo {
2 public static void main(String[] args) {
3 System.out.print("Hallo Welt!");
4 }
5 }
Listing 2.1: »Hallo Welt!« in Java
Zeilen und Zeilennummern, das wirst du später noch sehen, sind für Programmierer wichtige Informationen, denn wenn sie auf eine bestimmte Stelle in einem Programm hinweisen wollen, so tun sie das meist mithilfe der Zeilennummer. Oft sagt die Länge eines Programmes auch etwas über die Komplexität aus, zumindest aber über den Aufwand, deshalb sind Programmierer oft bemüht, Programme möglichst kurz zu halten. In Listing 2.2 kannst du den Code des »Hallo Welt!«-Programms in der Programmiersprache C sehen. Obwohl das Programm wegen anderer Formatierung, sogar eine Zeile länger ist sieht man doch sofort, dass es schon deutlich übersichtlicher ist als sein JavaPendant, selbst wenn man nicht weiß, wofür die Befehle eigentlichen stehen. 1 2 3 4 5 6
#include
int main(void)
{
puts("Hallo Welt!");
}
Listing 2.2: »Hallo Welt!« in C
Formatierung, also zum Beispiel das Einrücken von Zeilen, oder das Beginnen einer neuen Zeile, ist beim Programmieren extrem wichtig, das gilt für Python noch viel mehr als für jede andere Programmiersprache. Denn wie wir später noch sehen werden, macht die Einrückung einer Zeile in Python nicht nur einen optischen Unterschied, sondern kann auch die Funktionalität eines Programmes verändern.
Die Python-Version des »Hallo Welt!«-Programms siehst du in Listing 2.3. Mit gerade einmal einer Zeile ist es ohne Zweifel die kürzeste der drei Versionen. Auffallend ist auch die Abwesenheit von Sonderzeichen wie dem Semikolon und den geschweiften Klammern, die es in den beiden anderen Programmiersprachen gibt. print("Hallo Welt!")
Listing 2.3: »Hallo Welt!« in Python
Wie du also sehen kannst, wird dir Python das Leben als Programmierer nicht unnötig schwer machen, ganz im Gegenteil, es wird dir einen einfacheren Einstieg ermöglichen als die meisten anderen Programmiersprachen. Und obwohl die Arbeit mit Python vergleichsweise einfach ist, wirst du trotzdem im Verlaufe des Buches alle wichtigen Programmiertechniken kennenlernen, wie sie auch die anderen Programmiersprachen bieten. Da sich diese oft kaum zwischen den verschiedenen Sprachen unterscheiden, wird es dir, wenn du erst einmal Python gelernt hast, später nicht schwer fallen, noch weitere Programmiersprachen zu lernen, falls du das möchtest.
2.3 Einrichtung Wie bereits erwähnt, ist Minecraft selbst in Java programmiert. Daher benötigst du, bevor du Python-Plugins auf deinem Server ausführen kannst, einen Interpreter, der deinen Python-Code in Java übersetzt, und ein Plugin, das diesen Interpreter an deinen Server koppelt. Beides findest du auf der Website zum Buch, unter buch.daniel-braun.com zum Download.
2.3.1 Jython
Zunächst solltest du von dort den Interpreter herunterladen, dieser trägt den Namen Jython, ein Kofferwort aus Java und Python. Jython besteht lediglich aus einer einzelnen Datei, diese trägt den Namen jython.jar. Wenn du die Datei heruntergeladen hast, musst du nur noch einen neuen Ordner mit dem Namen lib in deinem Serververzeichnis erzeugen und die Datei dorthin kopieren. Sollte die Datei nach dem Download noch nicht den Namen jython.jar tragen, zum Beispiel, weil die Versionsnummer im Dateinamen enthalten ist, so muss sie zunächst umbenannt werden. Nur wenn die Datei den richtigen Namen trägt, kann sie vom Server gefunden werden. Damit ist die Einrichtung des Interpreters bereits abgeschlossen.
2.3.2 PPLoader Nun fehlt nur noch das Plugin, das den Interpreter mit deinem Server, egal ob Bukkit oder Spigot, verbindet. Das benötigte Plugin trägt den Namen PPLoader, was für Python Plugin Loader steht. Auch das Plugin besteht lediglich aus einer einzigen Datei, sie trägt den Namen pploader1.2.0.jar. Sie muss nach dem Download in den im Server-Ordner bereits vorhandenen Ordner plugins kopiert werden.
Merke Bevor du weiterliest, solltest du noch einmal überprüfen, ob du alle nötigen Schritte ausgeführt hast, um deinen Server auf die Verwendung von Python-Plugins vorzubereiten: 1. Jython herunterladen von buch.daniel-braun.com 2. Im Server-Ordner ein neues Verzeichnis lib anlegen und die jython.jar dorthin kopieren, wenn nötig, vorher umbenennen
3. PPLoader herunterladen von buch.daniel-braun.com 4. Die Datei pploader1.2.0.jar in den plugins-Ordner im Server-Verzeichnis kopieren
Um zu testen, ob alles funktioniert hat, kannst du den Server über die Eingabeaufforderung beziehungsweise das Terminal mit dem Befehl java -jar craftbukkit-1.18.jar, beziehungsweise java -jar spigot-1.18.jar, starten. Funktioniert alles ohne Probleme, so wird im Server-Fenster die in Abbildung 2.1 markierte Zeile angezeigt, die bestätigt, dass der PPLoader erfolgreich geladen wurde.
Abb. 2.1: Erfolgreich geladener PPLoader
2.4 Editor Bevor wir uns endgültig dem Programmieren von Plugins widmen, soll zunächst noch die Frage geklärt werden, welches Programm man eigentlich verwendet, um Python-Plugins zu schreiben. Grundsätzlich kannst du dazu nahezu jedes Programm verwenden, mit dem man Texte verfassen kann. Der mit Windows mitgelieferte Editor, den du im Startmenü unter Zubehör findest, ist zum Beispiel prinzipiell ausreichend. OS X bringt das Programm TextEdit mit, das sich im Ordner Programme befindet oder über die Suche gefunden werden kann. Solltest du dich für den Windows-Editor entscheiden, so musst du beim Speichern darauf achten, dass du als Dateityp den Eintrag Alle Dateien auswählst. Beim Programm TextEditsollte nach dem Neuanlegen eines Dokuments der Menüpunkt Format | In
reinen Text umwandeln ausgewählt werden. Und unabhängig davon, welches Programm du verwendest, solltest du beim Speichern an den Dateinamen die Endung .py anhängen, damit dein Computer weiß, dass es sich bei der gespeicherten Datei um Python-Code handelt. Du kannst dir das Leben aber auch deutlich einfacher machen, indem du einen Editor verwendest, der speziell für das Schreiben von Python-Code ausgelegt ist. Solche Editoren bieten oft zahlreiche Vorteile wie automatische Vervollständigung, farbliche Hervorhebung und weitere Komfortfunktionen. Im Internet kannst du dutzende, wenn nicht gar hunderte solcher Editoren finden. Eclipseund PyCharmsind zum Beispiel nur zwei bekanntere Vertreter. Für Einsteiger sehr empfehlenswert ist Komodo Edit, einen Link zum kostenlosen Download, für Windows, GNU/Linux und OS X, findest du auf der Website zum Buch unter buch.daniel-braun.com. Komo Edit bietet zahlreiche Komfortfunktionen, ist aber nicht so überladen wie zum Beispiel Eclipse und eignet sich daher besonders für Anfänger. Nach dem Download der Installationsdatei kann diese wie gewohnt mit einem Doppelklick geöffnet werden. Daraufhin öffnet sich die in Abbildung 2.2 gezeigte Installationsroutine.
Abb. 2.2: Schritt 1 der Installation
Dort kannst du mit einem Klick auf den Button Next zum zweiten Schritt der Installation springen, der in Abbildung 2.3 gezeigt ist.
Abb. 2.3: Schritt 2 der Installation
Hier werden nun die Lizenzbestimmungen angezeigt, die, nach ausführlicher Lektüre, mit einem Klick auf die Box neben I accept the terms in the License Agreement bestätigt werden müssen, damit der Editor installiert werden kann. Ein erneuter Klick auf den Next-Button bringt dich dann zum dritten und vorletzten Schritt der Installation, der in Abbildung 2.4 gezeigt ist.
Abb. 2.4: Schritt 3 der Installation
Hier kannst du nun noch auswählen, ob du ein Desktop-Icon und eine Startmenü-Verknüpfung einrichten möchtest. Auch dieses Fenster kannst du wieder mit einem Klick auf den Next-Button verlassen, was dich zum letzten Schritt der Installation führt. Hier musst du nun nur noch den Button Install drücken und schon wird Komodo Edit auf deinen Computer installiert. Beim ersten Starten des Programms erwartet dich dann das in Abbildung 2.5 gezeigte Fenster. Neben der Menüleiste am oberen Bildschirmrand besteht es im Wesentlichen aus zwei Bereichen. Auf der linken Seite werden dir die Dateien und Ordner auf deinem Computer angezeigt, so kannst du schnell und bequem auf verschiedene Dateien zugreifen, im
großen, dunklen Bereich auf der rechten Seite werden später geöffnete Datei angezeigt.
Abb. 2.5: Startbildschirm von Komodo Edit
Um eine neue Datei anzulegen, kannst du entweder in diesem Bereich auf New File klicken, oder oben in der Menüleiste auf das Blatt mit dem Pluszeichen. In Abbildung 2.6 kannst du sehen, wie Python-Code in Komodo Edit dargestellt wird. Der Inhalt ist an dieser Stelle noch egal, aber im Vergleich zum Beispiel zum Windows Editor sticht hier sofort die farbliche Hervorhebung ins Auge, außerdem gibt es eine Zeilennummerierung
und mithilfe der kleinen Dreiecke am linken Rand lassen sich einzelne Teile des Codes ein- und ausklappen. Noch mögen dir diese Unterschiede vielleicht banal erscheinen, aber je länger du programmierst, desto mehr wirst du diese kleinen Annehmlichkeiten schätzen lernen. Für den weiteren Verlauf des Buches ist es jedoch egal, für welchen Editor du dich entscheidest, denn in den folgenden Kapiteln werden wir uns ganz auf den Inhalt konzentrieren und der ist, unabhängig vom Editor, immer gleich.
Abb. 2.6: Python-Code in Komodo Edit
Auch bei Komodo Edit musst du beim Speichern von Dateien darauf achten, die korrekte Endung auszuwählen. Dateien speichern kannst du wie gewohnt über das Disketten-Symbol in der oberen Symbolleiste. Daraufhin öffnet sich der in Abbildung 2.7 gezeigte Speicherdialog. Als Dateityp musst du hier den Eintrag All Files auswählen und an den Dateinamen die Endung .py anhängen.
Abb. 2.7: Speicherdialog
Damit aber auch genug der Vorbereitungen, im nächsten Kapitel wirst du nun endlich dein erstes eigenes Plugin programmieren lernen. Und dank der bereits getroffenen Vorbereitungen wird das vermutlich schneller gehen, als du denkst.
Das erste Plugin Kapitel 3
Wie bereits im letzten Kapitel erwähnt, bieten »Hallo Welt!«Programme einen einfachen und schnellen Einstieg in eine Programmiersprache. Deshalb wird auch unser erstes Plugin ein »Hallo Welt!«-Plugin.
3.1 Ordner anlegen Zunächst solltest du für jedes Plugin, das du programmieren möchtest, einen neuen Ordner anlegen. Jedes Plugin muss sich in einem eigenen Unterordner innerhalb des plugins-Ordners im Serververzeichnis befinden. Der Name des Unterordners muss immer mit .py.direnden, davor kann ein beliebiger Name stehen, allerdings ohne Leer- oder Sonderzeichen. Für das »Hallo Welt!«Plugin könntest du den Ordner also zum Beispiel hallowelt.py.dir nennen.
Merke Jedes Plugin muss sich in einem eigenen Unterordner im pluginsOrdner des Serververzeichnisses befinden. Der Name dieser Unterordner muss immer mit .py.dir enden und darf keine Leeroder Sonderzeichen enthalten.
3.2 plugin.py
Im nächsten Schritt solltest du dann im Editor deiner Wahl eine neue Datei anlegen und sie mit den Inhalt aus Listing 3.1 füllen. Diese Datei musst du dann im eben angelegten Ordner hallowelt.py.dir unter dem Namen plugin.p y speichern. 1 2 3
class HalloWeltPlugin(PythonPlugin):
def onEnable(self):
self.getLogger().info("Hallo Welt!")
Listing 3.1: »Hallo Welt!«-Plugin
Tipp Es ist eine gute Übung, die Programme aus dem Buch abzutippen, da du dir so die verschiedenen Befehle schneller einprägen kannst. Allerdings musst du darauf achten, dabei sehr exakt vorzugehen, denn schon ein einziger Tippfehler kann dafür sorgen, dass dein Programm nicht mehr funktioniert. Möchtest du die Programme nicht abtippen, so findest du sie, wie bereits in der Einleitung erwähnt, unter buch.daniel-braun.com zum Download.
Das sind auf den ersten Blick erst einmal ziemlich viele Wörter und Zeichen, die wenig Sinn ergeben. Der Text in Listing 3.1 wird Programmcode, Sourcecode oder Quellcode genannt. Er enthält die Handlungsvorschriften, die der Computer ausführen soll. In diesem Fall also die Vorschrift: Zeige beim Starten des Servers die Nachricht »Hallo Welt!« an. Im Laufe der nächsten Kapitel wirst du alle Befehle genau kennen und verstehen lernen. Für den Anfang soll aber zunächst einmal ein grober Überblick reichen. Wie dir sicher schon aufgefallen ist, ist das Plugin mit gerade einmal drei Zeilen zwar immer noch sehr kurz, aber immerhin schon einmal zwei Zeilen länger als das »Hallo Welt!«-Programm in Python aus dem letzten Kapitel. In der ersten Zeile wird der sogenannte
Klassenname auf HalloWeltPlugin festgelegt, für den Anfang kannst du dir diesen einfach wie einen ganz normalen Namen vorstellen, mit dessen Hilfe das Plugin gerufen werden kann. Theoretisch gibt es für diesen Namen außer einem Verbot für Leer- und Sonderzeichen kaum Beschränkungen. In der Praxis gibt es unter Python-Programmierern aber einige ungeschriebene Gesetze oder Konventionen, die du beachten solltest. Zunächst einmal sollte der Name aussagekräftig sein, also eine gute Beschreibung dessen, was das Plugin tut. Außerdem sollten Klassennamen immer mit einem Großbuchstaben beginnen. Wenn sie sich, wie bei HalloWeltPlugin, aus mehreren Wörtern zusammensetzen, sollte außerdem der Anfangsbuchstabe jedes Wortes großgeschrieben werden, um die Lesbarkeit zu verbessern.
Merke Konventionen für Klassennamen: Aussagekräftig Beginnen mit einem Großbuchstaben Bei einer Zusammensetzung mehrerer Wörter wird der erste Buchstabe jedes Wortes großgeschrieben Keine Verwendung von Leer- und Sonderzeichen
In den Klammern hinter dem Klassennamen steht außerdem noch PythonPlugin, das sagt dem Computer, dass es sich bei diesem Python-Code um ein Minecraft-Python-Plugin handelt. In der zweiten Zeile wird die sogenannte onEnable-Funktion definiert. Was genau eine Funktion beim Programmieren ist, damit werden wir uns später noch ausführlich beschäftigen. onEnableist Englisch und
bedeutet so viel wie »beim Start«. Hier kannst du dem Plugin sagen, was es machen soll, wenn es gestartet wird. Wie du sehen kannst, ist die zweite Zeile eingerückt, sie steht also weiter rechts als die vorherige Zeile; das ist wichtig zu beachten, denn nur so funktioniert dein Plugin später auch. In Python zeigen solche Einrückungen nämlich an, dass die entsprechenden Zeilen zu etwas gehören. In diesem Fall also zum Beispiel, dass die Funktion onEnable zum Plugin mit dem Namen HalloWeltPlugin gehört. Wie weit man die Zeilen genau einrückt, ist eine Geschmacksfrage, allerdings sollte man zumindest konsistent bleiben, also die Zeilen immer gleich weit einrücken. In diesem Buch werden wir jeweils vier Leerzeichen zur Einrückung verwenden. In der dritten und letzten Zeile steht nun endlich, was das Plugin machen soll, nämlich den Text »Hallo Welt!« anzeigen.
3.3 plugin.yml Bevor du dein erstes Plugin testen kannst, benötigst du noch eine zweite Datei, die sogenannte plugin.yml. Diese besteht, wie du in Listing 3.2 sehen kannst, ebenfalls nur aus drei Zeilen. 1 2 3
name: Hallo Welt-Plugin
main: HalloWeltPlugin
version: 1.0
Listing 3.2: plugin.yml für das Hallo Welt!«-Plugin
Die plugin.yml gibt dem Server wichtige Informationen über dein Plugin, die er schon vor dessen Start benötigt. In der ersten Zeile wird mit dem Stichwort name der Name des Plugins festgelegt. Im Gegensatz zum Klassennamen kannst du deiner Kreativität hier freien Lauf lassen, auch Leer- und Sonderzeichen können verwendet werden.
Wichtig wird der Klassenname aber wieder in der zweiten Zeile, dort muss dieser nämlich hinter dem Stichwort main eingetragen werden und zwar exakt so, wie er in der Datei plugin.py steht, nur so weiß der Server später, wie er dein Plugin ausführen kann. Zu guter Letzt kannst du noch eine Versionsnummer für dein Plugin angeben, hinter dem Stichwort version. Solltest du eines deiner Plugins später einmal verändern oder weiterentwickeln, so kannst du – und andere, die es benutzen – mit der Versionsnummer leicht den Überblick behalten, ob eine Kopie deines Plugins schon auf dem neuesten Stand ist oder noch nicht.
Merke Jedes Plugin besteht aus mindestens zwei Dateien, der plugin.py und der plugin.yml.
3.4 Testen Nun wird es aber Zeit, dein erstes eigenes Plugin zu testen. Praktischerweise musst du dafür einfach nur den Server starten. Solltest du Bukkit benutzen, musst du also den Befehl java -jar craftbukkit-1.18.jar eingeben, benutzt du dagegen Spigot, so lautet der Befehl java -jar spigot-1.18.jar. Der PPLoader durchsucht dann automatisch den plugins-Ordner nach Ordnern mit der Endung .py.dir und öffnet die darin befindlichen Plugins. Was du dann auf dem Bildschirm sehen kannst, wenn der Server alles geladen hat, kannst du in Abbildung 3.1 sehen. Besonders interessant ist dabei die drittletzte Zeile, denn hier kannst du dein Plugin in Aktion sehen, die Meldung »Hallo Welt!« wird angezeigt. Zugegebenermaßen ist das noch nicht besonders spektakulär, aber
aller Anfang ist ja bekanntlich schwer und du hast damit die ersten Schritte auf dem Weg, ein Programmierer zu werden, gemacht. Mit jedem der folgenden Kapitel wirst du neue Dinge lernen, die es dir ermöglichen werden, immer spannendere Plugins zu programmieren.
Abb. 3.1: Ausgabe des »Hallo Welt«-Plugins nach dem Start
3.5 Fehler finden Sollte beim Starten deines Servers nicht der in Abbildung 3.1 gezeigte Text angezeigt werden, dann liegt das höchstwahrscheinlich daran, dass du in deinem Plugin einen Tippfehler hast. Computer nehmen es ziemlich genau, ein einziges falsches Zeichen kann dazu führen, dass dein komplettes Plugin nicht mehr funktioniert. In den meisten Fällen wird dir dann eine lange und kryptisch wirkende Fehlermeldung wie in Listing 3.3 angezeigt. [11:29:21] [Server thread/INFO]: [PPLoader] Loading Plugin hallowelt.py.dir
[11:29:22] [Server thread/WARN]: org.bukkit.plugin.InvalidPluginException: SyntaxError: ("no viable alternative at input '\\n'", ('', 3, 29, ' self.getLogger().info("Hallo Welt!)\n'))
[11:29:22] [Server thread/WARN]: at org.cyberlis.pyloader.PythonPluginLoader.loadPlugin(PythonPlu ginLoader.java:244)
[11:29:22] [Server thread/WARN]: at org.cyberlis.pyloader.PythonPluginLoader.loadPlugin(PythonPlu ginLoader.java:118)
[11:29:22] [Server thread/WARN]: at org.cyberlis.pyloader.PythonPluginLoader.loadPlugin(PythonPlu ginLoader.java:92)
[11:29:22] [Server thread/WARN]: at org.bukkit.plugin.SimplePluginManager.loadPlugin(SimplePlugin Manager.java:329)
[11:29:22] [Server thread/WARN]: at org.cyberlis.pyloader.PythonLoader.onLoad(PythonLoader.java:8 5)
[11:29:22] [Server thread/WARN]: at org.bukkit.craftbukkit.v1_9_R1.CraftServer.loadPlugins(CraftS erver.java:301)
[11:29:22] [Server thread/WARN]: at net.minecraft.server.v1_9_R1.DedicatedServer.init(DedicatedSe rver.java:201)
[11:29:22] [Server thread/WARN]: at net.minecraft.server.v1_9_R1.MinecraftServer.run(MinecraftSer ver.java:527)
[11:29:22] [Server thread/WARN]: at java.lang.Thread.run(Unknown Source)
[11:29:22] [Server thread/WARN]: Caused by: SyntaxError: ("no viable alternative at input '\\n'", ('', 3, 29, ' self.getLogger().info("Hallo Welt!)\n'))
[11:29:22] [Server thread/WARN]: at org.python.core.ParserFacade.fixParseError(ParserFacade.java: 95)
[11:29:22] [Server thread/WARN]: at org.python.core.ParserFacade.parse(ParserFacade.java:190)
[11:29:22] [Server thread/WARN]: at org.python.core.Py.compile_flags(Py.java:1956)
[11:29:22] [Server thread/WARN]: at org.python.util.PythonInterpreter.execfile(PythonInterpreter. java:296)
[11:29:22] [Server thread/WARN]: at org.python.util.PythonInterpreter.execfile(PythonInterpreter. java:291)
[11:29:22] [Server thread/WARN]: at org.cyberlis.pyloader.PythonPluginLoader.loadPlugin(PythonPlu ginLoader.java:215)
[11:29:22] [Server thread/WARN]: ... 8 more
Listing 3.3: Fehlermeldung beim Laden des Plugins
Auch wenn es auf den ersten Blick vielleicht nicht so wirkt, sind diese Fehlermeldungen oft sehr hilfreich und können dir dabei helfen, den Fehler schnell zu finden und zu beheben. Die wichtigen Information stecken in aller Regel in der ersten Zeile, im Beispiel also:
[11:29:22] [Server thread/WARN]: org.bukkit.plugin.InvalidPluginException: SyntaxError: ("no viable alternative at input '\\n'", ('', 3, 29, ' self.getLogger().info("Hallo Welt!)\n')).
Hier wird dir nämlich genau angezeigt, wo sich der Fehler befindet. Im Beispiel befindet sich der Fehler in Zeile 3, diese wird auch gleich nochmal komplett mit ausgegeben und lautet self.getLogger().info("Hallo Welt!)\n. Über das \n am Ende solltest du dich nicht weiter wundern, das bedeutet nur, dass danach eine neue Zeile anfängt. Wenn du die Zeile aber genau mit Listing 3.1 vergleichst, fällt dir auf, das am Ende des Textes das Anführungszeichen fehlt, also "Hallo Welt! statt "Hallo Welt!". Fügst du dieses hinzu und startest den Server neu, dann wird das Plugin ohne Probleme geladen. Natürlich können gerade längere Plugins auch mehr als einen Fehler enthalten, trotzdem solltest du beim Beheben von Fehlern immer nach dem gleichen Muster vorgehen: Den Server starten, in der ersten Zeile der Fehlermeldung herausfinden, wo sich der Fehler befindet, diesen beheben, den Server neu starten. Diesen Vorgang wiederholst du einfach solange, bis alle Fehler im Plugin behoben wurden.
3.6 Entdecken Es wird sicher des Öfteren vorkommen, dass deine Plugins nicht gleich beim ersten Versuch starten, das ist ganz normal und sollte dich nicht entmutigen. Selbst Profis schreiben selten ein Programm, das sofort auf Anhieb funktioniert, zu leicht passiert es, dass man ein Zeichen oder eine Einrückung vergisst. Programmieren kann man nur lernen, indem man das Gelernte auch in der Praxis anwendet, deshalb solltest du jedes Programm in diesem Buch auch gleich ausprobieren und nicht nur den Quellcode lesen. Außerdem solltest du immer neugierig bleiben und neue
Dinge ausprobieren. Was passiert zum Beispiel, wenn du den Text in Zeile 3 des Plugins veränderst? Was passiert, wenn du die in Python so wichtige Einrückung weglässt? Wenn du dir solche Fragen stellst und ihnen nachgehst, wirst du noch schneller zu einem guten Programmierer werden.
Chat-Kommandos Kapitel 4
Mit dem »Hallo Welt!«-Plugin aus dem letzten Kapitel hast du dafür gesorgt, dass dich dein Server in Zukunft bei jedem Start freundlich begrüßt. Da wäre es doch sehr unhöflich, wenn du nicht entsprechend antwortest. In diesem Kapitel soll es deshalb darum gehen, wie du (oder andere Spieler) Nachrichten in Form von sogenannten Chat-Kommandosbzw. Chat-Befehlenan den Server schicken kannst. Du kennst Chat-Befehle vermutlich schon vom Minecraft spielen. Es handelt sich dabei um die Befehle, die man in die Chat-Konsole eingibt, die sich mit der Taste T öffnen lässt. In Abbildung 4.1 ist zum Beispiel die Ausgabe für den Befehl /help zu sehen, der eine Liste aller verfügbaren Chat-Befehle anzeigt. Ziel dieses Kapitels ist es, einen Befehl /halloserver zu erstellen, mit dem ein Spieler einen Gruß an den Server senden kann.
Abb. 4.1: Chat-Befehl /help
4.1 Eigene Befehle definieren Um eigene Chat-Befehle zu definieren, musst du zunächst wieder einen Unterordner innerhalb des Plugins-Ordners für das neue Plugin anlegen, wie du das schon aus dem letzten Kapitel kennst. Der Name dieses Ordners könnte zum Beispiel halloserver.py.dir lauten. In diesem legst du dann wieder die zwei Dateien mit den bereits bekannten Namen plugin.py und plugin.yml an. Den Inhalt der plugin.py kannst du in Listing 4.1 sehen. Dort sollte dir einiges bekannt vorkommen. Zwar hat sich in der ersten Zeile der Klassenname geändert, ansonsten sind die ersten beiden Zeilen aber unverändert im Vergleich zum »Hallo Welt!«-Plugin. In der dritten Zeile steht diesmal statt eines Textes, der ausgegeben werden soll, nur der Befehl pas s. Dieser Befehl sagt dem Plugin, dass es hier erst einmal gar nichts tun soll. Schließlich soll das Plugin auf eine Eingabe des Spielers warten. 1 2 3
class HalloServerPlugin(PythonPlugin):
def onEnable(self):
pass
Listing 4.1: Hallo Server-Plugin Schritt 1
Mehr Änderungen gibt es dagegen in der Datei plugin.yml, deren Inhalt du in Listing 4.2 sehen kannst. In den ersten drei Zeilen legen wir auch hier wieder den Namen, die Versionsnummer und den Klassennamen fest, wie du es bereits aus dem letzten Kapitel kennst. Danach fangen aber die Neuerungen an. Verwendet ein Plugin nämlich eigene Chat-Befehle, so müssen diese in der zugehörigen plugin.yml festgelegt werden, und zwar hinter dem Begriff command s. Auch hier ist es wieder wichtig, auf die Einrückungen zu achten.
1 2 3 4 5 6 7
name: Hallo Server-Plugin
main: HalloServerPlugin
version: 1.0
commands:
halloserver:
description: Sendet einen Gruss an den Server
usage: /halloserver
Listing 4.2: plugin.yml des Hallo Server-Plugins
Der Befehl selbst, in diesem Fall also halloserver, wird in der Zeile direkt dahinter festgelegt. Darauf folgt die description, was Englisch ist und »Beschreibung« bedeutet. Hier kannst du einen Text eingeben, der beschreibt, was der Befehl macht. Dieser Text wird Spielern später in der Hilfe angezeigt. Eine ähnliche Funktion erfüllt die Angabe usage, was ebenfalls Englisch ist und so viel wie »Benutzung« bedeutet. Hier kannst du dem Spieler Tipps geben, wie man den Befehl korrekt verwendet. Bei diesem sehr einfachen Befehl reicht es, den Namen des Befehls, eingeleitet durch einen Schrägstrich, einzugeben. Bei beiden Angaben solltest du auf Umlaute wie ä, ö und ü sowie auf ß verzichten.
Merke In plugin.yml-Dateien dürfen keine Umlaute wie ä, ö oder ü und kein ß vorkommen.
Um dein neues Plugin zu testen, hast du diesmal gleich zwei Möglichkeiten. Nachdem du deinen Server gestartet hast, kannst du direkt im Serverfenster den Befehl help eingeben. Wenn in der Liste der Befehle nun, wie in Abbildung 4.2, der /halloserver-Befehl angezeigt wird, weißt du schon, dass alles funktioniert hat.
Abb. 4.2: Befehlsliste mit /halloserver
Du kannst dich aber auch mit deinem Minecraft-Client auf den Server verbinden, mit T den Chat öffnen und dort dann /help halloserver eingeben. Dann werden dir, wie in Abbildung 4.3 zu sehen, die Felder description und usage angezeigt. Wenn du den Befehl selbst, also halloserver, im Server-Fenster beziehungsweise /halloserver im Chat eingibst, passiert bisher allerdings noch nicht sehr viel, es wird lediglich, wie in Abbildung 4.4 gezeigt, der Befehl noch einmal ausgegeben.
Abb. 4.3: Ausgabe für /help halloserver
Abb. 4.4: Ausgabe für den Befehl /halloserver
4.2 Chat-Nachrichten versenden Ein Chat-Befehl, bei dessen Eingabe nichts passiert, ist nicht nur nicht besonders spannend, sondern auch nicht sehr nützlich. Im nächsten Schritt werden wir deshalb das Hallo Server-Plugin so erweitern, dass es auf die Eingabe des Chat-Befehls mit dem Versenden einer Nachricht reagiert. Wie das funktioniert, kannst du in Listing 4.3 sehen. 1 2 3 4 5 6 7
class HalloServerPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
self.getLogger().info("Hallo Spieler!")
return True
Listing 4.3: Hallo Server-Plugin Schritt 2
Wie du sehen kannst, haben wir hier nun in Zeile 5 eine neue Funktion, die den Namen onCommandträgt, was so viel bedeutet wie »bei Befehl«. Während die onEnable-Funktion, die du schon aus dem letzten Kapitel kennst, jedes Mal aufgerufen wird, wenn das Plugin gestartet wird, wird die onCommand-Funktion jedes Mal aufgerufen, wenn einer der vom Plugin definierten Chat-Befehle eingegeben wird. Wofür die Wörter in den runden Klammern hinter onCommand gut sind, damit werden wir uns in den nächsten Kapiteln noch genauer befassen, für den Moment wollen wir sie nicht weiter beachten. In der nächsten Zeile folgt dann der Befehl zum Ausgeben eines Textes, den du ebenfalls bereits aus dem vorherigen Kapitel kennst und später noch genauer kennenlernen wirst. Die letzte Zeile, return True, teilt dem Server mit, dass der Chat-Befehl an dieser Stelle fertig abgearbeitet wurde. Da du das Hallo Server-Plugin nun weiterentwickelt hast, kannst du noch, wenn du möchtest, in der plugin.yml, wie in Listing 4.4 gezeigt, die Versionsnummer ändern, zum Beispiel zu Version 1.1.
1 2 3 4 5 6 7
name: Hallo Server-Plugin
main: HalloServerPlugin
version: 1.1
commands:
halloserver:
description: Sendet einen Gruss an den Server
usage: /halloserver
Listing 4.4: Neue Version der plugin.yml des Hallo Server-Plugins
Nun wird es wieder Zeit, deinen Server neu zu starten und die überarbeitete Version des Plugins zu testen. Wenn du nun im Server-Fenster, wie in Abbildung 4.5 gezeigt, wieder den Befehl halloserver eingibst, antwortet dir dein Server mit einem freundlichen »Hallo Spieler!«. Ganz perfekt ist das Plugin aber auch in dieser Version noch nicht, denn wenn man den Befehl /halloserver über den Chat direkt im Spiel eingibt, kann man die Nachricht nicht lesen, da sie lediglich im Server-Fenster angezeigt wird.
Abb. 4.5: Ausgabe des neuen Hallo Server-Plugins
Im letzten Schritt wollen wir das Plugin daher noch einmal verbessern, und zwar so, dass die Antwort des Servers nicht mehr nur im Server-Fenster angezeigt wird, sondern per Chat versendet wird und damit sowohl im Server-Fenster als auch im Spiel zu sehen ist. Alles was du dafür tun musst, ist die sechste Zeile der plugin.py, wie in Listing 4.5 gezeigt, anzupassen. Danach kannst du, wenn du möchtest, die Versionsnummer in der plugin.yml noch einmal erhöhen, zum Beispiel auf 1.2. 1 2
class HalloServerPlugin(PythonPlugin):
def onEnable(self):
3 pass
4
5 def onCommand(self, sender, command, label, args):
6 self.getServer().broadcastMessage("Hallo Spieler!")
7 return True
Listing 4.5: Hallo Server-Plugin Schritt 3
Nun bekommst du auch im Spiel, wie in Abbildung 4.6 gezeigt, auf deine Begrüßung eine freundliche Antwort vom Server.
Abb. 4.6: Ausgabe des neuen Hallo Server-Plugins im Spiel
Variablen Kapitel 5
Nachdem du nun bereits dein zweites Plugin erfolgreich zum Laufen bekommen hast, wollen wir in diesem Kapitel etwas tiefer in die Programmiersprache einsteigen und anfangen zu verstehen, für was die verschiedenen Wörter stehen, aus denen sich die Plugins zusammensetzen. Die beiden Plugins, die du programmiert hast, haben gemeinsam, dass sie immer dasselbe tun. Das »Hallo Welt!«-Plugin zeigt bei jedem Start des Servers immer wieder dieselbe Meldung an und das »Hallo Server!«-Plugin versendet bei jeder Eingabe des Befehls dieselbe Chat-Nachricht. Für diese beiden kleinen Plugins mag das in Ordnung sein, die meisten Programme müssen aber flexibler sein. Ein Programm zum Online-Banking wäre zum Beispiel ziemlich nutzlos, wenn man damit immer nur Geld auf dasselbe Konto überweisen könnte. Oder stell dir vor, man könnte bei einer Suchmaschine immer nur nach demselben Begriff suchen. Damit das nicht der Fall ist, müssen Programme auf Nutzereingaben reagieren. Diese Eingaben werden dann von Programmen verarbeitet und es wird eine bestimmte Ausgabe erzeugt. Dieses Vorgehen ist in der Datenverarbeitung ein grundlegendes Prinzip und wird als EVA-Prinzipbezeichnet, nach den Anfangsbuchstaben von Eingabe, Verarbeitung und Ausgabe. Stell dir zum Beispiel ein Programm vor, das zu jeder ganzen Zahl, die eingegeben wird, die nächstgrößere ganze Zahl ausgibt. Also für »1« wäre die Ausgabe »2«, für »2« wäre die Ausgabe »3«, für »100« wäre es »101« und immer so weiter. Wenn man das Ganze ein wenig formaler ausdrücken möchte, könnte man zum Beispiel sagen: Für jede Zahl x, die eingegeben wird, wird die Zahl x+1 ausgegeben. Weil es unmöglich wäre, alle Fälle aufzuzählen (schließlich gibt es unendlich viele ganze Zahlen), wurde ein
Platzhalter eingefügt, der für eine Zahl steht, egal für welche. Nach dem gleichen Prinzip funktionieren auch Variablen. Bevor du einen Platzhalter, also eine Variable, in einem Programm verwenden kannst – man sagt auch: darauf zugreifen –, musst du zuerst einmal den Namen für diesen Platzhalter festlegen und ihm einen Wert zuweisen. Wie man das in Python macht, kannst du in Listing 5.1 an drei unterschiedlichen Beispielen sehen. Diesen Vorgang nennt man Deklaration einer Variable. 1 2 3
x = 3
y = "Hallo Welt!"
var1 = 13.37
Listing 5.1: Deklaration von Variablen
In der ersten Zeile wird der Variable x der Wert 3 zugewiesen. In der zweiten Zeile wird der Variable y der Text »Hallo Welt!« zugewiesen und in der dritten Zeile wird der Variable var1 der Wert 13,37 zugewiesen. An diesem Beispiel kannst du schon zwei wichtige Dinge über Variablen erfahren: Sie können völlig unterschiedliche Namen haben und sie können ganz unterschiedliche Werte speichern, zum Beispiel Zahlen mit und ohne Komma oder Texte.
5.1 Namen Bevor wir uns den Werten zuwenden, wollen wir uns zunächst noch die Namen etwas genauer anschauen, denn obwohl du generell bei der Namenswahl für Variablen relativ freie Hand hast, gibt es einige Regeln, die du beachten musst: Variablennamen dürfen aus Buchstaben, Ziffern und Unterstrichen bestehen. Sonderzeichen und Leerzeichen sind nicht erlaubt. Das erste Zeichen eines Variablennamens darf keine Ziffer sein.
Es dürfen keine bereits belegten Python-Schlüsselwörter (wie zum Beispiel def oder True) verwendet werden. Neben diesen festen Regeln, die unbedingt eingehalten werden müssen, weil deine Plugins sonst nicht funktionieren, gibt es auch noch einige inoffizielle Konventionen, an die du dich halten solltest: Versuche deinen Variablen möglichst aussagekräftige Namen zu geben, damit man weiß, wofür sie genutzt werden. Der erste Buchstabe eines Variablennamens sollte stets kleingeschrieben sein. Besteht ein Variablenname aus zwei oder mehr Wörtern, werden diese als sogenanntes CamelCaseoder Binnenmajuskelgeschrieben. Das bedeutet: Die Wörter werden hintereinander geschrieben und der Anfangsbuchstabe, bis auf denjenigen des ersten Wortes, jeweils groß. Also zum Beispiel: meineVariable, meineLangeVariable und meineSehrLangeVariable.
Hinweis Eigentlich gilt in Python die Konvention, dass Variablennamen komplett kleingeschrieben und mehrere Wörter durch einen Unterstrich getrennt werden, also zum Beispiel meine_variable statt meineVariable. Da Minecraft aber in Java geschrieben ist, verwenden die Variablen und Funktionen, die vom Server bereitgestellt werden, die Camel-Case-Schreibweise, wie zum Beispiel onEnable. Um in solchen Fällen eine einheitliche Schreibweise zu gewähren, ist es üblich auch in Python Binnenmajuskel zu verwenden, auch wenn man normalerweise Unterstriche verwendet.
5.2 Werte Wenn man den Wert einer Variable festlegt, also zum Beispiel durch x = 3 oder y = "Hallo Welt", dann spricht man von einer Wertzuweisung. Für den Anfang unterscheiden wir zwischen drei verschiedenen Typen von Werten, die Variablen annehmen können. Ganze Zahlen, also positive oder negative Zahlen ohne Komma, Kommazahlen, und Zeichenketten, die auch Stringsgenannt werden. In Listing 5.1 kannst du Werte aller drei Kategorien sehen. In der ersten Zeile siehst du eine ganze Zahl, in der zweiten Zeile einen String und in der dritten Zeile eine Kommazahl. Zwei Besonderheiten fallen dabei auf: Der String wird von Anführungszeichen umschlossen und die Kommazahl verwendet zur Trennung einen Punkt statt eines Kommas.
Merke Für das Aufschreiben von Kommazahlen verwendet man in Python einen Punkt statt eines Kommas. Das liegt daran, dass das Komma im Englischen als Tausendertrennzeichen verwendet wird. Während man bei uns für 1337 also zum Beispiel auch 1.337 schreiben kann, schreibt man im Englischen 1,337. Für 1/2 schreibt man dagegen im Deutschen 0,5 und im Englischen 0.5.
Natürlich macht es nur dann Sinn, Variablen Werte zuzuweisen, wenn man diese Werte später auch wieder aus der Variable herausbekommt, also damit weiterarbeiten kann. Wie das funktioniert, verdeutlicht die angepasste Version des »Hallo Welt!«Plugins in Listing 5.2. 1 2
class HalloWeltPlugin(PythonPlugin):
def onEnable(self):
3 4
nachricht = "Hallo Welt!"
self.getLogger().info(nachricht)
Listing 5.2: »Hallo Welt!«-Plugin mit Variable
Um den Wert aus einer Variable auszulesen, genügt es, einfach den Namen der Variable, also zum Beispiel nachricht, an die Stelle zu schreiben, an der ihr Wert benötigt wird. In Listing 5.2 geschieht das zum Beispiel in Zeile 4 beim Befehl self.getLogger().info(). Der Computer ersetzt dann jedes Vorkommen des Namens mit dem Wert, der der Variable zugewiesen wurde. Das besondere an Variablen ist aber, dass ihr Wert, wie der Name schon sagt, variabel, also veränderbar ist. Was das bedeutet, kannst du in Listing 5.3 sehen. 1 2 3 4
nachricht = "Hallo Welt!"
self.getLogger().info(nachricht)
nachricht = "Heute ist ein schöner Tag."
self.getLogger().info(nachricht)
Listing 5.3: Veränderung des Wertes einer Variablen
Dort findest du zweimal die Zeile self.getLogger().info(nachricht). Trotzdem würden in diesem Fall zwei unterschiedliche Nachrichten im Server-Fenster angezeigt werden. In der ersten Zeile wird der Variablen nachricht nämlich der String »Hallo Welt!« zugewiesen. Somit würde in der zweiten Zeile auch dieser String im ServerFenster ausgegeben. In der dritten Zeile wird der Wert der Variable dann aber verändert, weshalb in der vierten Zeile der String »Heute ist ein schöner Tag.« im Server-Fenster ausgegeben würde.
5.2.1 Operatoren Du kannst Variablen auch miteinander verbinden. Hast du zum Beispiel zwei Variablen x und y, so kannst du diese, wie in Listing 5.4
gezeigt, addieren. Die Variable z hätte dann entsprechend den Wert 5. 1 2 3
x = 3
y = 2
z = x + y
Listing 5.4: Addition zweier Variablen
Neben dem Additionsoperator, also dem Plus, kennt Python noch weitere sogenannte arithmetische Operatoren. Tabelle 5.1 zeigt eine Übersicht der verschiedenen Operatoren.
Symbol
Name
Beispiel
+
Summe
1+1=2
-
Differenz
1-1=0
*
Produkt
2 * 5 = 10
/
Quotient
2 / 1 = 2.0 5 / 2 = 2.5
%
Restwertbzw. Modulo 5 % 2 = 1 5 % 2.5 = 0
Tabelle 5.1: Arithmetische Operatoren in Python
Das Rechnen mit Variablen funktioniert also so, wie du es schon aus dem Alltag kennst. Etwas anders sieht es aber aus, wenn der Wert einer Variable keine Zahl, sondern ein String ist, wie in Listing 5.5. 1 2 3 4
a = "Hallo"
b = " Welt!"
c = a + b
Listing 5.5: Verbindung von Strings
Auch auf Strings kann der Plusoperator, wie in Listing 5.5, angewendet werden, allerdings steht er in diesem Zusammenhang nicht für Addition, sondern für Konkatenation. Das bedeutet, das Plus fügt zwei Strings zu einem zusammen. Die Variable c in Listing 5.5 hätte daher den Wert »Hallo Welt!«. Dieser kleine aber feine Unterschied wird in Listing 5.6 noch einmal verdeutlicht. In Zeile 3 steht das Plus für eine Addition, da a und b Zahlen sind. Entsprechend ist der Wert von Variable c 3+3, also 6. Die Anführungszeichen in Zeile 5 und 6 zeigen, dass es sich bei diesen Werten um Strings handelt, daher steht das Plus in der letzten Zeile für eine Konkatenation. Daher ist der Wert von f auch nicht, wie man vielleicht erwarten würde, 6, sondern »33«, also die Zusammensetzung der beiden Strings. 1 2 3 4 5 6 7
a b c
d e f
= 3
= 3
= a + b
= "3"
= "3"
= d + e
Listing 5.6: Addition und Konkatenation
Merke
3 + 3 = 6
"3" + "3" = "33"
5.2.2 Umwandlung Was nicht funktioniert und sofort zu einer Fehlermeldung führen würde, ist die Verbindung von Zahlen und Strings, wie sie in Listing 5.7 gezeigt ist. 1 2
a = 4
d = a + "3" #Fehler
Listing 5.7: Die Verbindung einer Zahl und eines Strings führt zu einem Fehler
Tipp In der letzten Zeile von Listing 5.7 findest du einen sogenannten Kommentar. Kommentare werden mit der Raute (#) eingeleitet und werden bei der Ausführung des Plugins vom Computer ignoriert, sie dienen nur dazu, den Quellcode für Menschen verständlicher und leichter lesbar zu machen. Sie sind besonders dann hilfreich, wenn du deinen Quellcode an andere Programmierer weitergeben möchtest, denn gerade bei komplexeren Plugins ist nicht immer gleich auf den ersten Blick ersichtlich, was genau an welcher Stelle des Quellcodes passiert.
In der Praxis wird es aber häufig passieren, dass du Zahlen und Strings miteinander verbinden möchtest, zum Beispiel wenn du ein Plugin schreibst, dass zwei Zahlen multiplizieren und das Ergebnis
auf der Server-Konsole ausgeben soll. Denn der Befehl self.getLogger().info(nachricht) funktioniert nur, wenn die Variable nachricht ein String ist; eine Zahl würde zu einer Fehlermeldung führen. Glücklicherweise lassen sich Zahlen aber mithilfe des Befehls str ganz einfach in Strings umwandeln. Wie genau, kannst du in Listing 5.8 sehen. Der Wert der Variablen nachricht ist »50« als String. 1 2 3 4
a = 10
b = 5
c = a * b
nachricht = str(c)
Listing 5.8: Umwandlung von Zahlen in Strings
Noch etwas schöner wäre die Ausgabe in Listing 5.9, hier hat die Variable nachricht den Wert »10 mal 5 ist 50«. 1 2 3 4
a = 10
b = 5
c = a * b
nachricht = str(a) + " mal " + str(b) + " ist " + str(c)
Listing 5.9: Umwandlung von Zahlen in Strings Teil 2
Diese Umwandlung funktioniert auch umgekehrt, es lassen sich also auch Strings in Zahlen umwandeln, aber natürlich nur, wenn die Strings auch tatsächlich nur eine Zahl enthalten. Wie das funktioniert, kannst du in Listing 5.10 sehen. 1 2 3
string1 = "3"
zahl1 = int(string1) #3
zahl2 = float(string1) #3.0
Listing 5.10: Umwandlung von Strings in Zahlen Teil 1
Wie du schon in Zeile 2 und 3 des Listings sehen kannst, gibt es zwei Befehle für die Umwandlung eines Strings in eine Zahl, einen
für ganze Zahlen, nämlich int(), und einen für Kommazahlen, nämlich float(). Besteht ein String, wie im Fall von string1, aus einer ganzen Zahl, so funktionieren beide Befehle. Im Fall des Strings »3« liefert int() das Ergebnis 3 und float() das Ergebnis 3,0. Anders ist es, wenn der String, wie im Falle von string2 in Listing 5.11, aus einer Kommazahl besteht. 1 2 3 4 5 6 7
string2 zahl1 = zahl2 =
string3 zahl1 = zahl2 =
= "2.5"
int(string2) #Fehler
float(string2) #2.5
= "Keine Zahl"
int(string3) #Fehler
float(string3) #Fehler
Listing 5.11: Umwandlung von Strings in Zahlen Teil 2
Dann kann nur der Befehl float() verwendet werden, int(), wie in Zeile 2, führt dagegen zu einem Fehler. Repräsentiert der String überhaupt keine Zahl, weder eine ganze noch eine Kommazahl, wie im Falle von string3, so führen beide Befehle zu einer Fehlermeldung.
5.2.3 Runden An diesem Beispiel merkst du schon, dass Python Zahlen manchmal unterschiedlich behandelt, abhängig davon, ob sie ein Komma haben oder nicht. Während für uns 3 in der Regel dasselbe bedeutet wie 3,0, unterscheidet Python hier. Allerdings kannst du auch hier die beiden Befehle float() und int() zur Umwandlung benutzen. Jede ganze Zahl kann, wie in Listing 5.12 gezeigt, in eine Kommazahl umgewandelt werden, dazu wird einfach ein .0 an die entsprechende Zahl angehängt.
1 2 3
a = 3
b = float(a) #3.0
c = float(4) #4.0
Listing 5.12: Umwandlung von ganzer Zahl in Kommazahl
Die umgekehrte Richtung ist in Listing 5.13 gezeigt, hier werden Kommazahlen in ganze Zahlen umgewandelt, indem die Kommastellen einfach abgeschnitten werden. 1 2 3
a = 3.1
b = int(a) #3
c = int(3.9) #3
Listing 5.13: Umwandlung von einer Kommazahl in eine ganze Zahl
Diese Art der Umwandlung ist in der Praxis allerdings oft zu ungenau. Dass 3,1 umgewandelt in eine ganze Zahl 3 ergibt, erscheint noch logisch. Allerdings würde man bei der Umwandlung von 3.9 wohl eher den Wert 4 erwarten statt einer 3. Auch das ist in Python möglich, indem man Zahlen nicht einfach nur umwandelt, sondern sie rundet. Wie das funktioniert, kannst du in Listing 5.14 sehen. Der round()-Befehl steht für das sogenannte kaufmännische Runden. Hierbei wird alles unter ,5 abgerundet und alles ab ,5 aufgerundet. 3,1 wird also zum Beispiel zu 3, wie in Listing 5.14 gezeigt. 3,4 würde ebenfalls zu 3, die Zahlen 3,5 und 3,9 aber zu 4. 1 2 3
a = 3.1
b = round(a) #3
c = round(3.9) #4
Listing 5.14: Runden von Kommazahlen
5.3 +1-Plugin
Nun wird es Zeit, das theoretisch Gelernte erneut in die Praxis umzusetzen. Dazu wollen wir das zu Anfang des Kapitels beschriebene Problem als Plugin umsetzen: Für jede Zahl x, die eingegeben wird, wird die Zahl x+1 ausgegeben. Dazu benötigst du wieder ein Plugin, welches einen Chat-Befehl verwendet, zum Beispiel /pluseins. Neu ist allerdings, dass wir diesem Chat-Befehl auch noch weitere Informationen übergeben müssen, nämlich zu welcher Zahl 1 addiert werden soll. Eine solche übergebene Information nennt man auch Parameter. Der ChatBefehl soll später nämlich zum Beispiel für /pluseins 3 die Zahl 4 ausgeben und für /pluseins 42 die Zahl 43. 3 und 42 wären hier also die Parameter, zu denen jeweils 1 addiert werden soll. Um ein solches Plugin umzusetzen, müssen wir uns die bereits bekannte onCommand-Funktion noch einmal etwas genauer anschauen.
Abb. 5.1: onCommand-Funktion
In Abbildung 5.1 kannst du noch einmal die dir bereits bekannte onCommand-Funktion sehen. Als die Funktion im letzten Kapitel eingeführt wurde, haben wir die Wörter in den Klammern zunächst einmal ignoriert, inzwischen kannst du dir vielleicht schon denken, dass es sich dabei um Variablen handelt. In diesem Abschnitt wollen wir uns auf die beiden letzten Variablen, label und args, konzentrieren. Zur Erinnerung: Die onCommandFunktion wird jedes Mal aufgerufen, wenn ein Chat-Befehl eingegeben wird. Was dabei in den beiden letzten Variablen gespeichert wird, kannst du in Abbildung 5.1 am Beispiel des pluseins-Befehls sehen: In der Variable label wird der Befehl selbst gespeichert, in diesem Fall also »pluseins«, in der Variable args wird der übergebene Parameter gespeichert, falls es einen gibt.
Mithilfe der args-Variable können wir uns nun an die Umsetzung des Plugins machen. Zunächst solltest du wieder einen Ordner für dein neues Plugin anlegen, zum Beispiel mit dem Namen pluseins.py.dir. Darin darf natürlich die plugin.yml wieder nicht fehlen. Deren Inhalt kannst du in Listing 5.15 sehen. Besonders interessant ist hier das Feld usage in der letzten Zeile, dessen Sinn du jetzt gut erkennen kannst. Es dient nämlich hauptsächlich dazu, dem Spieler zu zeigen, welchen Parameter ein Befehl erwartet, in diesem Fall also zum Beispiel die Zahl, zu der 1 addiert werden soll.
Hinweis Einige Codezeilen sind zu lang, um sie in diesem Buch in einer Zeile darzustellen. Der Code geht dann einfach in der nächsten Zeile weiter. Du erkennst diese Stellen an der fehlenden Zeilennummer der Folgezeile. Beim Abtippen musst du darauf achten, solche Codezeilen bei dir in eine Zeile zu schreiben, damit das Programm funktioniert.
1 name: Pluseins-Plugin
2 main: PluseinsPlugin
3 version: 1.0
4 commands:
5 pluseins:
6 description: Addiert 1 zur angegebenen Zahl und gibt das Ergebnis aus
7 usage: /pluseins
Listing 5.15: plugin.yml für das +1-Plugin
Den Quellcode der zugehörigen plugin.py kannst du in Listing 5.16 sehen. Am Grundgerüst hat sich im Vergleich zu den vorherigen Plugins erst einmal nichts geändert, wohl aber in der onCommandFunktion.
1 class PluseinsPlugin(PythonPlugin):
2 def onEnable(self):
3 pass
4
5 def onCommand(self, sender, command, label, args):
6 eingabe = args[0]
7 zahl = int(eingabe)
8 zahl = zahl + 1
9 ausgabe = str(zahl)
10 self.getServer().broadcastMessage("Das Ergebnis ist " + ausgabe)
11 return True
Listing 5.16: plugin.py für das +1-Plugin
In Zeile 6 wird hier zunächst die Eingabe des Spielers in die Variable eingabe gelesen. Dass sich diese in der Variablen args versteckt, das weißt du inzwischen; warum hinter dem Namen der Variable noch [0] steht, das werden wir uns im nächsten Abschnitt genauer anschauen. Dabei ist es wichtig zu beachten, dass die Variable args unabhängig von der Eingabe immer im String-Format ist und Zahlen deshalb von Python ebenfalls als Strings behandelt und nicht automatisch als Zahlen erkannt werden. Daher muss die eingegebene Zahl, bevor sie addiert werden kann, zunächst auch in eine »echte« Zahl umgewandelt werden, das geschieht in Zeile 7.
Merke Parameter von Chat-Befehlen sind immer im String-Format und müssen, falls es sich um Zahlen handelt, zunächst umgewandelt werden.
In Zeile 8 wird dann die Zahl 1 zur Eingabe addiert. Um das Ergebnis auch wieder per Chat-Nachricht ausgeben zu können,
muss die Zahl nun wieder in einen String umgewandelt werden, dies geschieht in Zeile 9 mithilfe der Variablen ausgabe. Die Ausgabe selbst geschieht dann in Zeile 10. Wenn du nun deinen Server neu startest und das Plugin testest, zum Beispiel mit der Eingabe 42, dann solltest du, wie in Abbildung 5.2, eine passende Nachricht angezeigt bekommen.
Abb. 5.2: Ausgabe des Plugins bei Eingabe des Befehls /pluseins 42
Wir haben also aus der Problemstellung Für jede Zahl x, die eingeben wird, wird die Zahl x+1 ausgegeben ein Minecraft-Plugin gemacht, das aus elf Zeilen Code besteht. Als du dir überlegt hast, wie man das Problem lösen kann, hattest du vielleicht die Idee für eine etwas andere Lösung, die vielleicht etwas weniger oder auch etwas mehr Zeilen hätte. Das ist ganz normal, denn beim Programmieren gibt es nie nur eine Lösung und nur selten eine Lösung, die besser ist als alle anderen. Viele Wege können zum Ziel führen. Listing 5.17 zeigt zum Beispiel eine Alternativversion des +1Plugins. In der Ausführung wirst du keinen Unterschied zwischen
den beiden Versionen des Plugins merken, die zweite ist aber mit sieben Zeilen etwas kürzer. 1 class PluseinsPlugin(PythonPlugin):
2 def onEnable(self):
3 pass
4
5 def onCommand(self, sender, command, label, args):
6 self.getServer().broadcastMessage("Das Ergebnis ist " + str(int(args[0])+1))
7 return True
Listing 5.17: Alternativversion der plugin.py für das +1-Plugin
Oft wirst du Plugins, die du in diesem Buch findest, auch kürzer umsetzen können, manchmal auch besser, lass dich also nicht entmutigen, falls deine Lösungsansätze anders sind als die hier vorgestellten. Da es in diesem Buch um das Lernen der Programmiersprache geht, sind die Plugins oft so gestaltet, dass sie möglichst leicht zu verstehen und nachzuvollziehen sind. Dabei hilft es oft, wenn in einer Zeile nur eine Sache geschieht, wie in Listing 5.16, auch wenn andere Versionen kürzer wären.
Merke Beim Programmieren führen viele Wege zum Ziel, die eine richtige Lösung gibt es nicht.
5.4 Listen und Arrays Als Nächstes wollen wir der Frage nachgehen, was genau hinter der Schreibweise mit den eckigen Klammern [0] bei der Variable args steckt. Beim Programmieren kommt es recht häufig vor, dass man
es mit einer Vielzahl zusammengehöriger Variablen zu tun hat. Zum Beispiel alle Blöcke auf einer Minecraft-Karte oder die Namen aller Spieler, die sich auf einem Minecraft-Server befinden. Mit den Variablen, wie du sie bisher kennst, würde der Versuch, die Namen aller Spieler zu speichern wie in Listing 5.18 aussehen. Selbst für vier Namen ist schon relativ viel Code notwendig, für 10 oder sogar 20 würdest du dir vermutlich schon die Finger wund tippen. 1 2 3 4
nameSpieler1 nameSpieler2 nameSpieler3 nameSpieler4
= = = =
"Sheldor"
"Rainer Zufall"
"Deep Thought"
"notch"
Listing 5.18: Spielernamen als einzelne Variablen
Praktischerweise bietet Python für dieses Problem eine Lösung in Form von sogenannten Listen. In einer Liste können mehrere Variablen bequem gespeichert und verwaltet werden. Die Erstellung einer Liste, die alle Spielernamen aus Listing 5.18 enthält, benötigt zum Beispiel nur eine Zeile, wie du in Listing 5.19 sehen kannst. nameSpieler = ["Sheldor", "Rainer Zufall", "Deep Thought", "notch"]
Listing 5.19: Spielernamen als Liste
Hier begegnen uns schon die eckigen Klammern wieder. Sie zeigen dem Computer, dass es sich hier um eine Liste handelt. Die einzelnen Elemente der Liste werden dann durch Kommata getrennt angegeben. Im Beispiel in Listing 5.19 besteht die Liste nur aus Strings, prinzipiell können Listen aber auch aus verschiedenen Variablentypen, also zum Beispiel Strings und Zahlen, bestehen. Es kann in der Praxis durchaus auch vorkommen, dass du beim Erstellen der Liste, anders als in Listing 5.19, noch nicht genau weißt, wie lang deine Liste einmal werden soll. Für diesen Fall gibt
es noch eine alternative Schreibweise zum Erstellen einer Liste, die du in Listing 5.20 sehen kannst. 1 2 3 4 5
nameSpieler = []
nameSpieler.append("Sheldor")
nameSpieler.append("Rainer Zufall")
nameSpieler.append("Deep Thought")
nameSpieler.append("notch")
Listing 5.20: Alternative Schreibweise zum Erstellen einer Liste
Mit nameSpieler = [] wird dort zunächst einmal eine komplett leere Liste erstellt. Dieser Liste kannst du dann jederzeit mit dem Befehl append()am Ende ein neues Element hinzufügen. Natürlich musst du später irgendwie auch wieder auf die Elemente in deiner Liste zugreifen können, um ihre Werte auszulesen. Genau hier kommt die dir bereits bekannte Schreibweise mit den eckigen Klammern wieder ins Spiel. Mit nameSpieler[0] greifst du nämlich auf das erste Element der Liste zu, in diesem Fall also zum Beispiel »Sheldor«, mit nameSpieler[1] auf »Rainer Zufall« und so weiter.
Merke Programmierer fangen in der Regel mit »0« an zu zählen. Das erste Element einer Liste hat daher die Nummer 0, das zweite die Nummer 1 und so weiter.
Zu guter Letzt kannst du auch Elemente aus einer Liste löschen, dazu benötigst du den in Listing 5.21 gezeigten Befehl remove() und die Nummer des Elementes, das gelöscht werden soll. Wenn wir aus der dort gezeigten Liste das Element mit dem Index 1, also "Rainer Zufall" löschen, dann bleibt eine Liste übrig, die nur noch das Element "Sheldor" enthält.
1 2
nameSpieler = ["Sheldor", "Rainer Zufall"]
nameSpieler.remove(1) #nameSpieler = ["Sheldor"]
Listing 5.21: Löschen von Elementen aus einer Liste
Wie du dir inzwischen sicher denken kannst, handelt es sich bei der Variable args der onCommand-Funktion ebenfalls um eine Liste. Genauer genommen handelt es sich um eine Sonderform der Liste, ein sogenanntes Array. Arrays sind in vielen Programmiersprachen häufig verwendete Variablen, werden in Python aber eher selten benutzt. Im Gegensatz zur Liste darf das Array nur einen Variablentyp enthalten, also entweder zum Beispiel nur ganze Zahlen oder nur Strings, nicht aber eine Mischung von beidem. Die Variable args darf daher nur Strings enthalten. Du kannst also zum Beispiel innerhalb der onCommand-Funktion ohne Probleme etwas schreiben wie args.append("3"); dagegen würde args.append(3) zu einer Fehlermeldung führen, da hier versucht wird, einem Array aus Strings eine Zahl hinzuzufügen.
Merke Arrays sind Listen, die nur Variablen von einem Typ enthalten dürfen, also entweder zum Beispiel nur ganze Zahlen oder nur Strings, nicht aber eine Mischung von beidem. Die Elemente von Arrays und Listen werden immer in eckigen Klammern dargestellt.
Inzwischen haben wir nun geklärt, dass die Variable args ein Array ist und wir deshalb mit args[0] auf das erste Element dieses Arrays zugreifen. Nun fragst du dich vielleicht noch, wieso es sich bei der Variable args um ein Array handelt. Um das zu beantworten, wollen wir noch einmal zu unserem Hallo Server-Plugin zurückkommen und es noch ein letztes Mal verbessern. Da wir mit Chat-Befehlen inzwischen auch Parameter übergeben können, wollen wir das
Plugin so anpassen, dass es in Zukunft nicht mehr einfach nur »Hallo Spieler!« anzeigt, sondern eine personalisierte Nachricht, also zum Beispiel »Hallo Daniel!«. Dafür muss der Spieler dem /halloserver-Befehl natürlich seinen Namen übergeben, also zum Beispiel /hallovserver Daniel eingeben. Dazu passen wir zunächst, wie in Listing 5.22 gezeigt, die plugin.yml an. Neben der Versionsnummer solltest du auch das usage-Feld anpassen, damit die Spieler in Zukunft auch wissen, dass sie ihren Namen als Parameter übergeben sollen. 1 2 3 4 5 6 7
name: Hallo Server-Plugin
main: HalloServerPlugin
version: 1.2
commands:
halloserver:
description: Sendet einen Gruss an den Server
usage: /halloserver
Listing 5.22: Neue Version der plugin.yml des Hallo Server-Plugins
Die geänderte plugin.py kannst du in Listing 5.23 sehen. Hier ändert sich sogar nur eine Zeile, nämlich die sechste, in der die Nachricht ausgegeben wird. 1 class HalloServerPlugin(PythonPlugin):
2 def onEnable(self):
3 pass
4
5 def onCommand(self, sender, command, label, args):
6 self.getServer().broadcastMessage("Hallo " + args[0] + "!")
7 return True
Listing 5.23: Neue Version der plugin.py des Hallo Server-Plugins
Wenn du den Befehl nun im Spiel mit deinem Namen eingibst, also zum Beispiel /halloserver Daniel, so bekommst du, wie in Abbildung 5.3 gezeigt, eine freundliche Nachricht des Servers. Das
ist zwar sehr nett, erklärt aber immer noch nicht, wieso es sich bei der Variable args nun um ein Array handelt.
Abb. 5.3: Ausgabe des Chat-Befehls /halloserver Daniel
Etwas klarer wird das allerdings, wenn du einen vollen Namen eingibst, also zum Beispiel /halloserver Daniel Braun, denn die Nachricht, die du dann vom Server bekommst bleibt unverändert, »Hallo Daniel!«. Würdest du in Zeile 6 in Listing 5.23 args[0] gegen args[1] austauschen und wieder /halloserver Daniel Braun eingeben, dann wäre die Ausgabe »Hallo Braun!«. Der Grund, warum es sich bei der args-Variable um ein Array handelt, ist nämlich, dass man theoretisch beliebig viele Parameter mit einem Chat-Befehl übergeben kann. Du könntest zum Beispiel auch /halloserver Karl-Theodor Maria Nikolaus Johann Jacob Philipp Franz eingeben. Der Server interpretiert jeden Text nach einem Leerzeichen als neuen Parameter und erstellt daraus das args-Array. Das zugehörige Array zu dieser Eingabe würde also zum
Beispiel so aussehen: ["Karl-Theodor",
"Maria", "Nikolaus", "Johann", "Jacob", "Philipp", "Franz"].
5.5 Konstanten Neben Variablen gibt es in vielen Programmiersprachen auch noch sogenannte Konstanten. Wie Variablen besitzen auch Konstanten einen Namen, über den ihr Wert abgerufen werden kann. Im Gegensatz zu Variablen kann dieser Wert bei Konstanten aber, nachdem er einmal festgelegt wurde, nicht mehr geändert werden. In Python gibt es technisch gesehen keine Konstanten, es gibt aber eine Übereinkunft, wonach Variablen, deren Namen komplett in Großbuchstaben geschrieben sind, wie Konstanten behandelt werden, das heißt ihr Wert wird nach der ersten Festlegung nicht mehr geändert. So kannst du dir als Programmierer sicher sein, dass sich der Wert einer »Konstanten« auch in Python während des Programmablaufs nicht ändert.
Merke Variablen, deren Namen komplett in Großbuchstaben geschrieben sind, werden in Python wie Konstanten behandelt.
In Listing 5.24 kannst du ein Beispiel für die Verwendung einer Konstanten in Python sehen. Im Vergleich zu ihren Verwandten, den Variablen, wirken die Konstanten auf den ersten Blick etwas langweilig. Warum sollte man sie überhaupt benutzen? Im Beispiel in Listing 5.24 könnte man gut auf den Einsatz einer Konstanten verzichten und damit sogar eine Zeile Code einsparen.
1 2
NACHRICHT = "Hallo Welt!"
self.getServer().broadcastMessage(NACHRICHT)
Listing 5.24: Verwendung einer Konstanten
Aber auch Konstanten haben natürlich ihre Daseinsberechtigung. Ihr Einsatz ist meistens dann sinnvoll, wenn derselbe Wert an vielen Stellen in einem Programm genutzt wird. Stell dir zum Beispiel vor, jemand programmiert einen Online-Shop für Bücher. In Deutschland muss für Bücher eine Mehrwertsteuer von 7% gezahlt werden. Für die Anzeige des Preises führt der Online-Shop eine Berechnung wie in Listing 5.25 durch. Die gleiche Berechnung führt er erneut durch, wenn die Rechnung geschrieben wird – und an vielen anderen Stellen auch. 1 2 3
preisOhneMwSt = 15.88
mwSt = 15.88 * 0.07
endpreis = preisOhneMwSt + mwSt
Listing 5.25: Preisberechnung
Nun kommt es hin und wieder vor, dass sich der Mehrwertsteuersatz ändert, früher betrug er für Bücher zum Beispiel einmal 6,5%. Für den Programmierer des Online-Shops bedeutet das, dass er durch seinen kompletten Quellcode gehen muss und jedes Mal * 0,065 durch * 0,07 ersetzen muss. Größere Softwareprojekte bestehen oft aus zehntausenden Zeilen Code, da ist es beinahe vorprogrammiert, dass die Änderung an irgendeiner Stelle übersehen wird. Genau in solchen Fällen bietet sich der Einsatz von Konstanten an. Denn wird der Prozentsatz in einer Konstanten gespeichert, wie in Listing 5.26, muss nur eine Zeile im Code geändert werden und man kann sicher sein, dass die Änderung an allen Stellen ankommt. 1 2 3 4 5
MWST_BUECHER = 0.07
preisOhneMwSt = 15.88
mwSt = preisOhneMwSt * MWST_BUECHER
endpreis = preisOhneMwSt + mwSt
Listing 5.26: Preisberechnung mit Konstante
Schleifen Kapitel 6
Dieses Kapitel befasst sich, nach den Variablen im letzten Kapitel, mit einem weiteren wichtigen Konzept der Programmierung, den sogenannten Schleifen. Zunächst geht es aber um etwas ganz anderes: um Kürbisse.
6.1 Kürbis-Plugin Ja, du hast dich nicht verlesen, es geht um Kürbisse. Genauer gesagt, um ein Plugin, das Kürbisse erscheinen lässt. Ziel soll es sein, einen Befehl /kuerbis zu erstellen, der vor dem Spieler, der den Befehl aufruft, einen Kürbis platziert. Das Prozedere zur Erstellung eines neuen Plugins ist dir inzwischen ja schon bestens bekannt. Zunächst solltest du wieder einen eigenen Ordner anlegen, zum Beispiel mit dem Namen kuerbis.py.dir. In diesem Ordner solltest du dann als nächstes eine Datei mit dem Namen plugin.yml anlegen, deren Inhalt du in Listing 6.1 sehen kannst. 1 name: Kuerbis-Plugin
2 main: KuerbisPlugin
3 version: 1.0
4 commands:
5 kuerbis:
6 description: Platziert einen Kuerbis vor dem Spieler
7 usage: /kuerbis
Listing 6.1: plugin.yml für das Kürbis-Plugin
Außerdem benötigst du natürlich noch eine plugin.py, die später den Quellcode des Plugins enthält. Zunächst kannst du diese Datei schon einmal mit dem Grundgerüst füllen, das jedes Plugin, welches
einen Chat-Befehl verwendet, benötigt. Dieses Grundgerüst findest du in Listing 6.2. 1 2 3 4 5 6
class KuerbisPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
return True
Listing 6.2: Grundgerüst für das Kürbis-Plugin
6.1.1 Positionierung Um einen Kürbis vor dem Spieler platzieren zu können, musst du zunächst einmal herausfinden, wo sich der Spieler befindet. Positionen in der Spielwelt von Minecraft werden mit drei Werten beschrieben, einem X-, einem Y- und einem Z-Wert. Wenn du im Spiel F3 drückst, wird dir, wie in Abbildung 6.1, deine Position angezeigt. Im gezeigten Beispiel ist die Position X: -242,881, Y: 7,65394, Z: 306,895. Außerdem erkennst du in der Mitte des Bildes statt des üblichen Fadenkreuzes drei bunte Balken, einen roten, einen grünen und einen blauen. Der rote Balken zeigt in X-Richtung, das heißt, wenn du in diese Richtung gehst, wird der Wert für X größer, gehst du genau in die entgegengesetzte Richtung, so wird er kleiner. Der grüne Balken, der in den Himmel zeigt, steht für die YRichtung. Wenn du also hochspringst oder auf einen Berg kletterst, wird der Y-Wert größer. Der blaue Balken schließlich steht für die ZRichtung. Mithilfe dieser drei Werte kann die Position jedes Objekts auf einer Karte eindeutig bestimmt werden.
Abb. 6.1: Anzeige der Koordinaten
Um also einen Kürbis vor einem Spieler platzieren zu können, musst du zunächst die X-, Y- und Z-Werte des Spielers herausfinden, die als Koordinatenbezeichnet werden. Wie das innerhalb der onCommand-Funktion geht, kannst du in Listing 6.3 sehen. 1 2 3
def onCommand(self, sender, command, label, args):
position = sender.getLocation()
return True
Listing 6.3: Bestimmung der Koordinaten eines Spielers
Dazu verwenden wir eine weitere Variable der onCommand-Funktion, nämlich die mit dem Namen sender. Sie enthält Informationen über den Spieler, der den Chat-Befehl eingegeben hat. Im Gegensatz zu den Variablen, die wir bisher kennengelernt haben, speichert diese also nicht nur einen Text oder eine Zahl, sie ist eine Repräsentation eines ganzen Spielers in Python und damit in ihrem Aufbau deutlich komplexer. Mit ihr zu arbeiten ist aber trotzdem, wie du gleich sehen wirst, ganz einfach. Mit dem Befehl getLocation(), in Zeile 2 in
Listing 6.3, speichern wir die Koordinaten des Spielers in der Variable position. Mithilfe dieser Variable und den Befehlen getX(), getY() und getZ() kann dann wie in Listing 6.4 gezeigt auf die X-, Yund Z-Werte des Spielers zugegriffen werden. 1 2 3
x = position.getX()
y = position.getY()
z = position.getZ()
Listing 6.4: Lesen von X-, Y-, und Z-Werten der Spielerkoordinaten
Die Variablen x, y und z beinhalten dann jeweils eine ganz gewöhnliche Kommazahl, wie du sie schon aus den vorherigen Kapitel kennst. Nun haben wir zwar die Position des Spielers herausgefunden, allerdings wollen wir den Kürbis ja vor dem Spieler platzieren und nicht in ihm. Wenn wir also die Variable position verwenden wollen, um einen Kürbis vor dem Spieler zu platzieren, dann müssen wir nicht nur Werte aus der Variable lesen, sondern sie auch verändern. Wie das funktioniert kannst du in Listing 6.5 sehen. 1 2 3
position.setX(0)
position.setY(13.37)
position.setZ(position.getZ() + 5)
Listing 6.5: Setzen von X-, Y-, und Z-Werten
Mit den Befehlen setX(), setY() und setZ() können die entsprechenden Werte einer Positions-Variable verändert werden. Im einfachsten Fall kannst du dazu in Klammern einfach, wie in Zeile 1, eine ganze Zahl angeben, oder, wie in Zeile 2, eine Kommazahl. In beiden Fällen wird der X- beziehungsweise Y-Wert der Variable dann genau auf diesen Wert gesetzt. Etwas ausgeklügelter ist die Anweisung in Zeile 3, hier wird der alte Z-Wert der Variable um 5 erhöht, dazu wird zunächst der alte Wert mit getZ() gelesen, dann mit 5 addiert und dann mit setZ() in der Variable gespeichert. Genau diese Vorgehensweise ist auch nötig, um einen Kürbis vor dem Spieler zu platzieren. Möchtest du das zum Beispiel in zwei
Blöcken Abstand in X-Richtung tun, so muss die zugehörige onCommand-Funktion wie in Listing 6.6 gezeigt aussehen. 1 2 3 4
def onCommand(self, sender, command, label, args):
position = sender.getLocation()
position.setX(position.getX() + 2)
return True
Listing 6.6: Bestimmung der zukünftigen Position des Kürbis
6.1.2 Blöcke platzieren Nun hast du schon die richtige Position, jetzt muss an dieser nur noch ein Kürbis platziert werden. Das funktioniert erfreulicherweise recht einfach, wie Listing 6.7 zeigt. 1 2 3 4 5 6 7 8 9
def onCommand(self, sender, command, label, args):
position = sender.getLocation()
position.setX(position.getX() + 2)
welt = sender.getWorld()
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
return True
Listing 6.7: Platzieren eines Kürbisses
Bevor du etwas in der Spielwelt verändern kannst, also zum Beispiel einen Kürbis in ihr platzieren, musst du dir zuerst einmal Zugriff auf die Welt verschaffen. Das passiert in Listing 6.7 in Zeile 5. Über die Spieler-Variable wird der Befehl getWorld( ) aufgerufen, und die Repräsentation der Spielwelt in der Variable welt gespeichert. Über die welt-Variable kannst du dann auf den Block zugreifen, der sich an der Stelle befindet, an der später der Kürbis stehen soll. Dazu verwendest du den getBlockAt()-Befehl. Anders als im Spiel selbst ist beim Programmieren nämlich auch Luft ein Block: Wo im
Spiel kein Block ist, ist für das Plugin ein Luft-Block, daher befindet sich an der Stelle auf jeden Fall bereits ein Block. Damit der Befehl auch weiß, welchen Block du auswählen möchtest, musst du ihm noch die genaue Position des Blocks übergeben, das geschieht mit getBlockAt(position). Mithilfe des Befehls setType(), den du in Zeile 7 findest, wird der Block dann, egal um welchen Block es sich vorher gehandelt hat, in einen Kürbis-Block verwandelt. bukkit.Material.PUMPKIN steht dabei für einen Kürbis, du könntest aber auch jeden beliebigen anderen Block an dieser Stelle platzieren, mit bukkit.Material.DIRT könntest du zum Beispiel auch einen Erd-Block platzieren. Eine Liste aller verfügbaren Blocktypen findest du im Anhang dieses Buches. Um das fertige Kürbis-Plugin testen zu können, musst du jetzt nur noch die onCommand-Funktion aus Listing 6.7 in das Grundgerüst aus Listing 6.2 einfügen. Das Ergebnis und damit die komplette plugin.py kannst du in Listing 6.8 sehen. 1 2 3 4 5 6 7 8 9 10 11 12 13
class KuerbisPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
position = sender.getLocation()
position.setX(position.getX() + 2)
welt = sender.getWorld()
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
return True
Listing 6.8: Komplettes Kürbis-Plugin
Wenn du nun deinen Server neu startest, dich mit deinem MinecraftClient auf den Server verbindest und den Befehl /kuerbis eingibst, dann sollte, wie in Abbildung 6.2, ein Kürbis vor dir in der Spielwelt erscheinen. Ein echter Meilenstein auf deinem Weg zum MinecraftPlugin-Profi, denn inzwischen kannst du nicht nur Textnachrichten
anzeigen lassen, du kannst jetzt auch die Welt von Minecraft mit deinen Plugins direkt verändern.
Abb. 6.2: Per Befehl platzierter Kürbis
Hinweis Wenn du versuchst, den Befehl kuerbis in das Server-Fenster einzugeben, wird der Server das mit einer Fehlermeldung quittieren. Schließlich bist du als Server-Administrator kein Spieler und hast dementsprechend auch keine Position innerhalb der Spielwelt. Wie man solche Fehlermeldungen vermeiden kann, darum wird es unter anderem im nächsten Kapitel gehen.
6.2 Die verschiedenen Schleifen
Mit den Befehlen, die du gerade kennengelernt hast, kannst du auch problemlos mehrere Blöcke aufeinander stapeln, wie Listing 6.9 zeigt. Zunächst wird die Position des Spielers geladen und der XWert dann in Zeile 3 um zwei erhöht. Danach wird nach jedem platzierten Block die Y-Koordinate jeweils um eins erhöht, damit die Blöcke übereinander positioniert werden. Der X-Wert bleibt für jeden Block gleich. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
welt = sender.getWorld()
position = sender.getLocation()
position.setX(position.getX() + 2)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
position.setY(position.getY() + 1)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
position.setY(position.getY() + 1)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
position.setY(position.getY() + 1)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
Listing 6.9: Manuell erzeugter Turm aus Kürbisblöcken
Selbst für vier Blöcke ist das, wie du siehst, schon eine ganze Menge Code, der sich noch dazu wiederholt. Würdest du jetzt den Typ des Blocks ändern wollen, so müsstest du dies für jeden Block einzeln erledigen. Wenn du 10 oder noch mehr Blöcke aufeinander stapeln möchtest, würde dein Programmcode schnell sehr unübersichtlich werden. In diesem Abschnitt wollen wir uns daher mit sogenannten Schleifen beschäftigen. Mit ihrer Hilfe kannst du einzelne Abschnitte deines Quellcodes mehrmals hintereinander ausführen, vermeidest somit unnötige Schreibarbeit und hältst deinen Code übersichtlich.
6.2.1 for-Schleife Python kennt zwei verschiedene Arten von Schleifen. Beginnen wollen wir mit der sogenannten for-Schleife, deren als Syntax bezeichnete Schreibweise findest du in Listing 6.10. 1 2
for x in y:
#Anweisung
Listing 6.10: Syntax der for-Schleife
In der ersten Zeile findest du den sogenannten Schleifenkopf. Der beginnt mit dem Stichwort for, woran sofort zu erkennen ist, dass es sich hierbei um eine for-Schleife handelt. Dahinter folgt die sogenannte Schleifenvariable. Mithilfe der for-Schleife kannst du schnell und unkompliziert eine Anweisung für jedes Element einer Liste ausführen. Ein Beispiel dafür findest du in Listing 6.11. 1 2 3 4
liste = ["Hallo", "Welt", "!"]
for element in liste:
self.getServer().broadcastMessage(element)
Listing 6.11: for-Schleife mit Textausgabe
Die Anweisung – oder Anweisungen, falls es mehrere sind –, die sich innerhalb einer Schleife befindet, in diesem Beispiel also self.getServer().broadcastMessage(element), nennt man auch Schleifenrumpf. Bei einer for-Schleife wird dieser Schleifenrumpf einmal für jedes Element der angegebenen Liste ausgeführt. Die Schleife in Listing 6.11 würde also insgesamt dreimal ausgeführt. Beim ersten Durchlauf hat die Variable element den Wert »Hallo«, beim zweiten Mal den Wert »Welt« und beim letzten Mal schließlich den Wert »!«. Die dazugehörige Ausgabe im Spiel kannst du in Abbildung 6.3 sehen.
Abb. 6.3: Ausgabe von Listing 6.11
Welche Anweisungen zum Schleifenrumpf gehören und welche nicht, wird, wie in Python üblich, durch Einrückungen markiert. Der Unterschied wird in Listing 6.12 und Listing 6.13 deutlich. Beide Listings unterscheiden sich lediglich durch die Einrückung in der letzten Zeile. In Listing 6.12 gehört die Anweisung self.getServer().broadcastMessage("Ende"), da sie nicht mehr eingerückt ist, nicht mehr zum Schleifenrumpf, in Listing 6.13 dagegen schon. 1 2 3 4 5
liste = ["Hallo", "Welt", "!"]
for element in liste:
self.getServer().broadcastMessage(element)
self.getServer().broadcastMessage("Ende")
Listing 6.12: for-Schleife mit zusätzlicher Anweisung nach dem Schleifenrumpf
1 2 3 4 5
liste = ["Hallo", "Welt", "!"]
for element in liste:
self.getServer().broadcastMessage(element)
self.getServer().broadcastMessage("Ende")
Listing 6.13: for-Schleife mit zusätzlicher Anweisung im Schleifenrumpf
Der Effekt: In Listing 6.12 wird die Schleife durchlaufen und der Befehl danach einmal ausgeführt, die Ausgabe würde also »Hallo Welt ! Ende« heißen, wobei jedes Wort in einer eigenen Nachricht steht, da der Befehl broadcastMesssage für jedes Wort einzeln aufgerufen wird. In Listing 6.13 dagegen wird der Befehl bei jedem Schleifendurchlauf ausgeführt, die Ausgabe wäre deshalb »Hallo Ende Welt Ende ! Ende«, wobei ebenfalls jedes Wort in einer eigenen Zeile steht. Oftmals, so auch beim Bau eines Turms, wird die for-Schleife aber nicht nur verwendet, um Anweisungen für jedes Element einer Liste auszuführen, sondern auch, um bestimmte Anweisungen einfach zu wiederholen, wie in Listing 6.14. 1 2
for i in range(0,10):
self.getServer().broadcastMessage("Ende")
Listing 6.14: for-Schleife mit range-Befehl
Die Ausgabe dieses Codes wären zehn Nachrichten mit dem Inhalt »Ende«. Auch wenn es auf den ersten Blick vielleicht nicht so scheint, auch in diesem Beispiel führt die for-Schleife die Anweisung im Prinzip für jedes Element einer Liste aus, denn der Befehl range erstellt eine Liste von Zahlen, die bei dem ersten Wert, in diesem Fall also 0, startet und vor dem letzten Wert (10) endet, in diesem Fall also 9. range(0,10) erstellt also eine Liste [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. Dementsprechend wäre die Ausgabe der Schleife in Listing 6.15 eine Nachricht mit dem Inhalt »10« und eine Nachricht mit dem
Inhalt »11«. 1 2
for i in range(10,12):
self.getServer().broadcastMessage(str(i))
Listing 6.15: for-Schleife mit range-Befehl und Ausgabe
Wie können wir dieses Wissen nun nutzen, um einen Turm aus Kürbissen zu bauen? Auf die verschiedensten Arten. Die wahrscheinlich offensichtlichste Methode findest du in Listing 6.16. Hier wurden einfach die Teile aus Listing 6.9, die sich mehrfach wiederholen, in den Rumpf einer for-Schleife gepackt. So kannst du durch Erhöhung der letzten Zahl des range-Befehls beliebig hohe Türme bauen, ohne dass dein Plugin dadurch länger wird. 1 2 3 4 5 6 7 8 9
welt = sender.getWorld()
position = sender.getLocation()
position.setX(position.getX() + 2)
for i in range(0,10):
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
position.setY(position.getY() + 1)
Listing 6.16: Kürbisturm mit for-Schleife Version 1
In Listing 6.17 siehst du eine andere Möglichkeit unter Verwendung einer Schleifenvariable. Bei dieser Version wird der Ausgangswert der Y-Koordinate in der Variable ursprungY gespeichert. Statt dann jedes Mal die Y-Koordinate um eins zu erhöhen, wird bei jedem Schleifendurchlauf zum Ursprungswert der aktuelle Wert der Schleifenvariable addiert, also im ersten Durchlauf ursprungY + 0, im zweiten Durchlauf ursprungY + 1 und so weiter. Das Ergebnis ist natürlich genau dasselbe wie in Listing 6.17. 1 2 3
welt = sender.getWorld()
position = sender.getLocation()
position.setX(position.getX() + 2)
4 5 6 7 8 9
ursprungY = position.getY()
for i in range(0,10):
position.setY(ursprungY + i)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
Listing 6.17: Kürbisturm mit for-Schleife Version 2
Eine dritte und letzte Möglichkeit findest du in Listing 6.18. Auch hier wird wieder die Schleifenvariable zum Bauen des Turms verwendet, allerdings wird die range hier gleich so gewählt, dass im Rumpf der Schleife überhaupt keine Addition mehr notwendig ist, sondern der Wert der Schleifenvariable jeweils direkt schon die korrekte YKoordinate ist. 1 2 3 4 5 6 7 8
welt = sender.getWorld()
position = sender.getLocation()
position.setX(position.getX() + 2)
for i in range(position.getY(), position.getY()+10):
position.setY(i)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
Listing 6.18: Kürbisturm mit for-Schleife Version 3
Egal für welche der drei Versionen du dich entscheidest, eine von ihnen kannst du nun in dein Kürbis-Plugin einbauen, dazu musst du nur die onCommand-Funktion anpassen, und wenn du möchtest, natürlich noch die Versionsnummer. Wie das neue Plugin mit der Lösung Version 3 aussieht, kannst du in Listing 6.19 sehen. 1 2 3 4 5 6 7 8
class KuerbisPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
welt = sender.getWorld()
position = sender.getLocation()
position.setX(position.getX() + 2)
9 10
11 12 13 14 15
16
ursprungY = position.getY()
for i in range(0,10):
position.setY(ursprungY + i)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.PUMPKIN)
return True
Listing 6.19: Kürbis-Plugin mit for-Schleife
Wenn du nun deinen Server neustartest, /kuerbis in den Chat eingibst, dann musst du zuerst einmal ein Stück zurücktreten, um dein Werk wie in Abbildung 6.4 in seiner vollen Größe bewundern zu können. Da steht er, dein selbst gebauter, oder viel mehr programmierter Kürbisturm. Ohne Schleife hättest du dafür 19 Zeilen Code benötigt, mit einer Schleife waren es gerade einmal vier. Du siehst also schon, dass Schleifen ein nützliches Werkzeug beim Programmieren sein können.
Abb. 6.4: Kürbisturm
6.2.2 while-Schleife Grund genug, um sich auch noch die zweite Schleifenart genau anzusehen, die sogenannte while-Schleife. Jede for-Schleife lässt sich auch als while-Schleife ausdrücken, in Listing 6.20 findest du zum Beispiel den Code aus Listing 6.15, der die Zahlen 10 und 11 ausgibt, mithilfe einer while-Schleife ausgedrückt. 1 2 3 4 5
i=10
while i < 12:
self.getServer().broadcastMessage(str(i))
i = i + 1
Listing 6.20: while-Schleife
Im Schleifenkopf der while-Schleife befindet sich, hinter dem Stichwort while, die sogenannte Laufbedingung, sie gibt an, wie oft die Schleife ausgeführt wird. Vor jeder Wiederholung prüft die Schleife diese Laufbedingung. Nur wenn sie erfüllt ist, wird der Schleifenrumpf ein weiteres Mal ausgeführt. Im Beispiel in Listing 6.20 ist die Laufbedingung, dass i kleiner als 12 ist. Sobald i also 12 oder größer ist, wird die Schleife nicht mehr wiederholt. Wäre die Laufbedingung i < 10, so würde die Schleife beim Startwert i = 10 kein einziges Mal ausgeführt, da bereits der Startwert die Laufbedingung nicht erfüllen würde. Da die Laufbedingung i < 12 ist, wird die Schleife zweimal ausgeführt, bei der ersten Prüfung ist i = 10 und bei der zweiten Prüfung ist i = 11.
Merke Eine Laufbedingung steuert, wie oft eine Schleife wiederholt wird. Vor jedem Schleifendurchlauf wird die Laufbedingung geprüft, nur wenn sie erfüllt ist, wird der Rumpf der Schleife ein weiteres Mal ausgeführt.
Bei
Gleich
==
Kleiner-gleich
=
Ungleich
!=
oder
Tabelle 6.1: Vergleichsoperatoren
Es ist wichtig darauf zu achten, dass sich die Laufbedingung auch tatsächlich beim Schleifendurchlauf ändern kann. Würde zum Beispiel in Listing 6.20 i nicht in der letzten Zeile um eins erhöht, so wäre die Laufbedingung immer erfüllt, und es würde sich um eine sogenannte Endlosschleife handeln, die endlos oft hintereinander ausgeführt wird.
Wahrheitswerte Eine Laufbedingung kann entweder erfüllt beziehungsweise wahr, englisch true, sein, dann wird die Schleife ein weiteres Mal durchlaufen, oder sie ist nicht erfüllt beziehungsweise falsch, englisch false, dann wird die Schleife nicht noch einmal durchlaufen. Man nennt diese beiden Zustände auch Wahrheitswerte. Nicht nur Zahlen und Strings, sondern auch Wahrheitswerte können in Python in Variablen gespeichert werden, wie das funktioniert, kannst du in Listing 6.21 sehen. 1 2
b1 = True #wahr
b2 = False #falsch
Listing 6.21: Speichern von Wahrheitswerten in Variablen
Merke und False müssen am Anfang immer mit einem Großbuchstaben geschrieben werden. True
Eine while-Schleife mit dem Schleifenkopf while(True) ist immer eine Endlosschleife, bei dem Schleifenkopf while(False) würde der Schleifenrumpf dagegen niemals ausgeführt. Wann immer du einen Vergleichsoperator aus Tabelle 6.1 verwendest, wandelt Python diesen Vergleich intern auch in einen Wahrheitswert um. Der Wert der Variable b3 in Listing 6.22 wäre daher False, der von b4 dagegen True. 1 2
b3 = 10 < 5 #False
b4 = 9 == 9 #True
Listing 6.22: Ergebnis eines Vergleichsoperators in einer Variable speichern
Mit dem Gleich-Operator lassen sich übrigens nicht nur Zahlen vergleichen, sondern auch, wie in Listing 6.23, Strings. Dabei zeigt sich Python wieder einmal von seiner pingeligen Seite. Während die beiden Strings »Hallo« und »Hallo« gleich sind, sind es die beiden Strings »Hallo« und »hallo« nicht, es wird also zwischen Groß- und Kleinschreibung unterschieden. 1 2 3 4 5 6
a b c
x y
= "Hallo"
= "Hallo"
= "hallo"
= a == b #True
= b == c #False
Listing 6.23: Vergleich von Strings
Tipp Möchtest du zwei Strings a und b ohne Berücksichtigung von Groß- und Kleinschreibung vergleichen, so kannst du das mit der lower()-Funktion. Sie sorgt dafür, dass beide Strings, für den Vergleich, komplett in Kleinbuchstaben umgewandelt werden. Wie so ein Vergleich dann aussieht, kannst du in Listing 6.24 sehen. Dort ist insbesondere die letzte Zeile interessant, da der Vergleich dort nun, anders als ohne die lower()-Funktion, ebenfalls True zurückliefert. Alternativ kannst du auch die Funktion upper( ) verwenden, die alle Buchstaben in Großbuchstaben umwandelt. 1 2 3 4 5 6
a b c
x y
= "Hallo"
= "Hallo"
= "hallo"
= a.lower() == b.lower() #True
= b.lower() == c.lower() #True
Listing 6.24: Vergleich von Strings mit der lower-Funktion
Verknüpfungen Es können aber nicht nur einfache Vergleiche von Zahlen oder Strings als Laufbedingungen verwendet werden, es sind auch noch deutlich komplexere Konstruktionen möglich. Stell dir zum Beispiel vor, du möchtest programmieren, dass eine Schleife solange ausgeführt wird, bis der Spieler sich bewegt. Die Laufbedingung kann man in natürlicher Sprache so formulieren: Führe die Schleife aus, solange sich der X-, Y-, und Z-Wert der Position des Spielers nicht verändert. Du hast zwar schon den Gleich-Operator kennengelernt, mit dem sich zwei Zahlen auf Gleichheit prüfen lassen, allerdings haben alle bisherigen Laufbedingungen immer aus nur einer Überprüfung bestanden, während diese aus drei besteht. Wie sich das umsetzen lässt, kannst du in Listing 6.25 in Zeile 7 sehen. 1 position = sender.getLocation()
2
3 startX = position.getX()
4 startY = position.getY()
5 startZ = position.getZ()
6
7 while (position.getX() == startX) & (position.getY() == startY) & (position.getZ() == startZ):
8 #Anweisungen
9 position = sender.getLocation()
Listing 6.25: Beispiel für die Anwendung des &-Operators
Dort findest du drei Bedingungen, die nach dem dir bereits bekannten Muster aufgebaut sind, allerdings sind sie diesmal mit dem Und-Operator& verbunden. Nun passiert Folgendes: Jede der drei Bedingungen wird einzeln ausgewertet. Sind alle Bedingungen wahr, hat sich der Spieler also noch nicht bewegt, so steht für den Computer dort True & True & True. Nun werden die Und-Operatoren von links nach rechts ausgewertet: ((True & True) & True). Wahr und wahr ergibt erwartungsgemäß ebenfalls wahr, sodass im nächsten Schritt nur noch True & True dort steht, was ebenfalls als wahr bewertet wird. Kurz gesagt: Zwei mit einem Und verknüpfte
Wahrheitswerte werden genau dann als »wahr« bewertet, wenn beide wahr sind. Tabelle 6.2 zeigt noch einmal in einer Übersicht, wie sich der UndOperator für alle möglichen Kombinationen von Wahrheitswerten verhält. Eine solche Übersicht wird auch Wahrheitstafel genannt.
a
b
a&b
falsch
falsch
falsch
falsch
wahr
falsch
wahr
falsch
falsch
wahr
wahr
wahr
Tabelle 6.2: Wahrheitstafel für &
Hätte sich der Spieler also zum Beispiel nur in Z-Richtung bewegt, so würde die Laufbedingung so aussehen: ((True & True) & False). Nun kannst du schrittweise streng nach der Tabelle vorgehen. Dann würde dort (True & False) stehen, da True & True laut Tabelle zu True wird. True & False findest du wiederum in der dritten Zeile der Tabelle – und das wird zu False. Die Schleife würde also, wie erwartet, nicht noch einmal ausgeführt werden, da der Spieler sich bewegt hat und die Laufbedingung damit nicht mehr erfüllt ist. Neben dem Und-Operator gibt es aber noch andere Möglichkeiten, zwei Wahrheitswerte zu verknüpfen. In Listing 6.26 siehst du zum Beispiel den Oder-Operator|.
1 position = sender.getLocation()
2
3 startX = position.getX()
4 startY = position.getY()
5 startZ = position.getZ()
6
7 while (position.getX() == startX) | (position.getY() == startY) | (position.getZ() == startZ):
8 #Anweisungen
9 position = sender.getLocation()
Listing 6.26: Beispiel für die Anwendung des |-Operators
Die Schleife in Listing 6.26 wird so lange ausgeführt, wie mindestens eine Koordinate der Spielerposition unverändert bleibt. In Worten ausgedrückt: Der Oder-Operator ist also genau dann wahr, wenn mindestens einer der verknüpften Wahrheitswerte wahr ist. Tabelle 6.3 zeigt die zugehörige Wahrheitstafel für den Oder-Operator.
a
b
a|b
falsch
falsch
falsch
falsch
wahr
wahr
wahr
falsch
wahr
wahr
wahr
wahr
Tabelle 6.3: Wahrheitstafel für |
Der Oder-Operator ist auch ein gutes Beispiel dafür, dass unsere Alltagssprache oft ziemlich ungenau ist. Das ist einer der Gründe,
wieso Mathematiker und Informatiker formale Darstellungen wie Wahrheitstafeln der Sprache oft vorziehen. Wenn zum Beispiel jemand sagt »Ich wäre froh, wenn es zum Nachtisch Himbeer- oder Meloneneis gibt.«, dann ist er froh, wenn es nur Himbeereis gibt, er ist froh wenn es nur Meloneneis gibt, er ist aber sicher auch froh, wenn es Himbeer- und Meloneneis gibt. Die Verwendung des Wortes »oder« entspricht hier also ganz der Tabelle 6.3: Das Ergebnis ist wahr, wenn a oder b, oder beide wahr sind. Wenn nun aber jemand sagt »Je nachdem wie das Wetter morgen wird, fahre ich mit dem Auto oder dem Fahrrad zur Arbeit.«, dann meint er damit: Ich fahre morgen entweder mit dem Auto zur Arbeit oder mit dem Fahrrad. Er wird nicht mit dem Auto und dem Fahrrad zur Arbeit fahren, egal wie das Wetter wird. Hierbei handelt es sich also um ein anderes »Oder«, das exklusive Oder. Auch für diese Verwendung des Wortes gibt es einen entsprechenden Python-Operator, den Entweder-Oder- beziehungsweise XOR-Operator, der nur aus dem Zeichenˆ besteht. In seiner Wahrheitstafel, die du in Tabelle 6.4 findest, kannst du genau diesen kleinen, aber feinen Unterschied erkennen, denn im Gegensatz zum Oder-Operator ist das Ergebnis für wahr und wahr in der letzten Zeile hier falsch.
a
b
aˆb
falsch
falsch
falsch
falsch
wahr
wahr
wahr
falsch
wahr
a
b
aˆb
wahr
wahr
falsch
Tabelle 6.4: Wahrheitstafel für ˆ
Einen vierten und letzten Operator wollen wir uns noch ansehen, bevor wir zum eigentlichen Thema, den Schleifen, zurückkehren: den Nicht-Operatornot. Die Wahrheitstafel in Tabelle 6.5 zeigt schon einen deutlichen Unterschied zu den anderen Operatoren. Beim Nicht-Operator handelt es sich um einen sogenannten einstelligen Operator. Während die anderen Operatoren zwei Werte verknüpfen, wird der Nicht-Operator nur auf einen Wert angewendet und negiert diesen. Das ist zum Beispiel hilfreich, wenn du eine Schleife ausführen willst, solange eine Variable a = False ist. Statt while a == False kannst du mithilfe des Nicht-Operators nämlich einfach while not a schreiben.
a
not a
falsch
wahr
wahr
falsch
Tabelle 6.5: Wahrheitstafel für not
Merke
In diesem Kapitel hast du zwei verschiedene Schleifen kennengelernt: for-Schleife while-Schleife
Den Teil einer Schleife, der festlegt, wie oft sie wiederholt wird, nennt man Schleifenkopf, der Teil mit den Anweisungen, die wiederholt werden sollen, heißt Schleifenrumpf.
6.2.3 Verschachtelte Schleifen Zum Abschluss des Kapitels über Schleifen wollen wir noch ein kleines Plugin programmieren, das nicht nur einen Turm, sondern gleich eine ganze Mauer baut. Wie üblich musst du für das neue Plugin zunächst einen eigenen Ordner anlegen, zum Beispiel mit dem Namen mauer.py.dir. In diesem erstellst du dann die plugin.yml mit dem in Listing 6.27 gezeigten Inhalt. 1 2 3 4 5 6 7
name: Mauer-Plugin
main: MauerPlugin
version: 1.0
commands:
mauer:
description: Platziert eine Mauer vor dem Spieler
usage: /mauer
Listing 6.27: plugin.yml für das Mauer-Plugin
Die plugin.py beginnst du zunächst am besten wieder mit dem Grundgerüst. Hierfür kannst du diesmal, wie in Listing 6.28 gezeigt, auf das Kürbis-Plugin zurückgreifen. Auch für dieses Plugin wirst du
wieder eine welt-Variable benötigen und auch die Mauer soll wieder kurz vor dem Spieler platziert werden. 1 2 3 4 5 6 7 8 9 10 11
class MauerPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
position = sender.getLocation()
position.setX(position.getX() + 2)
welt = sender.getWorld()
return True
Listing 6.28: Mauer-Plugin Schritt 1
Beim Kürbis-Plugin hast du einen Turm gebaut, das heißt, du hast Blöcke platziert, dabei die X- und Z-Koordinate unverändert gelassen und nur die Y-Koordinate schrittweise erhöht. Das entspricht dem grünen Pfeil in Abbildung 6.5. Das Mauer-Plugin soll nun keinen Turm mehr bauen, sondern eine Mauer. Dafür muss sich nicht nur die Y-Koordinate, sondern auch die Z-Koordinate ändern.
Abb. 6.5: Vorgehen beim Bau einer Mauer
Nachdem du einen Schritt in Y-Richtung nach oben gegangen bist, musst du jedes Mal auch noch in Z-Richtung gehen, in Abbildung 6.5 als blauer Pfeil dargestellt. Dieses Verhalten lässt sich am einfachsten erreichen, in dem du zwei Schleifen ineinander verschachtelst. 1 2 3
for y in range(0, 10):
for z in range (0, 20):
s = str(y) + "," + str(z)
Listing 6.29: Beispiel für verschachtelte Schleifen
Der Wert für y ist im ersten Durchlauf 0. Dann wird der Wert von z ebenfalls auf 0 gesetzt. Der String s hätte also den Wert 0,0. Nun wird die innere Schleife, die z hochzählt, wiederholt. s hätte dann im nächsten Schritt den Wert 0,1, dann 0,2 und immer so weiter bis 0,19. Da die zweite Schleife dann am Ende der Liste angelangt ist, wird sie nicht mehr wiederholt, sondern verlassen. Da sich das Programm nun wieder in der äußeren Schleife befindet, wird y nun auf 1 gesetzt. Jetzt beginnt wieder die Ausführung der inneren Schleife bis 1,19. Dann wird y wieder erhöht und immer so weiter, bis schließlich 9,19 erreicht ist und beide Schleifen nicht mehr wiederholt werden. Warum die innere Schleife zuerst immer bis zum Ende durchlaufen und erst dann die äußere Schleife wieder ausgeführt wird, wird auch deutlich, wenn du dir überlegst, wie das Konstrukt aussehen würde, wenn du die äußere Schleife einfach »ausschreibst«: 1 2 3 4 5 6 7 8 9 10
y = 0
for z in range s = str(y)
y = 1
for z in range s = str(y)
y = 2
for z in range
(0, 20):
+ "," + str(z)
(0, 20):
+ "," + str(z)
(0, 20):
11 12 13
s = str(y) + "," + str(z)
#und so weiter
Jetzt musst du nur noch die Zusammensetzung des Strings durch die Platzierung eines Blocks ersetzen und das Ganze in das vorgefertigte Gerüst packen – schon ist dein Mauer-Plugin fertig. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
class MauerPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
position = sender.getLocation()
position.setX(position.getX() + 2)
welt = sender.getWorld()
yStart = position.getY()
zStart = position.getZ()
for y in range (0, 10):
position.setY(yStart + y)
for z in range (0, 20):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.GOLD_BLOCK)
return True
Listing 6.30: Mauer-Plugin Schritt 2
Nun kannst du deinen Server wieder neu starten, um das Plugin zu testen. Sobald du dich dann mit dem Server verbunden hast, kannst du mit dem Befehl /mauer aus dem Nichts eine 10 x 20 Blöcke große Mauer vor dir entstehen lassen. Wie du in Abbildung 6.6 sehen kannst, handelt es sich dabei um eine ziemlich luxuriöse Mauer, die vollkommen aus Goldblöcken besteht. Natürlich kannst du aber auch jedes andere Material wählen. Dazu musst du einfach in Zeile 19 des Plugins den Blocktyp anpassen.
Abb. 6.6: Vom Plugin erzeugte Mauer
Verzweigungen Kapitel 7
Mit den Schleifen hast du gleich zwei wichtige Konzepte auf einmal kennengelernt, zum einen die wiederholte Ausführung von Code, zum anderen aber auch die bedingte Ausführung von Code. Die Befehle innerhalb einer Schleife werden nur solange ausgeführt, wie eine bestimmte Bedingung zutrifft. Diese bedingte Ausführung von Befehlen ist ein Konzept, das beim Programmieren sehr häufig gebraucht wird – und das nicht nur im Zusammenhang mit Schleifen. Oft möchte man Code, abhängig von einer Bedingung, entweder einmal oder überhaupt nicht ausführen. Dafür gibt es in Python sogenannte Verzweigungen, um die es in diesem Kapitel gehen soll.
7.1 if Das eben von dir programmierte Mauer-Plugin ist ein gutes Beispiel dafür, wo der Einsatz einer solchen Verzweigung sinnvoll sein kann. Wie schon beim Kürbis-Plugin erhältst du dort nämlich ebenfalls eine Fehlermeldung, wenn du versuchst, den Befehl aus dem ServerFenster heraus auszuführen. Das Plugin sollte also zunächst prüfen, ob es sich beim Aufrufer um einen Spieler handelt, und nur dann eine Mauer errichten, wenn das der Fall ist. Genau hier kommt eine Verzweigung ins Spiel. In Listing 7.1 kannst du die Syntax der sogenannten if-Verzweigung sehen. 1 2
if a > b:
#Anweisung
Listing 7.1: Syntax if-Verzweigung
Das englische Wort »if« bedeutet übersetzt so viel wie »falls« und beschreibt damit schon ziemlich gut, was die Verzweigung tut. Hinter dem if folgt eine Bedingung, wie du sie schon von der whileSchleife kennst. Dort wird also ebenfalls ein Wahrheitswert, True oder False, beziehungsweise ein Ausdruck erwartet, der zu einem solchen Wahrheitswert ausgewertet wird. Danach folgt der Rumpf der Verzweigung, der, abermals wie bei der Schleife, durch Einrückung markiert wird. Die if-Verzweigung funktioniert also im Prinzip wie eine while-Schleife, mit dem Unterschied, dass der Rumpf maximal einmal ausgeführt wird.
Hinweis Wegen der großen Ähnlichkeit von if-Verzweigung und Schleifen wird fälschlicherweise manchmal auch von einer if-Schleife gesprochen. Als guter Programmierer weißt du aber natürlich, dass es sich bei der if-Konstruktion um eine Verzweigung handelt – nicht um eine Schleife.
Zur Anpassung des Mauer-Plugins brauchst du nun noch eine Funktion, die überprüft, ob es sich beim Aufrufer um einen Spieler handelt, und entweder True oder False zurück gibt. Dabei hilft dir der isinstance-Befehl, wie du in Listing 7.2 sehen kannst. isinstance(variable, str) prüft zum Beispiel, ob die angegebene Variable oder ein angegebener Wert vom Typ String, also ein Text ist. 1 2
w1 = isinstance("Text", str) #True
w2 = isinstance(3, str) #False
Listing 7.2: Verwendung isinstance
Um zu prüfen, ob ein Befehl von einem Spieler eingegeben wurde, musst du testen, ob die Variable sender deiner onCommand-Funktion vom Typ Player ist. Wie genau du dabei vorgehen musst, kannst du in Listing 7.3 sehen. Besonders beachten solltest du dabei auch die erste Zeile, denn um den Typ Player benutzen zu können, musst du ihn zunächst importieren; das geschieht mit der Zeile from org.bukkit.entity import Player. 1 from org.bukkit.entity import Player
2
3 class MauerPlugin(PythonPlugin):
4 def onEnable(self):
5 pass
6
7 def onCommand(self, sender, command, label, args):
8 if isinstance(sender, Player):
9 position = sender.getLocation()
10 position.setX(position.getX() + 2)
11
12 welt = sender.getWorld()
13
14 yStart = position.getY()
15 zStart = position.getZ()
16
17 for y in range (0, 10):
18 position.setY(yStart + y)
19 for z in range (0, 20):
20 position.setZ(zStart + z)
21 block = welt.getBlockAt(position)
22 block.setType(bukkit.Material.GOLD_BLOCK)
23
24 return True
Listing 7.3: Mauer-Plugin Schritt 3
Wenn du nun mit dem so geänderten Plugin als Spieler versuchst, eine Mauer zu bauen, funktioniert das wie zuvor einwandfrei. Gibst du den Befehl aber direkt im Server-Fenster ein, passiert einfach nichts. Das ist zwar immerhin besser als eine Fehlermeldung, aber noch nicht optimal. Besser wäre es, den Nutzer darauf hinzuweisen, dass der Befehl nur für Spieler zur Verfügung steht.
7.2 else Wie fast immer beim Programmieren gibt es auch für dieses Problem die verschiedensten Lösungen. Eine mögliche Lösung kannst du in Listing 7.4 sehen. 1 def onCommand(self, sender, command, label, args):
2 if isinstance(sender, Player):
3 position = sender.getLocation()
4 position.setX(position.getX() + 2)
5
6 welt = sender.getWorld()
7
8 yStart = position.getY()
9 zStart = position.getZ()
10
11 for y in range (0, 10):
12 position.setY(yStart + y)
13 for z in range (0, 20):
14 position.setZ(zStart + z)
15 block = welt.getBlockAt(position)
16 block.setType(bukkit.Material.GOLD_BLOCK)
17
18 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgeführt werden!")
19 return True
Listing 7.4: Hinweis Alternative 1
In der dort gezeigten onCommand-Funktion wurde in der vorletzten Zeile ein zusätzlicher Befehl hinzugefügt, der den Text »Dieser Befehl kann nur von Spielern ausgeführt werden!« im Server-Fenster ausgibt. Dieser Befehl gehört, wie du an der Einrückung erkennen kannst, nicht mehr zum Rumpf der if-Verzweigung und wird daher auch ausgeführt, wenn der Befehl direkt über das Server-Fenster eingegeben wird. In diesem Fall wird also der entsprechende Hinweis angezeigt, aber der Befehl nicht ausgeführt, ganz so, wie wir es wollten. Der Nachteil dieser Lösung ist aber, dass der Text auch dann angezeigt wird, wenn ein Spieler den Befehl eingibt. Der kann die Nachricht dann zwar nicht sehen, weil sie nur im Server-
Fenster angezeigt wird, es kann dir dann aber passieren, dass du wichtigere Meldungen übersiehst, weil das ganze Fenster voll mit dem Hinweistext ist. Besser wäre es also, wenn der Text nicht immer angezeigt wird, sondern nur, wenn der Befehl auch tatsächlich aus dem ServerFenster heraus ausgeführt wird. Wie du das erreichen kannst, kannst du in Listing 7.5 sehen. 1 def onCommand(self, sender, command, label, args):
2 if isinstance(sender, Player):
3 position = sender.getLocation()
4 position.setX(position.getX() + 2)
5
6 welt = sender.getWorld()
7
8 yStart = position.getY()
9 zStart = position.getZ()
10
11 for y in range (0, 10):
12 position.setY(yStart + y)
13 for z in range (0, 20):
14 position.setZ(zStart + z)
15 block = welt.getBlockAt(position)
16 block.setType(bukkit.Material.GOLD_BLOCK)
17
18 if not isinstance(sender, Player):
19 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgeführt werden!")
20
21 return True
Listing 7.5: Hinweis Alternative 2
Dort findest du in Zeile 18 eine zusätzliche if-Verzweigung. Auf den ersten Blick sieht die Bedingung der in Zeile 2 sehr ähnlich, bei einem genaueren Blick fällt aber auf, dass hier der dir bereits bekannte not-Operator verwendet wird. Während der Rumpf der ersten if-Verzweigung also immer genau dann ausgeführt wird, wenn der Befehl von einem Spieler eingegeben wurde, wird der Rumpf der zweiten Verzweigung genau dann ausgeführt, wenn der Befehl nicht von einem Spieler eingegeben wurde.
Solche Konstruktionen benötigt man sehr häufig beim Programmieren: Falls eine Bedingung erfüllt ist, führe A aus, ansonsten B. So häufig sogar, dass es in fast allen Programmiersprachen, so auch Python, eine eigene Schreibweise dafür gibt, die dir das Leben erleichtert. Wie diese aussieht kannst du in Listing 7.6 sehen. 1 def onCommand(self, sender, command, label, args):
2 if isinstance(sender, Player):
3 position = sender.getLocation()
4 position.setX(position.getX() + 2)
5
6 welt = sender.getWorld()
7
8 yStart = position.getY()
9 zStart = position.getZ()
10
11 for y in range (0, 10):
12 position.setY(yStart + y)
13 for z in range (0, 20):
14 position.setZ(zStart + z)
15 block = welt.getBlockAt(position)
16 block.setType(bukkit.Material.GOLD_BLOCK)
17 else:
18 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgeführt werden!")
19
20 return True
Listing 7.6: Hinweis Alternative 3
Die zweite Bedingung, die es noch in Listing 7.5 gab, wurde hier durch das Stichwort else ersetzt. else, englisch für »sonst« oder »ansonsten«, kann immer nur nach einem if stehen und niemals alleine. Ist die Bedingung für den if-Teil nicht erfüllt, so wird stattdessen der else-Teil ausgeführt, ist die Bedingung erfüllt, so wird nur der if-Teil ausgeführt, nicht aber der else-Teil. In Listing 7.7 kannst du noch einmal die allgemeine Syntax einer if-Verzweigung mit else-Teil sehen.
1 2 3 4
if bedingung:
#Anweisung
else:
#Anweisung
Listing 7.7: Syntax if-else-Verzweigung
Das komplette Mauer-Plugin, ergänzt um den Hinweis nach Alternative 3, findest du in Listing 7.8. 1 from org.bukkit.entity import Player
2
3 class MauerPlugin(PythonPlugin):
4 def onEnable(self):
5 pass
6
7 def onCommand(self, sender, command, label, args):
8 if isinstance(sender, Player):
9 position = sender.getLocation()
10 position.setX(position.getX() + 2)
11
12 welt = sender.getWorld()
13
14 yStart = position.getY()
15 zStart = position.getZ()
16
17 for y in range (0, 10):
18 position.setY(yStart + y)
19 for z in range (0, 20):
20 position.setZ(zStart + z)
21 block = welt.getBlockAt(position)
22 block.setType(bukkit.Material.GOLD_BLOCK)
23 else:
24 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgeführt werden!")
25
26 return True
Listing 7.8: Mauer-Plugin Schritt 4
7.3 elif
Mithilfe von if und else lassen sich zwei Fälle einfach voneinander unterscheiden: Entweder ist die if-Bedingung erfüllt oder nicht. In der Praxis ist es aber häufig notwendig, zwischen vielen verschiedenen Fällen zu differenzieren. Der Einfachheit halber bemühen wir an dieser Stelle ein etwas abstrakteres Beispiel: Es gibt eine Variable namens b, in der irgendein Blocktyp gespeichert ist, also zum Beispiel b = bukkit.Material.GOLD_BLOCK. Wenn es sich bei b wie im Beispiel um den Typ Gold handelt, soll das Plugin die Nachricht »Ein wertvoller Block!« ausgeben. Handelt es sich dagegen um Wasser, so soll das Plugin »Mach mich nicht nass!« ausgeben. Bei Erde soll die Nachricht »Das ist ja nur Dreck!« lauten und bei allen anderen Blocktypen: »Das interessiert mich nicht!«. Es sind also insgesamt vier Fälle zu unterscheiden: 1. b ist Gold 2. b ist Wasser 3. b ist Erde 4. b ist weder Gold noch Wasser noch Erde Wie ist das mithilfe von if und else möglich? Eine Lösung, die ganz ohne else auskommt, kannst du in Listing 7.9 sehen. 1 if b == bukkit.Material.GOLD_BLOCK:
2 #Ein wertvoller Block!
3
4 if b == bukkit.Material.WATER:
5 #Mach mich nicht nass!
6
7 if b == bukkit.Material.DIRT:
8 #Das ist ja nur Dreck!
9
10 if (b != bukkit.Material.GOLD_BLOCK) & (b != bukkit.Material.WATER) & (b != bukkit.Material.DIRT):
11 #Das interessiert mich nicht!
Listing 7.9: Vier einzelne if-Verzweigungen hintereinander
Diese Lösung ist korrekt und tut auch was sie soll, sie ist aber ineffizient. Denn wenn b ein Goldblock ist, dann wissen wir bereits nach der ersten Bedingung in Zeile 1, das keine der folgenden Bedingungen wahr sein kann, sie werden aber trotzdem alle noch überprüft. Für andere Einsatzzwecke ist diese Lösung, das hintereinanderschreiben von if-Verzweigungen, gänzlich ungeeignet. Stell dir zum Beispiel folgende Aufgabe vor: Falls b Gold ist, dann mach daraus Wasser, falls b Wasser ist, dann mach daraus Gold. Auf den ersten Blick mag es so scheinen, als hätte der Code in Listing 7.10 das gewünschte Ergebnis, auf den zweiten Blick fällt aber auf, dass es nicht so ist. Egal ob b am Anfang Wasser oder Gold ist, am Ende von Listing 7.10 wird es immer Gold sein. 1 2 3 4 5
if b == b =
if b == b =
bukkit.Material.GOLD_BLOCK:
bukkit.Material.WATER
bukkit.Material.WATER:
bukkit.Material.GOLD_BLOCK
Listing 7.10: Problem beim hintereinanderschreiben von if-Verzweigungen
Ist b zu Beginn Wasser, so ist der Fall klar: Die erste Bedingung in Zeile 1 ist nicht erfüllt, der Rumpf der Verzweigung wird daher nicht ausgeführt. Die Bedingung in Zeile 4 ist dagegen erfüllt, der Rumpf wird ausgeführt und b wird auf Gold gesetzt, wir erhalten also das gewünschte Ergebnis. Zu einem unerwünschten Effekt kommt es aber, wenn b zu Beginn Gold ist. Zunächst ist die Bedingung in Zeile 1 dann erfüllt und b wird auf Wasser gesetzt. So weit, so gut. Dann wird allerdings auch noch die Bedingung in Zeile 4 geprüft und inzwischen ist b ja Wasser, die Bedingung ist also erfüllt und b wird wieder auf Gold gesetzt. Damit war b am Anfang Gold und ist es auch am Ende wieder, nicht das Ergebnis das wir erzielen wollten.
Die Lösung für dieses Problem lautet schachteln, denn genau wie Schleifen können auch Verzweigungen ineinander verschachtelt werden. Wie das funktioniert, kannst du in Listing 7.11 sehen. Die erste if-Verzweigung hat nun einen else-Teil, in dessen Rumpf sich die zweite if-Verzweigung befindet. 1 2 3 4 5
if b == bukkit.Material.GOLD_BLOCK:
b = bukkit.Material.WATER
else:
if b == bukkit.Material.WATER:
b = bukkit.Material.GOLD_BLOCK
Listing 7.11: Zwei verschachtelte Schleifen
Was nun passiert: Ist b zu Beginn Gold, so ist die Bedingung in Zeile 1 erfüllt, b wird zu Wasser und der else-Teil wird nicht ausgeführt, die zweite Bedingung damit auch nicht mehr geprüft. Ist b dagegen zu Beginn Wasser, so ist die erste Bedingung nicht erfüllt. Daher wird der else-Teil ausgeführt und damit die zweite Bedingung in Zeile 4 geprüft. Diese ist erfüllt und b wird auf Gold gesetzt. Damit haben wir nun nicht nur die gewünschte Funktionalität, sondern auch noch ein effizienteres Programm, da die zweite Bedingung erst gar nicht überprüft wird, falls die erste erfüllt ist. Diese Schachtelung von Verzweigungen lässt sich natürlich auch auf den Code in Listing 7.9 übertragen. Wie dieser mit verschachtelten Verzweigungen aussieht, kannst du in Listing 7.12 sehen. 1 2 3 4 5 6 7 8 9 10
if b == bukkit.Material.GOLD_BLOCK:
#Ein wertvoller Block!
else:
if b == bukkit.Material.WATER:
#Mach mich nicht nass!
else:
if b == bukkit.Material.DIRT:
#Das ist ja nur Dreck!
else:
#Das interessiert mich nicht!
Listing 7.12: Drei verschachtelte Schleifen
Die Funktionalität ändert sich hier zwar nicht, dafür ist der Code aber effektiver. Handelt es sich bei b um einen Goldblock, so wird zukünftig nur noch eine Bedingung überprüft statt vier. Wie du deutlich sehen kannst, hat allerdings die Übersichtlichkeit des Codes gelitten. Allerdings, du ahnst es vielleicht schon, bietet Python auch hierfür eine spezielle Schreibweise an, die die PerformanceVerbesserungen beibehält und die Übersichtlichkeit gleichzeitig erhöht. Du kannst sie in Listing 7.13 sehen. 1 2 3 4 5 6 7 8
if b == bukkit.Material.GOLD_BLOCK:
#Ein wertvoller Block!
elif b == bukkit.Material.WATER:
#Mach mich nicht nass!
elif b == bukkit.Material.DIRT:
#Das ist ja nur Dreck!
else:
#Das interessiert mich nicht!
Listing 7.13: if-elif-else-Verzweigung
Neben if und else taucht dort nun noch das Stichwort elifauf. Dabei handelt es sich um die Kurzschreibweise von else: if. elif verhält sich genau wie eine verschachtelte Schleife, auch hier werden die folgenden Bedingungen nur geprüft, falls noch keine der vorangegangenen Bedingungen erfüllt war.
Funktionen Kapitel 8
Nachdem du in den letzten beiden Kapiteln zwei wichtige neue Konzepte kennengelernt hast, Schleifen und Verzweigungen, gehen wir in diesem Kapitel wieder etwas Bekanntem auf den Grund, mit dem du dich bisher noch nicht weiter auseinandergesetzt hast, den sogenannten Funktionen. Eine Funktion ist ein Teil eines Programmes, der ausgegliedert wird, meistens deswegen, weil er an unterschiedlichen Stellen verwendet wird. Funktionen sind dir schon häufig begegnet, zum Beispiel in Form von onEnable- und onCommandFunktionen. Aber auch der Befehl self.getLogger().info() ruft eine Funktion auf. Dabei handelt es sich aber um Funktionen, die andere geschrieben haben, in diesem Fall die Entwickler von Bukkit – du verwendest sie nur. Im nächsten Abschnitt wirst du lernen, wie du eigene Funktionen erstellen kannst.
8.1 Deklaration von Funktionen Ein Beispiel für die Deklaration, also die Festlegung einer Funktion, kannst du in Listing 8.1 sehen. Dort wird eine Funktion mit dem Namen testFunktion erstellt, die den Text »Dies ist ein Test!« im Server-Fenster ausgibt. 1 2
def testFunktion():
self.getLogger().info("Dies ist ein Test!")
Listing 8.1: Deklaration einer Funktion
Wie eine Schleife oder eine Verzweigung besitzt auch die Funktion einen Rumpf, in dem der zugehörige Programmcode steht. Dieser wird ebenfalls wie bei Schleifen und Verzweigungen durch Einrückung markiert. Eine Funktion kann mithilfe ihres Namens von
anderen Stellen im Programm aufgerufen werden. Folgendes Programm: 1 2
testFunktion()
testFunktion()
würde daher zusammen mit der in Listing 8.1 gegebenen Funktionsdeklaration zweimal den Text »Dies ist ein Test!« im Server-Fenster ausgeben. Bei jedem Funktionsaufruf wird also der Code aus dem Funktionsrumpf ausgeführt, in diesem Fall also zum Beispiel der Befehl self.getLogger().info().
Hinweis Wie für Variablennamen gilt auch für Funktionsnamen die Konvention, dass der erste Buchstabe immer klein geschrieben wird und mehrere Wörter mit einem CamelCase bzw. einer Binnenmajuskel voneinander abgegrenzt werden, also jeweils der Anfangsbuchstabe eines neuen Wortes groß geschrieben wird. Dies gilt aber nur, da wir mit Minecraft arbeiten und Minecraft komplett in Java programmiert ist. Wenn wir Plugins für Minecraft in Python schreiben, halten wir uns deshalb an die Konventionen von Java. Normalerweise werden Funktionsnamen in Python, wie auch Variablennamen, komplett klein geschrieben und einzelne Wörter durch Unterstriche getrennt.
8.2 Rückgabewerte Funktionen können aber noch deutlich mehr: eine Funktion kann auch einen sogenannten Rückgabewertübergeben. Wie das dann aussieht, kannst du in Listing 8.2 sehen.
1 2
def eins():
return 1
Listing 8.2: Beispiel für eine Funktion mit Rückgabewert
Die dort gezeigte Funktion eins hat als Rückgabewert die Zahl »1«. Immer wenn die Funktion nun mit ihrem Namen aufgerufen wird, wird der Name an dieser Stelle, ähnlich wie bei einer Variable, durch den Rückgabewert ersetzt. Die Zeile zahl = 1 + eins(), würde zum Beispiel dafür sorgen, dass die Variable zahl den Wert 2 annimmt.
8.3 Parameter Auf den ersten Blick könnte man sich fragen, wozu man überhaupt Rückgabewerte braucht, wenn es doch Variablen gibt. Richtig nützlich werden die Rückgabewerte aber, wie du gleich sehen wirst, in Kombination mit einer weiteren praktischen Eigenschaft von Funktionen, den sogenannten Parametern. Diese kannst du in Listing 8.3 in den runden Klammern hinter dem Funktionsnamen sehen. 1 2
def addition(zahl1, zahl2):
return zahl1 + zahl2
Listing 8.3: Beispiel für eine Funktion mit Parametern
Hier stehen, getrennt durch ein Komma, zwei Variablendeklarationen. Die Variablen, die im Kopf einer Funktion in runden Klammern festgelegt werden, nennt man Parameter. Diese Parameter können genutzt werden, um Informationen an die Funktion zu übergeben. Beim Aufruf self.getLogger().info("Dies ist ein Test!") wird der Funktion zum Beispiel der Text, der ausgegeben werden soll, als Parameter übergeben. Der Funktion aus Listing 8.3 können dagegen zwei Zahlen als Parameter übergeben werden, die dann addiert werden. Der Aufruf
zahl = addition(1,2) würde zum Beispiel dafür sorgen, dass in der Variable zahl der Wert 3, eben 1 + 2, gespeichert wird. Somit ist der
Rückgabewert der Funktion auch nicht mehr fest vorgegeben, sondern hängt von den übergebenen Parametern ab.
Hinweis Auf die Parameter einer Funktion kann nur innerhalb des Rumpfes der Funktion zugegriffen werden.
8.4 Anwendungsbeispiel Zur Verdeutlichung der Funktionsweise wollen wir mithilfe einer Funktion ein konkretes Plugin programmieren. Um genau zu sein, soll ein bestehendes Plugin verbessert werden, nämlich das MauerPlugin. Beim Mauer-Plugin 2.0 soll es zukünftig möglich sein anzugeben, wie hoch und breit die Mauer sein soll, die gebaut wird. Außerdem soll der Bauvorgang in eine Funktion ausgelagert werden, der eben diese Angaben als Parameter übergeben werden. Die entsprechend angepasste plugin.yml für die neue Version des Plugins findest du in Listing 8.4. 1 name: Mauer-Plugin
2 main: MauerPlugin
3 version: 2.0
4 commands:
5 mauer:
6 description: Platziert eine Mauer mit den angegebenen Massen vor dem Spieler
7 usage: /mauer
Listing 8.4: Angepasste plugin.yml für Version 2.0 des Mauer-Plugins
Das Grundgerüst der neuen plugin.py findest du in Listing 8.5. Die größte Veränderung, die du dort vorfinden kannst im Vergleich zur vorherigen Version ist, dass es nun eine zusätzliche Funktion mit dem Namen mauerBauen gibt In diese werden wir später den eigentlichen Bau der Mauer auslagern. Wie dir dort vermutlich sofort auffällt, hat diese Funktion einen Parameter mit dem Namen self, wie auch die onEnable- und die onCommand-Funktion. Das liegt daran, dass die Funktion an dieser Stelle Teil der sogenannten Klasse MauerPlugin ist. Was genau Klassen sind, damit werden wir uns später noch beschäftigen, für den Moment reicht es uns zu wissen, dass jede Funktion, die sich in einer Klasse befindet, als ersten Parameter immer self haben muss. 1 2 3 4 5 6 7 8 9 10 11 12
from org.bukkit.entity import Player
class MauerPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
return True
def mauerBauen(self, spieler, hoehe, breite):
Listing 8.5: Grundgerüst für Version 2.0 des Mauer-Plugins
Bevor aber eine Mauer gebaut werden kann, muss zunächst einmal die Eingabe eingelesen und verarbeitet werden. Das wird auch weiterhin innerhalb der onCommand-Funktion passieren. Wie du bereits weißt, stecken die Parameter, die wir einem Befehl mitgeben, in der Variable args. Wie du schon Listing 8.4 entnehmen konntest, werden dem Befehl /mauer zukünftig zwei Parameter übergeben, der erste steht für die Höhe der Mauer und der zweite für ihre Breite. Natürlich müssen auch diese Eingaben zunächst wieder in Zahlen umgewandelt werden, wie in Listing 8.6 gezeigt.
1 def onCommand(self, sender, command, label, args):
2 if isinstance(sender, Player):
3 hoehe = int(args[0])
4 breite = int(args[1])
5
6 MauerPlugin.mauerBauen(self, sender, hoehe, breite)
7
8 else:
9 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
10
11 return True
Listing 8.6: Die neue onCommand-Funktion des Mauer-Plugins
Dort kannst du außerdem in Zeile 2 sehen, dass wir auch bei der neuen Version des Plugins zunächst überprüfen sollten, ob es sich beim Aufrufer der Funktion um einen Spieler handelt. Neben den beiden Parametern hoehe und breite benötigt unsere mauerBauenFunktion noch die Spieler-Variable, die wir später dazu benötigen, die Position herauszufinden, an der die Mauer gebaut werden soll, und um auf die Welt zugreifen zu können. Schlussendlich muss innerhalb der onCommand-Funktion noch die neue mauerBauen-Funktion aufgerufen werden, in der ja nun das eigentliche Bauen stattfinden soll. Auch hier fällt eine Besonderheit beim Aufruf der Funktion auf, die ebenfalls mit den gerade bereits angesprochenen Klassen zu tun hat: Vor dem Namen der Funktion steht nämlich noch der Name der Klasse, also MauerPlugin. Wie der Inhalt der mauerBauen-Funktion aussiehst, kannst du in Listing 8.7 sehen. 1 2 3 4 5 6 7 8 9 10 11
def mauerBauen(self, spieler, hoehe, breite):
position = spieler.getLocation()
position.setX(position.getX() + 2)
welt = spieler.getWorld()
yStart = position.getY()
zStart = position.getZ()
for y in range (0, hoehe):
position.setY(yStart + y)
12 13 14 15
for z in range (0, breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.GOLD_BLOCK)
Listing 8.7: mauerBauen-Funktion
Der Inhalt ist nahezu identisch mit der früheren onCommand-Funktion des Plugins. Einziger Unterschied ist, dass wir für die beiden Schleifen in Zeile 10 und 12 nun die Parameter hoehe und breite als Obergrenze verwenden und nicht mehr wie zuvor einen festen Wert. Nun musst du die einzelnen Teile des neuen Mauer-Plugins nur noch, wie in Listing 8.8 gezeigt, zu einer neuen plugin.py zusammenfügen und schon kannst du die neue Version des Plugins ausprobieren. 1 from org.bukkit.entity import Player
2
3 class MauerPlugin(PythonPlugin):
4 def onEnable(self):
5 pass
6
7 def onCommand(self, sender, command, label, args):
8 if isinstance(sender, Player):
9 hoehe = int(args[0])
10 breite = int(args[1])
11
12 MauerPlugin.mauerBauen(self, sender, hoehe, breite)
13
14 else:
15 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
16
17 return True
18
19
20 def mauerBauen(self, spieler, hoehe, breite):
21 position = spieler.getLocation()
22 position.setX(position.getX() + 2)
23
24 welt = spieler.getWorld()
25
26 27 28 29 30 31 32 33 34
yStart = position.getY()
zStart = position.getZ()
for y in range (0, hoehe):
position.setY(yStart + y)
for z in range (0, breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(bukkit.Material.GOLD_BLOCK)
Listing 8.8: Fertige Version 2.0 des Mauer-Plugins
Wenn der Server erst einmal mit der neuen Version des Plugins gestartet wurde, kannst du dich in Minecraft mit dem Server verbinden und die neue Funktionsweise des /mauer-Befehls ausprobieren. In Abbildung 8.1 kannst du zum Beispiel das Ergebnis der Eingabe /mauer 5 20 sehen, also eine 5 Blöcke hohe und 20 Blöcke breite Mauer.
Abb. 8.1: 5 x 20 Mauer
Kapitel 9
Bauen Nachdem es in den letzten Kapiteln hauptsächlich um Python und das Lernen neuer Konstrukte der Programmiersprache ging, liegt der Fokus in diesem Kapitel voll und ganz darauf, Dinge in Minecraft zu erschaffen. Dabei wird dir dein Wissen aus den letzten Kapiteln sehr hilfreich sein.
9.1 Notunterkunft Das Problem kennt fast jeder Minecraft-Spieler: Man ist gerade auf einer Erkundungstour in der Welt unterwegs, fernab vom eigenen Heim, als plötzlich die Nacht über der Spielwelt hereinbricht. Kein Bett in der Nähe, kein Unterschlupf in Sicht und hinter sich hört man schon den ersten Creeper, der sich langsam heranpirscht. Wie praktisch wäre es da doch, wenn man sich auf die Schnelle eine Notunterkunft bauen könnte. Schade, dass es so etwas in Minecraft nicht gibt. Für dich als Programmierer ist das aber überhaupt kein Problem, du kannst dir einfach ein Plugin dafür schreiben! Bevor du anfängst, ein neues Plugin zu programmieren, solltest du dir immer zuerst überlegen, was genau das Plugin leisten soll, wenn es einmal fertig ist. In diesem Fall also vor allem, wie die fertige Notunterkunftaussehen soll. Vier Wände und ein Dach sollte sie wohl schon einmal haben, schließlich sollen Monster draußen bleiben. Eine Tür wäre auch nicht schlecht, irgendwie musst du ja in deine Unterkunft hinein- und auch wieder hinauskommen. Im Inneren sollte sie außerdem über Licht und ein Bett verfügen. Das sollte fürs Erste aber auch genügen, es soll ja schließlich nur eine Notunterkunft für eine Nacht sein.
Beginnen wollen wir mit der Außenhülle, also den Wänden und der Decke. Der offensichtliche Weg wäre an dieser Stelle, zunächst vier Wände zu bauen und darauf dann noch ein Dach, allerdings gibt es auch noch eine elegantere Lösung: In Listing 9.1 findest du die Funktion quaderBauen. Wie der Name schon verrät, kannst du mit deren Hilfe einen Quader aus einem beliebigen Material bauen. 1 def quaderBauen(self, spieler, hoehe, breite, tiefe, material):
2 position = spieler.getLocation()
3 position.setX(position.getX() + 2)
4
5 welt = spieler.getWorld()
6
7 xStart = position.getX()
8 yStart = position.getY()
9 zStart = position.getZ()
10
11 for x in range (0, tiefe):
12 position.setX(xStart + x)
13 for y in range (0, hoehe):
14 position.setY(yStart + y)
15 for z in range (0, breite):
16 position.setZ(zStart + z)
17 block = welt.getBlockAt(position)
18 block.setType(material)
Listing 9.1: Quader-Funktion
Im Vergleich zur mauerBauen-Funktion ist hier eine weitere, dritte Schleife hinzu gekommen, die dafür sorgt, dass der Quader nicht nur in die Breite und Höhe, sondern auch in die Tiefe gebaut werden kann. Nun fragst du dich vielleicht, wie dir ein Quader beim Bau deiner Notunterkunft helfen soll, schließlich handelt es sich dabei um einen massiven Block und deine Notunterkunft sollte innen hohl sein, damit du überhaupt hineingehen kannst. Der Trick besteht darin, zwei Quader ineinander zu platzieren: Zunächst einen Quader aus dem Material, das später die Außenhülle bilden soll und darin dann einen Quader aus Luft, der in jeder Richtung zwei Blöcke kleiner ist. Fertig sind die Außenwände und die Decke deiner Notunterkunft.
9.1.1 Decke und Wände Dafür musst du die Funktion zum Bau des Quaders aber noch etwas anpassen, denn um zwei Quader ineinander platzieren zu können, musst du der Funktion noch die Position übergeben können, an der der Quader gebaut werden soll. Die entsprechend angepasste quaderBauen-Funktion findest du in Listing 9.2. 1 def quaderBauen(self, spieler, position, hoehe, breite, tiefe, material):
2 welt = spieler.getWorld()
3
4 xStart = position.getX()
5 yStart = position.getY()
6 zStart = position.getZ()
7
8 for x in range (0, tiefe):
9 position.setX(xStart + x)
10 for y in range (0, hoehe):
11 position.setY(yStart + y)
12 for z in range (0, breite):
13 position.setZ(zStart + z)
14 block = welt.getBlockAt(position)
15 block.setType(material)
Listing 9.2: Angepasste quaderBauen-Funktion
Wie du dort sehen kannst, wird die Position für den Bau nun nicht mehr von der Spieler-Position abgeleitet, sondern als Parameter der Funktion übergeben, ansonsten bleibt sie unverändert. Nun wollen wir beginnen, mit Hilfe dieser Funktion unser Notunterkunft-Plugin zu erstellen. Nachdem du einen Ordner für das neue Plugin erstellt hast, macht den Anfang wieder die plugin.yml, die du in Listing 9.3 findest. 1 2 3 4 5 6
name: Notunterkunft-Plugin
main: NotunterkunftPlugin
version: 1.0
commands:
notunterkunft:
description: Baut eine Notunterkunft fuer die
Nacht
7 8
usage: /notunterkunft
aliases: not
Listing 9.3: plugin.yml
Ganz neu ist dort diesmal die zusätzliche Option aliases in der letzten Zeile. Manchmal kann es praktisch sein, wenn ein bestimmter Befehl über mehrere Namen aufgerufen werden kann. Stell dir zum Beispiel vor, du befindest dich gerade auf der Flucht vor einem Creeper und brauchst dringend eine Notunterkunft. Dann möchtest du sicher nicht erst noch /notunterkunft in den Chat eingeben, sondern lieber etwas Kurzes, wie zum Beispiel /not. Genau dafür gibt es die Option aliases. Hier kannst du alternative Namen für deinen Befehl eingeben. Entweder wie in Listing 9.3 einen oder auch gleich eine ganze Liste, wie zum Beispiel aliases: [not, unterkunft]. Den Programmcode zum Bau der Decke und der Wände findest du in der plugin.py in Listing 9.4. Wie du dort in Zeile 27 sehen kannst, gibt es dort noch eine kleine Anpassung der quaderBauen-Funktion. Da diese nun Teil der Klasse NotunterkunftPlugin ist, muss ihr erster Parameter, wie du bereits aus dem letzten Kapitel weißt, self sein. Ansonsten ist die Funktion unverändert im Vergleich zu Listing 9.2. 1 from org.bukkit.entity import Player
2
3 class NotunterkunftPlugin(PythonPlugin):
4 def onEnable(self):
5 pass
6
7 def onCommand(self, sender, command, label, args):
8 if isinstance(sender, Player):
9 spieler = sender
10
11 #quader aussen
12 position = spieler.getLocation()
13 position.setX(position.getX() + 1)
14 NotunterkunftPlugin.quaderBauen(self, spieler, position, 4, 10, 10, bukkit.Material.BRICKS)
15
16 #quader innen
17 position = spieler.getLocation()
18 position.setX(position.getX() + 2)
19 position.setZ(position.getZ() + 1)
20 NotunterkunftPlugin.quaderBauen(self, spieler, position, 3, 8, 8, bukkit.Material.AIR)
21
22 else:
23 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
24
25 return True
26
27 def quaderBauen(self, spieler, position, hoehe, breite, tiefe, material):
28 welt = spieler.getWorld()
29
30 xStart = position.getX()
31 yStart = position.getY()
32 zStart = position.getZ()
33
34 for x in range (0, tiefe):
35 position.setX(xStart + x)
36 for y in range (0, hoehe):
37 position.setY(yStart + y)
38 for z in range (0, breite):
39 position.setZ(zStart + z)
40 block = welt.getBlockAt(position)
41 block.setType(material)
Listing 9.4: plugin.py für den Bau von Decken und Wänden
Aufgerufen wird die quaderBauen-Funktion natürlich aus der onCommand-Funktion, die wiederum durch Eingabe des Befehls /not oder /notunterkunft aufgerufen wird. Da zum Bau der Quader auf die Position des Spielers zugegriffen wird, sollte auch in diesem Plugin, wie schon beim Mauer-Plugin, geprüft werden, ob es sich beim Aufrufer des Befehls um einen Spieler handelt. Das geschieht in Zeile 8. Danach kann sicher auf die Spielerposition zugegriffen werden und, damit die Notunterkunft vor und nicht im Spieler gebaut wird, der XWert um eins erhöht werden. Mit dieser neuen Position kann dann,
wie in Zeile 14 zu sehen, die quaderBauen-Funktion zum ersten Mal aufgerufen werden. Im Beispiel in Listing 9.4 bestehen die späteren Wände und die Decke der Notunterkunft aus Backsteinen, daher wird als material-Parameter bukkit.Material.BRICKS übergeben. Natürlich kannst du hier aber auch jeden beliebigen anderen Blocktyp aussuchen. Die Höhe des äußeren Blocks sollte mindestens 4 sein, damit das Innere später mindestens 3 Blöcke hoch ist und somit genug Platz zum Stehen bietet. Breite und Tiefe können frei gewählt werden, für eine einfache Notunterkunft sollten zehn Blöcke in jede Richtung aber auf jeden Fall ausreichen. Nach diesem ersten Aufruf würde aber zunächst nur ein massiver Block aus Backsteinen vor dir stehen. Damit daraus eine Unterkunft für die Nacht werden kann, muss nun noch das Innere ausgehöhlt werden, also die Backsteine im Inneren durch Luft ersetzt werden. Dazu wird zunächst die Position dieses inneren Quaders festgelegt. Damit am Ende die Außenwände stehen bleiben, sollte diese in Xund Z-Richtung jeweils einen Block weiter liegen als beim äußeren Quader. Außerdem muss er einen Block niedriger und jeweils zwei Blöcke schmäler und kürzer sein, wie du in Zeile 20 sehen kannst. Das Ergebnis, wenn du das Plugin in seinem aktuellen Stand ausführst, kannst du in Abbildung 9.1 sehen: ein Quader, der außen aus Backstein besteht und innen hohl ist. Als Notunterkunft taugt das Ganze so allerdings noch nicht. Besonders offensichtlich wird das an der fehlenden Tür, denn ohne die hast du keine Chance, deinen neuen Schutzraum überhaupt zu betreten.
Abb. 9.1: Notunterkunft in der ersten Ausbaustufe
9.1.2 Tür Im nächsten Schritt wollen wir das Plugin daher so erweitern, dass in der Außenwand deiner Notunterkunft noch eine Holztür platziert wird, über die du sie betreten und verlassen kannst. Türen unterscheiden sich in einem ganz wesentlichen Punkt von den meisten anderen Blöcken in Minecraft, weshalb wir beim Platzieren etwas anders vorgehen müssen als gewöhnlich: Sie bestehen nicht nur aus einem Block, sondern aus zwei, einem Ober- und einem Unterteil. Wie du den Unterteil einer Tür platzieren kannst, siehst du in Listing 9.5. 1 2 3 4 5 6 7
tuerUnten = welt.getBlockAt(position)
tuerUnten.setType(bukkit.Material.OAK_DOOR)
tuerUntenDaten = tuerUnten.getBlockData()
tuerUntenDaten.setHalf(Bisected.Half.BOTTOM)
tuerUntenDaten.setFacing(BlockFace.WEST)
tuerUnten.setBlockData(tuerUntenDaten)
Listing 9.5: Platzieren eines Unterteils einer Tür
Bis einschließlich Zeile 2 dürfte dir hier bereits alles sehr bekannt vorkommen: Zunächst wird der Block, an dem die Tür platziert werden soll, über seine Position aufgerufen und dann wird ihm ein neuer Typ, nämlich der der Tür, zugewiesen. Neu ist dagegen der getBlockData( )-Befehl in Zeile 4. Mit seiner Hilfe kannst du auf zusätzliche Optionen spezieller Blöcke zugreifen, wie zum Beispiel bei der Tür, aber auch, wie du später noch sehen wirst, bei einem Bett. Sobald du den Zugriff auf die Daten auf diese Weise ermöglicht hast, kannst du mit dem setHalf() -Befehl festlegen, um welches Teil einer Tür es sich handelt. Hier ist das der untere Teil, auf Englisch bottom. Eine weitere Besonderheit, die Türen von den meisten anderen Blöcken unterscheidet, kannst du in Abbildung 9.2 sehen. Türen können nämlich auch innerhalb eines Blocks an verschiedenen Positionen platziert werden: westlich, östlich, nördlich oder südlich.
Abb. 9.2: Verschiedene Möglichkeiten eine Tür innerhalb eines Blocks zu positionieren
Mit dem setFacing()-Befehl kannst du daher festlegen, wo innerhalb des Blocks die Tür platziert werden soll. In Listing 9.5 ist das die Westseite des Blocks. Das Platzieren der oberen Hälfte der Tür funktioniert genau so, mit einem kleinen Unterschied: Beim Platzieren schwebt der obere Teile für einen kurzen Moment in der Luft, bevor er richtig ausgerichtet und mit dem unteren Teil verbunden wird. Damit der obere Teil
schwebt und nicht einfach herunterfällt, musst du für kurze Zeit die Regeln der Physik außer Kraft setzen. Im Gegensatz zur echten Welt funktioniert das in Minecraft zum Glück relativ einfach. Wie genau, das kannst du in Zeile 3 in Listing 9.6 sehen. Der Wert False im setType()-Befehl sorgt dafür. Wichtig ist natürlich auch, dass sich der neue Teil direkt über dem unteren Teil befindet. Ansonsten ändert sich nicht viel, außer dass es sich diesmal um die obere, also top, Hälfte handelt. 1 2 3 4 5 6 7 8
position.setY(position.getY() + 1)
tuerOben = welt.getBlockAt(position)
tuerOben.setType(bukkit.Material.OAK_DOOR, False)
tuerObenDaten = tuerOben.getBlockData()
tuerObenDaten.setHalf(Bisected.Half.TOP)
tuerObenDaten.setFacing(BlockFace.WEST)
tuerOben.setBlockData(tuerObenDaten)
Listing 9.6: Platzieren eines Oberteils einer Tür
Um das Gelernte nun für deine Notunterkunft anzuwenden, musst du nur noch die passende Position herausfinden, so dass sich die Tür auch in der Wand deiner Notunterkunft befindet. Wie das funktioniert, kannst du in Listing 9.7 ab Zeile 27 sehen. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
from org.bukkit.entity import Player
from org.bukkit.block import BlockFace
from org.bukkit.block.data import BlockData
from org.bukkit.block.data import Bisected
from org.bukkit.block.data.type import Door
class NotunterkunftPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
if isinstance(sender, Player):
spieler = sender
#quader aussen
position = spieler.getLocation()
18 position.setX(position.getX() + 1)
19 NotunterkunftPlugin.quaderBauen(self, spieler, position, 4, 10, 10, bukkit.Material.BRICKS)
20
21 #quader innen
22 position = spieler.getLocation()
23 position.setX(position.getX() + 2)
24 position.setZ(position.getZ() + 1)
25 NotunterkunftPlugin.quaderBauen(self, spieler, position, 3, 8, 8, bukkit.Material.AIR)
26
27 #tuer
28 position = spieler.getLocation()
29 position.setX(position.getX() + 1)
30 position.setZ(position.getZ() + 4)
31 welt = spieler.getWorld()
32
33 tuerUnten = welt.getBlockAt(position)
34 tuerUnten.setType(bukkit.Material.OAK_DOOR)
35
36 tuerUntenDaten = tuerUnten.getBlockData()
37 tuerUntenDaten.setHalf(Bisected.Half.BOTTOM)
38 tuerUntenDaten.setFacing(BlockFace.WEST)
39 tuerUnten.setBlockData(tuerUntenDaten)
40
41 position.setY(position.getY() + 1)
42 tuerOben = welt.getBlockAt(position)
43 tuerOben.setType(bukkit.Material.OAK_DOOR, False)
44
45 tuerObenDaten = tuerOben.getBlockData()
46 tuerObenDaten.setHalf(Bisected.Half.TOP)
47 tuerObenDaten.setFacing(BlockFace.WEST)
48 tuerOben.setBlockData(tuerObenDaten)
49
50
51 else:
52 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
53
54 return True
55
56 def quaderBauen(self, spieler, position, hoehe, breite, tiefe, material):
57 welt = spieler.getWorld()
58
59 xStart = position.getX()
60 yStart = position.getY()
61 62 63 64 65 66 67 68 69 70
zStart = position.getZ()
for x in range (0, tiefe):
position.setX(xStart + x)
for y in range (0, hoehe):
position.setY(yStart + y)
for z in range (0, breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(material)
Listing 9.7: plugin.py erweitert um den Bau einer Tür
Im Vergleich zu Listing 9.5 und Listing 9.6 ist hier nur noch die Bestimmung der Position der Tür hinzugekommen. Wie die Wand der Notunterkunft befindet sich die Tür einen Block weiter in XRichtung und vier Blöcke weiter in Z-Richtung, damit sie nicht an der Ecke der Notunterkunft, sondern in der Mitte der Wand ist. Wenn du deinen Server nun mit dem geänderten Plugin neu startest und den Befehl /notunterkunft, beziehungsweise /not, eingibst, so begrüßt dich, wie in Abbildung 9.3, eine Notunterkunft mit Tür.
Abb. 9.3: Notunterkunft mit Tür
9.1.3 Bett Damit du nicht tatenlos in deiner Notunterkunft stehen und darauf warten musst, dass die Nacht vorbeigeht, solltest du sie nun noch mit einem Bettausrüsten, in dem du die Nacht verbringen kannst. Wie Türen bestehen auch Betten aus zwei Blöcken, einem Fußteil und einem Kopfteil. Deshalb funktioniert auch das Platzieren von Betten ganz ähnlich wie bei Türen. Wie genau, das kannst du in Listing 9.8 sehen. 1 2 3 4 5 6 7
bettFuss = welt.getBlockAt(position)
bettFuss.setType(bukkit.Material.RED_BED)
bettFussDaten = bettFuss.getBlockData()
bettFussDaten.setPart(Bed.Part.FOOT)
bettFussDaten.setFacing(BlockFace.EAST)
bettFuss.setBlockData(bettFussDaten)
Listing 9.8: Platzieren des Fußteils eines Bettes
Auch beim Platzieren des Bettes muss sowohl festgelegt werden, um welchen Teil des Bettes es sich handelt (Zeile 5), als auch in welche Richtung das Bett ausgerichtet werden soll (Zeile 6). Statt die Position des Kopfteils wie üblich über das Verändern der position-Variable zu bestimmen, wird in Listing 9.9 der Befehl getRelativ e() auf dem Fußteil des Bettes aufgerufen. Mit dem Befehl und den Parametern BlockFace.EAST, BlockFace.WEST, BlockFace.NORTH und BlockFace.SOUTH, kannst du direkt auf den jeweils an der angegebenen Seite angrenzenden Block zugreifen. Der Vorteil dieses Verfahrens: Du musst dir nicht erst Gedanken machen, ob du nun in X- oder Z-Richtung addieren oder subtrahieren musst, um nach Osten zu kommen und das Bett somit korrekt zusammenbauen zu können. Wenn du möchtest, kannst du
aber auch weiterhin bei der gewohnten Schreibweise bleiben, beides funktioniert. 1 2 3 4 5 6 7
bettKopf = bettFuss.getRelative(BlockFace.EAST)
bettKopf.setType(bukkit.Material.RED_BED, False)
bettKopfDaten = bettKopf.getBlockData()
bettKopfDaten.setPart(Bed.Part.HEAD)
bettKopfDaten.setFacing(BlockFace.EAST)
bettKopf.setBlockData(bettKopfDaten)
Listing 9.9: Platzieren des Kopfteils eines Bettes
Nun geht es wieder darum, die entsprechenden Code-Stücke in das Plugin einzubauen und dafür zu sorgen, dass das Bett auch innerhalb deiner Notunterkunft platziert wird. Wie das geht, kannst du in Listing 9.10 sehen. Ganz wichtig, aber leicht zu übersehen, ist der zusätzliche Import in Zeile 6 des Quellcodes. 1 from org.bukkit.entity import Player
2 from org.bukkit.block import BlockFace
3 from org.bukkit.block.data import BlockData
4 from org.bukkit.block.data import Bisected
5 from org.bukkit.block.data.type import Door
6 from org.bukkit.block.data.type import Bed
7
8
9 class NotunterkunftPlugin(PythonPlugin):
10 def onEnable(self):
11 pass
12
13 def onCommand(self, sender, command, label, args):
14 if isinstance(sender, Player):
15 spieler = sender
16
17 #quader aussen
18 position = spieler.getLocation()
19 position.setX(position.getX() + 1)
20 NotunterkunftPlugin.quaderBauen(self, spieler, position, 4, 10, 10, bukkit.Material.BRICKS)
21
22 #quader innen
23 position = spieler.getLocation()
24 position.setX(position.getX() + 2)
25 position.setZ(position.getZ() + 1)
26 NotunterkunftPlugin.quaderBauen(self, spieler, position, 3, 8, 8, bukkit.Material.AIR)
27
28 #tuer
29 position = spieler.getLocation()
30 position.setX(position.getX() + 1)
31 position.setZ(position.getZ() + 4)
32 welt = spieler.getWorld()
33
34 tuerUnten = welt.getBlockAt(position)
35 tuerUnten.setType(bukkit.Material.OAK_DOOR)
36
37 tuerUntenDaten = tuerUnten.getBlockData()
38 tuerUntenDaten.setHalf(Bisected.Half.BOTTOM)
39 tuerUntenDaten.setFacing(BlockFace.WEST)
40 tuerUnten.setBlockData(tuerUntenDaten)
41
42 position.setY(position.getY() + 1)
43 tuerOben = welt.getBlockAt(position)
44 tuerOben.setType(bukkit.Material.OAK_DOOR, False)
45
46 tuerObenDaten = tuerOben.getBlockData()
47 tuerObenDaten.setHalf(Bisected.Half.TOP)
48 tuerObenDaten.setFacing(BlockFace.WEST)
49 tuerOben.setBlockData(tuerObenDaten)
50
51 #bett
52 position = spieler.getLocation()
53 position.setX(position.getX() + 8)
54 position.setZ(position.getZ() + 4)
55
56 bettFuss = welt.getBlockAt(position)
57 bettFuss.setType(bukkit.Material.RED_BED)
58
59 bettFussDaten = bettFuss.getBlockData()
60 bettFussDaten.setPart(Bed.Part.FOOT)
61 bettFussDaten.setFacing(BlockFace.EAST)
62 bettFuss.setBlockData(bettFussDaten)
63
64 bettKopf = bettFuss.getRelative(BlockFace.EAST)
65 bettKopf.setType(bukkit.Material.RED_BED, False)
66
67 bettKopfDaten = bettKopf.getBlockData()
68 bettKopfDaten.setPart(Bed.Part.HEAD)
69 bettKopfDaten.setFacing(BlockFace.EAST)
70 bettKopf.setBlockData(bettKopfDaten)
71
72 else:
73 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
74
75 return True
76
77 def quaderBauen(self, spieler, position, hoehe, breite, tiefe, material):
78 welt = spieler.getWorld()
79
80 xStart = position.getX()
81 yStart = position.getY()
82 zStart = position.getZ()
83
84 for x in range (0, tiefe):
85 position.setX(xStart + x)
86 for y in range (0, hoehe):
87 position.setY(yStart + y)
88 for z in range (0, breite):
89 position.setZ(zStart + z)
90 block = welt.getBlockAt(position)
91 block.setType(material)
Listing 9.10: plugin.py erweitert um den Bau eines Bettes
Die großen, leicht sichtbaren Änderungen folgen dann ab Zeile 52, denn hier wird das Bett positioniert. Um das Bett an die Wand gegenüber der Tür zu positionieren, muss es 8 Blöcke in X-Richtung und 4 Blöcke in Z-Richtung vom Spieler entfernt platziert werden. Den restlichen Code kannst du einfach aus Listing 9.8 und Listing 9.9 übernehmen.
9.1.4 Fackel Nachdem deine Notunterkunft nun auch über ein Bett verfügt, fehlt nur noch eine Kleinigkeit, bevor sie einsatzbereit ist. Damit dir nachts auch wirklich die Monster vom Leib bleiben, solltest du im Inneren
noch unbedingt eine Lichtquelle anbringen, zum Beispiel eine Fackel . Im Vergleich zum Bett und der Tür besteht die Fackel wieder wie gewohnt nur aus einem einzelnen Block, dafür bringt sie eine andere Eigenheit mit sich. Im Gegensatz zu den meisten anderen Blöcken kann die Fackel nämlich nicht alleine existieren, sie gehört immer zu einem Block neben oder unter ihr. Wird dieser zerstört, so wird auch die Fackel immer mit zerstört. In Listing 9.11 kannst du sehen, wie eine Fackel an der Wand deiner Notunterkunft angebracht wird. Das Verfahren hierfür ist dir inzwischen bereits bestens von der Platzierung der Tür und des Bettes bekannt. 1 2 3 4 5 6
fackel = welt.getBlockAt(position)
fackel.setType(bukkit.Material.WALL_TORCH, False)
fackelDaten = fackel.getBlockData()
fackelDaten.setFacing(BlockFace.WEST)
fackel.setBlockData(fackelDaten)
Listing 9.11: Platzieren einer hängenden Fackel an der Ost-Seite eines Blocks
Das fertige Notunterkunfts-Plugin kannst du in Listing 9.12 finden. Ab Zeile 74 findest du dort auch den neuen Code zum Platzieren der Fackel, die zwei Blöcke über dem Bett platziert wird. 1 2 3 4 5 6 7 8 9 10 11 12 13
from org.bukkit.entity import Player
from org.bukkit.block import BlockFace
from org.bukkit.block.data import BlockData
from org.bukkit.block.data import Bisected
from org.bukkit.block.data import Directional
from org.bukkit.block.data.type import Door
from org.bukkit.block.data.type import Bed
class NotunterkunftPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
14 if isinstance(sender, Player):
15 spieler = sender
16
17 #quader aussen
18 position = spieler.getLocation()
19 position.setX(position.getX() + 1)
20 NotunterkunftPlugin.quaderBauen(self, spieler, position, 4, 10, 10, bukkit.Material.BRICKS)
21
22 #quader innen
23 position = spieler.getLocation()
24 position.setX(position.getX() + 2)
25 position.setZ(position.getZ() + 1)
26 NotunterkunftPlugin.quaderBauen(self, spieler, position, 3, 8, 8, bukkit.Material.AIR)
27
28 #tuer
29 position = spieler.getLocation()
30 position.setX(position.getX() + 1)
31 position.setZ(position.getZ() + 4)
32 welt = spieler.getWorld()
33
34 tuerUnten = welt.getBlockAt(position)
35 tuerUnten.setType(bukkit.Material.OAK_DOOR)
36
37 tuerUntenDaten = tuerUnten.getBlockData()
38 tuerUntenDaten.setHalf(Bisected.Half.BOTTOM)
39 tuerUntenDaten.setFacing(BlockFace.WEST)
40 tuerUnten.setBlockData(tuerUntenDaten)
41
42 position.setY(position.getY() + 1)
43 tuerOben = welt.getBlockAt(position)
44 tuerOben.setType(bukkit.Material.OAK_DOOR, False)
45
46 tuerObenDaten = tuerOben.getBlockData()
47 tuerObenDaten.setHalf(Bisected.Half.TOP)
48 tuerObenDaten.setFacing(BlockFace.WEST)
49 tuerOben.setBlockData(tuerObenDaten)
50
51 #bett
52 position = spieler.getLocation()
53 position.setX(position.getX() + 8)
54 position.setZ(position.getZ() + 4)
55
56 bettFuss = welt.getBlockAt(position)
57 bettFuss.setType(bukkit.Material.RED_BED)
58
59 bettFussDaten = bettFuss.getBlockData()
60 bettFussDaten.setPart(Bed.Part.FOOT)
61 bettFussDaten.setFacing(BlockFace.EAST)
62 bettFuss.setBlockData(bettFussDaten)
63
64 bettKopf = bettFuss.getRelative(BlockFace.EAST)
65 bettKopf.setType(bukkit.Material.RED_BED, False)
66
67 bettKopfDaten = bettKopf.getBlockData()
68 bettKopfDaten.setPart(Bed.Part.HEAD)
69 bettKopfDaten.setFacing(BlockFace.EAST)
70 bettKopf.setBlockData(bettKopfDaten)
71
72 #fackel
73 position = spieler.getLocation()
74 position.setX(position.getX() + 9)
75 position.setZ(position.getZ() + 4)
76 position.setY(position.getY() + 2)
77
78 fackel = welt.getBlockAt(position)
79 fackel.setType(bukkit.Material.WALL_TORCH, False)
80
81 fackelDaten = fackel.getBlockData()
82 fackelDaten.setFacing(BlockFace.WEST)
83 fackel.setBlockData(fackelDaten)
84
85 else:
86 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
87
88 return True
89
90 def quaderBauen(self, spieler, position, hoehe, breite, tiefe, material):
91 welt = spieler.getWorld()
92
93 xStart = position.getX()
94 yStart = position.getY()
95 zStart = position.getZ()
96
97 for x in range (0, tiefe):
98 position.setX(xStart + x)
99 for y in range (0, hoehe):
100 position.setY(yStart + y)
101 for z in range (0, breite):
102 103 104
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(material)
Listing 9.12: Fertige plugin.py für das Noutunterkunft-Plugin
Den fertigen Innenraum deiner Notunterkunft kannst du in Abbildung 9.4 sehen. Nun verfügst du auf Knopfdruck über eine zwar nicht gerade luxuriöse, aber zweckmäßige Unterkunft, in der sich eine Nacht problemlos überstehen lässt.
Abb. 9.4: Innenraum einer Notunterkunft
Wenn du möchtest, kannst du das Plugin natürlich noch weiter anpassen und andere Materialien verwenden, Fenster hinzufügen oder was auch immer dir einfällt. Der Phantasie sind fast keine Grenzen gesetzt. Alles was du in Minecraft bauen kannst, kannst du auch mithilfe eines Plugins auf Knopfdruck bauen lassen. Du musst dir aus der Liste am Ende des Buches einfach das passende Material aussuchen, die richtige Position in oder an deiner
Unterkunft finden und schon kannst du den entsprechenden Block platzieren.
9.2 Runde Objekte Minecraft ist bekanntlich die Welt der Blöcke. Dementsprechend ist es gar nicht so einfach, etwas einigermaßen Rundes, wie zum Beispiel eine Kugel zu bauen. Jeder, der schon einmal versucht hat, so etwas von Hand zu bauen, weiß das. Man benötigt viel Geduld, ein gutes Auge und viele Hilfsblöcke, die man am Ende wieder abbauen muss. Als Programmierer dagegen ist alles, was du benötigst, ein wenig Mathemagie.
9.2.1 Kreise Genau wie die Welt von Minecraft besteht auch der Bildschirm deines Computers aus vielen Vierecken, den sogenannten Pixel. Aus diesem Grund haben sich bereits viele schlaue Menschen darüber Gedanken gemacht, wie man einen Kreisam besten aus Vierecken zusammensetzen kann. Man spricht hier auch von der Rasterungeines Kreises. Wie das Ganze im Prinzip aussieht, kannst du der Abbildung 9.5 entnehmen.
Abb. 9.5: Gerasterter Kreis
In Rot ist der Kreis zu sehen, in Grün die Annäherung durch Rasterung. Der blaue Pfeil repräsentiert den Radius des Kreises. Generell gilt, dass der gerasterte Kreis »runder« aussieht, je kleiner die einzelnen Vierecke sind und je größer der Kreis ist. Da es sich bei diesem Problem, wie bereits erwähnt, um ein sehr verbreitetes handelt, gibt es zahlreiche verschiedene Methoden zur Lösung. Zu den bekanntesten gehören der Midpoint-Algorithmus, die Methode von Metzgerund die Methode von Horn, die auch in unserem Plugin zum Einsatz kommen soll, da sie sich vergleichsweise unkompliziert in Python umsetzen lässt.
Wie die Methode von Horn als Code aussieht, zeigt Abbildung 9.13, wobei r hier der Radius des Kreises ist. Wie und warum genau diese Methode funktioniert, ist eigentlich gar nicht so wichtig, wir sind ja schließlich Programmierer und keine Mathematiker. Offensichtlich wird sich auf jeden Fall die Symmetrie des Kreises zunutze gemacht. Wichtig ist an dieser Stelle vor allem, dass du dir sicher sein kannst: Diese Methode funktioniert. Und wenn du sie verwenden möchtest, kannst du den Code aus dem Listing einfach so abtippen, wie er dort steht. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
d = -r
x = r
y = 0
while y 0:
d = d - 2*x + 2
x = x - 1
Listing 9.13: Die Methode von Horn zum Rastern von Kreisen
Nun gilt es noch, aus dieser grundsätzlichen Methode ein konkretes Plugin zu erstellen, das einen Kreis in Minecraft baut, wenn man den Befehl /kreis und einen Radius dazu angibt, also zum Beispiel /kreis 5 für einen Kreis mit dem Radius 5 Blöcke. Dafür erstellst du zunächst wieder einen eigenen Ordner, zum Beispiel mit dem Namen kreis.py.dir und eine passende plugin.yml, deren Inhalt du inzwischen auch ohne Hilfe problemlos hinbekommen wirst.
Den Inhalt der plugin.py kannst du in Listing 9.14 sehen. In den Zeilen 9 bis 14 werden zunächst einmal die grundlegenden Variablen definiert und die Startposition festgelegt, die später der Mitte des Kreises entspricht. In diesem Fall wird der Kreis direkt über dem Kopf des Spielers gebaut. Natürlich kannst du ihn aber auch vor, neben oder unter dem Spieler bauen, wenn du möchtest. In Zeile 16 wird der Radius als Parameter eingelesen. Das Vorgehen kennst du inzwischen ja auch bereits von mehreren vorangegangenen Plugins. 1 from org.bukkit.entity import Player
2
3 class KreisPlugin(PythonPlugin):
4 def onEnable(self):
5 pass
6
7 def onCommand(self, sender, command, label, args):
8 if isinstance(sender, Player):
9 spieler = sender
10 welt = spieler.getWorld()
11 position = spieler.getLocation()
12 xStart = position.getX()
13 zStart = position.getZ()
14 position.setY(position.getY() + 2)
15
16 r = int(args[0])
17
18 d = -r
19 x = r
20 z = 0
21
22 while z 0:
67 d = d - 2*x + 2
68 x = x - 1
69
70 else:
71 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
72
73 return True
Listing 9.14: plugin.py für das Kreis-Plugin
In Zeile 18 beginnt dann die Umsetzung der Methode von Horn aus Listing 9.13. Ein erster Unterschied ist, dass in Zeile 20 ein z statt ein y steht. Dadurch ist der Kreis später parallel zum Boden. Wenn
du stattdessen wieder y einsetzen würdest, stünde der fertige Kreis quasi aufrecht, was natürlich auch möglich ist, wenn du das möchtest. In Zeile 23 bis 61 findet dann die eigentliche Positionierung statt, bei der wie bereits erwähnt die Symmetrie des Kreises ausgenutzt wird. Natürlich kannst du beim setType-Befehl auch ein beliebiges anderes Material verwenden. In Abbildung 9.6 kannst du die Ergebnisse des Plugins bewundern. Wie du deutlich siehst, werden die Kreise besser, je größer sie werden. Während der Kreis mit Radius 1 einfach nur ein Quadrat ist, kann man bei einem Radius von 10 schon recht gut erkennen, dass es sich um einen Kreis handeln soll.
Abb. 9.6: Vom Plugin erstellte Kreise mit den Radien 1, 2, 10 und 20
9.2.2 Kugeln Während sich – gerade kleine – Kreise noch recht gut von Hand bauen lassen, stellen Kugelnselbst bei einem kleinen Radius wie in
Abbildung 9.7 einen großen Aufwand dar, da zahlreiche Hilfsblöcke benötigt werden und alle Abstände genau abgezählt werden müssen. Das sind die idealen Einsatzvoraussetzungen für ein Plugin. Auch diesmal soll dafür wieder ein Radius beim Aufrufen des Plugins angegeben werden, lediglich der Befehl soll sich von /kreis zu /kugel ändern.
Abb. 9.7: Kugel mit Radius 10
Nachdem du einen neuen Ordner und eine plugin.yml für das neue Kugel-Plugin angelegt hast, wird es Zeit, sich um die plugin.py zu kümmern, diese findest du in Listing 9.15. 1 2 3 4 5 6 7 8
from org.bukkit.entity import Player
class KugelPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
if isinstance(sender, Player):
9 spieler = sender
10 welt = spieler.getWorld()
11 position = spieler.getLocation()
12 xStart = int(position.getX())
13 yStart = int(position.getY()) + 20
14 zStart = int(position.getZ())
15
16 r = int(args[0])
17
18 for x in range (xStart - r, xStart + r + 1):
19 for y in range (yStart - r, yStart + r + 1):
20 for z in range (zStart - r, zStart + r + 1):
21 if (x-xStart)*(x-xStart)+(yyStart)*(y-yStart)+(z-zStart)*(z-zStart) < r*r:
22 position.setX(x)
23 position.setY(y)
24 position.setZ(z)
25 block = welt.getBlockAt(position)
26 block.setType(bukkit.Material.GOLD_BLOCK)
27
28
29 else:
30 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
31
32
33 return True
Listing 9.15: plugin.py für das Kugel-Plugin
Die Methode, die das Plugin zum Bauen der Kugel verwendet, ist mathematisch nicht so elegant wie die eben benutze Methode von Horn, sie erfüllt aber ihren Zweck – und der Programmcode fällt sogar noch etwas kürzer aus als beim Bauen des Kreises. Das Plugin »denkt« sich um den späteren Mittelpunkt der Kugel einen Quader, der in jeder Richtung eine Ausdehnung von r Blöcken besitzt. Beginnend in Zeile 18 geht das Plugin dann alle Blöcke durch, die sich in diesem gedachten Quader befinden und platziert einen Block an der entsprechenden Stelle, wenn der Quader
innerhalb der Kugel liegt. Dabei macht sich das Plugin in Zeile 21 die Tatsache zunutze, dass jeder Punkt (x, y, z) innerhalb einer Kugel um den Punkt (xStart, yStart, zStart), mit Radius r, die Ungleichung (xxStart)2 + (y-yStart)2 + (z-zStart)2 = len(schilderListe)):
14 spieler.sendMessage("Falsche Schild-ID!")
15 #text zuweisen
16 17 18
else:
schilderListe[schildId].setLine(zeile, text)
schilderListe[schildId].update()
Listing 10.23: Zuweisen von Texten zu Schildern
Wenn der Befehl /schild text eingegeben wird, dann soll das Plugin zunächst einmal prüfen, ob auch die weiteren benötigten Parameter, nämlich eine Schild-ID und ein Text, eingegeben wurden. Das passiert in Zeile 2. In Zeile 5 bis 10 werden diese Parameter dann eingelesen und in Variablen gespeichert. Für die Schild-ID und die Zeile ist das wenig spektakulär, etwas interessanter ist das Einlesen des Textes. Denn wie du dich sicher erinnerst, wird jedes Leerzeichen bei der Befehlseingabe als Beginn eines neuen Parameters interpretiert. Wenn du also zum Beispiel den Befehl /schild text 0 1 Hallo Welt eingibst, dann würde deine argsVariable so aussehen: 1 2 3 4 5
args[0] args[1] args[2] args[3] args[4]
= = = = =
"text"
"0"
"1"
"Hallo"
"Welt"
Listing 10.24: args-Variable für den Befehl /schild text 0 1 Hallo Welt
Da das Plugin den Text aber als einen einzigen String benötigt, werden Texteingaben, die aus mehr als einem Wort bestehen, durch die Schleife in Zeile 9 und 10 zu einer Variable zusammengefügt. In Zeile 13 folgt dann eine weitere Prüfung. Diesmal wird geprüft, ob die eingegebene Schild-ID gültig ist, ob sie also nicht zu klein, also negativ, oder zu groß, also größer als die höchste bisher vergebene Schild-ID ist. Ist auch diese Prüfung erfolgreich, so kann der Text auf dem Schild platziert werden. Wie das funktioniert, weißt du ja bereits aus Listing 10.4. Mit dem so angepassten Plugin kannst du nun nicht nur Schilder platzieren, sondern sie anschließend auch wie in Abbildung 10.6
beschriften und diese Beschriftung bei Bedarf jederzeit wieder ändern.
Abb. 10.6: Mit dem Plugin platziertes und beschriftetes Schild
Theoretisch wäre das Plugin an dieser Stelle schon voll einsatzbereit. Eine Kleinigkeit aus den Anforderungen, die wir am Anfang des Abschnitts formuliert haben, ist allerding noch nicht erfüllt. Noch kann jeder Spieler jedes aufgestellte Schild nach Herzenslust neu beschriften. Besser wäre es jedoch, wenn jeder Spieler nur die Schilder mit Text versehen kann, die er auch selbst aufgestellt hat. Besonders auf einem Server mit vielen Spielern ist das kein unwichtiger Aspekt. Wie immer gibt es die verschiedensten Möglichkeiten, wie sich das Ganze in einem Plugin umsetzen lässt. Du könntest zum Beispiel eine Liste anlegen, in der für jeden Spieler eine Liste mit Schildern gespeichert wird, also eine Liste von Listen. Oder – und dieses Vorgehen soll an dieser Stelle realisiert werden – du fügst deinem Plugin einfach eine zweite Liste hinzu, die für jedes aufgestellte
Schild speichert, von welchem Spieler es aufgestellt wurde. Möchte ein Spieler dann anschließend Text auf ein Schild schreiben, kann einfach mithilfe der Liste geprüft werden, ob er dazu berechtigt ist. Den entsprechend angepassten Quellcode des Plugins findest du in Listing 10.25. Die erste Änderung findet sich dort in Zeile 6. Hier wird die zusätzliche Liste für das Speichern der jeweiligen Besitzer deklariert. Natürlich muss diese neue globale Variable dann den beiden Funktionen onEnable und onCommand übergeben werden, das passiert in Zeile 10 und 17. In der onEnable-Funktion wird die Liste außerdem in Zeile 12, direkt nach der Schilder-Liste, initialisiert. 1 from org.bukkit.entity import Player
2 from org.bukkit.block import BlockFace
3
4 class SchilderPlugin(PythonPlugin):
5 schilderListe = None
6 spielerListe = None
7
8 def onEnable(self):
9 global schilderListe
10 global spielerListe
11 schilderListe = []
12 spielerListe = []
13 pass
14
15 def onCommand(self, sender, command, label, args):
16 global schilderListe
17 global spielerListe
18
19 if isinstance(sender, Player):
20 spieler = sender
21
22 if len(args) < 1:
23 spieler.sendMessage("Bitte Schild-Befehl angeben!")
24 elif args[0] == "bauen":
25 welt = spieler.getWorld()
26
27 #Schild platziert
28 finalBlock = spieler.getLocation()
29 blockListe = spieler.getLineOfSight(None, 30)
30
31 for block in blockListe:
32 if block.getType() == bukkit.Material.AIR:
33 finalBlock = block
34 else:
35 break
36
37 finalBlock.setType(bukkit.Material.OAK_SIGN)
38
39 #Rotation
40 rotationen = [BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST, BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST]
41
42 rotation = spieler.getLocation().getYaw()
43 rotation = (rotation + 180) % 360 #entgegengesetzte Richtung
44 rotation = rotation / 22 #umwandeln fuer Schild
45
46 schild = finalBlock.getState()
47 schildDaten = schild.getData()
48 schildDaten.setFacingDirection(rotationen[int(rotation)])
49 schild.setData(schildDaten)
50 schild.update()
51
52 #Schild zu Liste hinzufuegen
53 schilderListe.append(schild)
54 spielerListe.append(spieler.getUniqueId())
55 spieler.sendMessage("Das neu gebaute Schuld hat die ID " + str(schilderListe.index(schild)))
56 elif args[0] == "text":
57 if len(args) < 4:
58 spieler.sendMessage("Bitte Schild-ID, Zeile und Text angeben!")
59 else:
60 #parameter lesen
61 schildId = int(args[1])
62 zeile = int(args[2])
63 text = args[3]
64 for i in range(4, len(args)):
65 text = text + " " + args[i]
66
67 #ID pruefen
68 if (schildId < 0) | (schildId >= len(schilderListe)):
69 spieler.sendMessage("Falsche Schild-ID!")
70 #spieler pruefen
71 elif spielerListe[schildId] != spieler.getUniqueId():
72 spieler.sendMessage("Dieses Schild gehoert einem anderen Spieler!")
73 #Text zuweisen
74 else:
75 schilderListe[schildId].setLine(zeile, text)
76 schilderListe[schildId].update()
77 else:
78 spieler.sendMessage("Unbekannter SchildBefehl!")
79 else:
80 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
81
82 return True
Listing 10.25: Finales Schilder-Plugin
Beim Bauen eines Schildes wird jetzt außerdem in Zeile 54 in der neuen Spieler-Liste die unverwechselbare ID, in Englisch unique ID, des Spielers gespeichert. Beim Festlegen des Textes muss nun nur noch in Zeile 71 geprüft werden, ob die ID des Spielers, der den Text festlegen will, mit der des Spielers übereinstimmt, der das Schild aufgestellt hat, und schon ist das Plugin fertig. Zum Schluss solltest du noch die plugin.yml anpassen, denn in der ursprünglichen Version war der /schild text-Befehl mit seinen Parametern noch nicht enthalten. 1 2 3
name: Schilder-Plugin
main: SchilderPlugin
version: 1.2
4 commands:
5 schild:
6 description: Platziert ein Schild vor dem Spieler
7 usage: /schild
Listing 10.26: Angepasste plugin.yml
Hinweis Einen Makel hat das Plugin allerdings immer noch: Wird der Server einmal beendet und wieder neu gestartet, geht die Liste der Schilder verloren und bereits aufgestellte Schilder können daher nicht mehr geändert werden. Wie es möglich ist, Informationen dauerhaft zu speichern, damit sie erhalten bleiben, auch wenn der Server ausgeschaltet wird, wirst du im späteren Verlauf des Buches aber noch sehen.
Kapitel 11
Listener Inzwischen hast du schon viele verschiedene Möglichkeiten kennengelernt, die Spielwelt zu beeinflussen und zu verändern. Auslöser war dafür bisher immer die Eingabe eines Befehls durch den Spieler. Du wirst in diesem Kapitel aber sehen, dass es auch möglich ist, dein Plugin so zu programmieren, dass es auf Veränderungen in der Welt reagiert, zum Beispiel, wenn ein bestimmter Block zerstört wird oder ein Spieler ein bestimmtes Areal betritt. Solche Plugins werden auch als Listenerbezeichnet, was Englisch für »Zuhörer« ist, da sie ständig in die Spielwelt »hineinhören«, um Veränderungen zu entdecken.
11.1 Grundgerüst Mit der onCommand-Funktion haben alle Plugins, die auf Chat-Befehle reagieren, ein gemeinsames Grundgerüst. Ein solches Grundgerüst gibt es auch für Listener-Plugins, du kannst es in Listing 11.1 sehen. 1 2 3 4 5 6 7 8 9 10 11 12 13
from org.bukkit.event import EventPriority
class ListenerPlugin(PythonListener):
@PythonEventHandler(Event, EventPriority.NORMAL)
def onEvent(self, event):
#mach etwas
class MeinPlugin(PythonPlugin):
def onEnable(self):
pluginManager = self.getServer().getPluginManager()
listener = ListenerPlugin()
pluginManager.registerEvents(listener, self)
pass
Listing 11.1: Grundgerüst eines Listener-Plugins
Den eigentlichen Listener, also das Element, das auf Veränderungen in der Welt reagieren kann, findest du dort in den Zeilen 3 bis 6. Bisher bestanden deine Plugins immer aus einem der ominösen class-Konstrukten, die du im nächsten Kapitel genauer kennenlernen wirst, in diesem gibt es nun zwei. Statt einer onEable-Funktion besitzt das neue classKonstrukt eine onEvent-Funktion, die immer aufgerufen wird, wenn ein Ereignis, englisch Event, passiert. In der dir bereits bekannten onEnable-Funktion muss der Listener dann noch registriert werden, damit dein Plugin weiß, auf welche Ereignisse es achten muss. Das passiert in den Zeilen 10 bis 12.
11.2 Spieler-Events
Falls du das Prinzip bis hierher noch nicht ganz verstanden hast, keine Sorge, an einem konkreten Beispiel wird es ganz schnell deutlich. Stell dir vor, du möchtest ein Plugin programmieren, das jedem Spieler, sobald er deinen Server betritt, eine Willkommensnachricht sendet. Mit unseren bisherigen Mitteln wäre das nicht möglich gewesen, denn wie soll das Plugin erkennen, wann ein Spieler den Server betritt? Mit den neuen Listenern ist es dagegen kein Problem. Es gibt dutzende verschiedene Events in Minecraft, die du in diesem Kapitel noch kennenlernen wirst, und jeder Listener hört genau auf ein solches Event. Eines dieser Events ist das PlayerJoinEvent, das immer dann vorkommt, wenn ein Spieler den Server betritt. Wie du dieses Event in einem Plugin einsetzen kannst, kannst du in Listing 11.2 sehen. Dort findest du neben dem Import aus dem Grundgerüst noch einen weiteren in Zeile 2. Hier wird das PlayerJoinEvent importiert. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
from org.bukkit.event import EventPriority
from org.bukkit.event.player import PlayerJoinEvent
class ListenerPlugin(PythonListener):
@PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
def onEvent(self, event):
spieler = event.getPlayer()
spieler.sendMessage("Willkommen auf meinem Server!")
class MeinPlugin(PythonPlugin):
def onEnable(self):
pluginManager = self.getServer().getPluginManager()
listener = ListenerPlugin()
pluginManager.registerEvents(listener, self)
pass
Listing 11.2: Plugin mit PlayerJoinEvent
Wie bereits erwähnt, achtet jeder Listener immer nur auf genau ein Event, welches das ist, das wird in Zeile 5 festgelegt, in diesem Fall also natürlich das PlayerJoinEvent. Dahinter steht noch die sogenannte Event-Priorität EventPriority. Es kann häufig vorkommen, dass mehrere Events zeitgleich auftreten. In solchen Fällen legt die Event-Priorität fest, welches der Events am wichtigsten ist und deshalb vom Server zuerst abgearbeitet werden soll. Die wichtigsten Events tragen die Priorität HIGHEST, etwas weniger wichtig, aber immer noch wichtig, ist die Priorität HIGH. NORMAL ist, wie du sicher schon ahnst, die normale, durchschnittliche Priorität. Weniger wichtige Events können die Priorität LOW und besonders unwichtige Events die Priorität LOWEST tragen. Da in Zeile 5 festgelegt wurde, dass es sich hier um einen Listener für das PlayerJoinEvent handelt, wird die onEvent-Funktion in Zeile 6 immer dann aufgerufen, wenn ein Spieler den Server betritt. Besonders wichtig an der onEvent-Funktion ist die Variable event, die ihr übergeben wird, denn diese erhält zusätzliche Funktionen zum Event, mit denen du Information erhalten oder die Ereignisse beeinflussen kannst. Für das PlayerJoinEvent enthält die Variable zum Beispiel die Information welcher Spieler dem Server gerade beigetreten ist. Auf diese Information kannst du, wie in Zeile 7 gezeigt, mit dem Befehl getPlayer zugreifen. Danach brauchst du dann nur noch den bereits bekannten sendMessage-Befehl, um eine Nachricht an den Spieler zu verschicken.
Eine weitere Funktion, die das PlayerJoinEvent mitbringt, ist zum Beispiel setJoinMessage. Mit ihrer Hilfe kannst du festlegen, welche Nachricht an alle Spieler auf dem Server gesendet wird, wenn ein neuer Spieler hinzukommt. Also zum Beispiel event.setJoinMessage(event.getPlayer().getDisplayName() + "hat den Server betreten!").
Damit ist dein Listener zwar bereits fertig, damit er aber auch von deinem Plugin genutzt wird, musst du ihn noch, wie schon im Grundgerüst gezeigt, in deinem Plugin registrieren. Das geschieht in Zeile 12 bis 14. Zunächst einmal wird in Zeile 12 der Plugin-Manager geladen, der für die Verwaltung deines Plugins zuständig ist. In Zeile 13 wird dann der Listener geladen und in Zeile 14 wird er schließlich mit Hilfe des Plugin Managers registriert. Es ist auch möglich, mehrere Listener in einem Plugin zu registrieren, wie genau, dass erfährst du in Abschnitt 11.5. Es gibt so viele verschiedene Event-Typen, die jeweils über unterschiedliche Funktionen verfügen, dass es unmöglich wäre, hier auf alle im Detail einzugehen, und es wäre auch unnötig, da alle vom Prinzip her gleich funktionieren. Aus diesem Grund findest du in diesem Kapitel Tabellen mit Übersichten über alle verfügbaren Events. Die Events sind dabei gegliedert nach ihrem Typ. In diesem Abschnitt findest du zum Beispiel alle Events, die mit dem Spieler zu tun haben, im nächsten Abschnitt alle Ereignisse, die mit anderen Kreaturen wie Monstern oder Tieren zu tun haben. Die Liste mit den Spieler-Events findest du in Tabelle 11.1. Der Import aller Spieler-Events funktioniert immer nach dem Muster from org.bukkit.event.player import BeispielEvent, wobei BeispielEvent durch das entsprechende Event, also zum Beispiel PlayerJoinEvent, ersetzt wird.
Hinweis Die Bindestriche in den Event-Namen sind lediglich Folge des Zeilenumbruchs. Sie werden alle ohne Bindestrich geschrieben.
Event
Funktionen
Beschreibung
PlayerAchievementAwardedEvent
getPlayer(), getAchievement()
Wird aufgerufen, wenn ein Spieler ein Achievement erspielt.
PlayerAnimationEvent
getPlayer(), getAnimationType()
Wird aufgerufen, wenn eine Animation des Spieler ausgeführt wird.
Event
Funktionen
Beschreibung
PlayerBedEnterEvent
getPlayer(), getBed()
Wir aufgerufen, wenn ein Spieler sich in ein Bett legt.
PlayerBedLeaveEvent
getPlayer(), getBed()
Wird aufgerufen, wenn ein Spieler aus einem Bett aufsteht.
PlayerBucketEvent
getPlayer(), getBlockClicked(), getBlockFace(), getBucket(), getItemStack(), setItemStack(itemStack)
Wird aufgerufen, wenn ein Spieler einen Eimer verwendet.
PlayerChangedMainHandEvent
getPlayer(), getMainHand()
Wird aufgerufen, wenn ein Spieler in den Einstellungen die Standard-Hand ändert.
PlayerChangedWorldEvent
getPlayer(), getFrom()
Wird aufgerufen, wenn ein Spieler in eine andere Welt wechselt.
PlayerChannelEvent
getPlayer(), getChannel()
Wird aufgerufen, wenn ein Spieler einen neuen PluginChannel registriert oder abmeldet.
PlayerChatEvent
getPlayer(), getFormat(), getMessage(), getRecipients(), setFormat(format), setMessage(nachricht), setPlayer(spieler)
Wird aufgerufen, wenn ein Spieler den Chat benutzt.
Event
Funktionen
Beschreibung
PlayerChatTabCompleteEvent
getPlayer(), getChatMessage(), getLastToken(), getTabCompletions()
Wird aufgerufen, wenn ein Spieler die Autovervollständigung im Chat benutzt.
PlayerCommandPreprocessEvent
getPlayer(), getFormat(), getMessage(), getRecipients(), setFormat(format), setMessage(nachricht), setPlayer(spieler)
Wird aufgerufen, bevor ein vom Spieler eingegebener Befehl ausgeführt wird.
PlayerDropItemEvent
getPlayer(), getItemDrop()
Wird aufgerufen, wenn ein Spieler einen Gegenstand aus seinem Inventar entfernt.
PlayerEditBookEvent
getPlayer(), getNewBookMeta(), getPreviousBookMeta(), getSlot(), isSigning(), setNewBookMeta(), setSigning()
Wird aufgerufen, wenn ein Spieler den Inhalt eines Buches verändert.
PlayerEggThrowEvent
getPlayer(), getEgg(), getHatchingType(), getNumHatches(), isHatching(), setHatching(boolean), setHatchingType(EntityType), setNumHatches(num)
Wird aufgerufen, wenn ein Spieler ein Ei wirft.
PlayerExpChangeEvent
getPlayer(), getAmount(), setAmount(amount)
Wird aufgerufen, wenn sich die Erfahrungspunkte eines Spielers ändern.
Event
Funktionen
Beschreibung
PlayerFishEvent
getPlayer(), getCaught(), getExpToDrop(), getHook(), getState(), setExpToDrop(amount)
Wird aufgerufen, wenn ein Spieler fischt.
PlayerGameModeChangeEvent
getPlayer(), getNewGameMode()
Wird aufgerufen, wenn sich der Spielmodus eines Spielers ändert.
PlayerInteractEntityEvent
getPlayer(), getHand(), getRightClicked()
Wird aufgerufen, wenn ein Spieler mit der rechten Maustaste auf ein Lebewesen klickt.
PlayerInteractEvent
getPlayer(), getAction(), getBlockFace(), getClickedBlock(), getHand(), getItem(), getMaterial(), hasBlock(), hasItem(), isBlockInHand(), setUseInteractedBlock(block), setUseItemInHand(item), userInteractedBlock(), useItemInHand()
Wird aufgerufen, wenn ein Spieler auf einen Block, ein Objekt oder Luft klickt.
PlayerItemBreakEvent
getPlayer(), getBrokenItem()
Wird aufgerufen, wenn ein Gegenstand eines Spielers kaputtgeht (zum Beispiel Schaufel oder Schwert).
PlayerItemConsumeEvent
getPlayer(), getItem(), setItem(item)
Wird aufgerufen, wenn ein Spieler etwas verzehrt hat (zum Beispiel Essen oder Trank).
Event
Funktionen
Beschreibung
PlayerItemHeldEvent
getPlayer(), getNewSlot(), getPreviousSlot()
Wird aufgerufen, wenn ein Spieler den Gegenstand, den er in der Hand hält, wechselt.
PlayerJoinEvent
getPlayer(), getJoinMessage(), setJoinMessage(nachricht)
Wird aufgerufen, wenn ein Spieler den Server betritt.
PlayerKickEvent
getPlayer(), getLeaveMessage(), getReason(), setLeaveMessage(nachricht), setReason(nachricht)
Wird aufgerufen, wenn ein Spieler von einem Server geworfen wird.
PlayerLevelChangeEvent
getPlayer(), getNewLevel(), getOldLevel()
Wird aufgerufen, wenn sich das Level eines Spieler ändert.
PlayerLoginEvent
getPlayer(), allow(), disallow(result, nachricht), getAddress(), getHostname(), getKickMessage(), getResult(), setKickMessage(nachricht), setResult(result)
Wird aufgerufen, wenn sich ein Spieler einloggt.
PlayerMoveEvent
getPlayer(), getFrom(), getTo(), setFrom(position), setTo(position)
Wird aufgerufen, wenn sich ein Spieler bewegt.
PlayerPickupItemEvent
getPlayer(), getItem(), getRemaining()
Wird aufgerufen, wenn ein Spieler einen Gegenstand aufhebt.
Event
Funktionen
Beschreibung
PlayerQuitEvent
getPlayer(), getQuitMessage(), setQuitMessage(nachricht)
Wird aufgerufen, wenn ein Spieler den Server verlässt.
PlayerRespawnEvent
getPlayer(), getRespwanLocation(), isBedSpawn(), setRespawnLocation(position)
Wird aufgerufen, wenn ein Spieler respawnt.
PlayerShearEntityEvent
getPlayer(), getEntity()
Wird aufgerufen, wenn ein Spieler ein Lebewesen schert.
PlayerStatisticIncrementEvent
getPlayer(), getEntityType(), getMaterial(), getNewValue(), getPreviousValue(), getStatistics()
Wird aufgerufen, wenn sich die Spielstatistik eines Spieler verändert (ausgenommen Bewegungsstatistik).
PlayerSwapHandItemsEvent
getPlayer(), getMainHandItem(), getOffHandItem(), setMainHandItem(item), setOffHandItem(item)
Wird aufgerufen, wenn ein Spieler einen Gegenstand zwischen primärer und sekundärer Hand wechselt.
PlayerToggleFlightEvent
getPlayer(), isFlying()
Wird aufgerufen, wenn ein Spieler in den Flugmodus oder zurück wechselt.
PlayerToggleSneakEvent
getPlayer(), isSneaking()
Wird aufgerufen, wenn ein Spieler beginnt zu schleichen oder damit aufhört.
Event
Funktionen
Beschreibung
PlayerToggleSprintEvent
getPlayer(), isSprinting()
Wird aufgerufen, wenn ein Spieler beginnt zu sprinten oder damit aufhört.
PlayerVelocityEvent
getPlayer(), getVelocity(), setVelocity(vektor)
Wird aufgerufen, wenn sich die Geschwindigkeit eines Spielers ändert.
Tabelle 11.1: Spieler-Events
Vorsicht Einige dieser Events werden potenziell sehr häufig aufgerufen. Das PlayerMoveEvent wird zum Beispiel jedes Mal aufgerufen, wenn ein Spieler einen Schritt macht. Bei einem Server mit vielen Spielern kann dieses Event mehrere hundert Mal in wenigen Sekunden aufgerufen werden. Bei solchen Events solltest du es vermeiden, in der onEvent-Funktion aufwändige Berechnungen durchzuführen, da es sonst leicht passieren kann, dass dein Server überfordert wird und sehr langsam wird oder sogar komplett abstürzt.
11.3 Kreaturen-Events Die Gruppe der Kreaturen-Events kann nach folgenden Muster importiert werden: from org.bukkit.event.entity import BeispielEvent, wobei BeispielEvent wieder durch das entsprechende Event ersetzt wird. Die Übersicht über alle Kreaturen-Events findest du in Tabelle 11.2.
Event
Funktionen
Beschreibung
AreaEffectCloudApplyEvent
getEntity(), getEntityType(), getAffectedEntities()
Wird aufgerufen, wenn ein Verweiltrank seinen Effekt verbreitet.
Event
Funktionen
Beschreibung
CreatureSpawnEvent
getEntity(), getEntityType(), getLocation(), getSpawnReason()
Wird aufgerufen, wenn ein Lebewesen spawnt.
CreeperPowerEvent
getEntity(), getEntityType(), getCause(), getLightning()
Wird aufgerufen, wenn ein Creeper von einem Blitz getroffen wird.
EnderDragonChangePhaseEvent
getEntity(), getEntityType(), getCurrentPhase(), getNewPhase(), setNewPhase(modus)
Wird aufgerufen, wenn sich der Modus eines Enderdrachens ändert.
EntityAirChangeEvent
getEntity(), getEntityType(), getAmount(), setAmount()
Wird aufgerufen, wenn sich die Luftmenge ändert, die eine Kreatur zur Verfügung hat.
EntityBreedEvent
getEntity(), getEntityType(), getBredWith(), getBreeder(), getExperience(), getFather(), getMother(), setExperience(erfahrung)
Wird aufgerufen, wenn sich zwei Kreaturen paaren.
EntityChangeBlockEvent
getEntity(), getEntityType(), getBlock(), getData(), getTo()
Wird aufgerufen, wenn eine Kreatur, nicht, aber ein Spieler oder einen Block verändert.
EntityCombustEvent
getEntity(), getEntityType(), getDuration(), setDuration(dauer)
Wird aufgerufen, wenn eine Kreatur brennt.
Event
Funktionen
Beschreibung
EntityCreatePortalEvent
getEntity(), getEntityType(), getBlocks(), getPortalType()
Wird aufgerufen, wenn eine Kreatur ein Portal öffnet.
EntityDamageEvent
getEntity(), getEntityType(), getCause(), getDamage(), getFinalDamage(), getOriginalDamage(typ), setDamage(schaden)
Wird aufgerufen, wenn einer Kreatur Schaden zugefügt wird.
EntityDeathEvent
getEntity(), getEntityType(), getDroppedExp(), getDrops(), setDroppedEx(erfahrung)
Wird aufgerufen, wenn eine Kreatur stirbt.
EntityExplodeEvent
getEntity(), getEntityType(), blockList(), getLocation(), getYield(), setYield(prozent)
Wird aufgerufen, wenn eine Kreatur explodiert.
EntityInteractEvent
getEntity(), getEntityType(), getBlock()
Wird aufgerufen, wenn eine Kreatur mit einem Objekt interagiert.
EntityPortalEnterEvent
getEntity(), getEntityType(), getLocation()
Wird aufgerufen, wenn eine Kreatur in ein Portal tritt.
EntityRegainHealthEvent
getEntity(), getEntityType(), getAmount(), getRegainReason(), setAmount(num)
Wird aufgerufen, wenn sich die Gesundheit einer Kreatur regeneriert.
Event
Funktionen
Beschreibung
EntityShootBowEvent
getEntity(), getEntityType(), getBow(), getForce(), getProjectile(), setProjectile(entity)
Wird aufgerufen, wenn eine Kreatur einen Pfeil mit einem Bogen verschießt.
EntityTameEvent
getEntity(), getEntityType(), getOwner()
Wird aufgerufen, wenn eine Kreatur gezähmt wird.
EntityTargetEvent
getEntity(), getEntityType(), getReason(), getTarget(), setTarget(entity)
Wird aufgerufen, wenn eine Kreatur eine andere fokussiert oder den Fokus verliert.
EntityTeleportEvent
getEntity(), getEntityType(), getFrom(), getTo(), setFrom(position), setTo(position)
Wird aufgerufen, wenn sich eine Kreatur, nicht aber ein Spieler, versucht zu teleportieren.
EntityUnleashEvent
getEntity(), getEntityType(), getReason()
Wird aufgerufen, bevor eine Kreatur in die Welt entlassen wird.
ExplosionPrimeEvent
getEntity(), getEntityType(), getFire(), getRadius(), setFire(boolean), setRadius(float)
Wird aufgerufen, wenn sich eine Kreatur dazu entschieden hat zu explodieren, aber noch nicht explodiert ist.
FireworkExplodeEvent
getEntity(), getEntityType()
Wird aufgerufen, wenn Feuerwerk explodiert.
FoodLevelChangeEvent
getEntity(), getEntityType(), getFoodLevel(), setFoodLevel(int)
Wird aufgerufen, wenn sich die Nahrungsanzeige einer Kreatur verändert.
Event
Funktionen
Beschreibung
HorseJumpEvent
getEntity(), getEntityType(), getPower(), setPower(float)
Wird aufgerufen, wenn ein Pferd springt.
ItemDespawnEvent
getEntity(), getEntityType(), getLocation()
Wird aufgerufen, wenn ein Gegenstand aus der Welt verschwindet, weil er für mehr als 5 Minuten auf dem Boden lag.
ItemSpawnEvent
getEntity(), getEntityType(), getLocation()
Wird aufgerufen, wenn ein Gegenstand neu in der Welt erscheint.
PigZapEvent
getEntity(), getEntityType(), getLightning(), getPigZombie()
Wird aufgerufen, wenn ein Schwein von einem Blitz getroffen wird.
ProjectileHitEvent
getEntity(), getEntityType()
Wird aufgerufen, wenn ein Projektil (zum Beispiel ein Pfeil) auftrifft.
ProjectileLaunchEvent
getEntity(), getEntityType()
Wird aufgerufen, wenn ein Projektil abgeschossen wird.
SheepDyeWoolEvent
getEntity(), getEntityType(), getColor(), setColor(color)
Wird aufgerufen, wenn die Wolle eines Schafes eingefärbt wird.
SheepRegrowWoolEvent
getEntity(), getEntityType()
Wird aufgerufen, wenn die Wolle eines Schafes nachwächst.
Event
Funktionen
Beschreibung
SlimeSplitEvent
getEntity(), getEntityType(), getCount(), setCount(int)
Wird aufgerufen, wenn sich ein Schleimmonster teilt.
VillagerAcquireTradeEvent
getEntity(), getEntityType(), getRecipe(), setRecipe(recipe)
Wird aufgerufen, wenn eine neue Handelsmöglichkeit mit einem Dorfbewohner freigeschaltet wird.
VillagerReplenishTradeEvent
getEntity(), getEntityType(), getBonus(), getRecipe(), setBonus(int), setRecipe(recipe)
Wird aufgerufen, wenn sich die maximale Handelszahl eines Dorfbewohners ändert.
Tabelle 11.2: Kreaturen-Events
11.4 Block-Events Die Gruppe der Block-Events in Tabelle 11.3 umfasst alle Events, die, der Name lässt es erahnen, mit einem Block zu tun haben. Sie können mit from org.bukkit.event.block import BeispielEvent importiert werden.
Event
Funktionen
Beschreibung
BlockBurnEvent
getBlock()
Wird aufgerufen, wenn ein Block durch Feuer zerstört wird.
BlockCanBuildEvent
getBlock(), getMaterial(), isBuildable(), setBuildable(boolean)
Wird aufgerufen, wenn versucht wird, einen Block zu platzieren.
Event
Funktionen
Beschreibung
BlockDamageEvent
getBlock(), getInstaBreak(), getItemInHand(), getPlayer(), setInstaBreak(boolean)
Wird aufgerufen, wenn ein Block von einem Spieler beschädigt wird.
BlockDispenseEvent
getBlock(), getItem(), getVelocity(), setItem(item), setVelocity(velocity)
Wird aufgerufen, wenn ein Block einen Gegenstand erzeugt (zum Beispiel Erz).
BlockExpEvent
getBlock(), getExpToDrop(), setExpToDrop(int)
Wird aufgerufen, wenn ein Block Erfahrungspunkte abgibt.
BlockExplodeEvent
getBlock(), getYield(), setYield()
Wird aufgerufen, wenn ein Block explodiert.
BlockFadeEvent
getBlock(), getNewState()
Wird aufgerufen, wenn ein Block wegen Veränderungen in der Welt entfernt wird, zum Beispiel, wenn Schnee schmilzt.
BlockFromToEvent
getBlock(), getFace(), getToBlock()
Wird aufgerufen, wenn sich ein Lava- oder Wasserblock bewegt oder ein Drachenei teleportiert.
BlockGrowEvent
getBlock(), getNewState()
Wird aufgerufen, wenn ein Block wächst, zum Beispiel Melonen oder Weizen.
Event
Funktionen
Beschreibung
BlockIgniteEvent
getBlock(), getCause(), getIgnitingBlock(), getIgnitingEntity(), getPlayer()
Wird aufgerufen, wenn ein Block angezündet wird.
BlockPistonEvent
getBlock(), getDirection(), isSticky()
Wird aufgerufen, wenn ein Kolben-Block aktiviert wird.
BlockPlaceEvent
getBlock(), canBuild(), getBlockAgainst(), getBlockPlaced(), getBlockReplacedState(), getHand(), getItemInHand(), getPlayer(), setBuild(boolean)
Wird aufgerufen, wenn ein Block von einem Spieler platziert wird.
BlockRedstoneEvent
getBlock(), getNewCurrent(), getOldCurrent(), setNewCurrent(int)
Wird aufgerufen, wenn sich die RedstoneLadung eines Blocks ändert.
BrewEvent
getBlock(), getContents()
Wird aufgerufen, wenn ein Brauvorgang beendet wurde.
CauldronLevelChangeEvent
getBlock(), getEntity(), getNewLevel(), getOldLevel(), getReason(), setNewLevel(int)
Wird aufgerufen, wenn sich der Füllstand eines Kessels ändert.
FurnaceBurnEvent
getBlock(), getBurnTime(), getFuel(), isBurning(), setBurning(boolean), setBurnTime(int)
Wird aufgerufen, wenn Gegenstände in einem Ofen verbrannt werden.
FurnaceSmeltEvent
getBlock(), getResult(), getSource(), setResult(itemStack)
Wird aufgerufen, wenn ein Gegenstand in einem Ofen eingeschmolzen wird.
Event
Funktionen
Beschreibung
getBlock()
Wird aufgerufen, wenn Blätter verwelken beziehungsweise abfallen.
NotePlayEvent
getBlock(), getInstrument(), getNote() setInstrument(instrument), setNote(note)
Wird aufgerufen, wenn ein Notenblock einen Ton spielt.
SignChangeEvent
getBlock(), getLine(int), getLines(), getPlayer(), setLine(index, text)
Wird aufgerufen, wenn ein Schild verändert wird.
LeavesDecayEvent
Tabelle 11.3: Block-Events
11.5 Inventar-Events Die Inventar-Events umfassen sowohl Interaktion mit dem Inventar des Spielers als auch Interaktion mit dem Inventar eines Trichters, einer Kiste und ähnlichem. Der Import-Befehl lautet für diese Gruppe from org.bukkit.event.inventory import BeispielEvent.
Event
Funktionen
Beschreibung
EnchantItemEvent
getInventory(), getView(), getViewers(), getEnchantBlock(), getEnchanter(), getEnchantsToAdd(), getExpLevelCost(), getItem(), setExpLevelCost(int), whichButton()
Wird aufgerufen, wenn ein Gegenstand verzaubert wurde.
Event
Funktionen
Beschreibung
InventoryCloseEvent
getInventory(), getView(), getViewers(), getPlayer()
Wird aufgerufen, wenn ein Spieler ein Inventar schließt.
InventoryInteractEvent
getInventory(), getView(), getViewers(), getResult(), getWhoClicked(), setResult(result)
Wird aufgerufen, wenn ein Spieler mit dem Inhalt eines Inventars interagiert.
InventoryMoveItemEvent
getInventory(), getView(), getViewers(), getDestination(), getInitiator(), getItem(), getSource(), setItem(itemStack)
Wird aufgerufen, wenn ein Gegenstand von einem in ein anderes Inventar verschoben wird, zum Beispiel durch den Spieler, aber auch durch Trichter usw.
InventoryOpenEvent
getInventory(), getView(), getViewers(), getPlayer()
Wird aufgerufen, wenn ein Spieler ein Inventar öffnet.
InventoryPickupItemEvent
getInventory(), getView(), getViewers(), getItem()
Wird aufgerufen, wenn ein Trichter einen Gegenstand aufnimmt.
PrepareAnvilEvent
getInventory(), getView(), getViewers(), getResult(), setResult(result)
Wird aufgerufen, wenn ein Gegenstand zur Reparatur auf einen Amboss gelegt wird.
PrepareItemCraftEvent
getInventory(), getView(), getViewers(), getRecipe(), isRepair()
Wird aufgerufen, wenn ein Spieler die Zutaten für ein Rezept in das CraftingFeld gelegt hat.
Event
Funktionen
Beschreibung
PrepareItemEnchantEvent
getInventory(), getView(), getViewers(), getEnchantBlock(), getEnchanter(), getEnchantmentBonus(), getExpLevelCostsOffered(), getItem()
Wird aufgerufen, wenn ein Gegenstand auf einen Zaubertisch gelegt wird.
Tabelle 11.4: Inventar-Events
11.6 Server-Events Die Events, die du bisher kennengelernt hast, können immer wieder während des Spiels auftreten. Die meisten Events in dieser Kategorie dagegen treten auf, bevor das eigentliche Spiel losgeht, wenn der Server hochgefahren wird. Du kannst sie mit from org.bukkit.event.server import BeispielEvent importieren.
Event
Funktionen
Beschreibung
MapInitializeEvent
getMap()
Wird aufgerufen, wenn eine Karte initialisiert wird.
PluginEvent
getPlugin()
Wird aufgerufen, wenn ein Plugin aktiviert oder deaktiviert wird.
ServerCommandEvent
getCommand(), getSender(), setCommand(string)
Wird aufgerufen, wenn ein Befehl über die Server-Konsole abgeschickt wird.
ServerListPingEvent
getAddress(), getMaxPlayers(), getMotd(), getNumPlayers(), iterator(), setMotd(string), setServerIcon(icon)
Wird aufgerufen, wenn die Spielerliste des Servers angefordert wird.
Event
Funktionen
Beschreibung
ServiceEvent
getProvider()
Wird aufgerufen, wenn ein Service registriert wird.
Tabelle 11.5: Server-Events
11.7 Fahrzeug-Events Bei dieser Überschrift bist du vielleicht zunächst etwas stutzig geworden, schließlich gibt es keine wirklichen Fahrzeuge wie zum Beispiel Autos in Minecraft. Der Begriffe Fahrzeuge wird hier etwas missbraucht, um alles zu beschreiben, worin beziehungsweise worauf sich ein Spieler fortbewegen kann. Also zum Beispiel Loren oder Boote, aber auch Pferde oder Schweine. Um die Events verwenden zu können, benötigst du folgenden Import-Befehl: from org.bukkit.event.vehicle import BeispielEvent.
Event
Funktionen
Beschreibung
VehicleCollisionEvent
getVehicle()
Wird aufgerufen, wenn ein Fahrzeug mit etwas kollidiert.
VehicleCreateEvent
getVehicle()
Wird aufgerufen, wenn ein Fahrzeug erzeugt wird.
VehicleDamageEvent
getVehicle(), getAttacker(), getDamage(), setDamage(float)
Wird aufgerufen, wenn ein Fahrzeug beschädigt wird.
VehicleDestroyEvent
getVehicle(), getAttacker()
Wird aufgerufen, wenn ein Fahrzeug zerstört wird.
VehicleEnterEvent
getVehicle(), getEntered()
Wird aufgerufen, wenn eine Kreatur ein Fahrzeug betritt.
VehicleExitEvent
getVehicle(), getExited()
Wird aufgerufen, wenn eine Kreatur ein Fahrzeug verlässt.
Event
Funktionen
Beschreibung
getVehicle(), getFrom(), getTo()
Wird aufgerufen, wenn sich ein Fahrzeug bewegt.
getVehicle()
Wird aufgerufen, wenn ein Fahrzeug aktualisiert wird.
VehicleMoveEvent
VehicleUpdateEvent
Tabelle 11.6: Fahrzeug-Events
11.8 Wetter-Events Die kleinste Gruppe an Events sind die Welt-Events, von ihnen gibt es gerade einmal drei. Du findest sie in Tabelle 11.7 und kannst sie mit from org.bukkit.event.weather import BeispielEvent verwenden.
Event
Funktionen
Beschreibung
LightningStrikeEvent
getWorld(), getLightning()
Wird aufgerufen, wenn ein Blitz einschlägt.
ThunderChangeEvent
getWorld(), getThunderState()
Wird aufgerufen, wenn ein Gewitter beginnt oder endet.
WeatherChangeEvent
getWorld(), toWeatherState()
Wird aufgerufen, wenn sich das Wetter ändert.
Tabelle 11.7: Wetter-Events
11.9 Welt-Events Zu guter Letzt gibt es noch die Welt-Events, die auf alle möglichen Veränderungen in der Welt reagieren können und mit from org.bukkit.event.world import BeispielEvent importiert werden.
Event
Funktionen
Beschreibung
PortalCreateEvent
getWorld(), getBlocks(), getReason()
Wird aufgerufen, wenn ein Portal erzeugt wird.
SpawnChangeEvent
getWorld(), getPreviousLocation()
Wird aufgerufen, wenn sich der Spawnpunkt verändert.
StructureGrowEvent
getWorld(), getBlocks(), getLocation(), getPlayer(), getSpecies(), isFromBonemeal()
Wird aufgerufen, wenn etwas wächst, zum Beispiel Bäume oder Pilze.
WorldInitEvent
getWorld()
Wird aufgerufen, wenn eine Welt initialisiert wird.
WorldLoadEvent
getWorld()
Wird aufgerufen, wenn eine Welt geladen wird.
WorldSaveEvent
getWorld()
Wird aufgerufen, wenn eine Welt gespeichert wird.
WorldUnloadEvent
getWorld()
Wird aufgerufen, wenn eine Welt geschlossen wird.
Tabelle 11.8: Welt-Events
11.10 Mehrere Listener in einem Plugin Wie du in den letzten Abschnitten gesehen hast, gibt es wirklich eine Vielzahl an unterschiedlichen Events. Größere Plugins benutzen deshalb oft mehr als einen Listener, um auf verschiedene Events reagieren zu können. Wie das aussehen kann, kannst du in Listing 11.3 sehen: Das dort gezeigte Plugin reagiert sowohl auf das PlayerJoinEvent als auch auf das PlayerQuitEvent. 1 2 3 4 5 6 7
from org.bukkit.event import EventPriority
from org.bukkit.event.player import PlayerJoinEvent
from org.bukkit.event.player import PlayerQuitEvent
class JoinListener(PythonListener):
@PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
def onEvent(self, event):
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
spieler = event.getPlayer()
spieler.sendMessage("Willkommen auf meinem Server!")
class QuitListener(PythonListener):
@PythonEventHandler(PlayerQuitEvent, EventPriority.NORMAL)
def onEvent(self, event):
spieler = event.getPlayer()
spieler.sendMessage("Bis bald auf meinem Server!")
class MeinPlugin(PythonPlugin):
def onEnable(self):
pluginManager = self.getServer().getPluginManager()
joinListener = JoinListener()
pluginManager.registerEvents(joinListener, self)
quitListener = QuitListener()
pluginManager.registerEvents(quitListener, self)
pass
Listing 11.3: Verwendung mehrerer Listener in einem Plugin
Um zwei Listener innerhalb eines Plugins verwenden zu können, müssen natürlich zuerst auch beide importiert werden, das geschieht in Zeile 2 und 3. Danach musst du zunächst für beide Listener ein eigenes class-Objekt anlegen, dafür wiederholst du einfach zweimal den Code, wie du ihn auch für nur einen Listener schreiben würdest, du musst lediglich darauf achten, dass die beiden nicht den gleichen Namen tragen dürfen. Und auch in der onEnable-Funktion reicht es, wie ebenfalls in Listing 11.3 gezeigt ist, einfach die beiden Zeilen zu wiederholen, die du auch für einen einzelnen Listener verwenden würdest, nur mit angepasstem Namen. Natürlich lassen sich Listener, egal ob einer oder mehrere, auch mit Chat-Befehlen kombinieren. Dazu fügst du einfach unter der onEnable-Funktion noch eine onCommandFunktion ein und kannst diese wie gewohnt nutzen. Außerdem solltest du nicht vergessen, den entsprechenden Befehl auch noch in die plugin.yml einzutragen.
Klassen und Objekte Kapitel 12
Als du zum ersten Mal das »Hallo Welt!«-Plugin eingegeben hast, musstest du viel Programmcode noch einfach abschreiben, ohne genau zu wissen, worum es eigentlich geht. Fast alle Befehle des ersten Plugins und noch viele weitere mehr hast du inzwischen kennen und verstehen gelernt. In diesem Kapitel soll nun auch noch das letzte Geheimnis der Plugin-Struktur gelüftet werden. In diesem Kapitel wollen wir uns mit der sogenannten objektorientierten Programmierung, kurz OOP, beschäftigen. Obwohl das Konzept der Objektorientierung erst in den 1970erJahren entstanden und damit vergleichsweise jung ist, gehört es heute zu den wichtigsten Konzepten in der Programmierung und wird von einem großen Teil der modernen Programmiersprachen genutzt. Die ersten Programmiersprachen gab es übrigens schon lange, bevor es die ersten Computer gab. Bereits im Jahr 1805 erfand der Franzose Joseph-Marie Jacquard einen programmierbaren Webstuhl, der durch einen Streifen mit Löchern in unterschiedlichen Abständen darauf programmiert werden konnte. Nach einem ganz ähnlichen Prinzip funktionierte auch die Datenverarbeitung einiger der ersten Computer.
12.1 Die ganze Welt ist ein Objekt Bei der objektorientierten Programmierung geht es nicht hauptsächlich um eine bestimmte Syntax, sondern vielmehr um eine Idee und eine bestimmte Sichtweise auf die Welt. Die Welt von Minecraft besteht zum Beispiel aus unzähligen, aufeinander gestapelten Blöcken. Jeder dieser Blöcke hat gewisse
Eigenschaften, die ihn von den anderen Blöcken unterscheidet, wie zum Beispiel seine Position, das Material, aus dem er besteht, oder wie viele Schläge man benötigt, um ihn abzubauen. Als Programmierer wäre es für dich ein Leichtes, diese Informationen in einer für ein Programm nutzbaren Form darzustellen. Listing 12.1 zeigt, wie das aussehen könnte. 1 2 3 4 5 6 7 8 9 10 11
position1 material1 schlaege1
position2 material2 schlaege2
position3 material3 schlaege3
= Location(self.getWorld(), 0, 0, 0)
= bukkit.Material.GOLD_BLOCK
= 10
= Location(self.getWorld(), 0, 10, 10)
= bukkit.Material.SAND
= 1
= Location(self.getWorld(), 15, 3, 7)
= bukkit.Material.DIRT
= 3
Listing 12.1: Darstellung der Block-Eigenschaften als Variablen
Wie dir aber sicher bereits aufgefallen ist, funktionieren die Blöcke beim Programmieren von Plugins nicht so und das hat auch einen guten Grund, denn wie du in Listing 12.1 erkennen kannst, wiederholt sich dort zum Beispiel gleicher Code sehr oft, da für jeden Block dieselben Informationen gespeichert werden. So ein Block, da wirst du sicher zustimmen, ist ein Objektin der Welt von Minecraft. Im Alltag verstehen wir unter Objekten meist Dinge, die wir anfassen können. Im Kontext der objektorientieren Programmierung können auch durchaus Dinge Objekte sein, die man nicht anfassen kann, wie zum Beispiel eine bestimmte Witterung oder ein Gedanke. Alle drei Objekte in Listing 12.1 gehören zur Gruppe der Blöcke. In der OOP sagt man, die drei Objekte gehören zur Klasse Block, oder genauer, sie sind Instanzen der Klasse Block. Ein konkreter Block in der Spielwelt ist ist also eine Instanz der Klasse Block.
Aber der Reihe nach: Worum geht es hier eigentlich? Um die Beschreibung oder Repräsentation eines Gegenstandes in einem Programm. Die meisten Gegenstände sind allerdings so komplex, dass wir sie niemals komplett in einem Programm abbilden können. In den meisten Fällen ist das aber überhaupt nicht schlimm. So weiß das Programm in Listing 12.1 zum Beispiel nicht, wer den Block an dieser Stelle platziert hat, das ist aber überhaupt kein Problem, weil uns diese Information nicht weiter interessiert. Wir versuchen also nicht, den Gegenstand komplett abzubilden, sondern versuchen, die für die Anwendung wichtigen Eigenschaften abzubilden. Man bezeichnet diesen Prozess des Weglassens von Eigenschaften und des Findens von relevanten Eigenschaften für das Programm auch als Abstraktion. In der OOP bezeichnet man eine solche Abstraktion als Klasse. In Listing 12.2 kannst du eine Klasse sehen, in der die Eigenschaften, die uns momentan an Blöcken interessieren, zusammengefasst sind. Man nennt die Eigenschaften, die eine Klasse zusammenfasst, auch Attribute.
Merke Eine Klasse fasst Dinge oder Konzepte zusammen, die gemeinsame Eigenschaften, sogenannte Attribute, haben, wie zum Beispiel die verschiedenen Blöcke in Minecraft. Die konkreten Elemente einer Klasse, also zum Beispiel ein ErdBlock, heißen Objekte oder Instanzen.
Wie du in Listing 12.2 sehen kannst, beginnt die Definition einer Klasse mit dem Stichwort class, das dir sicher schon aus den bisherigen Plugins bekannt vorkommt, gefolgt vom Namen der Klasse. Es gibt die Konvention, dass Klassennamen immer mit einem Großbuchstaben beginnen.
Merke Klassennamen sollten immer mit einem Großbuchstaben beginnen.
1 2 3 4
class Block:
position = None
material = None
schlaege = None
Listing 12.2: Die Klasse Block
Eine Klasse allein ergibt wenig Sinn, denn sie ist nur ein abstraktes Konzept. Nützlich wird sie erst dadurch, dass man ein konkretes Objekt anlegt, das den einzelnen Attributen der Klasse konkrete Werte zuweist. Beim Erzeugen eines konkreten Objektes aus einer Klasse hilft in Python die sogenannte init-Funktion, die du für die Klasse Block in Listing 12.3 sehen kannst. Die init-Funktion ist eine spezielle Funktion, die immer dann aufgerufen wird, wenn ein neues Objekt einer Klasse erzeugt oder instanziiert wird. Häufig werden der Funktion deshalb als Parameter, wie in Listing 12.3, auch gleich Werte für die Attribute der Klasse übergeben. 1 2 3 4 5 6 7 8 9
class Block:
position = None
material = None
schlaege = None
def __init__(self, position, material, schlaege):
self.position = position
self.material = material
self.schlaege = schlaege
Listing 12.3: Die Klasse Block mit init-Funktion
Außerdem wird auch der init-Funktion, wie du es bereits in Kapitel 8 gelernt hast, der Parameter self übergeben. In Listing 12.3 kannst du nun auch sehen, wofür dieser Parameter ist. Wenn du innerhalb einer Funktion, auch der init-Funktion, eine Variable deklarierst, wie zum Beispiel a = 3, dann kannst du auf diese Variable, wie du ebenfalls bereits in Kapitel 8 gelernt hast, auch nur innerhalb dieser Funktion zugreifen. Auf die Attribute einer Klasse soll aber jede Funktion innerhalb dieser Klasse zugreifen können. Damit das möglich ist, enthält die Variable selfeinen Verweis auf das aktuelle Objekt und all seine Attribute. Mithilfe der init-Funktion können dann wie in Listing 12.4 Objekte der Klasse Block erstellt werden. 1 block1 = Block(Location(self.getWorld(), 0, 0, 0), bukkit.Material.GOLD_BLOCK, 10)
2
3 block2 = Block(Location(self.getWorld(), 0, 10, 10), bukkit.Material.SAND, 1)
4
5 block3 = Block(Location(self.getWorld(), 15, 3,7), bukkit.Material.DIRT, 3)
Listing 12.4: Erstellung von Objekten der Klasse Block
Auf die einzelnen Parameter aus Listing 12.3 kannst du dann zum Beispiel mit block1.material oder block2.schlaege zugreifen und sie auslesen oder auch verändern. Das Wichtige ist dabei, dass, obwohl du die Variable schlaege nur einmal aufgeschrieben hast, jedes Objekt seine eigene Version der Variable hat. Wenn du block1.schlaege veränderst, hat das also keine Auswirkung auf block2.schlaege. Manchmal benötigt man allerdings auch dieses Verhalten, also eine gemeinsame Variable, auf die alle Objekte einer Klasse zugreifen können. Man spricht dann von einer Klassenvariable. Ein Beispiel
hierfür wäre ein Zähler, der die Anzahl der Objekte einer Klasse speichert. Stell dir vor, du hast eine beliebige Klasse A und möchtest wissen, wie viele Objekte dieser Klasse bereits erzeugt wurden. Ein Attribut würde dir hier nicht weiterhelfen, denn jedes Objekt verwendet, wie du bereits weißt, seine eigenen Kopien der Attribute, auf die die anderen Objekte der Klasse nicht zugreifen können. Wenn also zum Beispiel ein Block sein Attribut material auf Gold setzt, dann werden dadurch nicht automatisch alle Blöcke zu Gold, da das Attribut immer nur für ein Objekt und nicht für die ganze Klasse Block gilt. Auf Klassenvariablen können dagegen alle Objekte einer Klasse zugreifen, so ähnlich wie du das bereits von den globalen Variablen kennst: Statt einer eigenen Variable in jeder Funktion, können dort alle Funktionen auf die globale Variable zugreifen und so auch Werte zwischen den Funktionen austauschen. Genauso verhält es sich auch mit Klassenvariablen. Jedes neue Objekt weiß dank des Zählers, der eine Klassenvariable ist, wie viele Objekte vor ihm bereits erzeugt wurden, und kann diesen Wert dann um eins erhöhen. Wie ein solcher Zähler funktioniert, kannst du in Listing 12.5 sehen. 1 2 3 4 5
class A:
zaehler = 0
def __init__(self):
A.zaehler = A.zaehler + 1
Listing 12.5: Beispiel für die Verwendung einer Klassenvariable
Die Festlegung einer Klassenvariable unterscheidet sich zunächst nicht von der eines Attributs, allerdings wird beim Aufruf der Variable statt self, oder des Names des Objektes, der Name der Klasse verwendet, in diesem Fall also A. Wie du den Zähler dann in der Praxis einsetzen kannst, siehst du in Listing 12.6. Beim Aufruf der init-Funktion für objekt1, wird die Klassenvariable zaehler von 0 auf 1 erhöht und objekt2 erhöht sie dann ein weiteres mal auf 2. Den aktuellen Wert der Variable kannst du jederzeit mit A.zaehler abrufen.
1 2 3
objekt1 = A() #zaehler wird auf 1 gesetzt
objekt2 = A() #zaehler wird auf 2 gesetzt
A.zaehler #liefert den Wert 2 zurueck
Listing 12.6: Verwendung des Zählers
12.2 Funktionen in Klassen Mit Attributen und Klassenvariablen kannst du Klassen bereits verwenden, um Informationen zu speichern. In diesem Abschnitt soll es nun darum gehen, wie du Funktionen innerhalb einer Klasse nutzen kannst, um deinen Objekten noch mehr Leben einzuhauchen. Um das zu erläutern, wollen wir uns wieder einem alten Bekannten zuwenden, dem Mauer-Plugin. Wenn du etwas mithilfe einer Klasse abbilden möchtest, dann stellt sich, egal ob es um einzelne Blöcke oder um Mauern geht, am Anfang immer die gleiche Frage: Welche Informationen über die einzelnen Objekte der Klasse sind für mein Programm relevant? Wenn du dir die Version des Mauer-Plugins aus Kapitel 8 anschaust, springen sofort drei Informationen, oder wie wir jetzt sagen würden: Attribute, ins Auge: die Höhe, die Breite und das Material. Das sind die drei Informationen, die du benötigst, um eine Mauer zu bauen. Daraus ergibt sich die in Listing 12.7 gezeigte Klassendefinition für die Klasse Mauer. Wie du vermutlich nicht anders erwartet hast, findest du hier die festgelegten drei Attribute und eine init-Funktion. 1 2 3 4 5 6 7 8 9
class Mauer:
hoehe = 0
breite = 0
material = None
def __init__(self, hoehe, breite, material):
self.hoehe = hoehe
self.breite = breite
self.material = material
Listing 12.7: Mauer-Klasse
In Listing 12.8 kannst du sehen, wie das Grundgerüst für die neue Version des Mauer-Plugins aussieht, das nun ein Mauer-Objekt verwendet. Was dort noch fehlt, ist der eigentliche Bauvorgang, der wie zuvor wieder in eine eigene Funktion ausgelagert werden soll. 1 from org.bukkit.entity import Player
2
3 class Mauer:
4 hoehe = 0
5 breite = 0
6 material = None
7
8 def __init__(self, hoehe, breite, material):
9 self.hoehe = hoehe
10 self.breite = breite
11 self.material = material
12
13 class MauerPlugin(PythonPlugin):
14 def onEnable(self):
15 pass
16
17 def onCommand(self, sender, command, label, args):
18 if isinstance(sender, Player):
19 hoehe = int(args[0])
20 breite = int(args[1])
21
22 mauer = Mauer(hoehe, breite, bukkit.Material.GOLD_BLOCK)
23
24 else:
25 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
26
27 return True
Listing 12.8: Grundgerüst für die neue Version des Mauer-Plugins
Wie du in Listing 12.9 sehen kannst, genügt es nun, der mauerBauenFunktion neben dem Spieler ein Mauer-Objekt zu übergeben, statt wie bisher Höhe und Breite einzeln.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def mauerBauen(self, spieler, mauer):
position = spieler.getLocation()
position.setX(position.getX() + 2)
welt = spieler.getWorld()
yStart = position.getY()
zStart = position.getZ()
for y in range (0, mauer.hoehe):
position.setY(yStart + y)
for z in range (0, mauer.breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(mauer.material)
Listing 12.9: Neue mauerBauen-Funktion
In Listing 12.10 kannst du noch einmal den inzwischen recht umfangreichen kompletten Quellcode für die neue Version des Mauer-Plugins sehen. Im Spiel funktioniert das Plugin genau wie zuvor. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
from org.bukkit.entity import Player
class Mauer:
hoehe = 0
breite = 0
material = None
def __init__(self, hoehe, breite, material):
self.hoehe = hoehe
self.breite = breite
self.material = material
class MauerPlugin(PythonPlugin):
def onEnable(self):
pass
def onCommand(self, sender, command, label, args):
if isinstance(sender, Player):
hoehe = int(args[0])
breite = int(args[1])
mauer = Mauer(hoehe, breite,
bukkit.Material.GOLD_BLOCK)
23 MauerPlugin.mauerBauen(self, sender, mauer)
24
25 else:
26 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
27
28 return True
29
30 def mauerBauen(self, spieler, mauer):
31 position = spieler.getLocation()
32 position.setX(position.getX() + 2)
33
34 welt = spieler.getWorld()
35
36 yStart = position.getY()
37 zStart = position.getZ()
38
39 for y in range (0, mauer.hoehe):
40 position.setY(yStart + y)
41 for z in range (0, mauer.breite):
42 position.setZ(zStart + z)
43 block = welt.getBlockAt(position)
44 block.setType(mauer.material)
Listing 12.10: Mauer-Plugin Version 3.0
Warum also überhaupt die ganze Mühe? Bisher hat der Einsatz von Objekten im Mauer-Plugin den Quellcode eher unübersichtlicher gemacht und kaum Vorteile gebracht. Das kannst du aber ändern, indem du die Funktion für das Bauen in die Mauer-Klasse selbst auslagerst. Denn Klassen können in Python nicht nur aus Variablen bestehen, also Attributen, sie können auch eigene Funktionen besitzen. Man spricht auch von den Methodeneiner Klasse. Die um die bauen-Funktion erweiterte Mauer-Klasse findest du in Listing 12.11. Von anfänglich drei Parametern, als sie noch ganz ohne Objekte gearbeitet hat, benötigt die Klasse nun nur noch einen Parameter, nämlich den Spieler. Da die Funktion nun als Teil der Mauer-Klasse an ein konkretes Objekt gebunden ist, nämlich an eine Mauer mit festgelegter Höhe und Breite sowie vorgegebenem
Material, hat sie nun auch direkten Zugriff auf diese Attribute und sie müssen daher nicht mehr als Parameter übergeben werden. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
class Mauer:
hoehe = 0
breite = 0
material = None
def __init__(self, hoehe, breite, material):
self.hoehe = hoehe
self.breite = breite
self.material = material
def bauen(self, spieler):
position = spieler.getLocation()
position.setX(position.getX() + 2)
welt = spieler.getWorld()
yStart = position.getY()
zStart = position.getZ()
for y in range (0, self.hoehe):
position.setY(yStart + y)
for z in range (0, self.breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(self.material)
Listing 12.11: Mauer-Klasse mit bauen-Methode
Das ist schon mal ein erster Vorteil, der zweite große Vorteil wird sichtbar, wenn du dir Listing 12.12 ansiehst. Das Plugin ist nun deutlich übersichtlicher, denn für das eigentliche Bauen der Mauer benötigst du nun nämlich gerade noch zwei Zeilen, die Zeilen 38 und 39, der Rest ist in die Mauer-Klasse ausgelagert. 1 2 3 4 5 6
from org.bukkit.entity import Player
class Mauer:
hoehe = 0
breite = 0
material = None
7
8 def __init__(self, hoehe, breite, material):
9 self.hoehe = hoehe
10 self.breite = breite
11 self.material = material
12
13 def bauen(self, spieler):
14 position = spieler.getLocation()
15 position.setX(position.getX() + 2)
16
17 welt = spieler.getWorld()
18
19 yStart = position.getY()
20 zStart = position.getZ()
21
22 for y in range (0, self.hoehe):
23 position.setY(yStart + y)
24 for z in range (0, self.breite):
25 position.setZ(zStart + z)
26 block = welt.getBlockAt(position)
27 block.setType(self.material)
28
29 class MauerPlugin(PythonPlugin):
30 def onEnable(self):
31 pass
32
33 def onCommand(self, sender, command, label, args):
34 if isinstance(sender, Player):
35 hoehe = int(args[0])
36 breite = int(args[1])
37
38 mauer = Mauer(hoehe, breite, bukkit.Material.GOLD_BLOCK)
39 mauer.bauen(sender)
40
41 else:
42 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
43
44 return True
Listing 12.12: Fertiges, überarbeitetes Mauer-Plugin
Hinweis
Eigentlich sind Funktionen innerhalb von Klassen überhaupt nichts Neues für dich, denn auch die alte Version des MauerPlugins hat mit der Funktion mauerBauen schon längst eine Funktion innerhalb einer Klasse enthalten. Diese wurde allerdings nicht über den Namen eines Objektes, sondern über den Klassennamen aufgerufen. Analog zu den Klassenvariablen handelt es sich dabei also um eine Klassenmethode. Sie kann daher auch nur auf Klassenvariablen, nicht aber auf die Attribute eines konkreten Objektes zugreifen.
Ein weiterer großer Vorteil ist die Tatsache, dass sich Klassen einfach in eigene Dateien auslagern und damit auch wiederverwenden lassen. Um das zu tun, erstellst du am besten zunächst in dem Ordner, in dem sich alle deine Plugins befinden, also zum Beispiel C:\server\plugins beziehungsweise /home/Benutzername/server/plugins einen neuen Ordner mit dem Namen libs. In diesem Ordner erzeugst du dann eine leere Datei mit dem Namen __init__.py. In diesem Ordner kannst du zukünftig alle Klassen ablegen, die du in mehreren Plugins gleichzeitig benutzen kannst. Als erstes kannst du dort also zum Beispiel eine Datei mit dem Namen mauerklasse.py erstellen und in dieser den Inhalt aus Listing 12.13 speichern. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
class Mauer:
hoehe = 0
breite = 0
material = None
def __init__(self, hoehe, breite, material):
self.hoehe = hoehe
self.breite = breite
self.material = material
def bauen(self, spieler):
position = spieler.getLocation()
position.setX(position.getX() + 2)
welt = spieler.getWorld()
17 18 19 20 21 22 23 24 25
yStart = position.getY()
zStart = position.getZ()
for y in range (0, self.hoehe):
position.setY(yStart + y)
for z in range (0, self.breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(self.material)
Listing 12.13: mauerklasse.py
Um diese Klasse nun in deinem Mauer-Plugin verwenden zu können, musst du das Plugin wie in Listing 12.14 gezeigt anpassen. Wie du dort sehen kannst, musst du dem Server zunächst einmal mitteilen, in welchem Ordner du die Klassen abgelegt hast, wie das für Windows funktioniert, kannst du in Zeile 4 sehen, für GNU/Linux in Zeile 5 und für OS X in Zeile 6. 1 from sys import path
2 from org.bukkit.entity import Player
3
4 path.append('C:\server\plugins\libs')
5 #path.append('/home/Benutzername/server/plugins/libs')
6 #path.append('/Users/Benutzername/server/plugins/libs')
7
8 import mauerklasse
9
10 class MauerPlugin(PythonPlugin):
11 def onEnable(self):
12 pass
13
14 def onCommand(self, sender, command, label, args):
15 if isinstance(sender, Player):
16 hoehe = int(args[0])
17 breite = int(args[1])
18
19 mauer = mauerklasse.Mauer(hoehe, breite, bukkit.Material.GOLD_BLOCK)
20 mauer.bauen(sender)
21
22 else:
23 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
24 25
return True
Listing 12.14: Mauer-Plugin unter Verwendung externer Klasse
Damit hast du zunächst einmal Zugriff auf alle Klassen, die in diesem Ordner liegen. Um dann auf eine konkrete Klasse zuzugreifen, benötigst du noch den import-Befehl, zusammen mit dem Dateinamen, in diesem Fall also mauerklasse. Im Code selbst ist dann nur noch eine Änderung, nämlich in Zeile 19, notwendig. Da die Klasse nun in einer anderen Datei liegt, musst du den Namen der Datei vor den Namen der Klasse schreiben, also zum Beispiel mauerklasse.Mauer statt nur Mauer. So hast du dein Plugin noch übersichtlicher gemacht und kannst die Klasse Mauer auch noch in anderen Plugins wiederverwenden, zum Beispiel im NotunterkunftPlugin.
12.3 Zugriffskontrolle Du kannst deine selbst erstellten Klassen sogar im Internet mit anderen teilen und ihnen somit Arbeit ersparen, schließlich muss das Rad nicht jedes Mal neu erfunden werden. Spätestens wenn du deinen Code mit anderen teilst, am besten aber auch, wenn er nur für dich selbst ist, solltest du dir allerdings Gedanken um die »Sicherheit« deines Codes machen. Mit Sicherheit ist dabei hier vor allem gemeint, dass du dich darauf verlassen kannst, dass deine Klasse tatsächlich auch das macht, was du von ihr erwartest. In Listing 12.15 findest du zum Beispiel die Klasse Halbieren. Ob diese besonders sinnvoll ist oder nicht, darüber kann man trefflich streiten, aber wenn man sie verwendet, dann würde man erwarten, dass eine übergebene Zahl halbiert, also durch zwei geteilt wird. 1 2 3
class Halbieren:
halb = 2.0
4 5
def halbieren(self, zahl):
print(str(zahl/self.halb))
Listing 12.15: Öffentliches Attribut
Wenn du zum Beispiel: 1 2
h = Halbieren()
h.halbieren(7)
eingibst, dann erwartest – und bekommst – du als Ergebnis 3.5 angezeigt. Durch eine Unachtsamkeit oder böse Absicht ist es allerdings möglich, dieses Verhalten zu manipulieren: 1 2 3
h = Halbieren()
h.halb = 5.0
h.halbieren(7)
Mit diesem Code bekommst du als Ergebnis nun nicht mehr 3.5, sondern 1.4. Der Grund dafür ist natürlich, dass das Attribut halb verändert wurde, womit die Klasse Halbieren nicht rechnet. Das ist möglich, da es sich bei halb um ein sogenanntes öffentlichesAttribut handelt. Öffentliche Attribute können nicht nur innerhalb der Klasse, sondern auch außerhalb der Klasse verändert werden. Die sogenannten privatenAttribute können im Gegensatz dazu nur innerhalb der Klasse, zu der sie gehören, geändert werden. In Listing 12.16 kannst du die Klasse Halbieren noch einmal mit privatem Attribut sehen. 1 2 3 4 5
class Halbieren:
__halb = 2.0
def halbieren(self, zahl):
print(str(zahl/self.halb))
Listing 12.16: Privates Attribut
Merke Private Attribute werden in Python durch zwei Grundstriche vor dem Namen (__) markiert. Auf demselben Weg ist es auch möglich, Methoden als privat zu markieren.
Wenn du nun wieder 1 2 3
h = Halbieren()
h.halb = 5.0
h.halbieren(7)
eingibst, so bleibt das Ergebnis trotzdem 3.5. Würdest du print(str(h.halb)) eingeben, so würde sogar eine Fehlermeldung erscheinen, dass es in der Klasse Halbieren kein Attribut halb gibt. Das Attribut ist nun also geschützt und kann nur noch innerhalb der Klasse Halbieren verwendet werden. Genauso, wie du Attribute als privat auszeichnen kannst, kannst du auch Methoden als privat markieren, diese können dann ebenfalls nur noch innerhalb der Klasse verwendet werden, zu der sie gehören. Wenn du dich länger mit Python beschäftigst, dann werden dir irgendwann auch Variablen begegnen, vor deren Namen du nur einen Grundstrich findest. In Python ist das ein Hinweis des Entwicklers, dass du diese Variable bitte unverändert lassen sollst, im Gegensatz zu privaten Variablen hast du aber weiterhin technisch die Möglichkeit diese zu verändern, es handelt sich dabei also um eine schwächere Form des Schutzes. Manchmal verwendet man private Attribute aber nicht, um den Zugriff auf sie zu verhindern, sondern um sicherzustellen, dass nur korrekte Werte in ihnen gespeichert werden. Ein Anwendungsbeispiel dafür findest du in Listing 12.17.
1 2 3 4 5
class Multiplizieren:
faktor = 1
def multiplizieren(self, faktor):
print(str(faktor * self.faktor))
Listing 12.17: Klasse Multiplizieren mit öffentlichem Attribut
Die dort gezeigte Klasse dient dazu, zwei Zahlen zu multiplizieren. Würdest du das Attribut faktor hier als privat kennzeichnen, so könntest du jede Zahl immer nur mit 1 multiplizieren. Schützt du das Attribut nicht, so sind aber auch unerwartete Eingaben möglich, es könnte zum Beispiel dem Attribut ein String als Wert zu gewiesen werden, was zu unerwarteten Ergebnissen führen könnte. In solchen Fällen, in denen man den externen Zugriff zwar prinzipiell zulassen, aber kontrollieren möchte, verwendet man häufig sogenannte getter- und setter-Methoden. Die Namen kommen aus dem Englischen von »get«, was so viel bedeutet wie etwas bekommen, und »set«, was so viel bedeutet wie etwas festlegen. Die einzige Aufgabe von getter- und setter-Methoden ist es, den externen Zugriff auf private Variablen zu ermöglichen. Sie tragen üblicherweise die Namen getVariablenname und setVariablenname, also zum Beispiel getFaktor und setFaktor. Der getter ermöglicht dabei den Lesezugriff, also das Abrufen des Wertes, und der setter den Schreibzugriff, also das Verändern oder Setzen des Wertes. Setzt du also eine Variable auf privat und richtest nur einen getter ein, so kann sie von außerhalb der Klasse zwar gelesen, aber nicht geändert werden. In Listing 12.18 findest du eine überarbeitete Version der Multiplizieren-Klasse, die nun einen setter einsetzt, um zu überprüfen, ob es sich bei einem eingegebenen Faktor um eine Zahl handelt. 1 2 3 4
class Multiplizieren:
__faktor = 1
def getFaktor(self):
5 return self.__faktor
6
7 def setFaktor(self, faktor):
8 if (type(faktor) is int) | (type(faktor) is float):
9 self.__faktor = faktor
10 else:
11 print("Fehler: Nur Zahlen als Faktor zulaessig")
12
13 def multiplizieren(self, faktor):
14 print(str(faktor * self.__faktor))
Listing 12.18: Klasse Multiplizieren mit privatem Attribut
12.4 Vererbung Was es mit dem Stichwort class und den Klassen, die dahinterstecken, auf sich hat, das hast du in diesem Kapitel nun bereits gelernt. Wenn du dir aber nochmal anschaust, wie die erste Zeile einer Plugin-Klasse normalerweise aussieht, dann wirst du dort noch etwas finden, das wir bisher in diesem Kapitel noch nicht näher betrachtet haben: class MauerPlugin(PythonPlugin). Hinter dem Namen der Klasse steht dort der Namen einer weiteren Klasse, die wir nicht kennen, die aber bei allen Plugins gleich ist. Dahinter steckt das Konzept der sogenannten Vererbung, das du in diesem Abschnitt kennenlernen wirst. Um zu verstehen, was dahintersteckt, schauen wir uns noch einmal eine gekürzte Version der Mauerklasse an. Wie du in Listing 12.19 sehen kannst, besteht die Mauer-Klasse im Wesentlichen aus Attributen für Höhe, Breite und Material der Mauer sowie einer initund bauen-Funktion. 1 2 3 4 5
class Mauer:
hoehe = 0
breite = 0
material = None
6 7 8
def __init__(self, hoehe, breite, material):
def bauen(self, spieler):
Listing 12.19: Gekürzte Übersicht über die Mauer-Klasse
In deinem Notunterkunfts-Plugin hast du nicht mit Mauern, sondern mit Quadern gearbeitet. Wenn du diese nun auch als Klasse darstellen wollen würdest, so besäßen diese ebenfalls eine initund eine bauen-Funktion, und auch Attribute für Höhe, Breite und Material, einziger Unterschied zur Mauer-Klasse wäre ein zusätzliches Attribut für die Tiefe des Quaders. Wie das in Quellcode aussehen würde, kannst du in Listing 12.20 sehen. 1 2 3 4 5 6 7 8 9
class Quader:
hoehe = 0
breite = 0
tiefe = 0
material = None
def __init__(self, hoehe, breite, tiefe, material):
def bauen(self, spieler):
Listing 12.20: Gekürzte Quader-Klasse
Sicher fällt dir sofort auf, dass es sehr viel Übereinstimmung zwischen Listing 12.19 und Listing 12.20 gibt. Code doppelt schreiben, das weißt du inzwischen, ist eine der Sachen, die wir unbedingt vermeiden wollen. Genau hier kommt die Vererbung zum Tragen und das funktioniert so: Zunächst werden alle Gemeinsamkeiten, die die beiden Klassen verbinden, in eine sogenannte Oberklasse übernommen. Also zum Beispiel die drei gemeinsamen Attribute. Für Mauer und Quader bietet sich zum Beispiel eine Oberklasse Viereckig an, die du in Listing 12.21 sehen kannst. 1 2
class Viereckig:
hoehe = 0
3 4
breite = 0
material = None
Listing 12.21: Viereckig-Klasse
Auf Grundlage dieser Oberklasse können dann sogenannte Unterklassenerstellt werden. Diese Unterklassen übernehmen, man kann auch sagen erben, automatisch alle Attribute und Methoden ihrer Oberklasse. Als Unterklasse der Viereckig-Klasse sieht die gekürzte Mauer-Klasse deshalb wie in Listing 12.22 aus. 1 2 3 4
class Mauer(Viereckig):
def __init__(self, hoehe, breite, material):
def bauen(self, spieler):
Listing 12.22: Mauer-Klasse als Unterklasse von Viereckig
Wie du dort sehen kannst, können die Attribute in der Mauer-Klasse nun komplett weggelassen werden, da sie aus der Oberklasse Viereckig geerbt werden. Du kannst sie dann innerhalb der MauerKlasse trotzdem ganz normal, zum Beispiel über self.hoehe und so weiter, verwenden. Ein wenig anders sieht es in der neuen Unterklasse Quader aus, die du in Listing 12.23 sehen kannst. Dort muss nun noch das Attribut definiert werden, das zwar Teil der Quader-Klasse ist, nicht aber Teil der Oberklasse. 1 2 3 4 5 6
class Quader(Viereckig):
tiefe = 0
def __init__(self, hoehe, breite, tiefe, material):
def bauen(self, spieler):
Listing 12.23: Quader-Klasse als Unterklasse von Viereckig
Hinweis Jede Klasse erbt von einer Oberklasse, auch wenn du das nicht explizit angibst. Immer wenn eine Klasse ohne konkrete Oberklasse angelegt wird, ergänzt Python automatisch die Oberklasse object. Statt class A: kannst du also immer auch class A(object): schreiben.
Die gemeinsamen Attribute der beiden Klassen hast du nun erfolgreich in eine gemeinsame Oberklasse überführt. Aber auch bei den Methoden der beiden Klassen gibt es durchaus Überschneidungen. In Listing 12.24 und Listing 12.25 siehst du zum Beispiel die init-Funktionen der beiden Klassen, die sich lediglich in einer Zeile unterscheiden. Auch hier gibt es also offensichtlich Optimierungspotenial. 1 2 3 4
def __init__(self, hoehe, breite, material):
self.hoehe = hoehe
self.breite = breite
self.material = material
Listing 12.24: init-Funktion der Mauer-Klasse 1 2 3 4 5
def __init__(self, hoehe, breite, tiefe, material):
self.hoehe = hoehe
self.breite = breite
self.tiefe = tiefe
self.material = material
Listing 12.25: init-Funktion der Quader-Klasse
Ganz ähnlich wie bei den Attributen kannst du auch hier die Gemeinsamkeiten der beiden Unterklassen in eine neue initFunktion in der Oberklasse überführen. Diese wäre dann identisch
mit der in Listing 12.24 gezeigten Funktion, da diese gerade alle Gemeinsamkeiten enthält. Die neue init-Funktion der Mauer-Klasse würde dann wie in Listing 12.26 gezeigt aussehen. Statt die Attribute selbst zu initialisieren, wird dort nun nur noch die init-Funktion der Oberklasse aufgerufen, statt drei Zeilen wird also nur noch eine benötigt. 1 2
def __init__(self, hoehe, breite, material):
Viereckig.__init__(self, hoehe, breite, material)
Listing 12.26: init-Funktion der Mauer-Klasse als Unterklasse von Viereckig
Die neue init-Funktion der Quader-Klasse fällt, wie du in Listing 12.27 sehen kannst, eine Zeile länger aus, da hier noch zusätzlich das Attribut für tiefe initialisiert werden muss. Trotzdem ist sie viel kürzer und übersichtlicher als vorher und du hast die Dopplung des Codes vermieden. 1 2 3
def __init__(self, hoehe, breite, tiefe, material):
Viereckig.__init__(self, hoehe, breite, material)
self.tiefe = tiefe
Listing 12.27: init-Funktion der Quader-Klasse als Unterklasse von Viereckig
12.5 Mehrfachvererbung und mehrstufige Vererbung Nun kennst du beinahe schon alle wichtigen Grundlagen der Vererbung. Die letzten beiden wirst du in diesem Abschnitt kennenlernen, die Mehrfachvererbungund die mehrstufige Vererbung. Von Mehrfachvererbung spricht man, wenn eine Klasse von mehr als einer Oberklasse erbt, ein ganz einfaches Beispiel dafür kannst du in
Listing 12.28 sehen. Hinter dem Klassennamen C stehen dort in Zeile 7 zwei Klassen in Klammern, A und B. Das heißt also, C erbt in diesem Fall von A und B. 1 2 3 4 5 6 7 8 9 10 11 12
class A:
x = 1
class B:
y = 2
class C(A, B):
def addition(self):
print(str(self.x + self.y))
c = C()
c.addition()
Listing 12.28: Mehrfachvererbung
In der Praxis bedeutet das, dass es innerhalb der Klasse C später sowohl die Attribute und, falls sie existieren, natürlich auch die Methoden aus der Klasse A als auch die Attribute aus der Klasse B gibt. Deshalb kann die Funktion addition in der Klasse C sowohl auf x als auch auf y zugreifen. Eine Frage, die sich bei der Mehrfachvererbung sofort aufdrängt, ist, wie Fälle gehandhabt werden, in denen eine oder mehrere Klassen Attribute oder Methoden mit denselben Namen enthalten, wie zum Beispiel in Listing 12.29. Hier enthält sowohl die Klasse A als auch die Klasse B ein Attribut mit dem Namen x. Die Methode anzeigen in der Klasse C gibt das Attribut x aus. Die Frage ist nun: Wird 1 oder 2 ausgegeben, schließlich erbt C von beiden Klassen. 1 2 3 4 5 6 7 8
class A:
x = 1
class B:
x = 2
class C(A, B):
def anzeigen(self):
9 10 11 12
print(str(self.x))
c = C()
c.anzeigen()
Listing 12.29: Mehrfachvererbung mit gleichen Namen
Und die Antwort ist 1, denn Python nimmt in diesem Fall immer die Attribute oder Methoden aus der Klasse, die bei der Vererbung zuerst gelistet wird. C(A, B) führt also dazu, dass bei Namensgleichheit die Elemente der Klasse A bevorzugt werden, umgekehrt würden bei C(B, A) die Elemente der Klasse B bevorzugt. Neben der Mehrfachvererbung, die es zum Beispiel in Java in dieser Form nicht gibt, gibt es auch ein noch verbreiteteres Konzept, die sogenannte mehrstufige Vererbung. Was es damit auf sich hat, kannst du in Listing 12.30 sehen. 1 2 3 4 5 6 7 8 9 10 11 12 13
class A:
x = 1
class B(A):
y = 1
class C(B):
def anzeigen(self):
print(str(self.x + self.y))
c = C()
c.anzeigen()
Listing 12.30: Mehrstufige Vererbung
Wie du in Zeile 7 eindeutig sehen kannst, erbt die Klasse C nur von der Klasse B, trotzdem kann innerhalb von C in Zeile 9 auf das Attribut x zugegriffen werden, das eigentlich nur Teil von A ist. Da B aber von A geerbt hat, sind alle Attribute von A, und damit auch x, Teil von B. Da C nun von B erbt, übernimmt es nicht nur die eigenen
Attribute von B, also y, sondern auch die Attribute, die B bereits von A geerbt hatte. Genau das ist eine mehrstufige Vererbung.
12.6 Bau-Plugin Nach recht viel Theorie in diesem Kapitel wollen wir das gelernte nun aber auch in die Praxis umsetzen und zwar in einem BauPlugin, das es sowohl ermöglicht Mauern zu bauen als auch Quader. Das alles soll mit einem einzigen Befehl funktionieren, dem dann als Parameter übergeben wird, was und in welcher Größe gebaut werden soll: /bauen mauer xy und /bauen quader x y z. Zur Implementierung wollen wir natürlich die Oberklasse Viereckig und die Unterklassen Mauer und Quader einsetzen, die uns einiges an Arbeit ersparen werden. Aber eins nach dem anderen. Zunächst solltest du natürlich einmal einen Ordner für dein neues Plugin anlegen, zum Beispiel mit dem Namen bauen.py.dir. In diesem Ordner legst du dann zunächst wie üblich eine Datei mit dem Namen plugin.yml an. Den passenden Inhalt der Datei findest du in Listing 12.31. 1 2 3 4 5 6 7
name: Bau-Plugin
main: BauPlugin
version: 1.0
commands:
bauen:
description: Baut etwas
usage: /bauen ()
Listing 12.31: plugin.yml
Als nächstes ist die Klasse Viereckig dran, die legst du am besten, wie bereits zuvor beschrieben, in den Ordner libs in deinem PluginVerzeichnis ab, der die leere __init__.py Datei enthält, und zwar in einer Datei mit dem Namen viereckig.py, deren Inhalt du in Listing 12.32 findest.
1 2 3 4 5 6 7 8 9
class Viereckig:
hoehe = 0
breite = 0
material = None
def __init__(self, hoehe, breite, material):
self.hoehe = hoehe
self.breite = breite
self.material = material
Listing 12.32: viereckig.py
Weiter geht es mit der Klasse für die Mauern, die du ebenfalls im libs-Ordner ablegen solltest. Dank der Oberklasse Viereckig besteht diese Klasse nun, wie du in Listing 12.33 sehen kannst, nur noch aus einer bauen-Funktion. Deren Inhalt kannst du im Wesentlichen aus dem Mauer-Plugin übernehmen. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
from viereckig import Viereckig
class Mauer(Viereckig):
def bauen(self, spieler):
position = spieler.getLocation()
position.setX(position.getX() + 2)
welt = spieler.getWorld()
yStart = position.getY()
zStart = position.getZ()
for y in range (0, self.hoehe):
position.setY(yStart + y)
for z in range (0, self.breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(self.material)
Listing 12.33: mauer.py
Im selben Ordner solltest du außerdem noch eine Datei mit der Klasse Quader anlegen. Neben der bauen-Funktion enthält diese auch eine eigene init-Funktion und ein zusätzliches Attribut für die Tiefe des Quaders.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
from viereckig import Viereckig
class Quader(Viereckig):
tiefe = 0
def __init__(self, hoehe, breite, tiefe, material):
Viereckig.__init__(self, hoehe, breite, material)
self.tiefe = tiefe
def bauen(self, spieler):
position = spieler.getLocation()
position.setX(position.getX() + 2)
welt = spieler.getWorld()
xStart = position.getX()
yStart = position.getY()
zStart = position.getZ()
for x in range (0, self.tiefe):
position.setX(xStart + x)
for y in range (0, self.hoehe):
position.setY(yStart + y)
for z in range (0, self.breite):
position.setZ(zStart + z)
block = welt.getBlockAt(position)
block.setType(self.material)
Listing 12.34: quader.py
Tipp Wenn du dir Listing 12.33 und Listing 12.34 ganz genau anschaust, wird dir sicher auffallen, dass es innerhalb der beiden bauen-Funktionen viele Gemeinsamkeiten gibt. Wenn du die Objektorientierung und Vererbung auf die Spitze treiben möchtest, kannst du auch diese Gemeinsamkeiten noch auflösen, schließlich ist ein Quader nichts anderes als x-mal hintereinander eine Mauer, wobei x der Tiefe entspricht. Du müsstest also die bauen-Funktion in der Klasse Mauer so anpassen, dass du ihr eine Position übergeben kannst, die Klasse Quader würde dann nicht
mehr direkt von der Klasse Viereckig erben, sondern von der Klasse Mauer und innerhalb der bauen-Funktion der Quader-Klasse würde dann – mithilfe einer Schleife – die bauen-Funktion aus der Mauer-Klasse mehrfach aufgerufen.
Nun fehlt nur noch das Plugin selbst, das du wie gewohnt in einer Datei mit dem Namen plugin.py im zugehörigen Plugin-Ordner ablegst. Den passenden Code dazu findest du in Listing 12.35. Das meiste davon hast du in dieser – oder ähnlicher – Form bereits in anderen Plugins gesehen. Du solltest darauf achten, das Hinzufügen des richtigen Pfades zu den anderen Klassen zu Beginn nicht zu vergessen, der Rest sollte dann keine große Hürde mehr sein. 1 from sys import path
2 from org.bukkit.entity import Player
3
4 path.append('C:\server\plugins\libs')
5 #path.append('/home/Benutzername/server/plugins/libs')
6 #path.append('/Users/Benutzername/server/plugins/libs')
7
8 import mauer
9 import quader
10
11 class BauPlugin(PythonPlugin):
12 material = bukkit.Material.GOLD_BLOCK
13
14 def onEnable(self):
15 pass
16
17 def onCommand(self, sender, command, label, args):
18 if isinstance(sender, Player):
19 spieler = sender
20
21 if len(args) < 1:
22 spieler.sendMessage("Bitte Bau-Befehl angeben!")
23 elif args[0] == "mauer":
24 if len(args) < 3:
25 spieler.sendMessage("Bitte Breite und Hoehe angeben!")
26 else:
27 m = mauer.Mauer(int(args[1]), int(args[2]), self.material)
28 m.bauen(spieler)
29 elif args[0] == "quader":
30 if len(args) < 4:
31 spieler.sendMessage("Bitte Breite, Hoehe und Tiefe angeben!")
32 else:
33 q = quader.Quader(int(args[1]), int(args[2]), int(args[3]), self.material)
34 q.bauen(spieler)
35 else:
36 spieler.sendMessage("Unbekannter BauBefehl!")
37 else:
38 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
39
40 return True
Listing 12.35: Bau-Plugin
Crafting-Rezepte Kapitel 13
Seit dem Erscheinen von Minecraft ist die Zahl der verfügbaren Crafting-Rezeptestetig gewachsen. Heute sind es bereits weiter über 200 Stück. Trotz dieser Fülle an verschiedenen Möglichkeiten fallen dir bestimmt einige Rezepte ein, die aus deiner Sicht noch fehlen. Und als Plugin-Programmierer kannst du diese nun tatsächlich zum Spiel hinzufügen! Dabei gibt es allerdings Grenzen: Du kannst zwar das Verhalten eines Gegenstandes ändern, sodass ein Feuerschwert, das alles in Brand setzt, oder ein Teleportationsbogen, der dich schnell von A nach B bringt, kein Problem sind, nicht aber das Aussehen oder die Form von Gegenständen.
13.1 Rezepte festlegen Bevor du ein neues Crafting-Rezept anlegen kannst, musst du zunächst entscheiden, ob es sich dabei um ein Rezept handeln soll, bei dem die Positionen der einzelnen Zutaten berücksichtigt werden oder nicht. Das Rezept für Holzbretter ist ein Beispiel für ein Rezept, bei dem die Positionierung egal ist. Egal in welches Crafting-Feld du das Holz platzierst, du kannst immer Bretter daraus erzeugen. Möchtest du dagegen ein Schwert craften, so musst du alle Bestandteile an die richtige Stelle bringen, da die Position bei diesem Rezept berücksichtigt wird. Etwas einfacher sind Rezepte, die die Positionierung nicht berücksichtigen, ein sogenanntes ShapelessRecipe. In Listing 13.1 kannst du am Beispiel des Steinknopfes sehen, wie solch ein Rezept ohne feste Positionierung festgelegt werden kann. Am Anfang steht zunächst einmal das Endprodukt, in diesem Fall also ein Steinknopf.
Das muss in einem sogenannten ItemStack-Objekt gespeichert werden. Danach wird das neue Rezept angelegt. Beim Initiieren des RezeptObjektes muss das gewünschte Ergebnis, also in diesem Fall die Variable steinknopf, als Parameter übergeben werden. Erst danach können in Zeile 3 die Zutaten des Rezeptes festgelegt werden. In diesem Fall ist das recht einfach, schließlich besteht das Rezept nur aus genau einer Zutat, einem Steinblock. Im Prinzip können aber bis zu neun Zutaten mit dem addIngredient-Befehl hinzugefügt werden. Möchtest du mehrere Zutaten vom gleichen Typ hinzufügen, so kannst du das auch bequem machen, indem du eine Zahl als zusätzlichen Parameter übergibst, also zum Beispiel steinknopfRezept.addIngredient(3, bukkit.Material.STONE), um dem Rezept drei Steine als Zutat hinzuzufügen. 1 2 3 4
steinknopf = ItemStack(bukkit.Material.STONE_BUTTON)
steinknopfRezept = ShapelessRecipe(steinknopf)
steinknopfRezept.addIngredient(bukkit.Material.STONE)
self.getServer().addRecipe(steinknopfRezept)
Listing 13.1: Steinknopf-Rezept
Das Rezept ist damit zwar im Prinzip schon fertig, bevor es aber auch tatsächlich von den Spielern genutzt werden kann, muss es dem Server noch bekannt gemacht werden. Dafür sorgt der Befehl in der letzten Zeile, der dem Server das übergebene Rezept hinzufügt. Etwas komplexer ist das Erstellen von Rezepten, bei denen die Position der einzelnen Zutaten fest ist, schließlich müssen hier nicht nur die Zutaten, sondern auch ihre Positionen angegeben werden. Ein Beispiel für ein solches Rezept, das du mit Sicherheit kennst, ist das in Abbildung 13.1 gezeigte Angel-Rezept. Nur wenn die drei Stöcke und die beiden Fäden exakt an den dargestellten Positionen sind, kann eine Angel entstehen.
Abb. 13.1: Angel-Rezept
Wie ein solches sogenanntes ShapedRecipe, also geformtes Rezept, erstellt werden kann, kannst du in Listing 13.2 sehen. Auch hier steht am Anfang zunächst einmal wieder das gewünschte Ergebnis, in diesem Fall also die Angel. Darauf folgt wieder die Initiierung des Rezeptes, diesmal natürlich als ShapedRecipe. Danach wird es allerdings anders: Statt die Zutaten festzulegen, wird hier zunächst mit der shape-Funktion die Form des Rezeptes festgelegt. 1 2 3 4 5 6
angel = ItemStack(bukkit.Material.FISHING_ROD)
angelRezept = ShapedRecipe(angel)
angelRezept.shape(" /", " /*", "/ *")
angelRezept.setIngredient('/', bukkit.Material.STICK)
angelRezept.setIngredient('*', bukkit.Material.STRING)
self.getServer().addRecipe(angelRezept)
Listing 13.2: Angel-Rezept
Auf den ersten Blick sehen die drei Strings, die hinter dem Befehl in Klammern stehen, vielleicht etwas kryptisch aus, eigentlich sind sie aber ganz einfach zu verstehen. Jeder der drei Strings steht für eine Reihe im Crafting-Feld. Die Strings bestehen selbst wieder jeweils aus genau drei Zeichen, wobei jedes Zeichen für ein Kästchen steht. Wenn man die drei Strings wie in Tabelle 13.1 einmal geordnet untereinander schreibt, dann hat das schon sehr viel Ähnlichkeit mit
dem Rezept der Angel in Abbildung 13.1. Jedes Leerzeichen im String steht für ein leeres Kästchen im Rezept; tauchen an zwei Stellen dieselben Zeichen im String auf, so müssen im Rezept dort später auch dieselben Zutaten stehen.
/
/
/ * *
Tabelle 13.1: Form des Angel-Rezeptes
Welches Zeichen du für welche Zutat verwendest, ist dabei egal, denn die Zuordnung kannst du selbst festlegen. So wird in Listing 13.2 in Zeile 4 festgelegt, dass das Zeichen / für einen Stock steht, und in Zeile 5, dass das Zeichen * für einen Faden steht. Genauso gut hätte man die Zuordnung auch umdrehen oder ganz andere Zeichen verwenden können, wichtig ist nur, dass für gleiche Zutaten auch gleiche Zeichen verwendet werden. Zum Schluss muss auch hier, das kennst du bereits, das Rezept noch an den Server übergeben werden, fertig ist das Angel-Rezept. Neben Rezepten ohne feste Positionierung und den Rezepten, die, wie die Angel, eine komplett feste Positionierung benötigen, gibt es auch Rezepte, die eine gewisse Variation zulassen. Ein Beispiel dafür ist das Fackel-Rezept, dessen verschiedene Variationen du in Abbildung 13.2 sehen kannst.
Abb. 13.2: Mögliche Variationen des Fackel-Rezeptes
Die Position ist hier nicht absolut festgelegt, sondern nur relativ zwischen den Zutaten: Die Kohle muss über dem Stock platziert werden, wo Kohle und Stock innerhalb des Crafting-Feldes platziert werden, ist aber egal. Wie ein solches Rezept als Programmcode ausgedrückt werden kann, kannst du in Listing 13.3 sehen. Auch hier wird wieder ein ShapedRecipe verwendet, es gibt nun aber nur noch zwei Strings und die bestehen auch jeweils nur noch aus einem Zeichen. So weiß der Server, dass die beiden Zutaten zwar übereinander platziert werden müssen, die Positionierung ansonsten aber egal ist. 1 2 3 4 5 6
fackel = ItemStack(bukkit.Material.TORCH)
fackelRezept = ShapedRecipe(fackel)
fackelRezept.shape("*", "/")
fackelRezept.setIngredient('/', bukkit.Material.STICK)
fackelRezept.setIngredient('*', bukkit.Material.COAL)
self.getServer().addRecipe(fackelRezept)
Listing 13.3: Fackel-Rezept
13.2 Eigene Rezepte entwerfen Nun hast du in diesem Kapitel zwar bereits einige Rezepte erstellt und das Funktionsprinzip hoffentlich verstanden, wenn du aber ein Plugin erstellen würdest, dass diese Rezepte tatsächlich dem Spiel hinzufügt, dann wäre das ziemlich unspektakulär, schließlich gibt es die Rezepte schon und es würde sich daher überhaupt nichts im Spiel ändern. Höchste Zeit also, ein wirklich eigenes Rezept zu entwerfen und es dem Spiel wirklich hinzuzufügen. Zu Beginn soll es wieder etwas Einfaches sein, auch wenn das Rezept, das du in Abbildung 13.3 sehen kannst, biologisch nicht ganz korrekt ist. Aus einem Ei soll eine rote Blume entstehen. Dabei handelt es sich, das dürfte klar sein, um ein Rezepte ohne feste Positionierung.
Abb. 13.3: Blumen-Rezept
Nachdem du für dein neues Blumen-Rezept-Plugin einen Ordner und eine .yml-Datei erstellt hast, kann es auch gleich schon an das Plugin selbst gehen, das du in Listing 13.4 findest. Und wie du siehst, siehst du dort nicht viel. Mit gerade einmal elf Zeilen ist das Plugin sehr übersichtlich und bis auf die beiden neuen Importe in den ersten beiden Zeilen, die du für die Verwendung des ShapelessRecipe und des ItemStack benötigst, sollte dir das alles auch schon sehr bekannt vorkommen. 1 2 3 4 5 6 7 8 9 10 11
from org.bukkit.inventory import ItemStack
from org.bukkit.inventory import ShapelessRecipe
class BlumenRezeptPlugin(PythonPlugin):
def onEnable(self):
blume = ItemStack(bukkit.Material.RED_ROSE)
blumeRezept = ShapelessRecipe(blume)
blumeRezept.addIngredient(bukkit.Material.EGG)
self.getServer().addRecipe(blumeRezept)
pass
Listing 13.4: Tulpen-Rezept-Plugin
13.3 Feuerschwert Aber wir wollten ja auch ganz bewusst einfach starten. Richtig interessant wird das Arbeiten mit eigenen Rezepten, wenn dabei
auch ein neuer Gegenstand entsteht, den es so bisher im Spiel noch nicht gab. Wie wäre es zum Beispiel mit einem echten Feuerschwert? Ein Eisenschwert und ein Feuerzeug sollen zukünftig eine Waffe ergeben, die jeden Block in Brand setzt, auf den sie trifft. Der erste Schritt ist natürlich die Erstellung eines entsprechenden Rezeptes. Anders als bisher geht es dabei aber nicht nur darum, die Zutaten festzulegen, diesmal soll vor allem der Gegenstand, der als Ergebnis des Rezeptes erstellt wird, verändert werden. Wie genau, kannst du in Listing 13.5 sehen. Zunächst einmal erhält das Schwert einen neuen Namen: Dazu werden in Zeile 9 zuerst die sogenannten Meta-Daten des Schwertes geladen. In den Meta-Daten werden zusätzliche Informationen zu Gegenständen gespeichert, wie zum Beispiel deren Name, die angewendeten Verzauberungen und andere besondere Fähigkeiten. Mit dem Befehl setDisplayNa me kann der ursprüngliche Name dann in einen beliebigen anderen geändert werden, zum Beispiel »Feuerschwert«. Die so geänderten MetaDaten können dann wieder dem Schwert zugewiesen werden. Damit man auch im Inventar sofort erkennen kann, dass es sich bei diesem Schwert um ein ganz besonderes Exemplar handelt, soll es außerdem noch das typische »Glitzern« eines verzauberten Gegenstandes erhalten. Da es nur um das Aussehen und nicht um den eigentlichen Effekt der Verzauberung geht, ist es am sinnvollsten, eine Verzauberung zu wählen, die normalerweise nicht auf ein Schwert angewendet werden kann, wie zum Beispiel »Glück des Meeres«. So kannst du ganz sicher sein, dass das Ergebnis deines Rezeptes garantiert einzigartig ist. Der Befehl zum Hinzufügen einer Verzauberung zu einem Gegenstand lautet addUnsafeEnchantm ent , wie du in Zeile 12 sehen kannst. Das Unsafe im Befehl sorgt dabei dafür, dass der Server ignoriert, dass diese Verzauberung eigentlich nicht für Schwerter gedacht ist, die Zahl legt die Stufe der Verzauberung fest. Der Rest des Rezeptes besteht dann nur noch aus dem bereits bekannten Hinzufügen der Zutaten und der Übergabe des Rezeptes an den Server. 1 2
from org.bukkit.inventory import ItemStack
from org.bukkit.inventory import ShapelessRecipe
3 from org.bukkit.enchantments import Enchantment
4 from org.bukkit.inventory.meta import ItemMeta
5
6 class FeuerschwertPlugin(PythonPlugin):
7 def onEnable(self):
8 feuerschwert = ItemStack(bukkit.Material.IRON_SWORD)
9 metaData = feuerschwert.getItemMeta()
10 metaData.setDisplayName("Feuerschwert")
11 feuerschwert.setItemMeta(metaData)
12 feuerschwert.addUnsafeEnchantment(Enchantment.LUCK, 1)
13
14 feuerschwertRezept = ShapelessRecipe(feuerschwert)
15 feuerschwertRezept.addIngredient(bukkit.Material.IRON_SWORD)
16 feuerschwertRezept.addIngredient(bukkit.Material.FLINT_AND_ST EEL)
17 self.getServer().addRecipe(feuerschwertRezept)
18
19 pass
Listing 13.5: Feuerschwert-Plugin Schritt 1
Nun hast du zwar bereits einen garantiert einmaligen Gegenstand, er verhält sich allerdings noch wie ein ganz normales Schwert aus Eisen, von Feuer gibt es noch keine Spur. Um es hinzuzufügen, kannst du die Listener einsetzen, die du bereits im vorletzten Kapitel kennengelernt hast. Dafür muss das Plugin natürlich zunächst, wie in Listing 13.6 zu sehen, an das dir bereits bekannte Grundgerüst für Listener-Plugins anpassen. Das bedeutet natürlich zunächst einmal die ListenerKlasse selbst hinzuzufügen, aber auch in der onEnable-Funktion den Listener zu registrieren. 1 2 3 4 5
from from from from from
org.bukkit.inventory import ItemStack
org.bukkit.inventory import ShapelessRecipe
org.bukkit.enchantments import Enchantment
org.bukkit.inventory.meta import ItemMeta
org.bukkit.event.player import PlayerInteractEvent
6 from org.bukkit.block import BlockFace
7
8 class ListenerPlugin(PythonListener):
9 @PythonEventHandler(PlayerInteractEvent, EventPriority.NORMAL)
10 def onEvent(self, event):
11 gegenstand = event.getItem()
12 metaData = gegenstand.getItemMeta()
13
14 if metaData.getDisplayName() == "Feuerschwert":
15 block = event.getClickedBlock()
16 blockH = block.getRelative(BlockFace.UP)
17 blockH.setType(bukkit.Material.FIRE)
18 event.setCancelled(True)
19
20 class FeuerschwertPlugin(PythonPlugin):
21 def onEnable(self):
22 feuerschwert = ItemStack(bukkit.Material.IRON_SWORD)
23 metaData = feuerschwert.getItemMeta()
24 metaData.setDisplayName("Feuerschwert")
25 feuerschwert.setItemMeta(metaData)
26 feuerschwert.addUnsafeEnchantment(Enchantment.LUCK, 1)
27
28 feuerschwertRezept = ShapelessRecipe(feuerschwert)
29 feuerschwertRezept.addIngredient(bukkit.Material.IRON_SWORD)
30 feuerschwertRezept.addIngredient(bukkit.Material.FLINT_AND_ST EEL)
31 self.getServer().addRecipe(feuerschwertRezept)
32
33 pluginManager = self.getServer().getPluginManager()
34 listener = ListenerPlugin()
35 pluginManager.registerEvents(listener, self)
36
37 pass
Listing 13.6: Feuerschwert-Plugin Schritt 2
Für die Listener-Klasse stellt sich zunächst einmal die Frage, welchen Listener du verwenden möchtest. Wie du in Listing 13.6 sehen kannst, fiel die Wahl dort auf das PlayerInteractEvent. Dieser
wird immer dann aufgerufen, wenn der Spieler auf einen Block klickt. Genau das Richtige also. Nun soll aber natürlich nicht immer, wenn der Spieler auf einen Block klickt, alles in Brand gesetzt werden, sondern nur dann, wenn er auch das Feuerschwert in der Hand hält. Glücklicherweise bietet uns das Event mit der Funktion getItem() eine einfache Möglichkeit zu prüfen, welchen Gegenstand der Nutzer gerade in der Hand hält. Statt wie vorher den Anzeigenamen zu setzen, musst du diesmal überprüfen, ob das Schwert den entsprechenden Namen trägt; nur wenn das der Fall ist, soll das Plugin den angeklickten Block in Brand setzen. Ist das der Fall, dann kannst du mit getClickedBlock() ganz einfach auf den angeklickten Block zugreifen. In Brand setzen, das bedeutet, um genau zu sein, den Block über dem angeklickten Block in einen Feuer-Block verwandeln und genau das passiert in den Zeilen 16 und 17. In Zeile 16 wird zunächst der darüber liegende Block in der Variable blockH gespeichert und in Zeile 17 wird dieser dann in Feuer verwandelt. Wichtig ist auch der setCancelled-Befehl in Zeile 18. Dieser Befehl kann für jedes beliebige Event aufgerufen werden. Jedes Event hat ein »normales« Verhalten. Wenn du zum Beispiel mit einem Schwert auf einen Block klickst, so wäre das normale Verhalten in Minecraft, dass dieser Block beschädigt wird. Klickst du mit einem Schwert auf einen Feuer-Block, so wird das Feuer gelöscht. Um einen Block in Brand setzen zu können, musst du dieses Standardverhalten also unterbinden und genau das geschieht, wenn du die setCancelled-Funktion mit der Parameter True aufrufst.
Merke Die Funktion setCancelled unterbindet das Standardverhalten eines Events.
13.4 Enderbogen Eine weitere Idee für einen spannenden neuen Gegenstand, den du dem Spiel hinzufügen könntest, ist der Enderbogen. Er kombiniert die besondere Fähigkeit der Enderperle, den Spieler zu teleportieren, mit der Reichweite des Bogens. Sein Rezept könnte zum Beispiel wie in Abbildung 13.4 aussehen.
Abb. 13.4: Enderbogen-Rezept
Alternativ könnte dein Rezept aber auch aus einem Bogen und einer einzelnen Enderperle bestehen, oder aus jeder beliebigen anderen Kombination, die dir gefällt. Der erste Schritt zur Realisierung des Enderbogens ist wieder das Hinzufügen des Rezeptes zum Server, wie in Listing 13.7 gezeigt. Auch hier wird die Verzauberung »Glück des Meeres« eingesetzt, um den Gegenstand von seiner normalen Version zu unterscheiden. 1 2 3 4 5 6 7
from org.bukkit.inventory import ItemStack
from org.bukkit.inventory import ShapedRecipe
from org.bukkit.enchantments import Enchantment
from org.bukkit.inventory.meta import ItemMeta
class EnderbogenPlugin(PythonPlugin):
def onEnable(self):
8 enderbogen = ItemStack(bukkit.Material.BOW)
9 metaData = enderbogen.getItemMeta()
10 metaData.setDisplayName("Enderbogen")
11 enderbogen.setItemMeta(metaData)
12 enderbogen.addUnsafeEnchantment(Enchantment.LUCK, 1)
13
14 enderbogenRezept = ShapedRecipe(enderbogen)
15 enderbogenRezept.shape("*S/", "S*/", "*S/")
16 enderbogenRezept.setIngredient('/', bukkit.Material.STICK)
17 enderbogenRezept.setIngredient('S', bukkit.Material.STRING)
18 enderbogenRezept.setIngredient('*', bukkit.Material.ENDER_PEARL)
19 self.getServer().addRecipe(enderbogenRezept)
20
21 pass
Listing 13.7: Enderbogen-Plugin Schritt 1
Im zweiten Schritt geht es dann wieder darum, dem neu geschaffenen Gegenstand die angedachte Funktionalität zu verleihen. Dafür ist ebenfalls wieder der Einsatz eines Listeners nötig, diesmal, wie du in Listing 13.8 sehen kannst, mit dem ProjectileHitEven t, der immer dann aufgerufen wird, wenn ein Geschoss sein Ziel trifft. 1 from org.bukkit.inventory import ItemStack
2 from org.bukkit.inventory import ShapedRecipe
3 from org.bukkit.enchantments import Enchantment
4 from org.bukkit.inventory.meta import ItemMeta
5 from org.bukkit.event.entity import ProjectileHitEvent
6 from org.bukkit.entity import EntityType
7 from org.bukkit.entity import Player
8
9
10 class ListenerPlugin(PythonListener):
11 @PythonEventHandler(ProjectileHitEvent, EventPriority.NORMAL)
12 def onEvent(self, event):
13 geschoss = event.getEntity()
14
15 if geschoss.getType() == EntityType.ARROW:
16 schuetze = geschoss.getShooter()
17 if isinstance(schuetze, Player):
18 gegenstand = schuetze.getItemInHand()
19 metaData = gegenstand.getItemMeta()
20 if metaData.getDisplayName()== "Enderbogen":
21 schuetze.teleport(geschoss.getLocation())
22
23
24 class EnderbogenPlugin(PythonPlugin):
25 def onEnable(self):
26 enderbogen = ItemStack(bukkit.Material.BOW)
27 metaData = enderbogen.getItemMeta()
28 metaData.setDisplayName("Enderbogen")
29 enderbogen.setItemMeta(metaData)
30 enderbogen.addUnsafeEnchantment(Enchantment.LUCK, 1)
31
32 enderbogenRezept = ShapedRecipe(enderbogen)
33 enderbogenRezept.shape("*S/", "S*/", "*S/")
34 enderbogenRezept.setIngredient('/', bukkit.Material.STICK)
35 enderbogenRezept.setIngredient('S', bukkit.Material.STRING)
36 enderbogenRezept.setIngredient('*', bukkit.Material.ENDER_PEARL)
37 self.getServer().addRecipe(enderbogenRezept)
38
39 pluginManager = self.getServer().getPluginManager()
40 listener = ListenerPlugin()
41 pluginManager.registerEvents(listener, self)
42
43 pass
Listing 13.8: Enderbogen-Plugin Schritt 2
Wenn das entsprechende Event ausgelöst wird, musst du zunächst einmal prüfen, ob es sich beim geworfenen bzw. geschossenen Objekt überhaupt um einen Pfeil handelt – und nicht etwa um einen Schneeball. Glücklicherweise wird das Geschoss vom Event übergeben, du kannst darauf mit getEntity zugreifen. Danach musst du nur noch den Typ des Geschosses überprüfen, wie es in Zeile 15 zu sehen ist.
Handelt es sich tatsächlich um einen Pfeil, muss im nächsten Schritt geprüft werden, ob der Pfeil überhaupt von einem Spieler abgeschossen wurde – und nicht etwa von einem Skelett. Mit dem Befehl getShooter kannst du auf die Kreatur zugreifen, die den Pfeil abgeschossen hat, und mit isinstanceanschließend überprüfen, ob es sich dabei um einen Spieler handelt. Ist auch diese zweite Bedingung erfüllt, muss im dritten und letzten Schritt, wie bereits zuvor beim Feuerschwert-Plugin, noch geprüft werden, ob auch tatsächlich der neue Gegenstand benutzt wurde, in diesem Fall also der Enderbogen. Das funktioniert wieder über die Überprüfung des Namens. Ist auch diese letzte Bedingung erfüllt, kann der Spieler ganz einfach mit dem bereits bekannten teleportBefehl an die Stelle teleportiert werden, an der der Pfeil eingeschlagen ist. Wenn du den Enderbogen benutzt, wird dir schnell auffallen, dass er eine mächtige Erweiterung für Minecraft ist, die gerade in gebirgigen Landschaften sehr nützlich sein kann, um schnell von A nach B zu gelangen. Die beiden in diesem Kapitel vorgestellten Gegenstände sind nur zwei Beispiele, wie du trotz der Einschränkungen mit eigenen Crafting-Rezepten das Spiel erweitern kannst. Und dir fallen garantiert noch viele weitere Möglichkeiten ein. Mit deinem bisherigen Wissen sollte es kein Problem sein, diese Ideen Wirklichkeit werden zu lassen!
Informationen dauerhaft speichern Kapitel 14
Bei der Programmierung des Schilder-Plugins bist du bereits auf einen Nachteil gestoßen, den der Einsatz von Variablen bringt: Wird der Server einmal gestoppt, gehen alle in ihnen gespeicherten Informationen verloren. Für viele Plugins stellt das kein Problem dar, für einige aber, wie zum Beispiel das Schilder-Plugin, bedeutet das, dass nach dem Neustart des Servers wichtige Informationen fehlen. Aus diesem Grund werden wir in diesem Kapitel verschiedene Möglichkeiten kennenlernen, Informationen dauerhaft zu speichern, sodass sie auch nach einem Neustart des Servers noch zur Verfügung stehen.
14.1 Konfigurationsdateien Beim Einrichten des Servers bist du bereits mit Konfigurationsdateienin Berührung gekommen. Es handelt sich dabei um einfache Textdateien, die hauptsächlich zum Speichern von Einstellungen verwendet werden. Zu ihren Vorteilen gehört, dass sie einfach einsetzbar sind und auch von Hand außerhalb des Spiels geändert werden können. Sie eignen sich hauptsächlich für Daten, die zwar häufig gelesen, aber eher selten geändert werden müssen. Für große Datenmengen sind sie dagegen weniger gut geeignet.
14.1.1 Lesen
Auch jedes Plugin, das du erstellst, besitzt eine eigene Konfigurations-Datei, die plugin.yml. Auch das nächste Plugin, das wir erstellen, beginnt zunächst mit einer solchen KonfigurationsDatei, die du in Listing 14.1 sehen kannst. Wie üblich legst du diese in einem neuen Ordner in deinem Plugin-Ordner ab. Dieser kann zum Beispiel den Namen config.py.dir tragen. 1 2 3
name: Config-Plugin
main: ConfigPlugin
version: 1.0
Listing 14.1: plugin.yml
Anders als sonst erstellen wir dieses Mal in diesem Ordner aber noch eine zweite Konfigurationsdatei, sie trägt den Namen config.yml und ihren Inhalt kannst du in Listing 14.2 sehen. nachricht: Hallo Welt!
Listing 14.2: config.yml
Nun fehlt noch das eigentliche Plugin. Das wird wie immer in der Datei plugin.py gespeichert und seinen Inhalt kannst du in Listing 14.3 sehen. Gerade einmal eine Zeile, nämlich Zeile 3, benötigst du, um einen Wert aus der Konfigurationsdatei config.yml zu lesen. Mit self.getConfig() erhältst du zunächst Zugriff auf die Datei, mit getString signalisierst du deinem Plugin dann, dass du einen String aus der Konfigurationsdatei auslesen möchtest. Nun musst du nur noch den Namen angeben, unter dem der Wert gespeichert wurde, in diesem Fall nachricht, und schon kann der Wert in einer Variable gespeichert und danach auf der Server-Konsole ausgegeben werden. 1 class ConfigPlugin(PythonPlugin):
2 def onEnable(self):
3 nachricht = self.getConfig().getString("nachricht")
4 5
print(nachricht)
pass
Listing 14.3: Config-Plugin
Auf diese Weise kannst du nicht nur Strings, sondern auch andere Datentypen aus der config.yml auslesen. In Listing 14.4 findest du eine erweiterte Version der config.yml, die nun auch weitere Datentypen beinhaltet wie Kommazahlen und Wahrheitswerte. Außerdem kannst du dort auch sehen, dass es möglich ist, Einträge durch Einrücken zu gruppieren, wie zahl1 und zahl2, die zur Gruppe zahlen zusammengefasst wurden. 1 2 3 4 5 6 7
nachricht: Hallo Welt!
version: 1.2
debug: True
zahlen:
zahl1: 42
zahl2: 84
array: ["Hallo", "Welt"]
Listing 14.4: config.yml mit verschiedenen Datentypen
Wie du auf die entsprechenden Einträge dann innerhalb eines Plugins zugreifen kannst, das kannst du in Listing 14.5 sehen. Kommazahlen können mit dem Befehl getDouble eingelesen werden, Wahrheitswerte mit getBoolean und Ganzzahlen mit getInt. Auf Werte einer Gruppe kannst du zugreifen, indem du den Namen der Gruppe, abgetrennt durch einen Punkt, vor den Namen des Eintrags setzt, also zum Beispiel zahlen.zahl2. Auch Arrays, also Listen mit mehreren Einträgen vom selben Typ, können in Konfigurationsdateien gespeichert werden. Ihre Werte können mit getList abgerufen werden. 1 class ConfigPlugin(PythonPlugin):
2 def onEnable(self):
3 nachricht = self.getConfig().getString("nachricht")
4 print(nachricht)
5
6 7 8 9 10 11 12 13 14 15 16 17 18
version = self.getConfig().getDouble("version")
print(version)
debug = self.getConfig().getBoolean("debug")
print(debug)
zahl1 = self.getConfig().getInt("zahlen.zahl1")
print(zahl1)
array = self.getConfig().getList("array")
print(array)
pass
Listing 14.5: Config-Plugin Version 2
Mit dem Befehl self.getConfig().getKeys(True) kannst du dir darüber hinaus auch die Namen aller vorhandenen Werte abrufen, für die Konfigurationsdatei in Listing 14.4 würde das folgendem Array entsprechen: ["nachricht", "version", "debug", "zahlen", "zahlen.zahl1", "zahlen.zahl2", "array"]. Wie du siehst, kannst du mit Konfigurationsdateien sehr einfach Werte auslesen, die von Hand in die Datei eingetragen wurden. Das ermöglicht es dir oder deinen Freunden, Plugins anzupassen, ohne programmieren zu können oder sich deinen Code anschauen zu müssen.
14.1.2 Schreiben Unserem Ziel, das wir in diesem Kapitel verfolgen, nämlich Informationen aus dem Plugin dauerhaft zu speichern, hat uns das aber noch nicht näher gebracht. Zwar können wir Informationen aus einer Konfigurationsdatei in das Plugin laden, bisher können wir aber noch keine Informationen aus dem Plugin abspeichern. Darum kümmern wir uns nun in diesem Abschnitt. Als einfaches Beispiel dient dabei ein Plugin, das zählt, wie oft der Server gestartet wird. Nur mit Variablen wäre ein solches Plugin
nicht umsetzbar, schließlich verlieren diese bei jedem Start des Servers ihr »Gedächtnis«. Mit Konfigurationsdateien lässt sich ein solches Plugin dagegen relativ einfach umsetzen. Den Anfang macht wie immer die plugin.yml, die du in Listing 14.6 sehen kannst. 1 2 3
name: Zaehl-Plugin
main: ZaehlPlugin
version: 1.0
Listing 14.6: plugin.yml
Wie schon beim vorherigen Plugin erstellen wir auch dieses Mal wieder im Plugin-Ordner eine Datei mit dem Namen config.yml. Sie besteht, wie du in Listing 14.7 sehen kannst, aus einem Eintrag mit dem Namen zaehler, dessen Startwert 0 ist. zaehler: 0
Listing 14.7: config.yml
Konfigurationsdateien im Plugin-Ordner enthalten immer die Standard- oder Startwerte einer Konfiguration und können nicht geändert werden. Wenn du also Werte speichern möchtest, musst du zunächst eine Kopie dieser Start-Konfiguration anlegen. Wie das funktioniert, kannst du in Listing 14.8 sehen. 1 2 3 4
class ZaehlPlugin(PythonPlugin):
def onEnable(self):
self.saveResource("config.yml", False)
pass
Listing 14.8: Kopieren der Standardkonfiguration
Wenn du das Plugin auf diesem Stand ausführst, dann wirst du beobachten können, dass es in deinem Plugin-Ordner einen neuen Ordner mit dem Namen »Zaehl-Plugin« anlegen wird, das ist der
sogenannte Daten-Ordner deines Plugins. In diesem Daten-Ordner findest du eine Kopie der config.yml. Dafür sorgt natürlich der saveResource-Befehl, der eine Datei aus dem Ordner deines Plugins in den Daten-Ordner des Plugins kopiert, wo die Datei dann geändert werden kann. Neben dem Namen der Datei, die kopiert werden soll, in diesem Fall also config.yml, musst du dazu noch angeben, wie sich das Plugin verhalten soll, falls die Datei schon kopiert wurde. Ein True als zweiter Parameter sorgt dafür, dass die Datei bei jedem Start wieder mit den Standard-Konfigurationen überschrieben wird, der Zähler würde also bei jedem Start wieder auf 0 zurückgesetzt. Das ist für das Zähl-Plugin natürlich nicht sinnvoll. Deshalb sorgt das False als Parameter dafür, dass die Datei nur beim ersten Mal kopiert wird, wenn sie im Daten-Ordner noch nicht existiert. In Listing 14.9 kannst du sehen, wie du aus dieser kopierten Konfigurationsdatei nun Werte auslesen kannst. Dazu musst du zunächst einmal in Zeile 8 den Pfad zur Datei angeben. Dieser besteht aus dem Pfad zum Daten-Ordner, den der Befehl getDataFolder liefert, und dem Dateinamen der Konfigurationsdatei. Mit diesem Pfad und dem Befehl kannst du die Konfigurationsdatei dann laden und in einer Variable speichern, hier config. Mit dieser Variable kannst du nun, wie du es vorher schon kennengelernt hast, also zum Beispiel mit getString, getInt und so weiter, auf die vorhandenen Werte zugreifen. YamlConfiguration.loadConfiguration
In Zeile 11 wird genau das getan und der aus der Konfigurationsdatei ausgelesene Wert um eins erhöht, um ihn anschließend in der Zeile darunter auszugeben. 1 from org.bukkit.configuration.file import YamlConfiguration
2 from java.io import File
3
4 class ZaehlPlugin(PythonPlugin):
5 def onEnable(self):
6 self.saveResource("config.yml", False)
7
8 datei = File(self.getDataFolder(), "config.yml")
9 config = YamlConfiguration.loadConfiguration(datei)
10
11 zaehler = config.getInt("zaehler") + 1
12 print("Der Server wird zum " + str(zaehler) +"ten Mal gestartet")
13
14 pass
Listing 14.9: Lesen der kopierten Konfigurationsdatei
Wenn du das Plugin nun ausführst, zeigt es bei jedem Start an, dass der Server gerade zum ersten Mal gestartet wird. Kein Wunder, schließlich wird die um 1 erhöhte Zählervariable bisher noch nicht in der Konfigurationsdatei gespeichert. Das ändert sich aber in Listing 14.10 und wie du sehen kannst, werden dafür gerade einmal zwei Zeilen benötigt. In Zeile 14 wird der neue Wert gesetzt. Dies geschieht, anders als beim Lesen, unabhängig vom Typ mit dem set-Befehl, dem als Parameter der Name des Eintrags und der neue Wert übergeben werden. Würdest du nun noch einmal config.getInt("zaehler") aufrufen, würdest du zwar schon den neuen Wert zurückbekommen, trotzdem ist die Änderung bisher nur in deinem Plugin gültig und wurde noch nicht in die Konfigurationsdatei geschrieben. Erst durch das Aufrufen des save-Befehl, dem wieder die Datei als Parameter übergeben wird, wird die Änderung auch endgültig in die Konfigurationsdatei geschrieben. Nun erhöht sich der Zähler bei jedem Start deines Servers automatisch um 1. 1 from org.bukkit.configuration.file import YamlConfiguration
2 from java.io import File
3
4 class ZaehlPlugin(PythonPlugin):
5 def onEnable(self):
6 self.saveResource("config.yml", False)
7
8 datei = File(self.getDataFolder(), "config.yml")
9 config = YamlConfiguration.loadConfiguration(datei)
10
11 zaehler = config.getInt("zaehler") + 1
12 print("Der Server wird zum " + str(zaehler) +"ten Mal gestartet")
13
14 config.set("zaehler", zaehler)
15 config.save(datei)
16
17 pass
Listing 14.10: Schreiben einer Konfigurationsdatei
Merke Eine Änderung in der Konfiguration wird erst durch den Aufruf der save-Methode auch in der dazugehörigen Konfigurationsdatei gespeichert.
Mit dem set-Befehl kannst du einer Konfigurationsdatei auch neue Einträge hinzufügen, denn nicht vorhandene Einträge, denen ein Wert zugewiesen wird, werden automatisch zur Datei hinzugefügt. Mithilfe der Punkt-Notation, also zum Beispiel set("zahlen.zahl1", 42), können auf diesem Wege auch Elemente gruppiert werden.
14.2 Objekte in Dateien speichern Wenn du größere Datenmengen speichern willst, sind Konfigurationsdateien, wie bereits erwähnt, keine besonders gute Lösung. Wenn du also zum Beispiel die Informationen des SchilderPlugins dauerhaft speichern möchtest, solltest du auf eine andere Methode ausweichen.
In Listing 14.12 siehst du noch einmal die letzte Version des Schilder-Plugins aus Kapitel 10. Damit das Plugin auch nach einem Neustart des Servers noch weiter die zuvor platzierten Schilder verwalten kann, müsste sowohl die Liste der Schilder als auch die Liste der Spieler gespeichert werden. 1 from org.bukkit.entity import Player
2 from org.bukkit.block import BlockFace
3
4 class SchilderPlugin(PythonPlugin):
5 schilderListe = None
6 spielerListe = None
7
8 def onEnable(self):
9 global schilderListe
10 global spielerListe
11 schilderListe = []
12 spielerListe = []
13 pass
14
15 def onCommand(self, sender, command, label, args):
16 global schilderListe
17 global spielerListe
18
19 if isinstance(sender, Player):
20 spieler = sender
21
22 if len(args) < 1:
23 spieler.sendMessage("Bitte Schild-Befehl angeben!")
24 elif args[0] == "bauen":
25 welt = spieler.getWorld()
26
27 #Schild platziert
28 finalBlock = spieler.getLocation()
29 blockListe = spieler.getLineOfSight(None, 30)
30
31 for block in blockListe:
32 if block.getType() == bukkit.Material.AIR:
33 finalBlock = block
34 else:
35 break
36
37 finalBlock.setType(bukkit.Material.OAK_SIGN)
38
39 #Rotation
40 rotationen = [BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST, BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST]
41
42 rotation = spieler.getLocation().getYaw()
43 rotation = (rotation + 180) % 360 #entgegengesetzte Richtung
44 rotation = rotation / 22 #umwandeln fuer Schild
45
46 schild = finalBlock.getState()
47 schildDaten = schild.getData()
48 schildDaten.setFacingDirection(rotationen[int(rotation)])
49 schild.setData(schildDaten)
50 schild.update()
51
52 #Schild zu Liste hinzufuegen
53 schilderListe.append(schild)
54 spielerListe.append(spieler.getUniqueId())
55 spieler.sendMessage("Das neu gebaute Schuld hat die ID " + str(schilderListe.index(schild)))
56 elif args[0] == "text":
57 if len(args) < 4:
58 spieler.sendMessage("Bitte Schild-ID, Zeile und Text angeben!")
59 else:
60 #parameter lesen
61 schildId = int(args[1])
62 zeile = int(args[2])
63 text = args[3]
64 for i in range(4, len(args)):
65 text = text + " " + args[i]
66
67 #ID pruefen
68 if (schildId < 0) | (schildId >= len(schilderListe)):
69 spieler.sendMessage("Falsche Schild-ID!")
70 #spieler pruefen
71 elif spielerListe[schildId] != spieler.getUniqueId():
72 spieler.sendMessage("Dieses Schild gehoert einem anderen Spieler!")
73 #Text zuweisen
74 else:
75 schilderListe[schildId].setLine(zeile, text)
76 schilderListe[schildId].update()
77 else:
78 spieler.sendMessage("Unbekannter SchildBefehl!")
79 else:
80 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
81
82 return True
Listing 14.11: Letzte Version des Schilder-Plugins
Statt die Daten aber einfach so zu speichern, solltest du zunächst einmal überlegen, ob es für das Plugin nicht generell noch Verbesserungspotenzial gibt, schließlich hast du seit dem zehnten Kapitel einiges dazugelernt. Dass die Schilder und deren Besitzer in zwei getrennten Listen gespeichert werden, ist alles andere als optimal und macht dein Plugin potenziell fehleranfällig, wenn zum Beispiel aus Versehen nur eine der beiden Listen aktualisiert wird. Diese Schwachstelle kannst du beseitigen, indem du dein Wissen aus Kapitel 12 anwendest und eine eigene Klasse zum Verwalten eines Schildes inklusive Besitzer erstellst. Außerdem müssen die aufgestellten Schilder etwas anders verwaltet werden, damit sie später in einer Datei gespeichert werden können. Statt einfach nur das Schild-Objekt zu speichern, wird zukünftig die Position des Schildes gespeichert. In Listing 14.12 findest du die Klasse SchilderEintrag, die genau diese Aufgabe übernimmt. Dadurch, dass die beiden Variablen __schild und __besitzer privat sind und nur beim Erzeugen eines neuen Objektes gesetzt werden können, sorgt die Klasse dafür, dass ein Schild und sein Besitzer
untrennbar miteinander verbunden sind. Du kannst die Klasse entweder in einer eigenen Datei in deinem libs Ordner speichern oder sie gleich dem Schilder-Plugin hinzufügen. Da die Klasse vermutlich nur in diesem einen Plugin verwendet wird und dazu auch noch recht kurz ist, bietet es sich an, sie gleich im Schilder-Plugin hinzuzufügen. 1 class SchilderEintrag:
2 def __init__(self, schild, besitzer):
3 self.__schild = {"x": int(schild.getLocation().getX()), "y": int(schild.getLocation().getY()), "z": int(schild.getLocation().getZ())}
4 self.__besitzer = str(besitzer)
5
6 def getSchild(self, welt):
7 return welt.getBlockAt(self.__schild.get("x"), self.__schild.get("y"), self.__schild.get("z")).getState()
8
9 def getBesitzer(self):
10 return self.__besitzer
Listing 14.12: SchilderEintrag-Klasse
Bevor du die neue Klasse verwenden kannst, um die Informationen zu speichern, musst du das Plugin selbst natürlich auch noch modifizieren. Wie, das kannst du in Listing 14.15 sehen. Neben der Tatsache, dass die Klasse SchilderEintrag jetzt Teil des Plugins ist, fällt gleich zu Beginn in Zeile 16 auch auf, dass es hier nun nur noch eine Liste gibt, was ja gerade das Ziel des Umbaus ist. Entsprechend entfällt auch die Initialisierung der ehemaligen SpielerListe in der onEnable-Funktion. In der onCommand-Funktion sind dann nur noch wenige kleine Änderungen nötig, um das Plugin wieder zum Laufen zu bekommen: Zunächst einmal wird die Zuweisung der welt-Variable in Zeile 28 vorgezogen, da du diese nun zukünftig auch brauchst, um den Text eines Schildes zu ändern. Beim Bauen eines neuen Schildes wird in Zeile 60 ein neues SchilderEintrag-Objekt erzeugt und in Zeile 61 der schilderListe hinzugefügt. Beim Ändern des Textes eines
Schildes muss der Besitzer nun in Zeile 78 über die getBesitzerFunktion abgerufen werden. Außerdem wird der Befehl spieler.getUniqueId() mit der Funktion str umklammert, da wir die ID in der Liste nun als String speichern und nicht mehr als Objekt. Das Schild wird zunächst in Zeile 82 in einer Variable gespeichert und in Zeile 83 und 84 wird dann, wie gehabt, der Text geändert. 1 from org.bukkit.entity import Player
2 from org.bukkit.block import BlockFace
3
4 class SchilderEintrag:
5 def __init__(self, schild, besitzer):
6 self.__schild = {"x": int(schild.getLocation().getX()), "y": int(schild.getLocation().getY()), "z": int(schild.getLocation().getZ())}
7 self.__besitzer = str(besitzer)
8
9 def getSchild(self, welt):
10 return welt.getBlockAt(self.__schild.get("x"), self.__schild.get("y"), self.__schild.get("z")).getState()
11
12 def getBesitzer(self):
13 return self.__besitzer
14
15 class SchilderPlugin(PythonPlugin):
16 schilderListe = None
17
18 def onEnable(self):
19 global schilderListe
20 schilderListe = []
21 pass
22
23 def onCommand(self, sender, command, label, args):
24 global schilderListe
25
26 if isinstance(sender, Player):
27 spieler = sender
28 welt = spieler.getWorld()
29
30 if len(args) < 1:
31 spieler.sendMessage("Bitte Schild-Befehl angeben!")
32 elif args[0] == "bauen":
33
34 #Schild platziert
35 finalBlock = spieler.getLocation()
36 blockListe = spieler.getLineOfSight(None, 30)
37
38 for block in blockListe:
39 if block.getType() == bukkit.Material.AIR:
40 finalBlock = block
41 else:
42 break
43
44 finalBlock.setType(bukkit.Material.OAK_SIGN)
45
46 #Rotation
47 rotationen = [BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST, BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST]
48
49 rotation = spieler.getLocation().getYaw()
50 rotation = (rotation + 180) % 360 #entgegengesetzte Richtung
51 rotation = rotation / 22 #umwandeln fuer Schild
52
53 schild = finalBlock.getState()
54 schildDaten = schild.getData()
55 schildDaten.setFacingDirection(rotationen[int(rotation)])
56 schild.setData(schildDaten)
57 schild.update()
58
59 #Schild zu Liste hinzufuegen
60 eintrag = SchilderEintrag(finalBlock, spieler.getUniqueId())
61 schilderListe.append(eintrag)
62 spieler.sendMessage("Das neu gebaute Schuld hat die ID " + str(schilderListe.index(eintrag)))
63 elif args[0] == "text":
64 if len(args) < 4:
65 spieler.sendMessage("Bitte Schild-ID,
Zeile und Text angeben!")
66 else:
67 #parameter lesen
68 schildId = int(args[1])
69 zeile = int(args[2])
70 text = args[3]
71 for i in range(4, len(args)):
72 text = text + " " + args[i]
73
74 #ID pruefen
75 if (schildId < 0) | (schildId >= len(schilderListe)):
76 spieler.sendMessage("Falsche Schild-ID!")
77 #spieler pruefen
78 elif schilderListe[schildId].getBesitzer() != str(spieler.getUniqueId()):
79 spieler.sendMessage("Dieses Schild gehoert einem anderen Spieler!")
80 #Text zuweisen
81 else:
82 schild = schilderListe[schildId].getSchild(welt)
83 schild.setLine(zeile, text)
84 schild.update()
85 else:
86 spieler.sendMessage("Unbekannter SchildBefehl!")
87 else:
88 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
89
90 return True
Listing 14.13: Neue Version des Schilder-Plugins
Mit dem so geänderten Plugin kannst du dich nun an die eigentliche Aufgabe machen, nämlich das Speichern der Liste. Da die Daten immer verloren gehen, wenn der Server heruntergefahren wird, ist das genau der Moment, an dem du die Informationen speichern solltest. Dafür könntest du entweder Events benutzen, oder, noch bequemer, die onDisable-Funktion. Sie ist das Gegenstück zur onEnable-Funktion und wird immer dann aufgerufen, wenn dein
Plugin abgeschaltet wird. Theoretisch könnte es nämlich auch passieren, dass dein Plugin von Hand abgeschaltet wird, ohne dass der Server neu gestartet wird. Auch dann würden die Daten verloren gehen und deshalb sollten sie auch in diesem Fall gespeichert werden. In Listing 14.14 kannst du die onDisable-Funktion zum Speichern der schilderListe sehen. In Zeile 4 wird zunächst einmal die Länge der schilderListe geprüft und damit auch, ob überhaupt schon ein Schild gebaut wurde – falls nicht, gibt es natürlich auch nichts zu speichern. Während sich um die Verwaltung der Konfigurationsdateien hauptsächlich der Server gekümmert hat, musst du dich um deine eigenen Dateien selbst kümmern, weshalb in Zeile 5 zunächst einmal mit dem exists-Befehl geprüft wird, ob der Daten-Ordner des Plugins überhaupt schon existiert. Ist das nicht der Fall, so wird er in Zeile 6 mit mkdirs angelegt. In Zeile 8 wird dann der Pfad zur Datei festgelegt, in dem die Liste gespeichert werden soll. Dieser besteht aus dem Pfad zum Daten-Ordner und dem Dateinamen inklusive Endung, also zum Beispiel .pkl. In Zeile 9 wird die Datei dann erstellt und der eigentliche Speichervorgang findet in Zeile 10 statt. Mit der PythonKlasse picklebenötigt es dafür nur eine Zeile Code. Dem dumpBefehl wird einfach das Objekt übergeben, das gespeichert werden soll, in diesem Fall die Liste schilderListe, und die Datei, in die das Objekt gespeichert werden soll. 1 def onDisable(self):
2 global schilderListe
3
4 if len(schilderListe) > 0:
5 if not self.getDataFolder().exists():
6 self.getDataFolder().mkdirs()
7
8 pfad = self.getDataFolder().getPath() + "/schilderListe.pkl"
9 datei = open(pfad, "wb")
10 pickle.dump(schilderListe, datei)
Listing 14.14: onDisable-Funktion
Mit dem Speichern der Daten alleine ist es aber noch nicht getan. Beim Starten müssen die gespeicherten Dateien auch wieder eingelesen werden, bevor sie verwendet werden können. Dazu muss die onEnable-Funktion wie in Listing 14.15 gezeigt angepasst werden. Hier wird zunächst einmal in Zeile 5 überprüft, ob die entsprechende Datei existiert. Ist das nicht der Fall, dann wurden offensichtlich noch keine Schilder gebaut und es kann wie zuvor mit einer leeren Liste begonnen werden. Existiert die Datei dagegen, dann wird sie zunächst in Zeile 6 geöffnet und kann dann mit dem einfachen Befehl load unter Angabe der Datei gelesen und in der Variablen schilderListe gespeichert werden. Schon sind alle Daten wieder dort, wo sie hingehören, und du kannst auch nach einem Neustart die Texte deiner Schilder weiterhin problemlos ändern. 1 def onEnable(self):
2 global schilderListe
3 pfad = self.getDataFolder().getPath() + "/schilderListe.pkl"
4
5 if os.path.isfile(pfad):
6 datei = open(pfad, "rb")
7 schilderListe = pickle.load(datei)
8 else:
9 schilderListe = []
10
11 pass
Listing 14.15: onEnable-Funktion
In Listing 14.16 kannst du noch einmal das komplette SchilderPlugin jetzt mit Speicherfunktion sehen. Besonders beachten solltest du dabei die beiden zusätzlichen Importe in den ersten beiden Zeilen, die benötigt werden, damit das Plugin gestartet werden kann. 1 2 3 4 5 6
import os.path
import pickle
from org.bukkit.entity import Player
from org.bukkit.block import BlockFace
class SchilderEintrag:
7 def __init__(self, schild, besitzer):
8 self.__schild = {"x": int(schild.getLocation().getX()), "y": int(schild.getLocation().getY()), "z": int(schild.getLocation().getZ())}
9 self.__besitzer = str(besitzer)
10
11 def getSchild(self, welt):
12 return welt.getBlockAt(self.__schild.get("x"), self.__schild.get("y"), self.__schild.get("z")).getState()
13
14 def getBesitzer(self):
15 return self.__besitzer
16
17 class SchilderPlugin(PythonPlugin):
18 schilderListe = None
19
20 def onEnable(self):
21 global schilderListe
22 pfad = self.getDataFolder().getPath() + "/schilderListe.pkl"
23
24 if os.path.isfile(pfad):
25 datei = open(pfad, "rb")
26 schilderListe = pickle.load(datei)
27 else:
28 schilderListe = []
29
30 pass
31
32 def onDisable(self):
33 global schilderListe
34
35 if len(schilderListe) > 0:
36 if not self.getDataFolder().exists():
37 self.getDataFolder().mkdirs()
38
39 pfad = self.getDataFolder().getPath() + "/schilderListe.pkl"
40 datei = open(pfad, "wb")
41 pickle.dump(schilderListe, datei)
42
43 def onCommand(self, sender, command, label, args):
44 global schilderListe
45
46 if isinstance(sender, Player):
47 spieler = sender
48 welt = spieler.getWorld()
49
50 if len(args) < 1:
51 spieler.sendMessage("Bitte Schild-Befehl angeben!")
52 elif args[0] == "bauen":
53
54 #Schild platziert
55 finalBlock = spieler.getLocation()
56 blockListe = spieler.getLineOfSight(None, 30)
57
58 for block in blockListe:
59 if block.getType() == bukkit.Material.AIR:
60 finalBlock = block
61 else:
62 break
63
64 finalBlock.setType(bukkit.Material.OAK_SIGN)
65
66 #Rotation
67 rotationen = [BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST, BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST, BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST, BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST]
68
69 rotation = spieler.getLocation().getYaw()
70 rotation = (rotation + 180) % 360 #entgegengesetzte Richtung
71 rotation = rotation / 22 #umwandeln fuer Schild
72
73 schild = finalBlock.getState()
74 schildDaten = schild.getData()
75 schildDaten.setFacingDirection(rotationen[int(rotation)])
76 schild.setData(schildDaten)
77 schild.update()
78
79 #Schild zu Liste hinzufuegen
80 eintrag = SchilderEintrag(finalBlock,
spieler.getUniqueId())
81 schilderListe.append(eintrag)
82 spieler.sendMessage("Das neu gebaute Schuld hat die ID " + str(schilderListe.index(eintrag)))
83 elif args[0] == "text":
84 if len(args) < 4:
85 spieler.sendMessage("Bitte SchildID, Zeile und Text angeben!")
86 else:
87 #parameter lesen
88 schildId = int(args[1])
89 zeile = int(args[2])
90 text = args[3]
91 for i in range(4, len(args)):
92 text = text + " " + args[i]
93
94 #ID pruefen
95 if (schildId < 0) | (schildId >= len(schilderListe)):
96 spieler.sendMessage("Falsche Schild-ID!")
97 #spieler pruefen
98 elif schilderListe[schildId].getBesitzer() != str(spieler.getUniqueId()):
99 spieler.sendMessage("Dieses Schild gehoert einem anderen Spieler!")
100 #Text zuweisen
101 else:
102 schild = schilderListe[schildId].getSchild(welt)
103 schild.setLine(zeile, text)
104 schild.update()
105 else:
106 spieler.sendMessage("Unbekannter SchildBefehl!")
107 else:
108 self.getLogger().info("Dieser Befehl kann nur von Spielern ausgefuehrt werden!")
109
110 return True
Listing 14.16: Schilder-Plugin mit Speicherfunktion
Eigene Spielmodi entwickeln Kapitel 15
Menschen, die der Faszination von Minecraft nicht erlegen sind, bemängeln oft, dass es in Minecraft kein vorgegebenes Spielziel gibt. Für viele Fans von Minecraft und kreative Köpfe macht genau das den Reiz des Spieles aus, trotzdem kannst du als PluginEntwickler, wenn du möchtest, eigene Spielmodientwickeln, bei denen der Spieler bestimmte Dinge tun muss, um zu gewinnen. Um einen solchen eigenen Spielmodus zu entwickeln, musst du beinahe alle Techniken, die du bisher gelernt hast, in einem Plugin kombinieren.
15.1 Schneeballschlacht Wie wäre es zum Beispiel mit einem Spielmodus »Schneeballschlacht«? Wie der funktionieren soll, lässt der Name schon erahnen: Die Spieler bewerfen sich gegenseitig mit Schneebällen und wer die meisten Treffer landet, gewinnt. Das hört sich erst einmal relativ einfach an. Bevor daraus aber tatsächlich ein Plugin entstehen kann, müssen noch einige Fragen geklärt werden. Zum Beispiel: Müssen sich die Spieler die Schneebälle selbst suchen oder haben sie unendlich viele zur Verfügung? Gibt es eine »ewige« Highscore-Liste oder zeitlich begrenzte Runden? Bilden die Spieler Teams oder kämpft jeder gegen jeden? Diese und viele weitere Fragen stellen sich, bevor es mit der Programmierung des Plugins losgehen kann. Wie immer gilt natürlich, dass du als Entwickler des Plugins völlig frei in diesen Entscheidungen bist und dein Plugin ganz nach deinen Wünschen gestalten kannst. Die hier abgedruckte Version stellt eine eher einfachere Variante dar, die gleichzeitig aber vor allem jede Menge Spaß für die Spieler bieten soll.
Im Detail soll das Plugin so funktionieren: Sobald ein Spieler den Server betritt, wird er mit 16 Schneebällen ausgestattet. Sobald er einen Schneeball wirft, wird sein Vorrat automatisch wieder aufgefüllt. In einer Tabelle wird gespeichert, wie viele andere Spieler jeder Spieler auf dem Server mit einem Schneeball getroffen hat. Wird der Befehl /highscore eingegeben, soll eine Highscore-Liste angezeigt werden, in der steht, wie viele Punkte die Spieler auf dem Server bereits gesammelt haben.
15.1.1 Schneebälle verteilen Der erste logische Schritt ist daher, jeden Spieler bei Betreten des Servers mit einem vollen Stapel Schneebälle auszustatten, also 16 Stück. Um etwas immer dann zu tun, wenn ein Spieler den Server betritt, benötigst du, wie du dir vermutlich schon denken kannst, einen Listener. Dieser Listener wartet auf das PlayerJoinEvent. Es gibt aber noch eine zweite Gelegenheit, zu der der Vorrat an Schneebällen wieder aufgefüllt werden muss, nämlich dann, wenn der Spieler nach dem Tod wieder auf der Karte erscheint. Dafür kannst du das PlayerRespawnEvent verwenden. In beiden Fällen soll also in der onEvent-Funktion dasselbe gemacht werden: Dem Spieler sollen 16 Schneebälle gegeben werden. Ein Anfänger würde den Code wahrscheinlich einfach kopieren, aber wenn dann die Zahl der Schneebälle geändert werden soll oder ähnliches, muss der Code immer an zwei Stellen angepasst werden. Als erfahrener – und fauler – Programmierer kennst du aber schon bessere Lösungen, zum Beispiel Vererbung. Mehrstufige Vererbung, um genau zu sein, wie du in Listing 15.1 sehen kannst. Denn die Klasse MasterListener erbt, wie du das für Events schon kennst, von der Klasse PythonListener, und die beiden Klassen JoinListener und RespawnListener wiederrum von MasterListener. Die eigentliche Funktionalität, also das Verteilen der Schneebälle an den Spieler, steckt in der schneebaelleGebenFunktion. Die beiden Klassen JoinListener und RespawnListener
warten dann nur noch darauf, dass ihr jeweiliges Event eintritt und rufen dann die schneebaelleGeben-Funktion auf. 1 class MasterListener(PythonListener):
2 def schneebaelleGeben(self, event):
3 spieler = event.getPlayer()
4 inventar = spieler.getInventory()
5
6 if inventar.contains(bukkit.Material.SNOW_BALL):
7 #Spieler hat bereits mindestens einen Schneeball
8 inventar.first(bukkit.Material.SNOW_BALL).setAmount(16)
9 else:
10 #Spieler hat noch keinen Schneeball
11 if (inventar.firstEmpty() < 0 | inventar.firstEmpty() > 35:
12 #Inventar voll
13 inventar.remove(inventar.getItem(0))
14 inventar.addItem(ItemStack(bukkit.Material.SNOW_BALL, 16))
15
16 class JoinListener(MasterListener):
17 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
18 def onEvent(self, event):
19 self.schneebaelleGeben(event)
20
21 class RespawnListener(MasterListener):
22 @PythonEventHandler(PlayerRespawnEvent, EventPriority.NORMAL)
23 def onEvent(self, event):
24 self.schneebaelleGeben(event)
Listing 15.1: Zwei Listener mit gleicher Funktionalität, realisiert durch Vererbung
In der Funktion schneebaelleGeben müssen drei verschiedene Fälle unterschieden werden, wie du sehen kannst. Der einfachste Fall, den du ab Zeile 6 findest, tritt ein, wenn bereits Schneebälle im Inventardes Spielers vorhanden sind. Ob das zutrifft, kannst du mit dem Befehl containstesten. In diesem Fall greifst du zunächst mit dem Befehl inventar.firstauf den ersten Stapel mit Schneebällen zu und füllst diesen dann mit dem Befehl setAmountauf.
Sind im Inventar des Spielers noch keine Schneebälle vorhanden, muss noch einmal zwischen zwei Fällen unterschieden werden: Ist noch ein Platz im Inventar, auch Slotgenannt, frei oder nicht? Dabei hilft dir der firstEmpty-Befehl. Er gibt die Nummer des ersten freien Slots zurück. Ist kein Slot frei, so liefert er den Wert -1 zurück. Gibt er hingegen einen Wert über 35 zurück, ist zwar noch ein Slot frei, allerdings keiner, der für das Ablegen von Schneebällen geeignet ist. Die normalen Felder im Inventar eines Spielers sind nämlich von 0 bis 35 durchnummeriert. Die darüber liegenden Zahlen gehören zum Beispiel zum Helm-Slot oder zum Crafting-Feld im Inventar. Aus diesem Grund wird in Zeile 11 von Listing 15.1 geprüft, ob kein Slot oder nur noch ein Slot mit einer ID größer als 35 frei ist. Tritt einer der beiden Fälle ein, so ist das Inventar voll und es können nicht einfach Schneebälle hinzugefügt werden. In diesem Fall hast du zwei Möglichkeiten: Entweder du gibst dem Spieler keine Schneebälle oder du schaffst Platz in seinem Inventar. Ersteres könnte den Spieler am Mitspielen hindern, zum Beispiel wenn er mitten in einer Wüste spawnt, ohne Schnee in seiner Nähe. Letzteres könnte ihn aber durchaus auch ungehalten machen, zum Beispiel wenn in seinem Inventar ein Stapel Diamanten plötzlich einfach durch Schneebälle ersetzt wird. Diesem Problem kannst du etwas entgegenwirken, indem du zum Beispiel darauf achtest, dass du keine zu wertvollen Gegenstände löschst, um Platz im Inventar zu schaffen. Das könntest du tun, indem du eine Liste mit besonders wertvollen Gegenständen erstellst, die du nicht löschen möchtest, und dann, statt einfach das erste Feld im Inventar des Spielers zu leeren, das erste Feld leerst, in dem sich kein wertvoller Gegenstand befindet. Die in Listing 15.1 gezeigte Version tut das allerdings nicht, hier wird einfach der erste Slot im Inventar freigeräumt. In Zeile 14 werden dem Inventar dann die Schneebälle hinzugefügt, unabhängig davon, ob der Platz gerade geschaffen wurde oder vorher schon frei war. Damit sind alle drei Fälle abgedeckt und du hast sichergestellt, dass jeder Spieler zu Beginn mit mindestens 16 Schneebällen ausgerüstet ist.
In Listing 15.2 findest du die drei Klassen nochmal zusammengebaut zu einem Plugin, inklusive der benötigten Importe und der Registrierung der Listener. 1 from org.bukkit.event import EventPriority
2 from org.bukkit.event.player import PlayerJoinEvent
3 from org.bukkit.event.player import PlayerRespawnEvent
4 from org.bukkit.inventory import ItemStack
5
6 class MasterListener(PythonListener):
7 def schneebaelleGeben(self, event):
8 spieler = event.getPlayer()
9 inventar = spieler.getInventory()
10
11 if inventar.contains(bukkit.Material.SNOW_BALL):
12 #Spieler hat bereits mindestens einen Schneeball
13 inventar.getItem(inventar.first(bukkit.Material.SNOW_BALL)).s etAmount(16)
14 else:
15 #Spieler hat noch keinen Schneeball
16 if (inventar.firstEmpty() < 0 | inventar.firstEmpty() > 35:
17 #Inventar voll
18 inventar.remove(inventar.getItem(0))
19 inventar.addItem(ItemStack(bukkit.Material.SNOW_BALL, 16))
20
21 class JoinListener(MasterListener):
22 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
23 def onEvent(self, event):
24 self.schneebaelleGeben(event)
25
26 class RespawnListener(MasterListener):
27 @PythonEventHandler(PlayerRespawnEvent, EventPriority.NORMAL)
28 def onEvent(self, event):
29 self.schneebaelleGeben(event)
30
31 class SchneeballschlachtPlugin(PythonPlugin):
32 def onEnable(self):
33 pluginManager = self.getServer().getPluginManager()
34 jListener = JoinListener()
35 36 37 38
rListener = RespawnListener()
pluginManager.registerEvents(jListener, self)
pluginManager.registerEvents(rListener, self)
pass
Listing 15.2: Schneeballschlacht-Plugin Schritt 1
15.1.2 Schneebälle auffüllen Als nächstes solltest du nun dafür sorgen, dass der Nachschub an Schneebällen nicht versiegt. Konkret heißt das: Du musst dafür sorgen, dass dem Inventar des Spielers jedes Mal, wenn er einen Schneeball wirft, ein neuer hinzugefügt wird beziehungsweise dass keiner verloren geht. Hierfür kommt wieder ein Listener zum Einsatz, der auf das PlayerInteractEvent wartet, wie du in Listing 15.3 sehen kannst. Da dieser aber nicht nur aufgerufen wird, wenn ein Schneeball geworfen wird, sondern zum Beispiel auch, wenn der Spieler mit einem Block interagiert, musst du zunächst überprüfen, ob es sich um eine Interaktion mit einem Gegenstand handelt. Wenn das der Fall ist, dann musst du im nächsten Schritt noch überprüfen, ob es sich bei diesem Gegenstand um einen Schneeball handelt. Diese beiden Überprüfungen findest du in den Zeilen 35 und 37. Werden beide Bedingungen erfüllt, musst du nur noch, wie in Zeile 38, dafür sorgen, dass die Menge der Schneebälle sich nicht verändert, trotz des Wurfs, und schon ist für unendlichen Nachschub an Schneebällen gesorgt. 1 2 3 4 5 6 7 8 9 10
from org.bukkit.event import EventPriority
from org.bukkit.event.player import PlayerJoinEvent
from org.bukkit.event.player import PlayerRespawnEvent
from org.bukkit.event.player import PlayerInteractEvent
from org.bukkit.inventory import ItemStack
class MasterListener(PythonListener):
def schneebaelleGeben(self, event):
spieler = event.getPlayer()
inventar = spieler.getInventory()
11
12 if inventar.contains(bukkit.Material.SNOW_BALL):
13 #Spieler hat bereits mindestens einen Schneeball
14 inventar.getItem(inventar.first(bukkit.Material.SNOW_BALL)).s etAmount(16)
15 else:
16 #Spieler hat noch keinen Schneeball
17 if inventar.firstEmpty() < 0 | inventar.firstEmpty() > 35:
18 #Inventar voll
19 inventar.remove(inventar.getItem(0))
20 inventar.addItem(ItemStack(bukkit.Material.SNOW_BALL, 16))
21
22 class JoinListener(MasterListener):
23 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
24 def onEvent(self, event):
25 self.schneebaelleGeben(event)
26
27 class RespawnListener(MasterListener):
28 @PythonEventHandler(PlayerRespawnEvent, EventPriority.NORMAL)
29 def onEvent(self, event):
30 self.schneebaelleGeben(event)
31
32 class InteractListener(PythonListener):
33 @PythonEventHandler(PlayerInteractEvent, EventPriority.NORMAL)
34 def onEvent(self, event):
35 if event.hasItem():
36 item = event.getItem()
37 if item.getType() == bukkit.Material.SNOW_BALL:
38 item.setAmount(item.getAmount())
39
40 class SchneeballschlachtPlugin(PythonPlugin):
41 def onEnable(self):
42 pluginManager = self.getServer().getPluginManager()
43 jListener = JoinListener()
44 rListener = RespawnListener()
45 iListener = InteractListener()
46 pluginManager.registerEvents(jListener, self)
47 pluginManager.registerEvents(rListener, self)
48 49
pluginManager.registerEvents(iListener, self)
pass
Listing 15.3: Schneeballschlacht-Plugin Schritt 2
15.1.3 Punkte zählen Um daraus nun einen richtigen Spielmodus zu machen, muss die Anzahl der Treffer bzw. Punkte gezählt werden. Die einfachste Möglichkeit ist, für jeden Treffer einen Punkt zu vergeben, dann ist dein Punktestand einfach gleich der Anzahl der Treffer. Wenn du ein etwas ausgeklügelteres System nutzen möchtest, kannst du aber auch pro gelandetem Treffer einen Punkt vergeben und pro kassiertem Treffer einen Punkt abziehen. So sind auch negative Punktestände möglich, und die Spieler müssen nicht nur darauf achten, möglichst viele Treffer zu landen, sondern auch darauf, sich selbst zu schützen. Um diese Punkte zu zählen und später auch zu speichern, bietet es sich an, wieder eine eigene Kleine Klasse für die Punktestände zu schreiben, wie du es im letzten Kapitel bereits für die Schilder getan hast. Wie diese Klasse aussehen sollte, kannst du in Listing 15.4 sehen. Für jeden Spieler werden in dieser Klasse seine eindeutige ID, sein Name und sein aktueller Punktestand gespeichert. Während der Name und die ID nach dem Erstellen der Klasse nur noch gelesen werden kann, kann der Punktestand mit den Funktionen plusPunkt und minusPunkt auch noch jeweils um 1 erhöht beziehungsweise verringert werden. 1 2 3 4 5 6 7 8 9
class Highscore:
def __init__(self, uniqueId, name):
self.__uniqueId = uniqueId
self.__name = name
self.__punkte = 0
def getUniqueId(self):
return self.__uniqueId
10 11 12 13 14 15 16 17 18 19 20
def getName(self):
return self.__name
def getPunkte(self):
return self.__punkte
def plusPunkt(self):
self.__punkte = self.__punkte + 1
def minusPunkt(self):
self.__punkte = self.__punkte - 1
Listing 15.4: Highscore-Klasse
Um die Treffer zu zählen, benötigst du dann noch einen weiteren Listener in deinem Plugin, diesmal für das EntityDamageByEntityEvent. Wie der Name schon vermuten lässt, wird dieses Event immer dann aufgerufen, wenn einer Kreatur von einer anderen Schaden zugefügt wird. Das ist für unsere Zwecke natürlich noch viel zu allgemein, daher müssen wir zunächst einmal überprüfen, ob der Schaden mithilfe eines Schneeballs zugefügt wurde und ob es sich bei beiden Kreaturen um Spieler handelt, denn nur dann gibt es auch Punkte zu verteilen. Wie diese Überprüfungen funktionieren, kannst du in Listing 15.5 sehen. Zunächst einmal wird in Zeile 4 überprüft, ob der Schaden mit einem Projektil verursacht wird, dabei hilft die getCause-Funktion des Events. Ist dass der Fall, dann kann mit getDamager, wie in Zeile 5, auf das Projektil zugegriffen werden. Den Umgang mit Projektilen kennst du ja bereits vom Enderbogen-Plugin. In Zeile 6 kann dann geprüft werden, ob es sich bei dem Projektil um einen Schneeball handelt, damit ist die erste wichtige Überprüfung bereits abgehakt. Nun muss noch geprüft werden, ob es sich bei Angreifer und Angegriffenem jeweils um einen Spieler handelt. Dabei hilft dir der ebenfalls bereits bekannte isinstance-Befehl, den du in Zeile 10 findest. 1 class HitListener(PythonListener):
2 @PythonEventHandler(EntityDamageByEntityEvent, EventPriority.NORMAL)
3 def onEvent(self, event):
4 if event.getCause() == DamageCause.PROJECTILE:
5 projektil = event.getDamager()
6 if projektil.getType() == EntityType.SNOWBALL:
7 #geschoss ist schneeball
8 angreifer = projektil.getShooter()
9 angegriffener = event.getEntity()
10 if isinstance(angreifer, Player) & isinstance(angegriffener, Player):
11 #punkte verteilen
Listing 15.5: HitListener
Nun müssen nur noch die Punkte verteilt werden. Dazu betrachten wir in Listing 15.6 wieder das komplette, mit über 100 Zeilen inzwischen schon recht umfangreiche Plugin. Zunächst einmal kannst du in den Zeilen 1 bis 10 einige zusätzliche Importe sehen. In Zeile 12 gibt es nun außerdem eine globale Variable für das Speichern der Highscores. gleich darunter, in Zeile 14 folgt eine Funktion, die aus der Liste der Highscores den heraussucht, der zum Spieler gehört, der als Parameter an die Funktion übergeben wurde. Hat der entsprechende Spieler noch keinen Eintrag, so wird dieser in Zeile 20 erzeugt und dann in Zeile 21 zurückgegeben. Dank dieser praktischen Funktion sind im HitListener dann nur noch vier zusätzliche Zeilen notwendig, die du ab Zeile 88 findest. Hier wird dem Highscore des Angreifers ein Punkt hinzugefügt und dem Highscore des Angegriffenen ein Punkt abgezogen. In der onEnable-Funktion muss dann nur noch die highscore-Liste initialisiert werden, das geschieht in Zeile 96, und der HitListener registriert werden, das geschieht in den Zeilen 102 und 106. 1 from org.bukkit.event import EventPriority
2 from org.bukkit.event.player import PlayerJoinEvent
3 from org.bukkit.event.player import PlayerRespawnEvent
4 from org.bukkit.event.player import PlayerInteractEvent
5 from org.bukkit.event.entity import EntityDamageByEntityEvent
6 from org.bukkit.event.entity.EntityDamageEvent import
DamageCause
7 from org.bukkit.inventory import ItemStack
8 from org.bukkit.entity import EntityType
9 from org.bukkit.entity import Projectile
10 from org.bukkit.entity import Player
11
12 global highscore
13
14 def getHighscoreForPlayer(player):
15 global highscore
16 for index in range(len(highscore)):
17 if player.getUniqueId().toString() == highscore[index].getUniqueId():
18 return highscore[index]
19 #noch kein highscore vorhanden
20 highscore.append(Highscore(player.getUniqueId().toString(), player.getName()))
21 return highscore[len(highscore)-1]
22
23 class Highscore:
24 def __init__(self, uniqueId, name):
25 self.__uniqueId = uniqueId
26 self.__name = name
27 self.__punkte = 0
28
29 def getUniqueId(self):
30 return self.__uniqueId
31
32 def getName(self):
33 return self.__name
34
35 def getPunkte(self):
36 return self.__punkte
37
38 def plusPunkt(self):
39 self.__punkte = self.__punkte + 1
40
41 def minusPunkt(self):
42 self.__punkte = self.__punkte - 1
43
44 class MasterListener(PythonListener):
45 def schneebaelleGeben(self, event):
46 spieler = event.getPlayer()
47 inventar = spieler.getInventory()
48
49 if inventar.contains(bukkit.Material.SNOW_BALL):
50 #Spieler hat bereits mindestens einen
Schneeball
51 inventar.getItem(inventar.first(bukkit.Material.SNOW_BALL)).s etAmount(16)
52 else:
53 #Spieler hat noch keinen Schneeball
54 if inventar.firstEmpty() < 0 | inventar.firstEmpty() > 35:
55 #Inventar voll
56 inventar.remove(inventar.getItem(0))
57 inventar.addItem(ItemStack(bukkit.Material.SNOW_BALL, 16))
58
59 class JoinListener(MasterListener):
60 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
61 def onEvent(self, event):
62 self.schneebaelleGeben(event)
63
64 class RespawnListener(MasterListener):
65 @PythonEventHandler(PlayerRespawnEvent, EventPriority.NORMAL)
66 def onEvent(self, event):
67 self.schneebaelleGeben(event)
68
69 class InteractListener(PythonListener):
70 @PythonEventHandler(PlayerInteractEvent, EventPriority.NORMAL)
71 def onEvent(self, event):
72 if event.hasItem():
73 item = event.getItem()
74 if item.getType() == bukkit.Material.SNOW_BALL:
75 item.setAmount(item.getAmount())
76
77 class HitListener(PythonListener):
78 @PythonEventHandler(EntityDamageByEntityEvent, EventPriority.NORMAL)
79 def onEvent(self, event):
80 if event.getCause() == DamageCause.PROJECTILE:
81 projektil = event.getDamager()
82 if projektil.getType() == EntityType.SNOWBALL:
83 #geschoss ist schneeball
84 angreifer = projektil.getShooter()
85 angegriffener = event.getEntity()
86 if isinstance(angreifer, Player) & isinstance(angegriffener, Player):
87 #punkte verteilen
88 highAngreifer = getHighscoreForPlayer(angreifer)
89 highAngreifer.plusPunkt()
90 highAngegriffener = getHighscoreForPlayer(angegriffener)
91 highAngegriffener.minusPunkt()
92
93 class SchneeballschlachtPlugin(PythonPlugin):
94 def onEnable(self):
95 global highscore
96 highscore = []
97
98 pluginManager = self.getServer().getPluginManager()
99 jListener = JoinListener()
100 rListener = RespawnListener()
101 iListener = InteractListener()
102 hListener = HitListener()
103 pluginManager.registerEvents(jListener, self)
104 pluginManager.registerEvents(rListener, self)
105 pluginManager.registerEvents(iListener, self)
106 pluginManager.registerEvents(hListener, self)
107 pass
Listing 15.6: Schneeballschlacht-Plugin Schritt 3
15.1.4 Punkte dauerhaft speichern An dieser Stelle tut sich nun wieder das Problem auf, mit dem du dich im letzten Kapitel bereits intensiv auseinandergesetzt hast: Solange die Highscore-Liste nur in einer Variablen gespeichert ist, geht sie bei einem Neustart des Servers verloren. Unter Umständen ist das unproblematisch, zum Beispiel wenn du die Runden ohnehin zeitlich begrenzen oder mit jedem Neustart des Servers auch eine neue Runde starten möchtest. Falls du aber eine »ewige« Bestenliste führen möchtest, so kannst du auf die Techniken aus dem letzten Kapitel zurückgreifen, um die Informationen vor dem Beenden des Servers dauerhaft zu speichern.
Dazu fügst du deinem Plugin einfach eine onDisable-Funktion hinzu, deren Inhalt du in Listing 15.7 findest, und erweiterst die onEnableFunktion wie in Listing 15.8 gezeigt. 1 def onDisable(self):
2 global highscore
3
4 if len(highscore) > 0:
5 if not self.getDataFolder().exists():
6 self.getDataFolder().mkdirs()
7
8 pfad = self.getDataFolder().getPath() + "/highscore.pkl"
9 datei = open(pfad, "wb")
10 pickle.dump(highscore, datei)
Listing 15.7: onDisable-Funktion 1 def onEnable(self):
2 global highscore
3 pfad = self.getDataFolder().getPath() + "/ highscore.pkl"
4
5 if os.path.isfile(pfad):
6 datei = open(pfad, "rb")
7 highscore = pickle.load(datei)
8 else:
9 highscore = []
10
11 pluginManager = self.getServer().getPluginManager()
12 jListener = JoinListener()
13 rListener = RespawnListener()
14 iListener = InteractListener()
15 hListener = HitListener()
16 pluginManager.registerEvents(jListener, self)
17 pluginManager.registerEvents(rListener, self)
18 pluginManager.registerEvents(iListener, self)
19 pluginManager.registerEvents(hListener, self)
20 pass
Listing 15.8: onEnable-Funktion
15.1.5 Highscore-Liste anzeigen
Damit ist dein erster eigener Spielmodus, die Schneeballschlacht, schon beinahe fertig. Was jetzt noch fehlt, ist der Befehl /highscore, der eine Punkteliste aller Spieler anzeigen soll. Jeder Eintrag soll die Platzierung, den Namen des Spielers und seine Punktzahl enthalten, also zum Beispiel »1. notch, 10 Punkte«. Das hört sich erst einmal einfach an, die Umsetzung bringt aber eine kleine Tücke mit sich, denn eine ordentliche Highscore-Liste sollte natürlich nach der Punktezahl sortiert sein, sodass der Spieler mit den meisten bzw. wenigsten Punkten ganz oben bzw. ganz unten steht. Praktischerweise bietet Python hierfür eine einfache Methode, wie du in der onCommand-Funktion in Listing 15.9 sehen kannst. Mit dem sor t-Befehl können Arrays schnell und einfach sortiert werden. Dazu musst du nur angeben, nach welchem Kriterium sortiert werden soll, in unseren Fall ist das der Wert der Funktion getPunkte. Da der Spieler mit den meisten Punkten ganz oben auf der Liste stehen soll, muss außerdem noch mit reverse = True die Reihenfolge der Sortierung umgedreht werden. 1 def onCommand(self, sender, command, label, args):
2 if isinstance(sender, Player):
3 global highscore
4 highscore.sort(key = lambda x: x.getPunkte, reverse=True)
5 for index in range(len(highscore)):
6 sender.sendMessage(str(index+1) + ". " + highscore[index].getName() + ", " + str(highscore[index].getPunkte()) + " Punkte")
Listing 15.9: onCommand-Funktion
Tipp Nicht vergessen: Der Befehl muss auch noch der plugin.yml hinzugefügt werden.
15.1.6 Vollständiger Quellcode Damit ist nun auch das letzte Puzzlestück des Plugins fertig. Zum Abschluss findest du in Listing 15.10 noch einmal den kompletten Quellcode inklusive aller Importe. 1 import os.path
2 import pickle
3
4 from org.bukkit.event import EventPriority
5 from org.bukkit.event.player import PlayerJoinEvent
6 from org.bukkit.event.player import PlayerRespawnEvent
7 from org.bukkit.event.player import PlayerInteractEvent
8 from org.bukkit.event.entity import EntityDamageByEntityEvent
9 from org.bukkit.event.entity.EntityDamageEvent import DamageCause
10 from org.bukkit.inventory import ItemStack
11 from org.bukkit.entity import EntityType
12 from org.bukkit.entity import Projectile
13 from org.bukkit.entity import Player
14
15 global highscore
16
17 def getHighscoreForPlayer(player):
18 global highscore
19 for index in range(len(highscore)):
20 if player.getUniqueId().toString() == highscore[index].getUniqueId():
21 return highscore[index]
22 #noch kein highscore vorhanden
23 highscore.append(Highscore(player.getUniqueId().toString(), player.getName()))
24 return highscore[len(highscore)-1]
25
26 class Highscore:
27 def __init__(self, uniqueId, name):
28 self.__uniqueId = uniqueId
29 self.__name = name
30 self.__punkte = 0
31
32 def getUniqueId(self):
33 return self.__uniqueId
34
35 def getName(self):
36 return self.__name
37
38 def getPunkte(self):
39 return self.__punkte
40
41 def plusPunkt(self):
42 self.__punkte = self.__punkte + 1
43
44 def minusPunkt(self):
45 self.__punkte = self.__punkte - 1
46
47 class MasterListener(PythonListener):
48 def schneebaelleGeben(self, event):
49 spieler = event.getPlayer()
50 inventar = spieler.getInventory()
51
52 if inventar.contains(bukkit.Material.SNOW_BALL):
53 #Spieler hat bereits mindestens einen Schneeball
54 inventar.getItem(inventar.first(bukkit.Material.SNOW_BALL)).s etAmount(16)
55 else:
56 #Spieler hat noch keinen Schneeball
57 if inventar.firstEmpty() < 0 | inventar.firstEmpty() > 35:
58 #Inventar voll
59 inventar.remove(inventar.getItem(0))
60 inventar.addItem(ItemStack(bukkit.Material.SNOW_BALL, 16))
61
62 class JoinListener(MasterListener):
63 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
64 def onEvent(self, event):
65 self.schneebaelleGeben(event)
66
67 class RespawnListener(MasterListener):
68 @PythonEventHandler(PlayerRespawnEvent, EventPriority.NORMAL)
69 def onEvent(self, event):
70 self.schneebaelleGeben(event)
71
72 class InteractListener(PythonListener):
73 @PythonEventHandler(PlayerInteractEvent, EventPriority.NORMAL)
74 def onEvent(self, event):
75 if event.hasItem():
76 item = event.getItem()
77 if item.getType() == bukkit.Material.SNOW_BALL:
78 item.setAmount(item.getAmount())
79
80 class HitListener(PythonListener):
81 @PythonEventHandler(EntityDamageByEntityEvent, EventPriority.NORMAL)
82 def onEvent(self, event):
83 if event.getCause() == DamageCause.PROJECTILE:
84 projektil = event.getDamager()
85 if projektil.getType() == EntityType.SNOWBALL:
86 #geschoss ist schneeball
87 angreifer = projektil.getShooter()
88 angegriffener = event.getEntity()
89 if isinstance(angreifer, Player) & isinstance(angegriffener, Player):
90 #punkte verteilen
91 highAngreifer = getHighscoreForPlayer(angreifer)
92 highAngreifer.plusPunkt()
93 highAngegriffener = getHighscoreForPlayer(angegriffener)
94 highAngegriffener.minusPunkt()
95
96 class SchneeballschlachtPlugin(PythonPlugin):
97 def onEnable(self):
98 global highscore
99 pfad = self.getDataFolder().getPath() + "/ highscore.pkl"
100
101 if os.path.isfile(pfad):
102 datei = open(pfad, "rb")
103 highscore = pickle.load(datei)
104 else:
105 highscore = []
106
107 pluginManager = self.getServer().getPluginManager()
108 jListener = JoinListener()
109 rListener = RespawnListener()
110 iListener = InteractListener()
111 hListener = HitListener()
112 pluginManager.registerEvents(jListener, self)
113 pluginManager.registerEvents(rListener, self)
114 pluginManager.registerEvents(iListener, self)
115 pluginManager.registerEvents(hListener, self)
116 pass
117
118 def onDisable(self):
119 global highscore
120
121 if len(highscore) > 0:
122 if not self.getDataFolder().exists():
123 self.getDataFolder().mkdirs()
124
125 pfad = self.getDataFolder().getPath() + "/highscore.pkl"
126 datei = open(pfad, "wb")
127 pickle.dump(highscore, datei)
128
129 def onCommand(self, sender, command, label, args):
130 if isinstance(sender, Player):
131 global highscore
132 highscore.sort(key = lambda x: x.getPunkte, reverse=True)
133 for index in range(len(highscore)):
134 sender.sendMessage(str(index+1) + ". " + highscore[index].getName() + ", " + str(highscore[index].getPunkte()) + " Punkte")
Listing 15.10: Fertiges Schneeballschlacht-Plugin
15.2 Sammelspiel Wenn du eher ein Sammler als ein Jäger bist, könntest du auch einen Spielmodus entwerfen, bei dem es nicht um das Jagen von Gegnern, sondern um das Sammeln von Gegenständen geht. Und um das Ganze für die Spieler etwas reizvoller zu gestalten, kannst du eine Belohnung für den Spieler ausloben, der den Sammelauftrag zuerst erfüllt. Diese Belohnung könnten zum Beispiel seltene Gegenstände wie Gold oder Diamanten sein oder auch Erfahrungspunkte.
15.2.1 Aufbau des Plugins
Zuerst einmal wollen wir uns mit dem groben Aufbau des Plugins beschäftigen. Als erstes brauchst du eine Variable gesuchterGegenstand, in der gespeichert wird, welche Art von Gegenstand die Spieler suchen sollen. Außerdem noch eine Variable anzahl, in der gespeichert werden kann, wie viele Einheiten dieses Gegenstandes gesammelt werden sollen. Dann brauchst du noch eine Möglichkeit um zu speichern, wieviel jeder Spieler bereits gesammelt hat, dafür könntest du zum Beispiel einfach die Highscore-Klasse aus dem letzten Plugin wiederverwenden. Und dann brauchst du auch noch, wieder einmal, einen Listener, diesmal für das PlayerPickupItemEvent. Dieses Event wird immer dann aufgerufen, wenn ein Spieler einen Gegenstand aufhebt. Das Grundgerüst für das Plugin, das sich aus diesem Aufbau ergibt, kannst du in Listing 15.11 sehen. Wie du in Zeile 35 sehen kannst, wurde die Highscore-Klasse für das neue Plugin leicht verändert: Statt einer Funktion minusPunkt zum Abziehen eines Punktes gibt es nun eine Funktion nullPunkte, die den Punktestand des Spieler auf 0 zurücksetzt. Wofür du diese Funktion brauchst, wirst du später noch sehen. 1 from org.bukkit.event.player import PlayerPickupItemEvent
2 from org.bukkit.entity import Player
3
4 global highscore
5 global gesuchterGegenstand
6 global anzahl
7
8 def getHighscoreForPlayer(player):
9 global highscore
10 for index in range(len(highscore)):
11 if player.getUniqueId().toString() == highscore[index].getUniqueId():
12 return highscore[index]
13 #noch kein highscore vorhanden
14 highscore.append(Highscore(player.getUniqueId().toString(), player.getName()))
15 return highscore[len(highscore)-1]
16
17 class Highscore:
18 def __init__(self, uniqueId, name):
19 self.__uniqueId = uniqueId
20 self.__name = name
21 self.__punkte = 0
22
23 def getUniqueId(self):
24 return self.__uniqueId
25
26 def getName(self):
27 return self.__name
28
29 def getPunkte(self):
30 return self.__punkte
31
32 def plusPunkt(self):
33 self.__punkte = self.__punkte + 1
34
35 def nullPunkte(self):
36 self.__punkte = 0
37
38 class SammelListener(PythonListener):
39 @PythonEventHandler(PlayerPickupItemEvent, EventPriority.NORMAL)
40 def onEvent(self, event):
41
42 class SammelspielPlugin(PythonPlugin):
Listing 15.11: Grundgerüst des Sammelspiels
15.2.2 Plugin starten Bevor das eigentliche Spiel losgehen kann, muss das Plugin natürlich zunächst einmal gestartet werden. Das passiert wie üblich in der onEnable-Funktion, die du in Listing 15.12 sehen kannst. Dort wird in der zweiten Zeile zunächst einmal die highscore-Variable initialisiert und in den darauf folgenden drei Zeilen wie üblich der Listener registriert. 1 2 3 4 5
def onEnable(self):
global highscore
highscore = []
pluginManager = self.getServer().getPluginManager()
sListener = SammelListener()
6 7
pluginManager.registerEvents(sListener, self)
self.neueRundeStarten()
Listing 15.12: onEnable-Funktion
Was noch fehlt, ist die Initialisierung des gesuchten Gegenstandes und der zum Sieg erforderlichen Anzahl. Da dieses Prozedere nicht nur beim Start des Plugins durchgeführt werden muss, sondern auch immer dann, wenn eine neue Runde beginnt, ist es sinnvoll, diesen Vorgang auszulagern, deshalb findest du am Ende von Listing 15.12 den Aufruf der neueRundeStarten-Funktion. Was diese Funktion macht, kannst du in Listing 15.13 sehen. Zunächst einmal werden dort alle vorhandenen Highscore-Einträge auf 0 gesetzt, das passiert in Zeile 6 und 7. Hierfür wird die neue nullPunkte-Funktion der Highscore-Klasse benötigt. Danach wird aus einer Liste von Gegenständen zufällig einer ausgewählt. Die Liste gegenstaende kannst du natürlich beliebig erweitern. Für die zufällige Auswahl sorgt der Befehl randin t. Er liefert eine zufällige ganze Zahl zwischen 0 und der Länge der Liste - 1, in diesem Fall also zwischen 0 und 2. Die Anzahl, wie häufig der Gegenstand gesucht wird, wird mit dem selben Befehl in Zeile 13 ebenfalls zufällig festgelegt, zwischen 1 und 5. Zum Schluss wird den Spielern noch mitgeteilt, welchen Gegenstand sie wie oft sammeln müssen. 1 def neueRundeStarten(self):
2 global highscore
3 global gesuchterGegenstand
4 global anzahl
5
6 for index in range(len(highscore)):
7 highscore[index].nullPunkte()
8
9 gegenstaende = [bukkit.Material.DANDELION, bukkit.Material.EGG, bukkit.Material.CACTUS]
10 zufallszahl = random.randint(0, len(gegenstaende)-1)
11
12 gesuchterGegenstand = gegenstaende[zufallszahl]
13 anzahl = random.randint(1, 5)
14
15 self.getServer().broadcastMessage("Neue Runde! Sammle
" + str(anzahl) + "x " + gesuchterGegenstand.toString() + "!")
Listing 15.13: neueRundeStarten-Funktion
15.2.3 Spieler betritt den Server Diese Nachrichten sehen aber nur Spieler, die sich bereits auf dem Server befinden, wenn die neue Runde startet. Deshalb solltest du neue Spieler beim Betreten des Servers darüber informieren, welchen Gegenstand sie sammeln müssen. Außerdem kannst du auch direkt beim Betreten des Servers einen Highscore-Eintrag für sie erzeugen. Dafür benutzt du am besten das dir bereits bekannte PlayerJoinEvent, wie du in Listing 15.14 sehen kannst. Die meiste Arbeit nimmt dir dabei die getHighscoreForPlayer-Funktion ab, die du aus dem letzten Plugin wiederverwenden kannst, denn sie legt, falls noch nicht vorhanden, automatisch einen Highscore-Eintrag an. 1 class JoinListener(PythonListener):
2 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
3 def onEvent(self, event):
4 global gesuchterGegenstand
5 global anzahl
6 spieler = event.getPlayer()
7 getHighscoreForPlayer(spieler)
8 spieler.sendMessage("Willkommen! Auf diesem Server wird das Sammelspiel gespielt. Sammle " + str(anzahl) + " " + gesuchterGegenstand.toString() + ", um zu gewinnen!")
Listing 15.14: PlayerJoinEvent
15.2.4 Gegenstände zählen Das Wichtigste an diesem Plugin ist natürlich das Zählen der gesammelten Gegenstände. Das PlayerPickupItemEvent wird immer dann aufgerufen, wenn ein Spieler einen Gegenstand aufhebt. Du musst dann nur noch prüfen, ob es sich bei dem aufgehobenen
Gegenstand um den gesuchten Gegenstand handelt, und Punkte vergeben. Wie du das machst, kannst du in Listing 15.15 sehen. Zunächst einmal fällt dir dort aber vermutlich auf, dass die Klasse SammelListener jetzt eine init-Funktion hat. Diese ist nötig, um die Funktion neueRundeStarten aufrufen zu können. Denn da diese ja zum Plugin-Objekt gehört, muss dieses an den Listener übergeben werden. Das geschieht in der init-Funktion. 1 class SammelListener(PythonListener):
2 def __init__(self, plugin):
3 self.plugin = plugin
4 super(SammelListener, self).__init__()
5
6 @PythonEventHandler(PlayerPickupItemEvent, EventPriority.NORMAL)
7 def onEvent(self, event):
8 global gesuchterGegenstand
9 global anzahl
10
11 spieler = event.getPlayer()
12
13 if event.getItem().getItemStack().getType() == gesuchterGegenstand:
14 highscore = getHighscoreForPlayer(spieler)
15 highscore.plusPunkt()
16 if highscore.getPunkte() >= anzahl:
17 spieler.sendMessage("Herzlichen Glueckwunsch, du hast die Runde gewonnen!")
18 spieler.giveExp(50)
19 self.plugin.neueRundeStarten()
20 else:
21 spieler.sendMessage("Du musst noch " + str(anzahl-highscore.getPunkte()) + " Gegenstaende sammeln.")
Listing 15.15: Zählen der gesammelten Gegenstände
Wenn ein Spieler einen Gegenstand aufhebt, dann wird in Zeile 13 geprüft, ob es sich um den gesuchten Gegenstand handelt, denn nur dann muss der Listener überhaupt etwas machen. Ist das der Fall, dann wird der Punktestand des Spieler zunächst in Zeile 15 um eins erhöht. Hat er noch nicht die geforderte Anzahl gesammelt, so wird
dem Spieler in Zeile 21 eine Nachricht gesendet, wie viele Gegenstände er noch sammeln muss. Hat er dagegen bereits die benötigte Anzahl gesammelt, so wird er in Zeile 17 darüber informiert und in Zeile 18 bekommt er seine Belohnung, hier zum Beispiel 50 Erfahrungspunkte. Danach wird in Zeile 19 automatisch eine neu Runde gestartet.
15.2.5 Auftrag anzeigen Wenn du möchtest, kannst du vergesslichen Spielern auch noch einen kleinen zusätzlichen Service bieten, indem du ihnen erlaubst, sich mit dem Chat-Befehl /aufgabe, die aktuelle Aufgabe noch einmal anzeigen zu lassen. Wie das funktioniert, zeigt dir Listing 15.16. 1 def onCommand(self, sender, command, label, args):
2 global anzahl
3 global gesuchterGegenstand
4
5 if isinstance(sender, Player):
6 spieler = sender
7 spieler.sendMessage("Sammle " + str(anzahl) + "x " + gesuchterGegenstand.toString() + "!")
8 return True
Listing 15.16: onCommand-Funktion
15.2.6 Vollständiger Quellcode Damit hast du nun alle Teile zusammen, die du benötigst, um deinen zweiten eigenen Spielmodus zu realisieren. Wie du die Teile korrekt zusammenfügst und welche Importe du dafür benötigst, kannst du in Listing 15.17 sehen, wo noch einmal der komplette Quellcode aufgeführt ist
1 from org.bukkit.event.player import PlayerPickupItemEvent
2 from org.bukkit.event.player import PlayerJoinEvent
3 from org.bukkit.entity import Player
4 from org.bukkit.inventory import ItemStack
5 import random
6
7 global highscore
8 global gesuchterGegenstand
9 global anzahl
10
11 def getHighscoreForPlayer(player):
12 global highscore
13 for index in range(len(highscore)):
14 if player.getUniqueId().toString() == highscore[index].getUniqueId():
15 return highscore[index]
16 #noch kein highscore vorhanden
17 highscore.append(Highscore(player.getUniqueId().toString(), player.getName()))
18 return highscore[len(highscore)-1]
19
20 class Highscore:
21 def __init__(self, uniqueId, name):
22 self.__uniqueId = uniqueId
23 self.__name = name
24 self.__punkte = 0
25
26 def getUniqueId(self):
27 return self.__uniqueId
28
29 def getName(self):
30 return self.__name
31
32 def getPunkte(self):
33 return self.__punkte
34
35 def plusPunkt(self):
36 self.__punkte = self.__punkte + 1
37
38 def nullPunkte(self):
39 self.__punkte = 0
40
41 class SammelListener(PythonListener):
42 def __init__(self, plugin):
43 self.plugin = plugin
44 super(SammelListener, self).__init__()
45
46 @PythonEventHandler(PlayerPickupItemEvent, EventPriority.NORMAL)
47 def onEvent(self, event):
48 global gesuchterGegenstand
49 global anzahl
50
51 spieler = event.getPlayer()
52
53 if event.getItem().getItemStack().getType() == gesuchterGegenstand:
54 highscore = getHighscoreForPlayer(spieler)
55 highscore.plusPunkt()
56 if highscore.getPunkte() >= anzahl:
57 spieler.sendMessage("Herzlichen Glueckwunsch, du hast die Runde gewonnen!")
58 spieler.giveExp(50)
59 self.plugin.neueRundeStarten()
60 else:
61 spieler.sendMessage("Du musst noch " + str(anzahl-highscore.getPunkte()) + " Gegenstaende sammeln.")
62
63 class JoinListener(PythonListener):
64 @PythonEventHandler(PlayerJoinEvent, EventPriority.NORMAL)
65 def onEvent(self, event):
66 global gesuchterGegenstand
67 global anzahl
68 spieler = event.getPlayer()
69 getHighscoreForPlayer(spieler)
70 spieler.sendMessage("Willkommen! Auf diesem Server wird das Sammelspiel gespielt. Sammle " + str(anzahl) + " " + gesuchterGegenstand.toString() + ", um zu gewinnen!")
71
72 class SammelspielPlugin(PythonPlugin):
73 def onEnable(self):
74 global highscore
75 highscore = []
76 pluginManager = self.getServer().getPluginManager()
77 sListener = SammelListener(self)
78 jListener = JoinListener()
79 pluginManager.registerEvents(sListener, self)
80 pluginManager.registerEvents(jListener, self)
81 self.neueRundeStarten()
82
83 def neueRundeStarten(self):
84 global highscore
85 global gesuchterGegenstand
86 global anzahl
87
88 for index in range(len(highscore)):
89 highscore[index].nullPunkte()
90
91 gegenstaende = [bukkit.Material.DANDELION, bukkit.Material.EGG, bukkit.Material.CACTUS]
92 zufallszahl = random.randint(0, len(gegenstaende)-1)
93
94 gesuchterGegenstand = gegenstaende[zufallszahl]
95 anzahl = random.randint(1, 5)
96
97 self.getServer().broadcastMessage("Neue Runde! Sammle " + str(anzahl) + "x " + gesuchterGegenstand.toString() + "!")
98
99 def onCommand(self, sender, command, label, args):
100 global anzahl
101 global gesuchterGegenstand
102
103 if isinstance(sender, Player):
104 spieler = sender
105 spieler.sendMessage("Sammle " + str(anzahl) + "x " + gesuchterGegenstand.toString() + "!")
106 return True
Listing 15.17: Fertiges Sammelspiel-Plugin
Eigenständige PythonProgramme Kapitel 16
Mit allem, was du inzwischen über Python gelernt hast, kannst du in Zukunft nicht nur Plugins programmieren, auch das Programmieren eigener Python-Programme, häufig auch Python-Skriptegenannt, sollte dir leicht von der Hand gehen. Natürlich gibt es einige Unterschiede und Besonderheiten, die zu beachten sind, aber die grundsätzlichen Regeln bleiben dieselben. Dieses Kapitel soll dir einen kleinen Ausblick darauf geben, wie du in Zukunft deine Python-Kenntnisse auch außerhalb von Minecraft einsetzen könntest. Solltest du dich dafür interessieren, so seien dir auch die Bücher »Python für Kids« und »Python 3 – Lernen und professionell anwenden«, die ebenfalls im mitp-Verlag erschienen sind, ans Herz gelegt. Dort wird, nicht nur für Kids, ausführlich erklärt, wie du eigene Programme mit Python erstellen kannst.
16.1 Python einrichten Nutzer von GNU/Linux und OS X können diesen Abschnitt überspringen, denn ihre Betriebssysteme kommen in der Regel schon mit einer vorinstallierten Version von Python. Das kannst du testen, indem du das Terminal öffnest und dort python --version eingibst. Daraufhin sollte dir die installierte Python-Version angezeigt werden. Windows-Nutzer müssen dagegen Pythonerst installieren. Einen Link zur Installationsdatei findest du auf der Website zum Buch. Nachdem du diese heruntergeladen hast, kannst du sie mit einem Doppelklick starten. Danach erscheint der in Abbildung 16.1 gezeigte Dialog.
Hier solltest du unbedingt das rot umrandete Häkchen setzen, wobei sich die Versionsnummer bei neueren Versionen ändern kann. Danach beginnst du die Installation mit einem Klick auf Install Now. Nun musst du nur noch etwas Geduld haben und damit ist die Installation auch bereits abgeschlossen. Wenn du nun die Eingabeaufforderung öffnest und dort python --version eingibst, sollte dort ebenfalls die installierte Python-Versionsnummer angezeigt werden.
Abb. 16.1: Startbildschirm Python-Installation
Hinweis Für die Experten, die sich bereits mit Python auskennen, sei an dieser Stelle noch erwähnt, dass die Python-Plugins, die wir im Rahmen dieses Buches programmiert haben, die Python Version
2 verwenden, da diese vom PythonPluginLoader für Bukkit beziehungsweise Spigot verwendet wird. In diesem Kapitel werden wir dagegen die aktuellste Python-Version, also 3, einsetzen.
16.2 Grundgerüst Das Grundgerüst von Minecraft-Plugins ist dir inzwischen mit Sicherheit mehr als vertraut. Plugins bestehen aus onEnable-, onDisable-, und onCommand-Funktionen, die Hauptklasse muss immer von der Klasse PythonPlugin erben und so weiter. Ein solches starres Grundgerüst gibt es für eigenständige PythonProgramme nicht, sie können deshalb sehr kurz sein, wie das »Hallo Welt«-Programm, das du bereits im zweiten Kapitel dieses Buches kennengelernt hast und in Listing 16.1 sehen kannst. print("Hallo Welt!")
Listing 16.1: Ein besonders kurzes – aber funktionierendes – Python-Programm
Um das »Hallo Welt«-Programm auszuführen, speicherst du es zunächst auf deiner Festplatte, unter Windows könntest du dafür zum Beispiel einen Ordner C:\Python-Programme anlegen, unter OS X /Users/Benutzername/Python-Programme und unter GNU/Linux home/Benutzername/Python-Programme. In diesem Ordner speicherst du das Python-Programm dann unter einem beliebigen Namen mit der Dateieendung .py, also zum Beispiel hallo.py. Jetzt musst du nur noch die Eingabeaufforderung beziehungsweise das Terminal öffnen und dort mit cd in das neu geschaffene Verzeichnis wechseln. Dort kannst du dein Programm dann mit dem Befehl python hallo.py starten. Das Ergebnis sollte ungefähr wie in Abbildung 16.2 aussehen.
Abb. 16.2: »Hallo Welt«-Programm in der Eingabeaufforderung
16.3 Ein- und Ausgabe Wenn wir heute an Computerprogramme denken, dann denken wir meist sofort an grafische Benutzeroberflächen, Buttons und Grafiken. In einer nicht allzu fernen Vergangenheit war das noch anders: Ein Computerprogramm bestand lediglich aus Text, der in der Kommandozeile ein- und ausgegeben werden konnte. Wenn du anfängst, deine ersten eigenen Programme zu schreiben, wirst du dich vorübergehend in diese Zeit zurückversetzt fühlen. Wie du Informationen auf der Kommandozeile ausgeben kannst, hast du bereits im »Hallo Welt«-Programm gesehen. Der printBefehl funktioniert in Plugins und »normalen« Python-Programmen. Andere Befehle wie sendMessage oder broadcastMessage funktionieren dagegen nur in Minecraft-Plugins. Wie es schon bei den Plugins der Fall war, werden auch Programme erst richtig interessant, wenn sie Daten verarbeiten können, die der Nutzer eingibt. Glücklicherweise geht das, wie du in Listing 16.2 sehen kannst, sehr einfach. 1 2
name = input("Wie heisst du? ")
print("Hallo " + name)
Listing 16.2: Einlesen einer Eingabe
Hinweis
Solltest du eine Version 2.x von Python verwenden, dann lautet der Befehl raw_input, statt input.
Der Befehl inputwartet auf die Eingabe eines Spielers. In Klammern kann ihm ein Text übergeben werden, der dem Benutzer erklärt, welche Eingabe erwartet wird. Das Programm bleibt dann solange an dieser Stelle stehen, bis der Benutzer eine Eingabe vornimmt und diese mit Enter bestätigt. Diese Eingabe wird dann von der Funktion als Rückgabewert an dein Programm geliefert und die Ausführung geht ganz normal weiter. Die Ausgabe des Programmes sieht dann wie in Abbildung 16.3 gezeigt aus.
Abb. 16.3: Ausgabe einer eingelesenen Eingabe
Tipp Besteht eine Eingabe aus Zahlen, so werden diese zunächst, wie du es bereits von den Plugins kennst, ebenfalls als String gespeichert und müssen umgewandelt werden, bevor zum Beispiel mit ihnen gerechnet werden kann.
16.4 Quiz programmieren Damit sind wir auch schon fast am Ende unseres kurzen Ausflugs in die Welt der Python-Programme angekommen. Allerdings nicht, bevor wir nicht zumindest ein kleines Spiel selbst programmiert
haben. Das nächste Minecraft werden wir an dieser Stelle wohl noch nicht entwickeln können, aber ein unterhaltsames Quiz sollte dir bereits problemlos gelingen. Der Aufbau soll dabei ganz klassisch sein: Der Spieler bekommt eine Frage gestellt und wählt eine von vier Antwortmöglichkeiten aus, von denen selbstverständlich nur eine richtig ist. Zu Beginn solltest du eine Klasse wie in Listing 16.3 anlegen, die für die Repräsentation einer Frage zuständig ist. Die Klasse muss den Fragetext, die richtige Antwort und drei falsche Antworten speichern. Diese Klasse kannst du in deinem eben erstellten Ordner in einer Datei mit dem Namen frage.py speichern. 1 class Frage:
2 def __init__(self, frage, richtige_antwort, falsche_antworten):
3 self.__frage = frage
4 self.__falsche_antworten = falsche_antworten
5 self.__richtige_antwort = richtige_antwort
6
7 def get_frage(self):
8 return self.__frage
9
10 def get_falsche_antworten(self):
11 return self.__falsche_antworten
12
13 def get_richtige_antwort(self):
14 return self.__richtige_antwort
Listing 16.3: Frage-Klasse
Hinweis In Kapitel 5 wurden Konventionen für die Schreibung von Variablennamen eingeführt. Vielleicht erinnerst du dich noch daran, dass Variablen in Python eigentlich komplett kleingeschrieben werden und verschiedene Worte durch
Unterstriche getrennt werden. Für die Programmierung von Plugins haben wir, da der Minecraft-Server in Java geschrieben ist, auf die Java-Konvention zurückgegriffen, die Binnenmajuskel verwendet. Da wir nun unabhängig vom Server arbeiten, können wir auch zur üblichen Python-Konvention zurückkehren und Unterstriche verwenden.
Für das eigentliche Quiz kannst du eine weitere Datei anlegen, die den Namen quiz.py trägt. Da sich beide Dateien im selben Ordner befinden, funktioniert der Import der Frage-Klasse innerhalb der Quiz-Datei, wie du in Listing 16.4 sehen kannst, denkbar einfach. Als from wird einfach der Dateiname ohne .py-Endung angegeben und als import der Klassenname. 1 from frage import Frage
2
3 fragen = []
4
5 fragen.append(Frage("Wie heisst die Hauptstadt von Frankreich?", "Paris", ["Bruessel", "Lyon", "Marseille"]))
6
7 fragen.append(Frage("Wie ist der Spitzname des MinecraftErfinders?", "notch", ["motch", "scotch", "botch"]))
8
9 fragen.append (Frage("Wie viele Bundeslaender hat die Bundesrepublik Deutschland?", "16", ["15", "17", "18"]))
10
11 fragen.append (Frage("Wie heisst die Hauptstadt der Steiermark?", "Graz", ["Wien", "Hartberg", "Eisenstadt"]))
Listing 16.4: Grundgerüst des Quiz
Ist die Klasse erst einmal importiert, so kannst du, wie ebenfalls in Listing 16.4 gezeigt, sehr einfach neue Fragen hinzufügen. Nun müssen die Fragen aber natürlich auch noch ausgegeben und auf die Antwort gewartet werden.
Du kannst die Fragen entweder in der Reihenfolge anzeigen, in der sie eingegeben wurden, oder in einer zufälligen Reihenfolge, wenn du eine etwas ausgeklügeltere Umsetzung wählst. Für letzteres kannst du wie in Listing 16.5 auf den dir bereits bekannten Zufallsgenerator zurückgreifen. Um zu verhindern, dass Fragen doppelt angezeigt werden, kannst du sie einfach mit dem dir bereits aus Kapitel 5 bekannten remove-Befehl aus der Liste der Fragen löschen, nachdem sie angezeigt wurden. 1 2 3 4 5
while(len(fragen) > 0):
zufallszahl = random.randint(0, len(fragen)-1)
frage = fragen[zufallszahl]
fragen.remove(zufallszahl)
print(frage.get_frage())
Listing 16.5: Randomisierte Anzeige der Fragen
Neben dem Fragetext selbst müssen dem Nutzer als nächstes noch die Antwortmöglichkeiten angezeigt werden. Hier solltest du wie in Listing 16.6 auf jeden Fall auf eine zufällige Reihenfolge setzen, denn wenn die richtige Antwort auf jede Frage immer die erste Antwortmöglichkeit ist, verliert das Quiz schnell seinen Reiz. 1 2 3 4 5 6 7 8 9 10 11 12 13 14
while(len(fragen) > 0):
zufallszahl = random.randint(0, len(fragen)-1)
frage = fragen[zufallszahl]
fragen.remove(zufallszahl)
print(frage.get_frage())
antworten = frage.get_falsche_antworten()
antworten.append(frage.get_richtige_antwort())
random.shuffle(antworten)
for i in range(0, len(antworten)):
print(str(i) + ": " + antworten[i], end=" ")
print("")
Listing 16.6: Anzeige der Antwortmöglichkeiten
Die zufällige Reihenfolge wird in Listing 16.6 durch den Befehl shuff le sichergestellt, der die Elemente eines Arrays zufällig durcheinander würfelt. Neu ist auch, dass der pri nt-Befehl noch den Zusatz end=" " bekommt. Dieser sorgt dafür, dass die Antwortmöglichkeiten alle in einer Reihe stehen und nicht, wie normal, nach jedem print-Befehl eine neue Reihe begonnen wird. Dafür brauchen wir dann aber in der letzten Reihe einen zusätzlichen print-Befehl, der die Reihe mit den Antwortmöglichkeiten beendet. Als nächstes muss nun noch die Antwort des Spielers eingelesen werden. Genau der richtige Einsatz für die input-Funktion also. Dabei solltest du bedenken, dass deine Nutzer möglicherweise nicht immer eine der Antwortmöglichkeiten, 0, 1, 2 oder 3, eingeben, sondern beliebigen anderen Text oder Zahlen. Deshalb solltest du, bevor du »Falsch« oder »Richtig« ausgibst, zunächst auf jeden Fall prüfen, ob es sich überhaupt um eine gültige Eingabe handelt. Mit der Funktion isdigit kannst du prüfen, ob es sich bei einem String um eine positive Zahl handelt. Das reicht allerdings noch nicht ganz aus, deshalb solltest du im nächsten Schritt auch noch überprüfen, ob diese Zahl kleiner als vier ist. 1 while(len(fragen) > 0):
2 zufallszahl = random.randint(0, len(fragen)-1)
3 frage = fragen[zufallszahl]
4 fragen.remove(zufallszahl)
5 print(frage.get_frage())
6
7 antworten = frage.get_falsche_antworten()
8
9 antworten.append(frage.get_richtige_antwort())
10 random.shuffle(antworten)
11
12 for i in range(0, len(antworten)):
13 print(str(i) + ": " + antworten[i], end=" ")
14 print("")
15
16 antwort = input("Bitte waehle deine Antwort: ")
17 if antwort.isdigit() & int(antwort) < 4:
18 if antworten[int(antwort)] == frage.get_richtige_antwort():
19 print("Richtig!")
20 else:
21 print("Falsch")
22 else:
23 print("Bitte gib eine Zahl zwischen 0 und 3 als Antwort ein.")
Listing 16.7: Einlesen der Antwort
Nun musst du nur noch die einzelnen Teile des Quiz zusammenführen und die erforderlichen Importe hinzufügen. Wie das Endergebnis aussehen sollte, kannst du in Listing 16.8 sehen. 1 import random
2 from frage import Frage
3
4 fragen = []
5
6 fragen.append(Frage("Wie heisst die Hauptstadt von Frankreich?", "Paris", ["Bruessel", "Lyon", "Marseille"]))
7
8 fragen.append(Frage("Wie ist der Spitzname des MinecraftErfinders?", "notch", ["motch", "scotch", "botch"]))
9
10 fragen.append (Frage("Wie viele Bundeslaender hat die Bundesrepublik Deutschland?", "16", ["15", "17", "18"]))
11
12 fragen.append (Frage("Wie heisst die Hauptstadt der Steiermark?", "Graz", ["Wien", "Hartberg", "Eisenstadt"]))
13
14 while(len(fragen) > 0):
15 zufallszahl = random.randint(0, len(fragen)-1)
16 frage = fragen[zufallszahl]
17 fragen.remove(zufallszahl)
18 print(frage.get_frage())
19
20 antworten = frage.get_falsche_antworten()
21
22 antworten.append(frage.get_richtige_antwort())
23 random.shuffle(antworten)
24
25 for i in range(0, len(antworten)):
26 print(str(i) + ": " + antworten[i], end=" ")
27 print("")
28
29 antwort = input("Bitte waehle deine Antwort: ")
30 if antwort.isdigit() & int(antwort) < 4:
31 if antworten[int(antwort)] == frage.get_richtige_antwort():
32 print("Richtig!")
33 else:
34 print("Falsch")
35 else:
36 print("Bitte gib eine Zahl zwischen 0 und 3 als Antwort ein.")
Listing 16.8: Komplettes Quiz
Damit sind wir auch schon am Ende dieses Buches und unseres kurzen Ausflugs in die Welt der Python-Programme angekommen. Ich hoffe, er hat dich auf die Möglichkeiten neugierig gemacht, die du als Python-Programmierer nun hast. Denn wenn du weiter am Ball bleibst und mehr über das Programmieren lernst, ist das, was du in diesem Buch gesehen und gelernt hast, erst der Anfang. Viel Spaß dabei!
Befehlsreferenz Anhang A
In diesem Kapitel findest du eine Übersicht über alle Befehle, die dir im Verlaufe dieses Buches begegnet sind. Es kann dir als Nachschlagewerk dienen, selbst wenn du das letzte Kapitel des Buches schon lange abgeschlossen hast. Die Befehle sind nach Themen und innerhalb der Themen alphabetisch sortiert.
A.1 Schleifen Befehl
Parameter
for element in liste:
#Anweisung
liste: Liste eines beliebigen Typs
while bedingung:
#Anweisung
bedingung: Wahrheitswert
Rückgabewert
Beschreibung Wiederholt die Befehle im Schleifenrumpf für jedes Element der Liste.
Wiederholt die Befehle im Schleifenrumpf, solange die Laufbedingung erfüllt ist.
A.2 Verzweigungen Befehl
Parameter
if bedingung1:
bedingung1, #Anweisungen bedingung2: elif bedingung2: Wahrheitswerte #Anweisungen
else:
#Anweisungen
Rückgabewert
Beschreibung Führt den if-Teil aus, falls bedingung1 True ist. Falls bedingung1 False ist und bedingung2 True, dann wird der elif-Teil ausgeführt. Sind bedingung1 und bedingung2 False, dann wird der else-Teil ausgeführt.
A.3 Variablen Befehl
Parameter
Rückgabewert
Beschreibung
a[i]
a: Liste, i: Ganzzahl
i-tes Element der Liste a
Gibt das i-te Element der Liste a zurück.
Befehl
Parameter
Rückgabewert
Beschreibung
global x
Macht eine globale Variable x innerhalb einer Funktion lokal verfügbar.
x: Variable
isinstance(var, klasse)
var: Variable, klasse: Klasse / Typ
Wahrheitswert
Gibt True zurück, falls die Variable ein Objekt der angegebenen Klasse ist, ansonsten False.
len(a)
a: Liste
Ganzzahl
Gibt die Länge der Liste a zurück.
string.lower()
string: String
String
Wandelt den String in Kleinbuchstaben um.
string.upper()
string: String
String
Wandelt den String in Großbuchstaben um.
x = []
x = [a, b, c]
a, b, c: beliebige Werte oder Objekte
x = y
y: beliebiger Wert oder beliebiges Objekt
x.append(a)
a: beliebiger Wert oder beliebiges Objekt
x.remove(i)
i: Ganzzahl
Erzeugt eine neue, leere Liste. Erzeugt eine neue Liste mit dem Namen x und den Elementen a, b und c. Erzeugt eine neue Variable mit dem Namen x und dem Wert y.
Fügt das Element a der Liste x hinzu.
Löscht das Elemente an der i-ten Stelle der Liste.
A.4 Klassen und Objekte Befehl
Parameter
Rückgabewert
Beschreibung
Befehl
Parameter
Rückgabewert
a = A()
class A(B):
#Inhalt
class A:
#Inhalt
def __init__(self):
#Anweisungen
import x
path.append(pfad)
pfad: String
Beschreibung Erzeugen eines neuen Objektes der Klasse A. Grundgerüst einer Klasse mit dem Namen A, die von einer Klasse B erbt. Grundgerüst einer Klasse mit dem Namen A.
Grundgerüst einer init-Funktion.
Importiert eine Datei x. Fügt einen Pfad zum Arbeitsverzeichnis hinzu.
A.5 Funktionen Befehl
Parameter
def onCommand(self, sender, command, label, args):
#Anweisungen
self: Plugin-Objekt, sender: Absender des Chat-Befehls, command: ausgeführter Befehl, label: eingegebener Befehlsname, args: String-Array der Argumente
def onEnable(self):
#Anweisungen
self: Plugin-Objekt
Rückgabewert
Beschreibung Diese Funktion wird aufgerufen, wenn ein Chat-Befehl eingegeben wird. Diese Funktion wird beim Starten eines Plugins aufgerufen.
Befehl
Parameter
def x():
return y
Rückgabewert
Beschreibung
y
Mit dem return-Befehl lässt sich festlegen, welchen Wert eine Funktion zurückgeben soll.
A.6 Logische Operatoren und Vergleiche Befehl
Parameter
Rückgabewert
Beschreibung
a & b
a, b: Wahrheitswert
Wahrheitswert
Liefert True zurück, wenn a und b True sind, ansonsten False.
a ˆ b
a, b: Wahrheitswert
Wahrheitswert
Liefert True zurück wenn entweder a oder b True ist, ansonsten False.
a | b
a, b: Wahrheitswert
Wahrheitswert
Liefert True zurück, wenn entweder a oder b oder a und b True sind, ansonsten False.
not a
a: Wahrheitswert
Wahrheitswert
Liefert True zurück, wenn a False ist, ansonsten True.
x != y
x, y: beliebige Variablen
Wahrheitswert
Liefert True zurück, wenn x ungleich y ist, ansonsten False.
x < y
x, y: beliebige Variablen
Wahrheitswert
Liefert True zurück, wenn x kleiner als y ist, ansonsten False.
x y
x, y: beliebige Variablen
Wahrheitswert
Liefert True zurück, wenn x größer als y ist, ansonsten False.
y >= y
x, y: beliebige Variablen
Wahrheitswert
Liefert True zurück, wenn x größer oder gleich y ist, ansonsten False.
A.7 Spieler Befehl
Parameter
Rückgabewert
Beschreibung
spieler.getLineOfSight(ignorieren, maxEntfernung)
spieler: Spieler, ignorieren: Liste von Materialien, maxEntfernung: Zahl
Liste von Blöcken
Funktioniert wie getTargetBlock, allerdings werden alle Blöcke auf der Sichtlinie als Liste zurückgegeben statt nur ein Block.
spieler.getLocation()
spieler: Player
Location
Gibt die Position des Spielers zurück.
spieler.getTargetBlock(ignorieren, maxEntfernung)
spieler: Spieler, ignorieren: Liste von Materialien, maxEntfernung: Zahl
Block
Gibt den Block zurück, auf den der Spieler gerade schaut, falls dieser nicht weiter als maxEntfernung entfernt ist. Dabei werden alle Blöcke, deren Material in der Liste ignorieren vorkommt, wie Luft behandelt.
A.8 Positionen Befehl
Parameter
Rückgabewert
Beschreibung
position.getX()
position: Location
Zahl
Gibt die X-Koordinate der Position zurück.
Befehl
Parameter
Rückgabewert
Beschreibung
position.getY()
position: Location
Zahl
Gibt die Y-Koordinate der Position zurück.
position.getYaw()
position: Location
Zahl
Gibt die Rotation eines Spielers, Gegenstandes oder Monsters zurück.
position.getZ()
position: Location
Zahl
Gibt die Z-Koordinate der Position zurück.
position.setX(x)
position: Location, x: Zahl
position.setY(y)
position: Location, y: Zahl
position.setZ(z)
position: Location, z: Zahl
Setzt die X-Koordinate der Position auf x.
Setzt die Y-Koordinate der Position auf y.
Setzt die Z-Koordinate der Position auf z.
A.9 Welt Befehl
Parameter
Rückgabewert
Beschreibung
spieler.getWorld()
spieler: Player
Welt
Gibt die aktuelle Welt als Variable zurück.
welt.getBlockAt(position)
welt: World, position: Location
Block
Gibt den Block an der übergebenen Position zurück.
A.10 Blöcke A.10.1 Allgemein Befehl
Parameter
Rückgabewert
Beschreibung
Befehl
Parameter
Rückgabewert
Beschreibung
block.getState()
block: Block
BlockState
Gibt den Gegenstand zurück, der aktuell an dieser Stelle steht (zum Beispiel ein Schild).
block.getType()
block: Block
Material
Gibt den Typ eines Blocks zurück.
block.setData(data)
block: Block, data: Zahl
block.setType(material)
block: Block, material: Material
block: Block, material: Material, physik: Wahrheitswert
block.setFacing(blockface)
block: Block, blockface: BlockFace.WEST, BlockFace.EAST, BlockFace.SOUTH, BlockFace.NORTH
block.setHalf(haelfte)
block: Block, haelfte: Bisected.Half.BOTTOM, Bisected.Hald.TOP
Kurzschreibweise für block.setType(material, True).
block.setType(material, physik)
Setzt die Meta-Daten des Blocks auf den Wert von data.
Legt den Typ des Blocks fest. Der Wahrheitswert entscheidet, ob die Gesetze der Physik kurz ignoriert werden sollen, zum Beispiel, um den oberen Teil einer Tür zu bauen. Legt fest, in welche Richtung der Block ausgerichtet wird (zum Beispiel bei Betten oder Fackeln).
Legt fest, ob es sich bei dem Block um die obere oder untere Hälfte einer Tür handelt.
A.10.2 Schilder Befehl
Parameter
Rückgabewert
Beschreibung
Befehl
Parameter
Rückgabewert
schild.setLine(i, string)
schild: Schild-Block, i: Ganzzahl, string: String
schild.update()
schild: Schild
Beschreibung Schreibt den übergebenen String in die i-te Zeile des Schildes.
Wendet Änderungen, zum Beispiel im Text, auf ein Schild an.
A.11 Logging Befehl
Parameter
self.getLogger().info(string)
self: PluginObjekt, string: String
Rückgabewert
Beschreibung Schreibt einen Log-Eintrag mit dem angegebenen String als Text.
A.12 Gegenstände Befehl
Parameter
gegenstand.addUnsafeEnchantment(verzauberung, stufe)
verzauberung: Enchantment, stufe: Ganzzahl
gegenstand.getItemMeta()
gegenstand: ItemStack
gegenstand.setAmount(i)
i: Ganzzahl
Rückgabewert
ItemMeta
Beschreibung Verzaubert den Gegenstand mit der angegebenen Verzauberung auf der angegebenen Stufe. Gibt die MetaDaten eines Gegenstandes zurück. Legt die Anzahl der Gegenstände im Stapel fest.
Befehl
Parameter
gegenstand.setItemMeta(metaData)
gegenstand: ItemStack, metaData: ItemMeta
Rückgabewert
Beschreibung
ItemStack(material)
material: Material
metaData.setDisplayName(string)
metaData: ItemMeta, string: String
Weist einem Gegenstand Meta-Daten zu. ItemStack
Erzeugt einen neuen Stapel von Gegenständen des angegebenen Typs. Legt den Anzeigenamen des Gegenstandes fest.
A.13 Inventar Befehl
Parameter
Rückgabewert
inventar.addItem(gegenstand)
inventar: Inventory, gegenstand: ItemStack
inventar.contains(material)
inventar: Inventory, material: Material
Wahrheitswert
Gibt True zurück, falls der übergebene Gegenstand im Inventar mindestens einmal vorhanden ist.
inventar.first(material)
inventar: Inventory, material: Material
ItemStack
Gibt den ersten Stapel im Inventar zurück, der den übergebenen Gegenstand enthält.
Beschreibung Fügt dem Inventar den übergebenen Gegenstand hinzu.
Befehl
Parameter
Rückgabewert
Beschreibung
inventar.firstEmpty()
inventar: Inventory
Ganzzahl
Gibt die Nummer des ersten freien Slots im Inventar zurück.
inventar.getItem(i)
inventar: Inventory, i: Ganzzahl
ItemStack
Gibt den Stapel von Gegenständen im i-ten Slot des Inventars zurück.
inventar.remove(gegenstand)
inventar: Inventory, gegenstand: ItemStack
spieler.getInventory()
spieler: Spieler
Entfernt den übergebenen Gegenstand aus dem Inventar.
Gibt das Inventar eines Spielers zurück.
Inventory
A.14 Crafting-Rezepte Befehl
Parameter
rezept.addIngredient(material)
rezept: ShapelessRecipe, material: Material
rezept.setIngredient(b, material)
rezept: ShapedRecipe, b: String, material: Material
rezept.shape(reihe1, reihe2, reihe3)
rezept: ShapedRecipe, reihe1, reihe2, reihe3: String
self.getServer().addRecipe(rezept)
self: Plugin-Klasse, rezept: Recipe
Rückgabewert
Beschreibung Fügt eine Zutat zu einem Rezept ohne feste Anordnung hinzu. Fügt einem Rezept mit fester Anordnung eine Zutat hinzu. Legt die Form eines Rezeptes mit fester Anordnung fest. Fügt dem Server ein Rezept hinzu.
Befehl
Parameter
Rückgabewert
Beschreibung
ShapedRecipe(gegenstand)
gegenstand: ItemStack
ShapedRecipe
Erzeugt ein neues Rezept mit fester Anordnung.
ShapelessRecipe(gegenstand)
gegenstand: ItemStack
ShapelessRecipe
Erzeugt ein neues Rezept ohne feste Anordnung.
A.15 Rechnen Befehl
Parameter
Rückgabewert
Beschreibung
float(a)
a: String
Kommazahl
Wandelt a in eine Kommazahl um.
int(a)
a: String
Ganzzahl
Wandelt a in eine Ganzzahl um.
pow(x, y)
x, y: Zahl
Zahl
Berechnet xy
range(x, y)
x,y: Ganzzahlen
Liste mit Ganzzahlen
Erstellt eine Liste aller Ganzzahlen von x (einschließlich) bis y (ausschließlich)
round(a)
a: Zahl
Zahl
Rundet a (kaufmännisch) auf die nächste ganze Zahl.
str(a)
a: beliebiger Wert oder Objekt
String
Wandelt a in einen String um.
x - y
x, y: Zahlen
Zahl
Subtrahiert y von x.
x % y
x, y: Zahlen
Zahl
Berechnet den Rest beim Teilen von x durch y.
x * y
x, y: Zahlen
Zahl
Multipliziert die beiden Zahlen x und y.
x / y
x, y: Zahlen
Zahl
Teilt x durch y.
Befehl
Parameter
x + y
Rückgabewert Zahl
Addiert die beiden Zahlen x und y.
x, y: Zahlen
x + y
Beschreibung
x, y: String
String
Konkateniert (verbindet) die beiden Strings x und y.
A.16 Chat-Nachrichten Befehl
Parameter
Rückgabewert
self.getServer().broadcastMessage(string)
self: PluginObjekt, string: String
spieler.sendMessage(string)
spieler: Spieler, string: String
Beschreibung Sendet den übergebenen String als ChatNachricht an alle Spieler.
Sender den übergebenen String als ChatNachricht an den Spieler.
A.17 Listener Befehl class ListenerPlugin (PythonListener):
@PythonEventHandler(Event, EventPriority.NORMAL)
def onEvent(self, event):
#Anweisungen
pluginManager = self.getServer().getPluginManager()
listener = ListenerPlugin()pluginManager.registerEvents(listener, self)
Parameter
Rückgabewert
Beschreibu Grundgerüs einer Listene Klasse.
Registrierun eines Listeners.
A.18 Dateien Befehl
Parameter
Rückgabewert
Beschreibung
open(pfad, "rb")
pfad: String
File
Öffnet die Datei unter dem angegebenen Pfad zum Lesen.
open(pfad, "wb")
pfad: String
File
Öffnet die Datei unter dem angegebenen Pfad zum Schreiben.
pickle.dump(objekt, datei)
objekt: beliebiges Objekt, datei: File
pickle.load(datei)
datei: File
Objekt
Gibt das in der übergebenen Datei gespeicherte Objekt zurück.
self.getDataFolder().exists()
self: Plugin-Klasse
Wahrheitswert
Überprüft, ob für das Plugin bereits ein DatenOrdner existiert.
self.getDataFolder().getPath()
self: Plugin-Klasse
String
Gibt den Pfad zum Daten-Ordner des Plugins zurück.
self.getDataFolder().mkdirs()
self: Plugin-Klasse
Speichert das übergebene Objekt in der übergebenen Datei.
Erzeugt einen DatenOrdner für das Plugin.
A.19 Konfigurationen Befehl
Parameter
Rückgabewert
Beschreibun
Befehl
Parameter
Rückgabewert
Beschreibun
config.getBoolean(name)
config: FileConfiguration, name: String
Wahrheitswert
Gibt einen Wahrheitswer der Konfigura zurück, der un dem Namen n gespeichert w
config.getDouble(name)
config: FileConfiguration, name: String
Kommazahl
Gibt einen Kommazahl-W aus der Konfiguration zurück, der un dem Namen n gespeichert w
config.getInt(name)
config: FileConfiguration, name: String
Ganzzahl
Gibt einen Ganzzahl-We der Konfigura zurück, der un dem Namen n gespeichert w
config.getList(name)
config: FileConfiguration, name: String
Liste
Gibt eine Liste der Konfigura zurück, der un dem Namen n gespeichert w
config.getString(name)
config: FileConfiguration, name: String
String
Gibt einen Str Wert aus der Konfiguration zurück, der un dem Namen n gespeichert w
config.save(datei)
config: FileConfiguration, datei: File
Speichert die Konfiguration der angegebe Datei.
Befehl
Parameter
config.setBoolean(name, wert)
config: FileConfiguration, name: String, wert: Wahrheitswert
config.setDouble(name, wert)
config: FileConfiguration, name: String, wert: Kommazahl
config.setInt(name, wert)
config: FileConfiguration, name: String, wert: Ganzzahl
config.setList(name, wert)
config: FileConfiguration, name: String, wert: Liste
config.setString(name, wert)
config: FileConfiguration, name: String, wert: String
self.getConfig()
self: PluginKlasse
Rückgabewert
FileConfiguration
Beschreibun Speichert den übergebenen in der Konfiguration unter dem angegebenen Namen. Speichert den übergebenen in der Konfiguration unter dem angegebenen Namen. Speichert den übergebenen in der Konfiguration unter dem angegebenen Namen. Speichert den übergebenen in der Konfiguration unter dem angegebenen Namen. Speichert den übergebenen in der Konfiguration unter dem angegebenen Namen. Liest die Date config.yml ei
Befehl
Parameter
self.saveResource(dateiname, ueberschreiben)
self: PluginKlasse, dateiname: String, ueberschreiben: Wahrheitswert
Rückgabewert
Beschreibun Kopiert die angegebene D aus dem Plug das DatenVerzeichnis. W ueberschreibe True gesetzt i
wird eine eve bereits vorhan gleichnamige überschrieben YamlConfiguration.loadConfiguration(datei)
datei: File
FileConfiguration
Lädt eine Konfiguration der übergebe Datei.
Materialien Anhang B
In diesem Kapitel findest du eine Liste aller Materialien in Minecraft und der dazu gehörigen Bezeichner, die du benötigst, um sie in einem Plugin verwenden zu können. Die Liste der Materialien umfasst sowohl alle Blöcke als auch alle Gegenstände. Unter Blöcken versteht man dabei im Gegensatz zu Gegenständen nur solche Dinge, die man auch in der Welt platzieren kann. Ein Schwert oder ein Schneeball gehören also zum Beispiel nicht dazu. Die meisten Materialien existieren sowohl als Gegenstand als auch als Block, wie zum Beispiel Treppen oder Holzblöcke. Bei einigen wenigen Materialien gibt es unterschiedliche Einträge für Gegenstände und Blöcke, so zum Beispiel bei allen Gegenständen, die in der Welt aus zwei Blöcken bestehen, im Inventar aber nur ein Gegenstand sind, wie zum Beispiel Türen und Betten.
Bezeichner
Material
Ackerboden
FARMLAND
Aktivierungsschiene
ACTIVATOR_RAIL
Amboss
ANVIL
Angel
FISHING_ROD
Antriebsschiene
POWERED_RAIL
Bezeichner
Material
Apfel
APPLE
Apfel (Gold)
GOLDEN_APPLE
Axt (Diamant)
DIAMOND_AXE
Axt (Eisen)
IRON_AXE
Axt (Gold)
GOLD_AXE
Axt (Holz)
WOOD_AXE
Axt (Stein)
STONE_AXE
Banner (stehend, blau)
BLUE_BANNER
Banner (Wand, blau)
BLUE_WALL_BANNER
Barriere
BARRIER
Befehlsblock
COMMAND
Befehlsblock (ketten)
COMMAND_CHAIN
Bezeichner
Material
Befehlsblock (wiederholen)
COMMAND_REPEATING
Befehlsblocklore
COMMAND_MINECART
Beinschutz (Diamant)
DIAMOND_LEGGINGS
Beinschutz (Eisen)
IRON_LEGGINGS
Beinschutz (Gold)
GOLD_LEGGINGS
Beinschutz (Ketten)
CHAINMAIL_LEGGINGS
Beinschutz (Leder)
LEATHER_LEGGINGS
Bett (rot)
RED_BED
Blätter (Eiche)
OAK_LEAVES
Blume (Löwenzahn)
DANDELION
Blumentopf
FLOWER_POT
Bezeichner
Material
Blumentopf (Gegenstand)
FLOWER_POT_ITEM
Bogen
BOW
Boot (Eiche)
OAK_BOAT
Boot (Akazienholz)
ACACIA_BOAT
Boot (Birkenholz)
BIRCH_BOAT
Boot (Fichtenholz)
SPRUCE_BOAT
Boot (Schwarzeichenholz)
DARK_OAK_BOAT
Boot (Tropenholz)
JUNGLE_BOAT
Braustand
BREWING_STAND
Braustand (Gegenstand)
BREWING_STAND_ITEM
Brot
BREAD
Bruchstein
COBBLESTONE
Bezeichner
Material
Bruchstein (bemoost)
MOSSY_COBBLESTONE
Brustpanzer (Diamant)
DIAMOND_CHESTPLATE
Brustpanzer (Eisen)
IRON_CHESTPLATE
Brustpanzer (Gold)
GOLD_CHESTPLATE
Brustpanzer (Ketten)
CHAINMAIL_CHESTPLATE
Brustpanzer (Leder)
LEATHER_CHESTPLATE
Buch
BOOK
Buch (geschrieben)
WRITTEN_BOOK
Buch (verzaubert)
ENCHANTED_BOOK
Buch und Feder
BOOK_AND_QUILL
Bücherregal
BOOKSHELF
Bruchsteinmauer
COBBLE_WALL
Bezeichner
Material
Busch
DEAD_BUSH
Chorusblüte
CHORUS_FLOWER
Chorusfrucht
CHORUS_FRUIT
Chorusfrucht (geplatzt)
CHORUS_FRUIT_POPPED
Choruspflanze
CHORUS_PLANT
Diamant
DIAMOND
Diamantblock
DIAMOND_BLOCK
Diamanterz
DIAMOND_ORE
Drachenatem
DRAGONS_BREATH
Drachenei
DRAGON_EGG
Druckplatte (Holz)
WOOD_PLATE
Druckplatte (Stein)
STONE_PLATE
Bezeichner
Material
Ei
EGG
Eimer
BUCKET
Eis
ICE
Eis (brüchig)
FROSTED_ICE
Eisenbarren
IRON_INGOT
Eisenblock
IRON_BLOCK
Eisenerz
IRON_ORE
Eisentür
IRON_DOOR
Eisentür (Gegenstand)
IRON_DOOR_BLOCK
Elytren
ELYTRA
Enderauge
EYE_OF_ENDER
Enderperle
ENDER_PEARL
Bezeichner
Material
Enderportal
ENDER_PORTAL
Enderportalrahmen
ENDER_PORTAL_FRAME
Enderstein
ENDER_STONE
Endertruhe
ENDER_CHEST
Endkristall
END_CRYSTAL
Endstab
END_ROD
Endstein
END_BRICKS
Endtransitportal
END_GATEWAY
Erde
DIRT
Erfahrungstrank
EXP_BOTTLE
Fackel
TORCH
Faden
STRING
Bezeichner
Material
Falltür
TRAP_DOOR
Falltür (Eisen)
IRON_TRAPDOOR
Feder
FEATHER
Feuer
FIRE
Feuerkugel
FIREBALL
Feuerstein
FLINT
Feuerwerk
FIREWORK
Feuerwerksstern
FIREWORK_CHARGE
Feuerzeug
FLINT_AND_STEEL
Fisch (roh)
RAW_FISH
Fleisch (verrottet)
ROTTEN_FLESH
Gebratener Fisch
COOKED_FISH
Bezeichner
Material
Gebratener Hase
COOKED_RABBIT
Gebratenes Hammelfleisch
COOKED_MUTTON
Gebratenes Hühnchen
COOKED_CHICKEN
Gebratenes Schweinefleisch
GRILLED_PORK
Gemälde
PAINTING
Ghastträne
GHAST_TEAR
Glas
GLASS
Glas (dünn)
THIN_GLASS
Glas (gefärbt)
STAINED_GLASS
Glasflasche
GLASS_BOTTLE
Glasscheibe (gefärbt)
STAINED_GLASS_PANE
Bezeichner
Material
Glowstone
GLOWSTONE
Glowstonestaub
GLOWSTONE_DUST
Goldbarren
GOLD_INGOT
Goldblock
GOLD_BLOCK
Golderz
GOLD_ORE
Goldnugget
GOLD_NUGGET
Gras
GRASS
Gras (lang)
LONG_GRASS
Grundgestein
BEDROCK
Güterlore
STORAGE_MINECART
Hacke (Diamant)
DIAMOND_HOE
Hacke (Eisen)
IRON_HOE
Bezeichner
Material
Hacke (Gold)
GOLD_HOE
Hacke (Holz)
WOOD_HOE
Hacke (Stein)
STONE_HOE
Haken
TRIPWIRE_HOOK
Hammelfleisch
MUTTON
Hebel
LEVER
Helm (Diamant)
DIAMOND_HELMET
Helm (Eisen)
IRON_HELMET
Helm (Gold)
GOLD_HELMET
Helm (Ketten)
CHAINMAIL_HELMET
Helm (Leder)
LEATHER_HELMET
Hohes Gras
DOUBLE_PLANT
Bezeichner
Material
Holz (Eiche)
OAK_WOOD
Holzknopf
WOOD_BUTTON
Hühnchen (roh)
RAW_CHICKEN
Kakao
COCOA
Kaktus
CACTUS
Kaninchen
RABBIT
Kaninchenfell
RABBIT_HIDE
Kaninchenfleisch
RABBIT_FOOT
Kaninchenragout
RABBIT_STEW
Karotte
CARROT
Karotte (Gegenstand)
CARROT_ITEM
Karotte (Gold)
GOLDEN_CARROT
Bezeichner
Material
Karottenrute
CARROT_STICK
Karte
MAP
Karte (leer)
EMPTY_MAP
Kartoffel
POTATO
Kartoffel (Gegenstand)
POTATO_ITEM
Kartoffel (giftig)
POISONOUS_POTATO
Keks
COOKIE
Kessel
CAULDRON
Kessel (Gegenstand)
CAULDRON_ITEM
Kies
GRAVEL
Knochen
BONE
Kohle
COAL
Bezeichner
Material
Kohleblock
COAL_BLOCK
Kolben (klebrige Platte)
PISTON_STICKY_BASE
Kolben (Platte)
PISTON_BASE
Kompass
COMPASS
Konstruktionsblock
STRUCTURE_BLOCK
Kuchen
CAKE
Kuchenblock
CAKE_BLOCK
Kürbis
PUMPKIN
Kürbiskerne
PUMPKIN_SEEDS
Kürbiskuchen
PUMPKIN_PIE
Kürbislaterne
JACK_O_LANTERN
Kürbisstamm
PUMPKIN_STEM
Bezeichner
Material
Lapislazuliblock
LAPIS_BLOCK
Lapislazulierz
LAPIS_ORE
Lava
LAVA
Lava (stehend)
STATIONARY_LAVA
Lavaeimer
LAVA_BUCKET
Leder
LEATHER
Leine
LEASH
Leiter
LADDER
Leuchtfeuer
BEACON
Lohenrute
BLAZE_ROD
Lohenstaub
BLAZE_POWDER
Lore
MINECART
Bezeichner
Material
Lore (angetrieben)
POWERED_MINECART
Luft
AIR
Magmacreme
MAGMA_CREAM
Melone
MELON
Melone (Block)
MELON_BLOCK
Melone (glitzernd)
SPECKLED_MELON
Melonenkerne
MELON_SEEDS
Melonenstrunk
MELON_STEM
Milcheimer
MILK_BUCKET
Mob-Spawner
MOB_SPAWNER
Monsterei
MONSTER_EGG
Myzel
MYCEL
Bezeichner
Material
Nachtlichtsensor
DAYLIGHT_DETECTOR_INVERTED
Namensschild
NAME_TAG
Netherquarz
QUARTZ_ORE
Netherrack
NETHERRACK
Netherstern
NETHER_STAR
Netherwarze
NETHER_STALK
Netherwarze
NETHER_WARTS
Netherziegel
NETHER_BRICK
Netherziegel (Gegenstand)
NETHER_BRICK_ITEM
Notenblock
NOTE_BLOCK
Obsidian
OBSIDIAN
Bezeichner
Material
Ofen
FURNACE
Ofen (brennend)
BURNING_FURNACE
Ofenkartoffel
BAKED_POTATO
Packeis
PACKED_ICE
Papier
PAPER
Pfeil
ARROW
Pfeil (getränkt)
TIPPED_ARROW
Pferderüstung (Diamant)
DIAMOND_BARDING
Pferderüstung (Eisen)
IRON_BARDING
Pferderüstung (Gold)
GOLD_BARDING
Pilz (braun)
BROWN_MUSHROOM
Pilz (groß)
HUGE_MUSHROOM_1
Bezeichner
Material
Pilz (groß)
HUGE_MUSHROOM_2
Pilz (rot)
RED_MUSHROOM
Pilzsuppe
MUSHROOM_SOUP
Plattenspieler
JUKEBOX
Portal
PORTAL
Prismarin
PRISMARINE
Prismarinkristall
PRISMARINE_CRYSTALS
Prismarinscherbe
PRISMARINE_SHARD
Purpurblock
PURPUR_BLOCK
Purpursäule
PURPUR_PILLAR
Quarz
QUARTZ
Quarzblock
QUARTZ_BLOCK
Bezeichner
Material
Rahmen
ITEM_FRAME
Ranken
VINE
Redstone
REDSTONE
Redstone-Fackel (an)
REDSTONE_TORCH_ON
Redstone-Fackel (aus)
REDSTONE_TORCH_OFF
Redstone-Kabel
REDSTONE_WIRE
Redstone-Komparator (Block, an)
REDSTONE_COMPARATOR_ON
Redstone-Komparator (Block, aus)
REDSTONE_COMPARATOR_OFF
Redstone-Komparator (Gegenstand)
REDSTONE_COMPARATOR
Redstone-Lampe (an)
REDSTONE_LAMP_ON
Redstone-Lampe (aus)
REDSTONE_LAMP_OFF
Bezeichner
Material
Redstone-Verstärker (Block, an)
DIODE_BLOCK_ON
Redstone-Verstärker (Block, aus)
DIODE_BLOCK_OFF
Redstone-Verstärker (Gegenstand)
DIODE
Redstoneblock
REDSTONE_BLOCK
Redstoneerz
REDSTONE_ORE
Redstoneerz (leuchtend)
GLOWING_REDSTONE_ORE
Redstonetruhe
TRAPPED_CHEST
Rindfleisch (roh)
RAW_BEEF
Rose (rot)
RED_ROSE
Rote Bete
BEETROOT
Rote Bete Pflanze
BEETROOT_BLOCK
Bezeichner
Material
Rote Bete Samen
BEETROOT_SEEDS
Rote Bete Suppe
BEETROOT_SOUP
Rüstungsständer
ARMOR_STAND
Sand
SAND
Sandstein
SANDSTONE
Sandstein (rot)
RED_SANDSTONE
Sattel
SADDLE
Schädel
SKULL
Schädel (Gegenstand)
SKULL_ITEM
Schallplatte
RECORD_10
Schallplatte
RECORD_11
Schallplatte
RECORD_12
Bezeichner
Material
Schallplatte
RECORD_3
Schallplatte
RECORD_4
Schallplatte
RECORD_5
Schallplatte
RECORD_6
Schallplatte
RECORD_7
Schallplatte
RECORD_8
Schallplatte
RECORD_9
Schallplatte (Gold)
GOLD_RECORD
Schallplatte (Grün)
GREEN_RECORD
Schaufel (Diamant)
DIAMOND_SPADE
Schaufel (Eisen)
IRON_SPADE
Schaufel (Gold)
GOLD_SPADE
Bezeichner
Material
Schaufel (Holz)
WOOD_SPADE
Schaufel (Stein)
STONE_SPADE
Schere
SHEARS
Schiene
RAILS
Schild
SIGN
Schild (Kampf)
SHIELD
Schild (Pfosten)
SIGN_POST
Schild (Wand)
WALL_SIGN
Schleimball
SLIME_BALL
Schleimblock
SLIME_BLOCK
Schnee
SNOW
Schneeball
SNOWBALL
Bezeichner
Material
Schneeblock
SNOW_BLOCK
Schuhe (Eisen)
IRON_BOOTS
Schüssel
BOWL
Schwamm
SPONGE
Schwarzpulver
SULPHUR
Schweinefleisch
PORK
Schwert (Diamant)
DIAMOND_SWORD
Schwert (Eisen)
IRON_SWORD
Schwert (Gold)
GOLD_SWORD
Schwert (Holz)
WOOD_SWORD
Schwert (Stein)
STONE_SWORD
Seelaterne
SEA_LANTERN
Bezeichner
Material
Seelensand
SOUL_SAND
Seerose
WATER_LILY
Sensorschiene
DETECTOR_RAIL
Setzling
SAPLING
Smaragd
EMERALD
Smaragdblock
EMERALD_BLOCK
Smaragderz
EMERALD_ORE
Spektralpfeil
SPECTRAL_ARROW
Spender
DROPPER
Spinnenauge
SPIDER_EYE
Spinnenaugen (fermentiert)
FERMENTED_SPIDER_EYE
Bezeichner
Material
Spinnennetz
WEB
Spitzhacke (Diamant)
DIAMOND_PICKAXE
Spitzhacke (Eisen)
IRON_PICKAXE
Spitzhacke (Gold)
GOLD_PICKAXE
Spitzhacke (Holz)
WOOD_PICKAXE
Spitzhacke (Stein)
STONE_PICKAXE
Steak
COOKED_BEEF
Stein
STONE
Steinknopf
STONE_BUTTON
Steinkohle
COAL_ORE
Steinziegel (gemeißelt)
SMOOTH_BRICK
Stiefel (Diamant)
DIAMOND_BOOTS
Bezeichner
Material
Stiefel (Gold)
GOLD_BOOTS
Stiefel (Ketten)
CHAINMAIL_BOOTS
Stiefel (Leder)
LEATHER_BOOTS
Stock
STICK
Stolperdraht
TRIPWIRE
Strohballen
HAY_BLOCK
Stufe
STEP
Stufe (doppelt)
DOUBLE_STEP
Stufe (Holz, doppelt)
WOOD_DOUBLE_STEP
Stufe (Holz)
WOOD_STEP
Stufe (Purpur, doppelt)
PURPUR_DOUBLE_SLAB
Stufe (Purpur)
PURPUR_SLAB
Bezeichner
Material
Stufe (Stein, doppelt)
DOUBLE_STONE_SLAB2
Stufe (Stein)
STONE_SLAB2
Tageslichtsensor
DAYLIGHT_DETECTOR
Teppich
CARPET
Tintenbeutel
INK_SACK
TNT
TNT
TNT-Lore
EXPLOSIVE_MINECART
Ton
CLAY
Ton (gebrannt)
HARD_CLAY
Ton (gefärbt)
STAINED_CLAY
Tonklumpen
CLAY_BALL
Trampelpfad
GRASS_PATH
Bezeichner
Material
Trank
POTION
Trank (werfbar)
SPLASH_POTION
Treppe (Akazienholz)
ACACIA_STAIRS
Treppe (Birkenholz)
BIRCH_WOOD_STAIRS
Treppe (Bruchstein)
COBBLESTONE_STAIRS
Treppe (Fichtenholz)
SPRUCE_WOOD_STAIRS
Treppe (Holz)
WOOD_STAIRS
Treppe (Netherziegel)
NETHER_BRICK_STAIRS
Treppe (Purpur)
PURPUR_STAIRS
Treppe (Quarz)
QUARTZ_STAIRS
Treppe (Sandstein, rot)
RED_SANDSTONE_STAIRS
Treppe (Sandstein)
SANDSTONE_STAIRS
Bezeichner
Material
Treppe (Schwarzeichenholz)
DARK_OAK_STAIRS
Treppe (Steinziegel, gemeißelt)
SMOOTH_STAIRS
Treppe (Tropenholz)
JUNGLE_WOOD_STAIRS
Treppe (Ziegelsteine)
BRICK_STAIRS
Trichter
HOPPER
Trichterlore
HOPPER_MINECART
Truhe
CHEST
Tür (Akazienholz, Gegenstand)
ACACIA_DOOR_ITEM
Tür (Akazienholz)
ACACIA_DOOR
Tür (Birkenholz, Gegenstand)
BIRCH_DOOR_ITEM
Bezeichner
Material
Tür (Birkenholz)
BIRCH_DOOR
Tür (Fichtenholz)
SPRUCE_DOOR
Tür (Gegenstand, Fichtenholz)
SPRUCE_DOOR_ITEM
Tür (Holz)
WOOD_DOOR
Tür (Holz)
WOODEN_DOOR
Tür (Schwarzeichenholz, Gegenstand)
DARK_OAK_DOOR_ITEM
Tür (Schwarzeichenholz)
DARK_OAK_DOOR
Tür (Tropenholz, Gegenstand)
JUNGLE_DOOR_ITEM
Tür (Tropenholz)
JUNGLE_DOOR
Uhr
WATCH
Verweiltrank
LINGERING_POTION
Bezeichner
Material
Verzauberungstisch
ENCHANTMENT_TABLE
Wägeplatte (Eisen)
IRON_PLATE
Wägeplatte (Gold)
GOLD_PLATE
Wasser
WATER
Wasser (stehend)
STATIONARY_WATER
Wassereimer
WATER_BUCKET
Weizen
CROPS
Weizen
WHEAT
Weizenkörner
SEEDS
Werfer
DISPENSER
Werkbank
WORKBENCH
Wolle
WOOL
Bezeichner
Material
Zaun
FENCE
Zaun (Akazienholz)
ACACIA_FENCE
Zaun (Birkenholz)
BIRCH_FENCE
Zaun (Eisen)
IRON_FENCE
Zaun (Fichtenholz)
SPRUCE_FENCE
Zaun (Netherziegel)
NETHER_FENCE
Zaun (Schwarzeichenholz)
DARK_OAK_FENCE
Zaun (Tropenholz)
JUNGLE_FENCE
Zauntor
FENCE_GATE
Zauntor (Akazienholz)
ACACIA_FENCE_GATE
Zauntor (Birkenholz)
BIRCH_FENCE_GATE
Zauntor (Fichtenholz)
SPRUCE_FENCE_GATE
Bezeichner
Material
Zauntor (Schwarzeichenholz)
DARK_OAK_FENCE_GATE
Zauntor (Tropenholz)
JUNGLE_FENCE_GATE
Ziegel
CLAY_BRICK
Ziegelsteine
BRICKS
Zucker
SUGAR
Zuckerrohr (Block)
SUGAR_CANE_BLOCK
Zuckerrohr (Gegenstand)
SUGAR_CANE