Pyxen Script
Pyxen-Spiele werden in Pyxen Script geschrieben, einem für Spiele gemachten Python-Dialekt. Er hat die Syntax und das Gefühl von Python: Einrückung, def, Listen, Dictionaries, f-Strings, Comprehensions. Was sich ändert, liegt darunter. Dein Code wird als Ganzes geprüft und kompiliert, bevor das Spiel startet, und er läuft auf einer Laufzeitumgebung, die für genau eine Aufgabe gebaut ist: ein Spiel auszuführen, und zwar auf jedem Gerät gleich.
Wenn du Python schon kennst, funktioniert das meiste, was du schreibst, wie erwartet. Diese Seite erklärt, warum es Pyxen Script gibt, was es mit Python teilt und was es bewusst weglässt.
Warum ein Dialekt
Ein Spiel verlangt von einer Sprache andere Dinge als ein Skript.
- Es läuft überall gleich. Dasselbe kompilierte Programm läuft in der App auf iPad und Mac, in einem für Windows exportierten Spiel und über den Web-Export im Browser. Pyxen Script wurde von Anfang an für WebAssembly entworfen, der Browser ist also kein Ziel zweiter Klasse.
- Keine Pausen durch einen Garbage Collector. Pyxen Script hat keinen Garbage Collector. Temporäre Werte, die während eines Frames entstehen, liegen in einem Speicherbereich, der am Ende dieses Frames in einem Schritt geleert wird; Werte, die du behältst, etwa eine Liste in einer globalen Variable, werden freigegeben, sobald nichts mehr auf sie verweist. Nichts hält dein Spiel mitten im Frame zum Aufräumen an.
- Fehler, bevor das Spiel läuft. Das ganze Projekt wird vor dem ersten Frame geprüft. Ein falsch geschriebener Name, der Aufruf einer Modulfunktion, die es nicht gibt, oder ein Konstrukt, das Pyxen Script nicht unterstützt, wird an seiner Zeile gemeldet, bevor das Spiel überhaupt startet.
- Fehler an der richtigen Zeile. Wenn während des Spiels etwas schiefgeht, wird der Fehler dort festgehalten, wo er passiert, mit der noch vollständigen Kette der Aufrufe, die dorthin geführt haben. So kann Pyxen dir die genaue Zeile zeigen.
Frühere Versionen von Pyxen nutzten einen MicroPython-Interpreter; Pyxen Script ersetzt ihn.
Was wie in Python ist
Pyxen Script wird gegen Hunderte einzeiliger Python-Idiome getestet, jedes verglichen mit der Ausgabe von Standard-Python. Alles Folgende verhält sich wie in Python:
- Blöcke und Kontrollfluss: Einrückung,
if/elif/else,forundwhilemitbreak,continueundelse, bedingte Ausdrücke (a if ok else b), verkettete Vergleiche (0 <= x < 320),pass,assert. - Funktionen:
defmit Standard- und Schlüsselwortargumenten, mehrere Rückgabewerte als Tupel,lambda, Funktionen in Listen und Dictionaries, verschachtelte Funktionen und Closures, Rekursion,global. - Daten: Listen, Dictionaries (die die Einfügereihenfolge behalten), Sets, Tupel, Slicing, negative Indizes, Entpacken (
x, y = pos,first, *rest = items) sowie List-, Dict- und Set-Comprehensions. - Strings: f-Strings mit Formatangaben (
f"{score:05d}",f"{t:.2f}"),str.format, Formatierung mit%und die üblichen Methoden (split,join,strip,replace,find,upper,startswith,zfill, …). Längen und Indizes zählen Zeichen, daher funktioniert Text in jeder Sprache. - Eingebaute Funktionen:
print,len,range,enumerate,zip,sortedundmin/maxmitkey=undreverse=,sum,any,all,map,filter,round,abs,int,float,str,isinstanceund weitere. - Zahlen: Ganzzahlen haben 64 Bit und Fließkommazahlen doppelte Genauigkeit, daher liefern
2 ** 40und0.1 + 0.2dieselben Ergebnisse wie Python.//und%runden in Richtung minus unendlich, wie in Python. math:import mathfunktioniert, mitsqrt,floor,sin,atan2,hypot,piund den übrigen üblichen Funktionen.
Was anders ist
Keine Klassen
Pyxen Script hat kein class. Das ist so gewollt.
In einem Pyxen-Spiel liegt der Zustand auf Entitäten und ihren Komponenten, die der Engine gehören, von ihr kompakt gespeichert und dir im Frame-Inspektor gezeigt werden. Eine Klasse wäre ein zweiter Ort für denselben Zustand, unsichtbar für die Engine und für den Inspektor. Wo Python-Code also eine Klasse definieren würde, erzeugt Pyxen-Code eine Entität, hängt eine Komponente an und schreibt Funktionen, die damit arbeiten.
Eine typische Python-Klasse:
class Enemy:
def __init__(self, x, y):
self.x = x
self.y = y
self.hp = 3
def hit(self):
self.hp -= 1
Dasselbe in Pyxen Script:
def spawn_enemy(x, y):
e = world.spawn(name="enemy", x=x, y=y, sprite=Sprite("slime"))
e.enemy = {"hp": 3, "speed": 1.5}
return e
def hit(e):
e.enemy.hp -= 1
if e.enemy.hp <= 0:
world.destroy(e)
def update():
for e in world.all("enemy"):
e.x -= e.enemy.speed
Die Entität hat bereits eine Position, ein Sprite und einen Platz in der Szene. Die Komponente enemy enthält, was für Gegner spezifisch ist, und world.all("enemy") findet sie alle, sodass es keine Liste von Instanzen gibt, die synchron gehalten werden muss. Für einfache Daten, die keine Spielobjekte sind, etwa eine Highscore-Tabelle oder eine Levelbeschreibung, nimm Dictionaries und Listen. Das vollständige Modell findest du unter Entitäten & Komponenten.
Keine Exceptions
Pyxen Script hat kein try, except, finally oder raise. Auch das ist so gewollt.
Ein Fehler in Pyxen Script hält das Spiel an und wird an seiner Zeile gemeldet, zusammen mit den Aufrufen, die dorthin geführt haben. Nichts kann ihn unterwegs abfangen, also wird ein Fehler nie von einem except: pass verschluckt und zu einem Spiel, das sich drei Level später still falsch verhält.
Die meisten try-Blöcke in Spielcode stellen eine Frage, auf die es eine direkte Antwort gibt. Stell stattdessen die Frage:
# hat diese Entität eine Komponente?
hp = e.health.value if e.has("health") else 0
# ist dieser Schlüssel im Dictionary?
best = scores.get(name, 0)
# ist dieser Text eine Zahl?
if text.isdigit():
level = int(text)
# liegt dieser Index in der Liste?
if i < len(items):
item = items[i]
assert ist verfügbar und hält das Spiel an seiner Zeile an, wenn seine Bedingung falsch ist.
Ebenfalls nicht in Pyxen Script
with,yield(Generatorfunktionen),nonlocal, Dekoratoren,asyncundawaitsowie der Walross-Operator:=. Generator-Ausdrücke wiesum(x * x for x in xs)funktionieren.*argsund**kwargsin den Parametern einer Funktion. Eine Liste oder ein Dictionary in einen Aufruf zu entpacken, wie beiprint(*items), funktioniert.- Die Standardbibliothek von Python.
mathist verfügbar; Pyxen bringt eigene Modulerandom,time,inputundstoragemit, beschrieben in der Skript-Referenz. Module wieos,sys,jsonoderrewerden gemeldet, bevor das Spiel läuft. - Ganzzahlen jenseits von 64 Bit. Ein Ergebnis, das nicht hineinpasst, hält das Spiel mit einem
OverflowErroran; es läuft nie still über. Zahlen in den Ganzzahlfeldern einer Komponente haben 32 Bit. - Tabulatoren zum Einrücken. Rücke mit Leerzeichen ein: Die Breite eines Tabulators hängt vom Editor ab, und ein Block, der davon abhängt, könnte für verschiedene Menschen Verschiedenes bedeuten.
Jeder dieser Punkte wird mit Namen und Zeile gemeldet, bevor das Spiel startet.
Module und Imports
Ein Projekt wird als ein Programm kompiliert. Jedes Skript darin ist für die anderen bereits unter seinem Dateinamen sichtbar: Eine Funktion spawn() in monsters.py wird aus jedem anderen Skript als monsters.spawn() aufgerufen, ohne Import. Die Engine-API (world, input, Sprite, …) ist ebenfalls immer verfügbar.
Imports, die du schon hast, etwa from pyxen import * oder import monsters, funktionieren weiterhin. Pyxen Script behandelt sie als unnötig, und du kannst sie entfernen.